←kierangraham.com Linux on Apple Silicon
Linux on Apple Silicon · Kernel notes

LG 5K over Thunderbolt on a MacBook with Omarchy

An LG 5K Thunderbolt monitor is two screens in one box. A MacBook running Linux can drive it at full resolution once the kernel sends both halves at once, at a speed that leaves room for both. This page explains why, and how to build a kernel that does it.

By Kieran Graham 20 October 2026 Tested on MacBook Pro 16″ M2 Max Monitor LG UltraWide 5K Kernel Omarchy Aurora 7.1.12

The short version

  1. The monitor needs two video streams, one for each half of the panel. With one stream it only offers smaller fallback modes, such as 3440×1440.
  2. Both streams share one Thunderbolt cable. If the first stream takes the fastest link speed, the second can't get enough bandwidth.
  3. The patch caps both streams at the same speed, HBR2, and joins them on one display controller. The monitor is recognised by its Thunderbolt ID, so this works on any port.
  4. You build the patched kernel, add two boot options, and ask Hyprland for the native mode.
Result: 5120×2160 at 59.66 HzScale 160%In daily use since 4 October 2026
Using a different 5K monitor?

Other two-tile Thunderbolt monitors, such as the LG UltraFine 5K 27″, use the same design. I've only tested the UltraWide 5K. Will it work for you? covers the differences, and what to try first.

01The monitor is two screens in one box

The panel is driven as two halves, called tiles. Each tile is 2560×2160. The monitor's EDID, the description it sends to the computer, says so:

Tile (0,0) 2560 × 2160 stream 1 Tile (1,0) 2560 × 2160 stream 2 one picture: 5120 × 2160
Each half is its own video stream. The computer has to send both, and treat them as one screen.
Read the monitor's EDID
$ edid-decode /sys/class/drm/card*-DP-*/edid | grep -A4 'Tiled Display'
  Tiled Display Topology Data Block (0x12):
    Num horizontal tiles: 2 Num vertical tiles: 1    Tile location: 0, 0
    Tile resolution: 2560x2160

If only one tile arrives, you get a fallback mode. These are the modes the monitor offers, from hyprctl monitors all. Only the first needs both tiles:

availableModes on the LG
[email protected]   ← native: needs both tiles[email protected]   ← one stream, scaled
[email protected]   ← one stream, what the stock kernel gave me
[email protected]   ← one tile on its own
[email protected]
…

02Both streams travel inside one Thunderbolt cable

Thunderbolt carries DisplayPort video inside tunnels. Each tile gets its own tunnel. On the Mac side, each tunnel ends at one port of Apple's display controller, the DCP. Both ports must belong to the same controller, so the controller can join the tiles into one picture.

MacBook one display controller (DCP) DP port 0 DP port 1 Thunderbolt 3 cable · 40 Gb/s DP tunnel 1 DP tunnel 2 LG 5K Thunderbolt router left tile right tile
Two tunnels, one cable. They share its bandwidth, which is where the trouble starts.

boltctl list shows the monitor and the cable speed. The uuid line matters later: it's how the kernel recognises this monitor.

The monitor as Thunderbolt sees it
$ boltctl list
 * LG Electronics UltraWide 5K
   |- generation:    Thunderbolt 3
   |- uuid:          ca030000-0062-6c1e-83aa-dd82aef29101   |- status:        authorized
   |  |- rx speed:   40 Gb/s = 2 lanes * 20 Gb/s

03Why the second half went dark: bandwidth

Each DisplayPort tunnel picks a link speed. The common speeds, with four lanes:

SpeedUsable video bandwidthEnough for one tile?
HBR8.64 Gb/sNo. Less than 2% headroom.
HBR217.28 Gb/sYes, with twice the room it needs
HBR325.92 Gb/sYes, but it hogs the cable

What one tile needs

A tile needs about 8.48 Gb/s. The EDID gives its pixel clock, and each pixel is 24 bits:

Worked example: one 2560×2160 tile at 59.66 Hz
pixel clock      353.13 MHz      from the EDID's 2560x2160 timing
bits per pixel   × 24            8 bits each for red, green, blue
                 ─────────────
needed           ≈ 8.48 Gb/s     per tile, so 16.95 Gb/s for both

What went wrong, and the fix

Without the patch, the first tunnel trained at HBR3. That left only HBR for the second tunnel, which is too little for a tile. Apple's display firmware refused the second stream with kIOReturnNoBandwidth.

With the patch, both tunnels are capped at HBR2 before either reserves bandwidth. Each gets twice what its tile needs.

Before first come, first served

Tunnel 1 · HBR325.92 Gb/s
Tunnel 2 · HBR8.64 ✗

After balanced HBR2

Tunnel 1 · HBR217.28 Gb/s
Tunnel 2 · HBR217.28 Gb/s
010203040 Gb/s
what one tile needs: 8.48 Gb/s
Drawn to scale against the 40 Gb/s cable. In the top row, the second tunnel's bandwidth barely covers its tile. In the bottom row, both have room to spare.

04What the patch changes

Omarchy's Mac kernel, Aurora, already knows how to join two tiles. It reads the tile layout from the monitor and puts both streams on one display controller. The patch adds three things on top:

01

Recognise the monitor

A boot option names the monitor's Thunderbolt UUID. The match works on any port.

02

Cap both tunnels at HBR2

Before either tunnel reserves bandwidth, so the second always gets its share.

03

Always pair the tiles

Even when the monitor's firmware is slow to report its tile layout.

1. Recognise the monitor

The kernel compares each display's Thunderbolt UUID with the one you set at boot. If no UUID is set, nothing changes. Simplified from drivers/thunderbolt/tunnel.c:

tunnel.c · new boot option and check
static char apple_tiled_uuid[UUID_STRING_LEN + 1];module_param_string(apple_tiled_uuid, ...);   /* thunderbolt.apple_tiled_uuid= */bool tb_dp_is_apple_tiled(struct tb_nhi *nhi, const struct tb_port *out){	if (/* not an Apple host, not a display output, or no UUID set */)		return false;	return uuid_equal(out->sw->uuid, &uuid);   /* is this the LG? */}

2. Cap both tunnels at HBR2

The cap goes in before the two ends of the tunnel exchange capabilities. Bandwidth is reserved from those capabilities, so capping later is too late. 5400 is HBR2's rate in Mb/s per lane:

tunnel.c · tb_dp_xchg_caps()
if (tb_dp_is_apple_tiled(tunnel->tb->nhi, out)) {	in_dp_cap  = tb_dp_cap_set_rate(in_dp_cap,  min(rate(in_dp_cap),  5400U));	out_dp_cap = tb_dp_cap_set_rate(out_dp_cap, min(rate(out_dp_cap), 5400U));	tb_tunnel_info(tunnel, "Apple tiled display: balanced HBR2 capability limit\n");}
/* Write IN local caps to OUT remote caps */
ret = tb_port_write(out, &in_dp_cap, ...);

3. Always pair the tiles

Aurora waits for the firmware to report the tile layout, and gives up if it never arrives. For the named monitor, the patch skips that wait. Simplified from drivers/gpu/drm/apple/dcp-fabric.c:

dcp-fabric.c · dcp_tb_split_locked()
if (!tile_hint) {if (!req->tiled && !tile_hint) {   /* the named monitor doesn't wait */	if (still_waiting) return -ENODEV;
	return -EOPNOTSUPP;
}

What happens when you plug it in

This is the order of events, matched to real kernel log lines from my machine:

Plug-in sequence
LG connected (Thunderbolt authorises it at 40 Gb/s)
├─ UUID matches thunderbolt.apple_tiled_uuid
├─ tunnel 1  0:5 ↔ 1:10   capped at HBR2 → paths up → DCP port 0
├─ tunnel 2  0:6 ↔ 1:11   capped at HBR2 → paths up → DCP port 1
└─ display controller
   ├─ port 0 carries tile (0,0)
   ├─ port 1 joins port 0
   └─ port 1 carries tile (1,0)  →  one 5120×2160 output
journalctl -k · what success looks like
$ journalctl -k -b | grep -E 'balanced HBR2|carries tile|joins dpin0'
0:5 <-> 1:10 (DP): Apple tiled display: balanced HBR2 capability limit
0:6 <-> 1:11 (DP): Apple tiled display: balanced HBR2 capability limit
DPTXPort: port 0 carries tile (0,0) of a 2x1 tiled 5120x2160 display
tiled: dpin1 joins dpin0 on DPTX port 1 (crossbar state 1)
DPTXPort: port 1 carries tile (1,0) of a 2x1 tiled 5120x2160 display

05Will it work for you?

It should work for any two-tile Thunderbolt monitor on a Mac that Aurora drives over Thunderbolt. I've proven it on one combination only:

My setupAnother monitor: LG UltraFine 5K 27″
MacMacBook Pro 16″ M2 Max, 2023 (Mac14,6)Check that Aurora supports Thunderbolt displays on your chip
KernelOmarchy's Aurora kernelNot tried on Asahi's own kernel. The Thunderbolt display work is in Aurora.
ConnectionThunderbolt 3, two tilesThunderbolt 3, two tiles
Tile size2560×21602560×2880
Bandwidth per tile≈ 8.48 Gb/sover 10.6 Gb/s
Needs HBR2?YesYes. HBR's 8.64 Gb/s is too little.
StatusIn daily useUntested

The UltraFine figure is a lower bound. 2560 × 2880 × 60 Hz × 24 bits is 10.6 Gb/s before blanking, which only adds to it.

Try this first

Update to the latest Omarchy Mac kernel, plug the monitor in, and check hyprctl monitors all. Aurora already pairs tiles when the monitor reports them. If 5120x2880 or 5120x2160 is listed and works, you don't need the patch.

06Build it, step by step

Before you start
  • Keep the stock kernel installed. It's your way back if the new one doesn't boot.
  • This needs Omarchy for Mac, booting with Limine. The commands assume Arch's tools.
  • The kernel build took about 9 minutes on an M2 Max.
  • Values in <angle brackets> are placeholders. Replace them with your own.
  1. Check what you get today

    Plug the monitor in. Note its name and current mode.

    Current mode
    $ hyprctl monitors
    Monitor DP-2 (ID 2):
    	[email protected] at 0x0          ← fallback: only one stream
    	description: LG Electronics LG HDR 5K 0x00030BFB
  2. Find the monitor's Thunderbolt UUID

    Copy the uuid line from boltctl list. Each monitor has its own. Mine is shown as an example.

    Find the UUID
    $ boltctl list | grep -E '^ \*|uuid'
     * LG Electronics UltraWide 5K
       |- uuid:          ca030000-0062-6c1e-83aa-dd82aef29101
  3. Install the build tools

    The kernel includes Rust code, so it needs the pinned Rust version as well as the usual tools.

    Toolchain
    $ sudo pacman -S --needed base-devel git bc cpio pahole rustup rust-bindgen
    $ rustup toolchain install 1.93.1 --component rust-src,rustfmt
  4. Get the kernel source and apply the patch

    Use this exact commit. It's the Aurora kernel published with Omarchy's aurora-2026.10.13.18 release, and the one the patches were made for. On newer commits the patches may need adapting.

    Source and patch
    $ git init linux-lg5k && cd linux-lg5k
    $ git fetch --depth=1 https://github.com/omacom/linux-aurora \
        6f63677751854d57bcfac172314957bc2197c05f
    $ git checkout -b lg-5k FETCH_HEAD
    $ curl -fLO https://kierangraham.com/lg-5k-thunderbolt-macbook-omarchy/patches/0004-drm-apple-thunderbolt-retain-LG-UUID-pairing-and-bal.patch
    $ git am 0004-*.patch
    Applying: drm/apple, thunderbolt: retain LG UUID pairing and balanced HBR2
    $ rustup override set 1.93.1

    Patch 0004 is the only one the display needs. The patches lists the other five, which I also run.

  5. Build the kernel

    Start from the configuration of the Omarchy kernel you run now. The running kernel exposes it at /proc/config.gz. Give the new kernel its own name, so it installs beside the stock one.

    Configure and build
    $ mkdir -p build
    $ zcat /proc/config.gz > build/.config
    $ sed -i 's/^CONFIG_LOCALVERSION=.*/CONFIG_LOCALVERSION="-lg5k"/' build/.config
    $ make O=build ARCH=arm64 olddefconfig
    $ make O=build ARCH=arm64 rustavailable
    Rust is available!
    $ make O=build ARCH=arm64 -j$(nproc)
    $ make O=build -s kernelrelease
    7.1.12-lg5k
  6. Package and install it

    A package keeps the kernel, its modules and its device trees together. Save this as PKGBUILD in the linux-lg5k folder. It's a cut-down version of mine.

    PKGBUILD
    pkgname=linux-aurora-lg5k
    pkgver=7.1.12
    pkgrel=1
    pkgdesc="Omarchy Aurora kernel with LG 5K tiled Thunderbolt support"
    arch=('aarch64')
    license=('GPL-2.0-only')
    depends=('coreutils' 'kmod' 'initramfs' 'm1n1')
    options=('!strip' '!debug')
    package() {
      local kernver="$(make -s -C "$startdir" O=build kernelrelease)"
      local modulesdir="$pkgdir/usr/lib/modules/$kernver"
      make -C "$startdir" ARCH=arm64 O=build INSTALL_MOD_PATH="$pkgdir/usr" INSTALL_MOD_STRIP=1 modules_install
      install -Dm644 "$startdir/build/arch/arm64/boot/Image" "$modulesdir/vmlinuz"
      printf '%s\n' "$pkgname" > "$modulesdir/pkgbase"        # Limine names the boot entry from this
      install -Dm644 "$startdir/build/.config" "$modulesdir/config"
      install -Dt "$modulesdir/dtbs" "$startdir"/build/arch/arm64/boot/dts/apple/*.dtb
      rm -f "$modulesdir/build" "$modulesdir/source"
    }
    Build the package and install it
    $ makepkg --force --nodeps
    $ sudo pacman -U linux-aurora-lg5k-7.1.12-1-aarch64.pkg.tar.*
  7. Add the two boot options

    Give the new kernel its own command line in /etc/default/limine. Copy your existing KERNEL_CMDLINE[default] line, rename it after the package, and add two options to the end:

    /etc/default/limine
    KERNEL_CMDLINE[default]="<your existing options>"
    KERNEL_CMDLINE[linux-aurora-lg5k]="<your existing options> pcie_apple.tunnel_kernel_init=1 thunderbolt.apple_tiled_uuid=<your-uuid>"
    OptionWhat it does
    pcie_apple.tunnel_kernel_init=1Sets up the PCIe devices behind Thunderbolt at boot
    thunderbolt.apple_tiled_uuid=…Turns on the HBR2 cap and tile pairing for this one monitor

    Then point m1n1, the first-stage boot loader, at the new kernel's device trees, and rebuild the boot entries:

    Device trees and boot menu
    $ echo 'DTBS="/usr/lib/modules/<kernel-release>/dtbs/*.dtb"' | sudo tee /etc/default/update-m1n1
    $ sudo update-m1n1
    $ sudo limine-update

    <kernel-release> is the name printed at the end of step 5, such as 7.1.12-lg5k.

  8. Ask Hyprland for the native mode

    Request 5K as soon as the monitor connects. In my early tests, switching up to 5K from a fallback mode failed once the second stream had gone idle. Omarchy's monitor rules live in ~/.config/hypr/monitors.lua. Use your monitor's description from step 1:

    ~/.config/hypr/monitors.lua
    hl.monitor({ output = "desc:LG Electronics LG HDR 5K 0x00030BFB", mode = "[email protected]", position = "0x0", scale = 1.6 })

    Matching by description means the rule follows the monitor between ports, and never forces 5K onto a different screen.

  9. Reboot and check

    Choose linux-aurora-lg5k in the Limine menu. Then run these three checks:

    Did it work?
    $ uname -r
    7.1.12-lg5k                         ← the new kernel
    $ journalctl -k -b | grep -c 'balanced HBR2'
    2                                   ← both tunnels capped
    $ hyprctl monitors | grep -A1 '^Monitor'
    Monitor DP-2 (ID 2):
    	[email protected] at 0x0      ← native 5K

07If it goes wrong

What you seeLikely causeWhat to do
Machine won't boot the new kernelBuild or boot problemPick the stock linux-aurora entry in the Limine menu. It waits a few seconds before booting the default.
Only 3440×1440, no 5K modeThe UUID didn't matchCompare cat /proc/cmdline with boltctl list. invalid apple_tiled_uuid in the kernel log means a typo.
3840×2160, and only one balanced HBR2 lineThe second tunnel didn't come upUnplug and replug once. If it stays that way, reboot.
5K worked, then the screen stays dark after unplugging or wakingThe Thunderbolt controller is stuckReboot. In my tests, replugging didn't clear it.
Device trees

Step 7 points m1n1 at the new kernel's device trees, so the stock kernel will boot with them too. I haven't tested that pairing. If the stock kernel misbehaves, point /etc/default/update-m1n1 back at its own dtbs folder and run sudo update-m1n1 again.

08Everything else I had to fix

Getting 5K on screen once was the easy part. Keeping it there through lid closes, sleep, unplugs and link drops took most of the work, over seven kernel builds between 4 and 15 October 2026.

ProblemWhat fixed itStatus
5K only worked on one portRecognise the monitor by UUID, not by portWorks on my port; others untested
Closing the lid put it to sleepA 90 s lid holdoff, plus a service that blocks lid sleep while the LG is attachedIn daily use
A keyboard on the monitor couldn't wake itPatch 0005: USB wake from the Thunderbolt portBuilt; real sleep untested
The picture didn't come back after a dropPatches 0001–0003: recovery inside the driverRecovered twice; not fully tested
A stuck controller needed a rebootFound and removed my own deadlock; recovery never resets the whole routerFixed; a stuck controller still needs a reboot
PCIe behind Thunderbolt on the 14″ and 16″Patch 0006: the missing reset lines in the device treeBooted

5K only worked on the left port

My first working kernels picked the display by port number. Two boot options tied the tile pairing and the HBR2 cap to the left front port. On any other port, the monitor fell back. The fix was to recognise the monitor itself, by its Thunderbolt UUID, so the settings follow it:

Boot options, before and after
appledrm.tiled_tbt_port=1thunderbolt.apple_dp_hbr2_port=1thunderbolt.apple_tiled_uuid=<your-uuid>

While a port still mattered, the right-hand USB-C port was a useful fallback. It gave 3840×2160 straight away, without a reboot, when the left port had stopped working.

Closing the lid put the Mac to sleep

With the lid closed, the Mac should keep running on the monitor alone. Two things broke that:

  • The screen blanking looked like an unplug. When the idle lock blanked the LG, its connector reported "disconnected". With the lid closed and no display left, logind put the Mac to sleep. Sleep then broke Thunderbolt.
  • Closing the lid during boot did the same. Logind suspended 30 seconds after boot, while Thunderbolt was still starting up. Afterwards the controller stayed stuck until the next reboot.
4 October: how a blank screen became sleep
19:03     lid closed                  the session carries on, on the LG
19:08     idle lock
19:15:15  LG blanks → "disconnected"19:15:16  logind suspends            nobody chose Sleep
20:25     lid opened                  Thunderbolt USB failed to reset; typing dropped keys

Two small pieces of configuration fixed it. Neither is a kernel change:

/etc/systemd/logind.conf.d/30-thunderbolt-holdoff.conf
[Login]
HoldoffTimeoutSec=90s        # ignore the lid for 90 s after boot and resume (default 30 s)
The lid service, in outline
while the service runs:
  is the LG attached?
    # either identity counts:
    #   USB-PD partner 043e:9a50  (seen even while Thunderbolt is failing)
    #   authorised Thunderbolt router with the LG's UUID
  yes             → hold a logind block on handle-lid-switch
  gone for 5 s    → release the block
  can't read bus  → keep whatever state it's in

The service watches whether the monitor is plugged in, not whether it's lit. So a blanked screen no longer counts as a missing one. Choosing Sleep from the menu still works; only the lid is blocked.

Omarchy's own lid helper doesn't work on Apple Silicon. It reads /proc/acpi/button/lid/, which these Macs don't have. A small replacement reads logind's LidClosed state, and turns the built-in screen off when the lid closes with a monitor attached.

Waking from sleep with the lid closed

A sleeping, closed Mac should wake from a key press on a keyboard plugged into the monitor. It couldn't. The USB controller on the Thunderbolt port had no wake interrupt wired up. Patch 0005 wires it up. It also lets the interrupt controller skip a wake-setting step this kind of sleep doesn't need:

How the wake should travel
key press on the keyboard
└─ monitor's USB hub
   └─ Thunderbolt port's USB controller (xHCI)
      └─ wake interrupt, now wired up              patch 0005
         └─ interrupt controller (AIC)
            skips a wake-setting step it doesn't need  patch 0005
            └─ Mac wakes

This is built and installed, but not yet tested through a real sleep.

Reconnecting after a link drop

Now and then, the link to the monitor drops for a moment. The cause, whether cable, monitor, firmware or driver, is still unknown. What matters is getting the picture back by itself. I wanted that to happen inside the driver, with no recovery command and no background script polling the display.

Three things got in the way:

0001

An empty port took the display

A USB-C port with nothing plugged in reserved the first display pipeline. The LG landed on another one, and the compositor rejected all 19 of its modes. Now a port only reserves a pipeline once a display is really there.

0002

One try, then give up

When the firmware lost the mode, Thunderbolt displays got one attempt to retrain the link. Now they get five, like other displays, without resetting the whole Thunderbolt router.

0003

Link faults from the firmware

Firmware link faults are now handled in a background worker. Repeats are merged, and it gives up after three tries with no frame shown.

Recovery, in order
link drops, or firmware reports a fault
├─ is a display actually there?              0001 · wait for hot-plug detect
├─ retrain the link, up to 5 times           0002
├─ handle faults in a worker                 0003
│   ├─ merge repeated faults
│   ├─ stop after 3 tries with no frame
│   └─ cancel on unplug, sleep or shutdown
└─ leave it off if you turned the display off

On 12 October the link dropped three times. Twice, both halves came back and paired again within 2 seconds. The third time, the driver deadlocked, for a reason explained next.

When only a reboot works

Some failures leave the Thunderbolt controller stuck. Unplugging and replugging doesn't clear it. Neither does turning the monitor off and on. Only a reboot does. So the recovery above only retrains the link. It never resets the whole Thunderbolt router.

The worst stuck state was my own bug. One of my patches stopped taking a reference to the Thunderbolt controller, but kept releasing it. Each time a display tunnel came up, the count fell by one. On 12 October it reached zero:

12 October: how the count running out froze the display
display worker takes the Thunderbolt lock
└─ tunnel comes up, releases a reference   count reaches 0
   └─ controller is freed
      └─ its work queue is destroyed
         └─ waits for every job on the queue to finish
            … including this one, which is waiting  → forever
the lock is never released
└─ every later display or Thunderbolt step waits for it, including recovery

The fix was to stop changing that code. Upstream Linux already takes and releases the reference in matching pairs; the current patches leave that alone. The lesson: recovery that needs the same lock as the thing that's stuck can't help.

09The patches

I run six patches on top of Aurora. Only 0004 is needed for the picture. The rest make the display recover by itself, and fix wake and PCIe on 14″ and 16″ M2 Pro and Max MacBooks.

patches/
patches/
├── 0001-…wait-for-sink-HPD          # an empty USB-C port can't take a display pipeline
├── 0002-…bounded-link-retraining    # five reconnect attempts, not one
├── 0003-…link-fault-recovery        # recover from firmware link faults in the background
├── 0004-…LG-UUID-pairing-and-HBR2   # the display fix: everything on this page
├── 0005-…USB-remote-wake            # wake from a USB keyboard on the monitor's hub
└── 0006-…J414-J416-PCIe-C-resets    # device-tree resets for 14″/16″ M2 Pro and Max

To apply all six, run git am on the whole folder, in order. They apply cleanly to the commit in step 4. A check against the next Aurora release found that 0001 and 0003 need adapting there; 0004 still applied.

10What isn't proven yet

This runs every day on my machine, on the port I use. Some things haven't been tested on hardware:

  • Native 5K on all three Thunderbolt ports. The UUID match should make the port irrelevant.
  • Sleep and wake with the monitor attached and the lid closed.
  • Automatic recovery after the link drops. Patches 0002 and 0003 aim at this.
  • Any other monitor, or any other Mac.

The proper fix would need no boot option. The kernel would read the tile layout and bandwidth needs from the monitor and cap the link speed itself. Until Aurora does that, the UUID is the switch.

The patches were written with an AI coding agent, and each carries an Assisted-by line. They aren't signed off or submitted upstream. Read them before you run them.