流程

事件發生的時候,架構那幾個角色實際跑了什麼。 每一個箭頭都是架構 §4 那五組框架介面的其中一組 —— 先看完那一頁,這裡才讀得動。

§1開機:這條路怎麼建立起來

這一節是階段 0 的前置知識。你要多裝一顆 kernel、要能在兩顆之間切換、 之後還要讓 m1n1 換個模式跑 —— 這些都建立在「開機時到底發生什麼」之上。

上排是平常開機的五棒,下排是 m1n1 的另一種用法 —— 分歧點就在第二棒。

五棒接力

角色做什麼
iBootApple 的韌體。驗證簽章後交棒,你完全碰不到
m1n1初始化硬體、產生 device tree。Asahi 安裝時就放進 stub 分割區了
U-Boot把 Apple 的特殊開機環境轉成標準的 Linux 開機介面
GRUB讀開機項目清單、顯示選單、把你選的那顆 kernel 載進記憶體
Linux kernel掛載 rootfs、載入模組,最後交給 systemd → Hyprland

kernel 是一組帶版本後綴的檔案

所以多顆可以並存 —— 裝新的不會蓋掉舊的:

/boot/vmlinuz-<版本>              # kernel 本體
/boot/initramfs-<版本>.img        # 開機用的暫時根目錄
/boot/dtb-<版本>/                 # device tree(Asahi 必要,別忘了裝)
/lib/modules/<版本>/              # 驅動模組

那份「有哪些 kernel 可以選」的清單

Fedora 用 BLS(Boot Loader Specification):一顆 kernel 一個設定檔。

ls /boot/loader/entries/     # 每個 .conf 就是選單上的一項
sudo grubby --info=ALL       # 人話版:列出每一項的 kernel / initrd / 參數

裝完 fairydust 之後這裡會多一項 —— 那是「有沒有裝成功」最直接的驗證,比重開機試快得多。

動手 build 之前,先讓 GRUB 選單顯示出來。 Asahi 預設會跳過選單(為了開機快)。編輯 /etc/default/grub, 把 GRUB_TIMEOUT 設成 5,同時確認 GRUB_TIMEOUT_STYLE=menu —— 只改 timeout 而 style 還是 hidden 的話選單照樣不會出現。 然後 sudo grub2-mkconfig -o /boot/grub2/grub.cfg。

不先做這件事,新 kernel 開不起來時你會找不到地方選回舊的。

m1n1 不是要新裝的東西

它現在就在你的開機鏈裡,每次開機都在跑 —— 沒有它 Asahi 根本開不起來。 之後要做的不是安裝新程式,而是讓同一支程式換個模式:

模式做什麼
平常(你現在)初始化硬體 → 交棒給 U-Boot → 一路到 Linux
hypervisor(之後)進 EL2、攔截 MMIO、把 macOS 當 guest 開起來

實務上會有兩份 m1n1 並存:stub 裡那份繼續負責開你的 Asahi; hypervisor 用的是你自己 build 的新版(官方文件強調「每次都先 chainload working tree 的 m1n1」)。 兩份互不干擾,正常開機流程不受影響。

§2插線與拔線:從中斷到 mux

這一節只講左前 USB-C 孔(0-003f)的 Type-C/DP 狀態路徑。 重點不是「插線就切 DP」,而是:硬體先改 register、用 IRQ 提醒 kernel;kernel 讀回狀態後,才決定要不要設定 ATC PHY。

狀態到底在何時變?

時間哪一份狀態改變誰改的
螢幕狀態改變時HPD 電位;CD321x 內部的 register螢幕與 CD321x 硬體
IRQ handler 讀完後tps->status、tps->data_status 等 driver cachecd321x_interrupt()
500 ms 後 work 執行時Linux Type-C framework:partner、角色、方向;cd321x->statecd321x_update_work()
需要切換時ATC PHY 的硬體 register,實際高速 lane 連法typec_mux_set() → atcphy_mux_set()

用 log 對照圖

# 這兩句代表圖中的③ DP branch 已經走到 ATC PHY,且回傳成功
tps6598x 0-003f: DP mux request: mode 4, result 0
phy-apple-atc b03000000.phy: Type-C mux configure mode 6: 0

# 這句代表③仍判斷為 DP,但 cd321x->state 已經是 DP,故不重設 mux
tps6598x 0-003f: DP mux already in mode 4

§3送一張畫面:page flip 與冷插/熱插的差別

先分清楚這顆 kernel 上疊了哪些東西

三層,容易搞混,先分清楚 —— 只有第三層是這次為了熱插拔加的:

是什麼誰做的跟外接螢幕的關係
底層 fairydust 分支本身 Asahi 上游的實驗分支 USB-C 出圖的程式碼全部在這裡。正式 kernel 沒有,所以非用不可
patch 0001 0001-drm-apple-reconnect-DP2HDMI-output-on-resume.patch build 腳本作者自己加的(原作者 rgvxsthi) 無關。它修的是內建 HDMI 孔睡醒之後不亮,走的是另一條路徑。 跟著腳本一起來的附贈品,SKIP_PATCHES=1 可以不要
patch 0002 0002-pr622-fix-dp-altmode-hotplug.patch(= PR #622) 這次自己加的 這才是這一節在講的東西,修 USB-C DP 的熱插拔

換句話說「fairydust 是分支,不是 patch」;而 0001 雖然跟著 fairydust 的 build 腳本一起來, 但它不是 fairydust 的一部分,也跟 USB-C 沒關係。下面所有的 before/after 都只在講 0002。

它只改兩個檔、42 行,但是兩個完全不同的問題。

第一塊 · iomfb.c —— 為什麼會卡住幾十秒

先講「page flip」是什麼:合成器每畫好一張畫面,就交給 kernel 說「換成這張」, 然後停下來等 kernel 回報「換好了」才畫下一張。這是正常運作方式。

問題在於,螢幕剛接上、DCP 還沒真正把顯示管線開起來的那段時間 (driver 裡那個旗標叫 valid_mode,這時候是 0), DCP 會收下那張畫面然後默默丟掉,不回報。firmware 自己的 log 寫得很直白:

swap_submit_dcp: swallowed swap ID 452 as fControllerPowerState is 0
swap_submit_dcp: swallowed swap ID 452 as timings are not enabled

合成器不知道畫面被丟了,就一直等,直到內建的 10 秒逾時。 它想畫幾張就卡幾個 10 秒 —— 這就是「整台電腦卡住幾十秒」的來源。

patch 做的事:送出去之前先看一眼 valid_mode。 還是 0 就別送了,直接自己標記「這一幀完成」讓合成器繼續跑下去。三行:

if (!dcp->valid_mode) {
        schedule_work(&dcp->vblank_wq);
        return;
}

注意這不是「修好顯示」,是「不要因為顯示還沒好就把整台機器拖住」。 差別很重要 —— 它解釋了為什麼套完之後不卡了,但畫面依然沒出來。

第二塊 · atc.c —— 為什麼熱插不會亮、冷插卻會

USB-C 那條線裡有幾組高速通道,同一組實體線路可以拿來跑 USB3,也可以拿來跑 DisplayPort, 兩者互斥。要從 USB3 切成 DP,得先把負責 USB3 的那個電路(叫 PIPE handler)停下來, 否則就是在一條還在通電的線路底下重新接線 —— 硬體會保護性地退回只剩 USB2,DP 根本起不來。

這就是冷插會動、熱插不會動的原因。 開機時就插著的話,那時 USB3 還沒起來,沒有東西要停; 熱插的時候 USB3 已經在跑了,才會撞到。

原本的程式碼發現「PIPE handler 還開著」時,只是印一個警告然後硬切 —— 那正是你在 dmesg 看到的:

WARNING: drivers/phy/apple/atc.c:2328 at atcphy_mux_set+0x138/0x230 [phy_apple_atc]

patch 做的事:把那個「只抱怨不處理」換成真的去處理 —— 切換前先把 PIPE handler 停到一個空的 dummy PHY 上,讓線路安靜下來,再切模式。 USB3 會暫時斷掉(等 dwc3 重新設定回來),USB2 不受影響。

驗證方式:strings phy-apple-atc.ko | grep 'park PIPE handler'。 有這個字串才代表模組是打過 patch 的 —— 這行已經在 實作 §2.2 的迴圈裡。

§4目前卡住的那一步

套完 #622 之後,熱插拔的失敗鏈條變成這樣:

  1. 拔線那一刻,DCP 吞掉一張 page flip,永遠不回報完成
  2. aquamarine(Hyprland 的 DRM 後端)那張 flip 就一直掛在「等待中」
  3. 之後每一次 commit 都被它自己的保護擋掉 —— hyprland.log 裡 Cannot commit when a page-flip is awaiting 出現了 14 次
  4. 連手動用 wlr-randr 強制 modeset 都救不回來: kernel 那邊確實跑完了(set_digital_out_mode finished:8162), 但畫面 commit 進不去

關鍵是 Hyprland 並不是「沒反應」。它的 log 顯示它有偵測到、有列出 16 個模式、也有嘗試 modeset:

drm: Connector DP-1 connected
drm: Modesetting DP-1 with 1920x1080@60.00Hz
ERR: drm: Cannot commit when a page-flip is awaiting
層可以說是它的問題的地方
kernel / driver DCP 收下 page flip 卻從不送完成事件。#622 讓「等待」不再卡 10 秒, 但遺失的 flip 在合成器那邊的帳沒有被清掉
Hyprland / aquamarine 一旦有 flip 卡住,就再也不接受任何 commit,沒有逾時、沒有復原路徑。 硬體丟事件是常態,合成器不該因此永久鎖死一個輸出

兩邊都有道理,所以先別急著替誰定罪 —— 這正是值得寫進 issue 讓真正懂的人判斷的東西。