實作

手放在鍵盤上的時候開這頁。越前面越常用 —— §1 抄一行就走,§4 之後才是要坐下來讀的。名詞和連結在附錄。

依「你現在在幹嘛」排:§1 抄一行就走、§2 照著打、 §3 卡住時掃、§4–§6 想讀懂機制時才讀、§7–§8 查名詞與出處。

§1速查

三十秒內要拿到的東西。細節往下。

DP 只在左前那一孔

sysfsi2c實體位置DP
port00-0038USB-C Left Back(靠螢幕轉軸)❌
port10-003fUSB-C Left Front(靠觸控板)✅
port20-003bUSB-C Right❌
port30-003a(內部,非實體孔)❌

對照表可以自己讀出來,不用記:

for c in /proc/device-tree/soc/*/*/connector; do
  echo "$(basename $(dirname $c)): $(tr -d '\0' < $c/label)"
done

插對之後 ls /sys/class/typec/port*-partner 會出現 port1-partner。 插錯孔完全沒有反應,也不會有任何錯誤訊息。

走的是 atcphy1。atcphy3 在 dts 裡註明 ATC3 is used for DisplayPort -> HDMI only,那是內建 HDMI 孔的專用路徑。

現在跑的是哪顆,以及怎麼確認

uname -r                     # 要是 7.1.13-fairydust+
strings /lib/modules/$(uname -r)/kernel/drivers/phy/apple/phy-apple-atc.ko \
  | grep 'park PIPE handler'  # 有這行 = #622 真的編進去了
「明明沒動什麼,某天就壞了」 —— 先查這個。 官方 kernel 一更新就會把 /boot/dtb 搶回去(原因見 §2.4):
readlink /boot/dtb          # 指到 dtb-…asahi… 就是被搶走了
sudo ln -sfn /boot/dtbs/<fairydust 版本> /boot/dtb
sudo update-m1n1

§2照著做

怎麼 build、怎麼裝、怎麼退回、更新之後要檢查什麼。全部是照著做就好的東西。

2.1 build 一顆 fairydust kernel

這顆 kernel 要滿足的五件事(為什麼要做這一步見 方向 §3):

  1. clone fairydust 分支
  2. .config 一定要從現在的 kernel-16k config 起手。 Apple Silicon 上 16K pages 是必需的,這是最容易踩的坑
  3. 開啟 DP alt mode 相關選項,建議編成 module(方便用 dmesg 確認有沒有載到)
  4. build kernel 加 DTB —— Asahi 靠 device tree,別只裝 vmlinuz
  5. 確認 GRUB 選單會顯示(Asahi 預設會跳過),否則你沒地方選回舊 kernel
可能不用自己來 —— 而且它的支援表上就有你的機型。 bharambetejas/asahi-fairydust-display 是針對 Fedora Asahi Remix 的一鍵腳本, 支援表把 MacBook Pro M1 Pro 標成「Tested by community」。 它會處理相依套件、Rust、GPU driver、DP alt mode 模組、DTB、m1n1、GRUB、typec 模組自動載入, 還附一個 hotplug 腳本。需要 15GB 空間、60~90 分鐘。

它的 README 也獨立寫了同一件事:「Plug adapter into the FRONT-MOST USB-C port」 —— 跟 dts 裡的 USB-C Left Front 對得上。

它預設會多打一個 patch(修內建 HDMI 在 suspend/resume 後不亮), 想要原汁原味的分支就 SKIP_PATCHES=1。
讀過腳本之後的三件事。
  • 材料只有 .config 來自本機。原始碼是 git clone AsahiLinux/linux --branch fairydust --filter=blob:none(約 3GB); .config 則是 cp /boot/config-$(uname -r) —— 所以 16K page 那個坑它自動幫你避開了
  • README 的第 9 步不會跑。main() 裡 setup_display_script 是註解掉的,註解寫「causes login loop on KDE/Wayland」。 對 Hyprland 使用者是好消息 —— 那段會寫一個用 xrandr、 還硬寫死 2560x1600 的腳本(14" 是 3024x1964), 外加 XDG autostart 和一條 RUN+= 跑 sleep 2 的 udev rule。全部不會發生
  • 它改 GRUB 的那行對你是空轉。它用 sed 's/GRUB_TIMEOUT_STYLE=.*/…/',只改既有的行; /etc/default/grub 裡沒有這一行的話什麼都不會發生,要自己加

2.2 只改模組時的 40 秒迴圈

改動只在 *.ko(例如只動了 iomfb.c 或 atc.c)的話, 不需要重裝 kernel 本體,也就不會觸發那個把 /boot/dtb 指歪的 hook:

cd ~/linux-fairydust
make -j8                                    # 增量,約 40 秒
strings drivers/phy/apple/phy-apple-atc.ko | grep 'park PIPE handler'   # 驗證
sudo make INSTALL_MOD_STRIP=1 modules_install
sudo dracut -f --kver 7.1.13-fairydust+
readlink -f /boot/dtb                       # 要是 /boot/dtbs/7.1.13-fairydust+
sudo reboot
上面那行 strings 不要跳過。 2026-09-24 那次中途跑過 git apply -R,之後 build 出來的模組其實沒有 patch, 測了一輪結果當然跟沒改一樣。 光看 git diff 或檔案時間戳不夠 —— 要確認編出來的東西裡面有。

2.3 退回與移除

  • 暫時退回:重開機,在 GRUB 選 7.1.13-402.asahi.fc44.aarch64+16k。 官方 kernel 完全沒被動過
  • 退掉某個 patch: cd ~/linux-fairydust && git apply -R ~/dev/asahi-fairydust-display/patches/<檔名>, 然後照上面那組重編重裝。退完記得也把 patches/ 裡那個檔移走, 否則下次跑 build 腳本又會自動套回去
  • 完全移除:~/dev/asahi-fairydust-display/asahi-fairydust-uninstall.sh。 它會把 kernel、模組、DTB、/boot/dtb、m1n1 全部還原, 但要先重開機回官方 kernel 才跑得動
它動到的全域設定只有一個,而且退回 stable 也吃得到。 就是上面那個 /boot/dtb + update-m1n1 —— m1n1 之後每次開機都帶著 fairydust 的 device tree,包括你在 GRUB 選回官方 kernel 那次。 其餘(模組、DTB、vmlinuz、initramfs)都帶 -fairydust 後綴並存,不覆蓋任何東西。

repo 裡的 asahi-fairydust-uninstall.sh 會把這些全部還原, 包含把 /boot/dtb 指回官方版本再跑一次 update-m1n1。 它會擋你在 fairydust kernel 裡執行,要先重開機回官方 kernel。

2.4 官方 kernel 更新之後要檢查

每次官方 kernel 更新,都會把 /boot/dtb 搶回去 —— 這是最容易中招的一點。

Fedora 的 /usr/lib/kernel/install.d/10-devicetree.install 在每次 kernel add 時會無條件做這件事:
rm -f /boot/dtb
ln -s dtb-$KERNEL_VERSION /boot/dtb
它掃的是 /boot/dtb-*(連字號), 而 make dtbs_install 把 fairydust 的 DTB 裝到 /boot/dtbs/<版本>(複數、斜線)—— 兩者對不上,所以 fairydust 的 DTB 對那支 hook 是隱形的。 緊接著 15-update-m1n1.install 會跑 update-m1n1 把官方 DTB 刷進去。

結果:omarchy update 帶進一顆新官方 kernel 之後, 你的 fairydust kernel 會帶著官方 DTB 開機 —— ENABLE_DCPEXT_TYPEC 那些節點不見了,外接螢幕安靜地不再亮。 症狀是「明明沒動什麼,某天就壞了」。

檢查與修復的三行指令在 §1 速查, 值得做成 omarchy 的 post-update hook,跟你其他那幾個放在一起。 (這台跑的是 omadora —— omarchy 的 Fedora Asahi 版,指令名稱還是 omarchy。)
另外兩件關於 omadora 的事。
  • installonly_limit 管不到它。 那個限制只作用在 RPM 標記為 installonly 的套件;腳本走的是 make install, 裝出來的不是 RPM,dnf 不會把它擠掉。 (會被擠掉的是最舊那顆官方 kernel。)
  • omarchy update 不會幫你重 build fairydust,上游有新 commit 要自己重跑

2.5 m1n1 雙機 trace:怎麼架起來

這一節是遠期的(方向 §3 的階段 3)。現在不會用到,但它是操作,所以住在這裡。

這不是虛擬機。macOS 跑在 target 真正的 Apple Silicon CPU 上,原生速度、真的硬體; m1n1 待在 EL2,只把幾頁 MMIO 設成 trap,攔下來記錄再放行。macOS 本體不用改。 完整的角色分配見 圖 3。

在 target(Mac mini)的 1TR Recovery Terminal 裡(對測試 volume 做)

bputil -nkcas          # 降安全模型
csrutil disable        # 關 SIP
nvram boot-args=-v
kmutil configure-boot -c build/m1n1.bin --raw --entry-point 2048 \
  --lowest-virtual-address 0 -v /Volumes/macOSTest

取得 macOS kernel

在 target 上開進測試 volume,從這裡挖 kernelcache,用 img4tool 解出 Mach-O:

/System/Volumes/Preboot/[UUID]/boot/[hash]/System/Library/Caches/com.apple.kernelcaches/

在 host(MacBook Pro)上跑

這兩支都是 proxyclient 工具,跑在 host 上,隔著 USB 線指揮 target。

export M1N1DEVICE=/dev/ttyACM0   # host 是 Linux;host 若是 macOS 則為 /dev/cu.usbmodemP_01

# 每次都先 chainload working tree 的 m1n1
python3 proxyclient/tools/chainload.py build/m1n1.macho

# 掛著 DCP tracer 開 macOS
python3 proxyclient/tools/run_guest.py \
  -m proxyclient/hv/trace_dcp.py \
  path/to/kernelcache.macho \
  -- "debug=0x14e serial=3 apcie=0xfffffffe -enable-kprintf-spam wdt=-1 clpc=0"

兩個終端機視窗

裝置用途
ttyACM0
host 若是 macOS:cu.usbmodemP_01
proxy —— run_guest.py 走這條,trace 輸出印在這裡
ttyACM1
host 若是 macOS:cu.usbmodemP_03
guest 虛擬 UART —— macOS 自己的 console
關於「接線孔會不會跟螢幕打架」。 Mac mini M1 有 2 個 Thunderbolt 孔 —— 一個接 host 的 USB 線、一個給 Studio Display,剛好夠。 而且 m1n1 文件寫得很清楚:proxy mode 的 USB 除錯介面 「available on all Thunderbolt ports」 —— 也就是每個孔都能用, 不用挑。

(進 DFU 模式是另一回事:Apple 對每個機型指定了特定的孔,那要用時再查。 角色若對調成 MBP 當 target,就要記得 14" MBP 的 DP 只在左前那一孔, proxy 的線別插在那裡。)

§3卡住了怎麼查

每個工具只看得到一層。搞混層次就會把時間花在錯的地方 —— 這一節就是為了避免那件事。

各個工具在看哪一層

工具它看得到什麼看不到什麼
/sys/class/typec/port*-partner 有沒有東西插進來、插在哪一個孔那東西是不是螢幕
/sys/class/drm/*/status
.../modes
kernel 認不認得這個顯示接頭、有沒有拿到 EDID畫面有沒有真的出去
journalctl -k driver 和 DCP firmware 在講什麼(最底層的真話)userspace 做了什麼
udevadm monitor --udev --subsystem-match=drm kernel 有沒有通知 userspaceuserspace 有沒有理它
hyprland.log
$XDG_RUNTIME_DIR/hypr/*/
合成器收到之後怎麼想、做了什麼、失敗在哪硬體狀態
wlr-randr / hyprctl monitors 合成器目前認為的輸出狀態那個認知對不對
hyprland.log 是最容易被忘記、但最常給出答案的那一個。 2026-09-24 那次熱插拔,前面四個工具都顯示「一切正常」, 只有它講出真話:Cannot commit when a page-flip is awaiting。

一張決策表

你的 MBP 跑 Hyprland,這會製造一種很容易誤判的情況:kernel 其實已經把螢幕接起來了,但畫面就是沒出來。 這不是假設 —— PR #622 的作者就留了這個已知問題: 「重新接上之後,userspace 沒有對 BAD link status 做出反應,要手動 toggle output」。那是合成器層,不是 kernel。

每次測試都照這個順序查,對應圖 1 的層次:

# ① kernel 有沒有看到這個接頭?
ls /sys/class/drm/
cat /sys/class/drm/*/status          # connected / disconnected

# ② 有沒有拿到 mode?
cat /sys/class/drm/card*/*/modes
wc -l < /sys/class/drm/card2-DP-1/modes

# ③ 合成器有沒有接手?
hyprctl monitors all
wlr-randr

# ④ 底層在講什麼
journalctl -k -b | grep -iE 'dcp|atcphy|tipd|typec|altmode|oob_hotplug|flip_done|valid_mode'

# ⑤ 合成器怎麼想的 —— 最容易被忘記,但 2026-09-24 那次是它給出答案
tail -f $XDG_RUNTIME_DIR/hypr/*/hyprland.log

# 拔插的當下開著這個看:kernel 到底有沒有通知 userspace
udevadm monitor --udev --subsystem-match=drm

# 是哪一台螢幕、插在哪個孔
ls -d /sys/class/typec/port*-partner
① connector② modes③ Hyprland結論
disconnected——卡在斷點 1(atcphy,訊號沒出去)—— 先確認你用的是 fairydust、插的是左前那一孔、而且是冷插
connected空的—卡在斷點 2(DCP 沒給 mode)
connected有 mode沒看到Hyprland 沒接手,kernel 那邊其實通了
connected有 mode看得到、還說 enabled 但畫面還是黑的 —— 這一種 2026-09-24 實際撞到了,見附錄 §4 日誌。 去看 hyprland.log 找 Cannot commit when a page-flip is awaiting

第三種是好消息 —— 代表你已經穿過兩個斷點了,剩下的是 userspace 設定。 第四種最難認,因為每一層都回報「我沒問題」。

要手動 toggle output 時:hyprctl dispatch dpms off DP-1 在 Lua 設定下會被剖析器拒絕。 繞過去的辦法是直接用 wlr-randr(走 Wayland 協定,不經過 Hyprland 的設定層):
wlr-randr --output DP-1 --off && sleep 1 && wlr-randr --output DP-1 --on
(這次它成功逼出了 modeset,但因為上面那個 page flip 卡死,畫面還是沒回來。)