Hiệu năng công cụ phấn: Từ các pass toàn màn hình đến scissor rectangle
Công cụ phấn của Lulucat Notes chậm lại trong các vùng viết tay dày đặc. Nút thắt không phải là 3,571 mẫu đầu vào — mà là 70 scratch pass toàn màn hình mỗi khung hình. Một cache độ phân giải thấp bị loại bỏ và một scissor rectangle cho từng stroke kể tiếp câu chuyện.

Phấn đỏ và xanh trong bản dựng thiết bị dùng để kiểm thử, 155 stroke ở mức zoom 255%.
Công cụ phấn trong Lulucat Notes gặp một vấn đề hiệu năng rất cụ thể: viết trên vùng trống vẫn mượt, nhưng khi di chuyển vào vùng đã có đầy stroke phấn, đầu bút bắt đầu bị trễ. Tiếp tục viết trong cùng vùng đó cũng dần làm thao tác pan canvas chậm lại.
Chỉ một trang viết tay thông thường cũng đủ làm vấn đề xuất hiện: zoom 300%, có 70 stroke phấn nhìn thấy trong vùng cục bộ, tổng cộng 3,571 điểm mẫu đầu vào. Các vùng trống vẫn mượt; chỉ vùng tập trung các stroke đó trở nên chậm.
Sau khi sửa, cùng trang đó có thể tiếp tục nhận nét viết mới ở mức zoom 255%, còn các stroke hiện có vẫn giữ nguyên độ rõ trong cả lúc đặt bút lẫn lúc pan.

Ảnh chụp màn hình thiết bị cuối, tổng cộng 155 stroke. Ở mức zoom này, độ rõ của các stroke hiện có không tạm thời thay đổi khi đặt bút hoặc pan.
Vì sao phấn cần scratch texture
Một cây bút thông thường có thể composite từng stamp hình tròn trực tiếp lên texture mực bằng source-over blending. Phấn có thêm một lớp grain-gating: renderer trước hết tích lũy body coverage và depth cho toàn bộ stroke, sau đó dùng một grain texture cố định để quyết định vị trí nào nhận bụi phấn, rồi composite kết quả lên mực hiện có.
Scratch texture này cô lập một stroke phấn duy nhất. Việc cô lập rất quan trọng vì các stamp trong cùng một stroke chồng lấn nhiều; nếu mỗi stamp được grain-gate riêng, đường trung tâm của stroke sẽ tích lũy màu lặp lại, còn các lỗ phấn sẽ thay đổi theo mật độ lấy mẫu.
Ở các mức zoom cao, Lulucat Notes vẽ lại các stroke vector nhìn thấy trong viewport hiện tại. Implementation cũ thực hiện các bước sau cho từng stroke phấn nhìn thấy:
- Kết thúc main render encoder;
- Xóa scratch texture;
- Vẽ stroke phấn này vào scratch;
- Mở lại main render encoder;
- Composite scratch trở lại drawable bằng một full-screen triangle.
Ngữ nghĩa của một stroke là đúng, nhưng phạm vi công việc lớn hơn rất nhiều. Drawable của iPad là 2732×2048 — khoảng 5.6 million pixels. Mỗi stroke phấn kích hoạt một scratch pass và một full-screen composite. 70 stroke phấn có nghĩa là khoảng 141 render encoder và 70 full-screen composite.
Gọi số stroke phấn nhìn thấy là
Mỗi stroke phấn còn mang một chi phí cố định cho render pass, nên chi phí đó cũng tăng tuyến tính theo
Các phép đo được thực hiện trên một 12.9-inch iPad Pro (5th generation, M1) chạy iPadOS 18.6.2. Chúng tôi so sánh GPU timestamp từ cùng viewport trước và sau thay đổi, dùng command-buffer timestamp trong cùng Debug device build trên chính iPad này — bên dưới gọi là LucasPad. Các khoảng dưới đây là dao động điển hình từ log nhiều khung hình, không phải cam kết về frame rate của bản phát hành. Với 70 stroke phấn nhìn thấy, một khung hình thường cần 52–60 ms GPU time; trong vùng có khoảng 120 stroke, GPU time tăng lên 77–80 ms.
Ước tính theo diện tích hình chữ nhật full-screen của các scratch và composite pass, phạm vi công việc lý thuyết mỗi khung hình tăng từ khoảng 783 million pixels lên 1.34 billion pixels. Con số này là tổng diện tích các hình chữ nhật, không tương đương với số lần gọi fragment, số byte đọc/ghi video memory hay các GPU hardware counter. Fast clear, attachment load/store và pass switching của Metal vẫn do GPU và driver kiểm soát.
Điều này cũng giải thích vì sao vùng trống vẫn mượt. Visibility culling bỏ qua các stroke nằm ngoài viewport;
Một câu trả lời sai ở 0.85 ms
Ứng dụng đã có một ink texture toàn trang được bake ở mức 2 pixel trên mỗi point. Chúng tôi thử hiển thị trực tiếp texture này trong lúc viết, pan và zoom, chỉ giữ stroke Apple Pencil hiện tại dưới dạng vector sống; sau khi tương tác kết thúc, thêm một khung hình để render lại kết quả vector độ phân giải cao.
Cách này có hiệu năng rất tốt. Trong cùng vùng dày đặc ở mức zoom 300%, GPU time giảm xuống 0.84–0.85 ms và không còn tăng theo số stroke phấn hiện có.
Vấn đề trên thiết bị thật cũng rõ ràng không kém. Ở mức zoom 300%, cần khoảng 6 pixel màn hình cho mỗi point, nhưng cache chỉ cung cấp 2. Ngay khi Apple Pencil chạm xuống, mọi stroke hiện có biến thành một hình ảnh mềm, độ phân giải thấp; khi nhấc Pencil lên, chúng mới trở lại rõ nét hoàn toàn.
Người kiểm thử nói: “Khi tôi viết, toàn bộ canvas bị mờ. Nó rõ trở lại khi tôi nhấc bút lên.”
Chúng tôi đã bỏ optimization này. 0.85 ms là kết quả thấp nhất từng đo được, nhưng đó không phải là một công cụ phấn chấp nhận được. Các stroke hiện có là một phần của phản hồi khi viết; độ rõ của chúng không thể thay đổi lúc đặt bút.
Giới hạn mỗi stroke phấn trong hình chữ nhật riêng
Bản sửa cuối giữ lại scratch theo từng stroke và composite theo từng stroke, chỉ giảm phạm vi công việc pixel của chúng. Mỗi stroke vốn đã có một canvas bounding box được suy ra từ hợp của tất cả bán kính stamp. Renderer biến đổi bounding box này sang tọa độ drawable của viewport hiện tại và đệm thêm 2 pixel làm biên cho antialiasing:
Cùng một scissor rectangle sau đó được dùng cho ba việc: xóa scratch, vẽ stroke và composite kết quả trở lại main surface.
let rect = displayScissorRect(for: stroke.bounds, viewport: viewport)
scratchEncoder.setScissorRect(rect)
clearScratchExplicitly()
drawStrokeIntoScratch(stroke)
mainEncoder.setScissorRect(rect)
compositeChalkFromScratch(stroke)
mainEncoder.setScissorRect(fullDrawable)
Logic tương tự cũng được dùng để bake và partial replay trên ink texture 4096², vì vậy phần hiển thị ở zoom cao và lớp mực đã ổn định không tạo ra hai cách xử lý phấn khác nhau.
Có hai chi tiết dễ bị bỏ qua.
Thứ nhất, loadAction = .clear của một render pass xảy ra trong attachment load stage và không chịu giới hạn của rasterization scissor. Nếu tiếp tục dùng nó, toàn bộ scratch texture vẫn bị xóa. Pass đã sửa dùng .dontCare, sau đó vẽ một clear_fragment bên trong scissor. Hình chữ nhật này tiếp theo sẽ được ghi đầy đủ, còn composite chỉ đọc đúng hình chữ nhật đó, nên không cần load nội dung attachment cũ.
Thứ hai, sau khi composite của mỗi stroke phấn hoàn tất, outer scissor phải được khôi phục. Nếu bỏ qua dòng khôi phục state này, các bút, hình ảnh hoặc selection tiếp theo sẽ tiếp tục bị clip bởi bounds của stroke phấn trước đó, khiến stroke hoặc hình ảnh bị thiếu.
Grain của phấn vẫn được lấy mẫu từ tọa độ canvas tuyệt đối thay vì UV cục bộ trong hình chữ nhật. Di chuyển scissor chỉ thay đổi những pixel mà GPU xử lý; nó không thay đổi vị trí grain texture mà mỗi pixel đọc. Vì vậy các hình chữ nhật liền kề không tạo seam texture, và việc kéo canvas không làm grain trôi.
Chỉ xét tải pixel, phạm vi công việc mới gần với
trong đó
Vì sao không batch các stroke phấn cùng màu
Phần lớn stroke phấn trên trang dùng chung màu và mật độ, nên rất dễ muốn vẽ hàng chục stroke vào scratch cùng lúc rồi chỉ composite một lần. Điều này sẽ tiếp tục giảm số render pass, nhưng làm thay đổi ngữ nghĩa màu và grain ở các vùng chồng lấn.
Xét một trường hợp được cố ý đơn giản hóa: hai stroke có cùng giá trị grain-gate
còn gộp các thân nét trước rồi áp dụng một gate duy nhất tạo ra
Hiệu giữa hai kết quả là
Batch chính xác đòi hỏi phải chứng minh các pixel của stroke đôi một không giao nhau, hoặc cấp một vùng atlas độc lập cho mỗi stroke và composite theo thứ tự ban đầu. Bài kiểm thử cuối trên thiết bị giữ lại cách tiếp cận scissor, nên vòng này không đưa vào atlas hay độ phức tạp quản lý của nó.
Từ một tỷ pixel trở về vài triệu pixel
Các phép đo từ bài kiểm thử cuối trên thiết bị:
| Tình huống | Trước khi sửa | Scissor chính xác |
|---|---|---|
| 70 stroke phấn nhìn thấy, zoom 300%, đang viết | GPU 52–60 ms | ≈ 9–10 ms |
| ≈ 121 stroke phấn nhìn thấy, zoom 300% | GPU 77–80 ms | 13.7–15.6 ms |
| Phạm vi hình chữ nhật lý thuyết mỗi khung hình (scratch + composite) | 783 M–1.34 B pixels | ≈ 1.7 M–3 M pixels |
Chúng tôi cũng xác minh bounds clipping bằng 3,452 stroke và 202,710 điểm mẫu từ một tài liệu trên thiết bị. Ở các mức zoom 0.5×, 1×, 2×, 3×, 5× và 8×, đã tạo ra 186,408 trường hợp viewport; mọi point sprite có thể tạo coverage khác không đều nằm trong scissor đã tính. Kiểm tra này bao phủ các cạnh canvas, cạnh viewport và nhiều tổ hợp offset.
Code được kiểm thử không chuyển sang LOD độ phân giải thấp dựa trên trạng thái tương tác. Ở zoom thấp, nó vẫn hiển thị ink texture toàn trang; ở zoom cao, nó vẫn render lại các stroke nhìn thấy dưới dạng vector. Ở cùng phía của ngưỡng, đặt bút và pan không thay thế các stroke hiện có bằng một mức độ rõ khác. Trong lúc render lại vector ở zoom cao, scratch clear và composite của mỗi stroke phấn chỉ phủ bounding box màn hình riêng của nó.
GPU time không ghi nhận độ rõ
Các vấn đề hiệu năng GPU không nhất thiết tăng theo đại lượng dễ thấy nhất trong cấu trúc dữ liệu. Trong trường hợp này, 3,571 điểm đầu vào là một nghi phạm dễ nghĩ tới; thứ quyết định frame time là công việc toàn màn hình do từng stroke phấn trong số 70 stroke kích hoạt, cùng với việc chuyển render pass của chúng.
Ngữ nghĩa thị giác cũng giới hạn các optimization có thể dùng. Scratch theo từng stroke, thứ tự composite ban đầu và tọa độ grain trên canvas tuyệt đối không thể tùy tiện loại bỏ. Cùng màu và cùng mật độ chỉ có nghĩa là các tham số khớp nhau — chúng không chứng minh rằng các kết quả chồng lấn có thể được gộp.
Phản hồi trên thiết bị thật đã loại bỏ phiên bản có GPU time thấp nhất. Nhận xét “toàn bộ canvas bị mờ” bổ sung ràng buộc sản phẩm mà chỉ phép đo không thể diễn đạt: khi Apple Pencil chạm xuống, người dùng cũng đang quan sát các stroke hiện có.
Bản được kiểm thử không thêm lớp cache tương tác mới và không làm giảm độ rõ. Việc render lại vector ở zoom cao chỉ giới hạn công việc của mỗi stroke phấn trong bounding box màn hình riêng của nó. Sau khi nạp lại test build lên LucasPad, phản hồi là: “Trông tuyệt vời.”