為粉筆工具進行調優的工程實踐
Lulucat Notes 的粉筆工具在密集的手寫區域變慢了。瓶頸不是 3,571 個輸入取樣點,而是每幀 70 次全螢幕 scratch pass。一個遭到否決的低解析度快取,以及按筆畫限制的 scissor rectangle,說明了後續的故事。

最終裝置版本中的紅藍粉筆,255% 縮放下的 155 道筆畫。
Lulucat Notes 的粉筆工具遇到了一種很具體的效能問題:在空白區域書寫時感覺流暢,但筆尖進入已經填滿粉筆筆畫的區域後,就會落後於筆的移動。繼續在同一個區域書寫,還會逐漸拖慢畫布平移。
一頁普通的手寫內容就足以觸發這個問題:300% 縮放,局部區域內有 70 道可見粉筆筆畫,共計 3,571 個輸入取樣點。空白區域仍然流暢;只有這些筆畫集中的區域變慢了。
修正後,同一頁可以在 255% 縮放下繼續接收新的書寫,而既有筆畫在下筆和進行平移時都保持完整清晰度。

最終裝置截圖,共 155 道筆畫。在這個縮放層級下,不論下筆還是平移,既有筆畫都不會暫時切換清晰度。
為什麼粉筆需要 scratch texture
普通的筆可以用 source-over 混合,把每個圓形印章直接合成到墨跡紋理上。粉筆多了一層顆粒閘控:渲染器先為整道筆畫累積筆身覆蓋率和深度,再用固定的顆粒紋理決定哪些位置接收粉塵,最後把結果合成到既有墨跡上。
這張 scratch texture 會隔離單獨的一道粉筆筆畫。隔離很重要,因為同一道筆畫中的印章會大量重疊;如果每個印章都單獨經過顆粒閘控,筆畫中心線就會累積重複顏色,粉筆孔隙也會隨取樣密度改變。
在高倍率縮放下,Lulucat Notes 會重繪目前視埠中可見的向量筆畫。舊實作會對每一道可見粉筆筆畫執行以下步驟:
- 結束主要渲染編碼器;
- 清除 scratch texture;
- 把這一道粉筆筆畫繪製到 scratch texture 中;
- 重新開啟主要渲染編碼器;
- 用一個全螢幕三角形把 scratch 合成回 drawable。
單一道筆畫的語意是正確的,但工作範圍遠遠過大。iPad 的 drawable 是 2732×2048,約 5.6 million pixels。每道粉筆筆畫都會觸發一次 scratch pass 和一次全螢幕合成。70 道粉筆筆畫大約意味著 141 個渲染編碼器和 70 次全螢幕合成。
令可見粉筆筆畫的數量為
每道粉筆筆畫還帶有固定的渲染通道開銷,因此這項成本也隨
測量使用的是一台執行 iPadOS 18.6.2 的 12.9-inch iPad Pro(5th generation,M1)。我們在同一個視埠比較變更前後的 GPU 時間戳記,使用的是這台特定 iPad 上同一個 Debug 裝置版本中的 command-buffer 時間戳記,下面稱它為 LucasPad。以下範圍是多幀記錄中的常見波動,不是正式版本的幀率承諾。在 70 道可見粉筆筆畫下,單幀通常需要 52–60 ms 的 GPU 時間;在約 120 道筆畫的區域,GPU 時間會上升到 77–80 ms。
按照 scratch 與 composite pass 的全螢幕矩形面積估算,每幀的理論工作範圍從約 783 million pixels 增長到 1.34 billion pixels。這個數字是矩形面積之和,不等同於片段呼叫次數、顯示記憶體讀寫位元組數或 GPU 硬體計數器。Metal 的 fast clear、attachment load/store 和 pass switching 仍由 GPU 與驅動程式控制。
這也解釋了為什麼空白區域保持流暢。可見性剔除會跳過視埠之外的筆畫;空白區域裡的
0.85 ms 的錯誤答案
App 原本已經有一張按每個點 2 個像素烘焙的整頁墨跡紋理。我們嘗試在書寫、平移和縮放期間直接顯示這張紋理,只保留目前的 Apple Pencil 筆畫作為即時向量;互動結束後,再用一個額外的幀重繪高解析度向量結果。
這個方案的效能非常好。在 300% 縮放的同一個密集區域中,GPU 時間降到了 0.84–0.85 ms,而且不再隨既有粉筆筆畫的數量增加。
但在真實裝置上,問題同樣清楚。在 300% 縮放下,每個點大約需要 6 個螢幕像素,而快取只提供了 2 個。Apple Pencil 一落筆,所有既有筆畫就會變成柔軟的低解析度影像;抬起 Pencil 後,它們才恢復完整清晰度。
測試者說:「寫字的時候,整個畫布都變得模糊了。放手之後就又清晰了。」
我們移除了這項最佳化。0.85 ms 是測得的最低結果,但它不是一個可以接受的粉筆工具。既有筆畫是書寫回饋的一部分;下筆時不能改變它們的清晰度。
把每道粉筆筆畫限制在自己的矩形內
最終修正保留了按筆畫設定的 scratch 與按筆畫合成,只縮小它們的像素工作範圍。每道筆畫原本就有一個畫布包圍盒,由所有印章半徑的聯集得到。渲染器會把這個包圍盒轉換到目前視埠的 drawable 座標中,並為反鋸齒邊緣留出 2 個像素的邊距:
同一個 scissor rectangle 接著會用於三件事:清除 scratch、繪製筆畫,以及把結果合成回主要表面。
let rect = displayScissorRect(for: stroke.bounds, viewport: viewport)
scratchEncoder.setScissorRect(rect)
clearScratchExplicitly()
drawStrokeIntoScratch(stroke)
mainEncoder.setScissorRect(rect)
compositeChalkFromScratch(stroke)
mainEncoder.setScissorRect(fullDrawable)
同樣的邏輯也用於在 4096² 墨跡紋理上進行烘焙和局部重播,因此高倍率顯示與穩定後的墨跡層不會產生兩種不同的粉筆行為。
這裡有兩個容易忽略的細節。
第一,渲染 pass 的 loadAction = .clear 發生在附件載入階段,不受光柵化 scissor 限制。繼續使用它仍然會清除整張 scratch texture。固定後的 pass 使用 .dontCare,然後在 scissor 內繪製 clear_fragment。這個矩形接著會被完整寫入,而合成也只讀取同一個矩形,因此不需要載入舊的附件內容。
第二,每道粉筆筆畫的合成完成後,都必須恢復外層 scissor。如果省略這行狀態恢復,後續的筆、圖片或選取範圍就會繼續受到上一道粉筆筆畫邊界的裁切,表現為筆畫缺失或圖片缺失。
粉筆顆粒仍然從絕對畫布座標取樣,而不是從矩形內的局部 UV 取樣。移動 scissor 只會改變 GPU 處理哪些像素,不會改變每個像素讀取的顆粒紋理位置。因此相鄰矩形之間不會產生紋理接縫,拖動畫布也不會讓顆粒漂移。
只考慮像素工作量時,新的工作範圍接近
其中
為什麼沒有批次合成同色粉筆筆畫
頁面上的大多數粉筆筆畫共用相同的顏色和密度,很容易讓人想把幾十道筆畫一次繪製到 scratch 中,再只合成一次。這樣可以進一步減少渲染 pass,但會改變重疊區域的顏色和顆粒語意。
考慮一個刻意簡化的情況:兩道筆畫在某個像素處恰好共用同一個顆粒閘控值
而先合併筆身、再套用一次閘控會產生
兩者的差異是
要進行精確批次處理,必須證明筆畫像素彼此不相交,或為每道筆畫分配獨立的圖集區域,並依原始順序合成。最終裝置驗收保留了 scissor 方案,因此這一輪沒有引入圖集或其管理複雜度。
從十億像素回到幾百萬像素
最終裝置測試的測量結果:
| 情境 | 修正前 | 精確 scissor |
|---|---|---|
| 70 道可見粉筆筆畫,300% 縮放,書寫 | GPU 52–60 ms | ≈ 9–10 ms |
| ≈ 121 道可見粉筆筆畫,300% 縮放 | GPU 77–80 ms | 13.7–15.6 ms |
| 每幀的理論矩形範圍(scratch + composite) | 783 M–1.34 B pixels | ≈ 1.7 M–3 M pixels |
我們還使用一份裝置文件中的 3,452 道筆畫和 202,710 個取樣點驗證了裁切邊界。在 0.5×、1×、2×、3×、5× 和 8× 縮放下,共產生了 186,408 個視埠案例;所有可能產生非零覆蓋的點精靈都落在計算出的 scissor 內。這項檢查涵蓋了畫布邊緣、視埠邊緣和各種偏移組合。
測試中的程式碼不會根據互動狀態切換到低解析度 LOD。低倍率仍顯示整頁墨跡紋理;高倍率仍將可見筆畫以向量重繪。在閾值的同一側,下筆和平移都不會用另一種清晰度取代既有筆畫。在高倍率向量重繪期間,每道粉筆筆畫的 scratch 清除和合成都只涵蓋自己的螢幕包圍盒。
GPU 時間沒有捕捉到清晰度
GPU 效能問題不一定會隨著資料結構中最顯眼的數量增長。在這個案例裡,3,571 個輸入點很容易成為懷疑對象;真正決定幀時間的,是 70 道粉筆筆畫各自觸發的全螢幕工作,以及隨之而來的渲染 pass 切換。
視覺語意也限制了可用的最佳化。按筆畫設定的 scratch、原始合成順序,以及絕對畫布顆粒座標,都不能隨意移除。同色同密度只說明參數相同,並不能證明重疊結果可以合併。
真實裝置回饋否決了 GPU 時間最低的版本。「整個畫布都變得模糊了」這句話補上了單靠測量無法表達的產品限制:Apple Pencil 落筆時,使用者也在觀察既有筆畫。
驗收實作沒有引入新的互動快取層,也沒有降低清晰度。高倍率向量重繪只是把每道粉筆筆畫的工作限制在自己的螢幕包圍盒內。再次將測試版本載入 LucasPad 後,回饋變成了:「效果很不錯。」