The short version
- 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.
- Both streams share one Thunderbolt cable. If the first stream takes the fastest link speed, the second can't get enough bandwidth.
- 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.
- You build the patched kernel, add two boot options, and ask Hyprland for the native mode.
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:
$ 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:
[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.
boltctl list shows the monitor and the cable speed. The uuid line matters later: it's how the kernel recognises this monitor.
$ 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:
| Speed | Usable video bandwidth | Enough for one tile? |
|---|---|---|
| HBR | 8.64 Gb/s | No. Less than 2% headroom. |
| HBR2 | 17.28 Gb/s | Yes, with twice the room it needs |
| HBR3 | 25.92 Gb/s | Yes, 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:
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
After balanced HBR2
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:
Recognise the monitor
A boot option names the monitor's Thunderbolt UUID. The match works on any port.
Cap both tunnels at HBR2
Before either tunnel reserves bandwidth, so the second always gets its share.
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:
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:
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:
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:
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 -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 setup | Another monitor: LG UltraFine 5K 27″ | |
|---|---|---|
| Mac | MacBook Pro 16″ M2 Max, 2023 (Mac14,6) | Check that Aurora supports Thunderbolt displays on your chip |
| Kernel | Omarchy's Aurora kernel | Not tried on Asahi's own kernel. The Thunderbolt display work is in Aurora. |
| Connection | Thunderbolt 3, two tiles | Thunderbolt 3, two tiles |
| Tile size | 2560×2160 | 2560×2880 |
| Bandwidth per tile | ≈ 8.48 Gb/s | over 10.6 Gb/s |
| Needs HBR2? | Yes | Yes. HBR's 8.64 Gb/s is too little. |
| Status | In daily use | Untested |
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.
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
- 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.
-
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
-
Find the monitor's Thunderbolt UUID
Copy the
uuidline fromboltctl 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
-
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
-
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.18release, 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.
-
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
-
Package and install it
A package keeps the kernel, its modules and its device trees together. Save this as
PKGBUILDin thelinux-lg5kfolder. It's a cut-down version of mine.PKGBUILDpkgname=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.*
-
Add the two boot options
Give the new kernel its own command line in
/etc/default/limine. Copy your existingKERNEL_CMDLINE[default]line, rename it after the package, and add two options to the end:/etc/default/limineKERNEL_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>"
Option What 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 as7.1.12-lg5k. -
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.luahl.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.
-
Reboot and check
Choose
linux-aurora-lg5kin 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 see | Likely cause | What to do |
|---|---|---|
| Machine won't boot the new kernel | Build or boot problem | Pick the stock linux-aurora entry in the Limine menu. It waits a few seconds before booting the default. |
| Only 3440×1440, no 5K mode | The UUID didn't match | Compare cat /proc/cmdline with boltctl list. invalid apple_tiled_uuid in the kernel log means a typo. |
3840×2160, and only one balanced HBR2 line | The second tunnel didn't come up | Unplug and replug once. If it stays that way, reboot. |
| 5K worked, then the screen stays dark after unplugging or waking | The Thunderbolt controller is stuck | Reboot. In my tests, replugging didn't clear it. |
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.
| Problem | What fixed it | Status |
|---|---|---|
| 5K only worked on one port | Recognise the monitor by UUID, not by port | Works on my port; others untested |
| Closing the lid put it to sleep | A 90 s lid holdoff, plus a service that blocks lid sleep while the LG is attached | In daily use |
| A keyboard on the monitor couldn't wake it | Patch 0005: USB wake from the Thunderbolt port | Built; real sleep untested |
| The picture didn't come back after a drop | Patches 0001–0003: recovery inside the driver | Recovered twice; not fully tested |
| A stuck controller needed a reboot | Found and removed my own deadlock; recovery never resets the whole router | Fixed; a stuck controller still needs a reboot |
| PCIe behind Thunderbolt on the 14″ and 16″ | Patch 0006: the missing reset lines in the device tree | Booted |
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:
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.
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:
[Login] HoldoffTimeoutSec=90s # ignore the lid for 90 s after boot and resume (default 30 s)
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:
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:
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.
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.
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.
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:
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/ ├── 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.