因為這段時間一直在使用 Flutter 多視窗的原因,有一個 Windows 多視窗的 bug 一直挺讓人難受的,就是在切換到專案子視窗的時候,會預設啟用主視窗,之後才能再次點擊回到子視窗:
更準確地說,就是啟用一個 child
RegularWindow後,主視窗會重新回到前景並蓋住它。
這個問題的誘因很多,但實際上主要就是程式碼寫得太草率。
原因之一是在 Engine 裡,Engine 收到 Windows 的 WM_ACTIVATE 訊息時,不管視窗是啟用還是失去啟用,都會執行:
kotlin 代碼解讀複製代碼FocusRootViewOf(this);
也就是讓目前視窗裡的 Flutter view 取得焦點,但問題在於,當主視窗正在失去焦點時,它也會執行這句,然後就導致主視窗本來應該退到後面,卻又主動 SetFocus 自己,接著 Windows 又把主視窗重新啟用並拉到最前面。
這個 Bug 就先不說了,主要是 Flutter framework 還把多個視窗的 focus tree 掛錯層級。Flutter 的多視窗裡,每個視窗都有自己的 View,但 Framework 裡子視窗的 FocusScope 居然還會有掛在父視窗的 FocusScope 下面的情況,這樣會造成一個新問題:
子視窗獲得焦點時,主視窗的 focus scope 也被認為「有焦點」,然後主視窗又又又通知 engine:「我有焦點了,把我的 native window 啟用」,然後又又又又會把主視窗拉到前面……
而且整個過程,也沒有判斷是否已經有另一個視窗拿到焦點,這就導致這個 Bug 是由好幾個地方綜合造成,之前很久一直沒能根本修復問題。
一直只能寫兜底程式碼來強制切換。
不過針對這三個問題,最近終於修復了:
WA_INACTIVE,也就是視窗正在失去啟用,就直接返回,不再搶焦點View 的 focus subtree 直接掛到全域 root scope 底下然後在我合併完這部分程式碼之後,馬上又踩到另一個坑:Flutter Windows 開始啟用 Impeller 了,是的,Flutter 開始在 Windows 上用 OpenGLESSDF 來適配 Impeller 了:

然後問題又出現了,在繪製 canvas 的時候,出現了細線看起來比 Skia 粗約 1.3–1.6 個物理像素,而且更暗的情況:

如果疊加 alpha,還會直接失效。這些以前在我專案裡若隱若現的細線,在 Impeller 出來後直接就變成又大又粗:

根據 issue 的計算,目前不同粗細在 Impeller 上的變化倍率也不一樣:
| 筆畫寬度(logical→physical) | Skia 覆蓋度 | Impeller 覆蓋度倍數 |
|---|---|---|
| 0.5 → 0.75 px | 0.75 | 2.0× |
| 1.0 → 1.5 px | 1.50 | 2.78× |
| 2.0 → 3.0 px | 3.00 | 4.59× |
| 4.0 → 6.0 px | 6.00 | 7.50× |
而且粗細還不是主要問題,主要問題是:
0xCC 和 0x66 渲染結果像素完全一致這個問題看起來像是 Impeller 的 SDFAlpha 實作有問題,smoothstep 範圍是固定的物理像素寬度,比如 smoothstep(-fade_size, fade_size, sdf) 會在筆畫邊緣兩側分別產生約半個 pixel_size * aa_pixels 的漸變過渡帶,而這個過渡帶是固定寬度(取決於 aa_pixels 參數,程式碼中設定為常數)。
然後對於 Skia 這樣的解析式 AA,覆蓋度 = 筆畫寬度;但是對於 SDF 的
smoothstep,覆蓋度 = 筆畫寬度 + 固定邊緣過渡帶。所以當筆畫只有 0.75 物理像素寬時,這個固定的 ~1.2px 過渡帶就變得很明顯,導致渲染出來的線是「實心寬線 + 兩側暈染」。
另外 float half_stroke = max(stroke_width, base_pixel_size) * 0.5 的最小寬度鉗制沒有補償 alpha,當 stroke_width < 1 像素時,這裡會把寬度強制鉗制到至少 1 個像素(base_pixel_size),但沒有按比例降低 alpha 來補償。
而 Skia 的做法是次像素線寬:把線變成 1 像素寬,但按比例降低 alpha(0.75px 線 ≈ 38% 不透明的 1px 線)。Impeller 的 SDF 實作直接給了滿 alpha 的 1px+ 寬線,所以看起來更暗更粗。
也就是 SDFStroke 根本沒有把 alpha 納入計算,alpha 值到 fragment shader 時已經在別處處理,沒有和寬度鉗制聯動。
目前暫時緩解的辦法有:
drawRect 代替 drawLine:用高度等於預期線寬的 filled rect 代替 stroked line,fill 路徑的 UberSDF 覆蓋度是正確的--no-enable-impeller目前看來,Impeller 在 Windows 上估計還是要經歷一次 Android 的問題,不得不說,自繪製的維護成本確實高。不過多視窗在 Windows 上確實也可以用了,自從 bug 解決之後,基本上沒什麼大問題了。
