Performa Alat Kapur: Dari Pass Layar Penuh ke Persegi Panjang Scissor
Alat kapur di Lulucat Notes melambat di area tulisan tangan yang padat. Hambatan utamanya bukan 3,571 sampel input, melainkan 70 scratch pass layar penuh per frame. Cache beresolusi rendah yang ditolak dan persegi panjang scissor per goresan melengkapi ceritanya.

Kapur merah dan biru pada build perangkat yang digunakan untuk hasil akhir, 155 goresan pada zoom 255%.
Alat kapur di Lulucat Notes mengalami masalah performa yang spesifik: menulis di area kosong terasa mulus, tetapi ketika masuk ke area yang sudah dipenuhi goresan kapur, ujung pena mulai tertinggal. Terus menulis di area yang sama juga secara bertahap memperlambat panning kanvas.
Satu halaman tulisan tangan biasa sudah cukup untuk memicunya: zoom 300%, 70 goresan kapur yang terlihat di area lokal, dengan total 3,571 titik sampel input. Area kosong tetap lancar; hanya area tempat goresan terkonsentrasi yang menjadi lambat.
Setelah perbaikan, halaman yang sama dapat terus menerima tulisan baru pada zoom 255%, dan goresan yang sudah ada tetap sepenuhnya jelas selama pen-down maupun panning.

Tangkapan layar perangkat yang digunakan untuk hasil akhir, total 155 goresan. Pada tingkat zoom ini, baik pen-down maupun panning tidak mengubah kejernihan goresan yang sudah ada untuk sementara.
Mengapa kapur membutuhkan tekstur scratch
Pena biasa dapat mengompositkan setiap cap lingkaran langsung ke tekstur tinta dengan blending source-over. Kapur menambahkan lapisan grain-gating: renderer terlebih dahulu mengakumulasikan body coverage dan depth untuk seluruh goresan, lalu menggunakan tekstur grain tetap untuk menentukan posisi yang menerima debu kapur, dan akhirnya mengompositkan hasilnya ke tinta yang sudah ada.
Tekstur scratch ini mengisolasi satu goresan kapur. Isolasi penting karena cap dalam satu goresan saling tumpang tindih dengan kuat; jika setiap cap diberi grain gate secara terpisah, garis tengah goresan akan mengakumulasi warna berulang, dan pori-pori kapur akan bergeser mengikuti kepadatan sampling.
Pada tingkat zoom tinggi, Lulucat Notes menggambar ulang goresan vektor yang terlihat di viewport saat ini. Implementasi lama melakukan langkah-langkah berikut untuk setiap goresan kapur yang terlihat:
- Mengakhiri main render encoder;
- Mengosongkan tekstur scratch;
- Menggambar satu goresan kapur ini ke dalam scratch;
- Membuka kembali main render encoder;
- Mengompositkan scratch kembali ke drawable dengan segitiga layar penuh.
Semantik satu goresan sudah benar, tetapi cakupan pekerjaannya jauh lebih besar. Drawable iPad berukuran 2732×2048 — kira-kira 5.6 juta piksel. Setiap goresan kapur memicu satu scratch pass dan satu komposit layar penuh. Tujuh puluh goresan kapur berarti sekitar 141 render encoder dan 70 komposit layar penuh.
Misalkan jumlah goresan kapur yang terlihat adalah
Setiap goresan kapur juga membawa overhead render-pass tetap, sehingga biaya itu pun meningkat linear terhadap
Pengukuran dilakukan pada iPad Pro 12.9 inci (generasi ke-5, M1) yang menjalankan iPadOS 18.6.2. Kami membandingkan timestamp GPU dari viewport yang sama sebelum dan sesudah perubahan, menggunakan timestamp command-buffer dalam build perangkat Debug yang sama pada iPad ini — yang selanjutnya disebut LucasPad. Rentang di bawah adalah fluktuasi umum dari log multi-frame, bukan komitmen frame rate untuk versi yang akan didistribusikan. Pada 70 goresan kapur yang terlihat, satu frame biasanya memerlukan 52–60 ms waktu GPU; di area dengan kira-kira 120 goresan, waktu GPU naik menjadi 77–80 ms.
Dengan memperkirakan berdasarkan luas persegi panjang penuh dari pass scratch dan komposit, cakupan kerja teoretis per frame tumbuh dari sekitar 783 juta piksel menjadi 1.34 miliar piksel. Angka ini adalah jumlah luas persegi panjang dan tidak setara dengan jumlah pemanggilan fragment, byte baca/tulis memori video, atau counter perangkat keras GPU. Fast clear Metal, attachment load/store, dan pergantian pass tetap berada di bawah kendali GPU dan driver.
Ini juga menjelaskan mengapa area kosong tetap lancar. Visibility culling melewati goresan di luar viewport;
Jawaban yang salah pada 0.85 ms
Aplikasi ini sudah memiliki tekstur tinta seluruh halaman yang dipra-render pada dua piksel per titik. Kami mencoba menampilkan tekstur ini secara langsung selama menulis, panning, dan zoom, dengan hanya goresan Apple Pencil saat ini yang tetap berupa vektor live; setelah interaksi berakhir, satu frame tambahan akan merender ulang hasil vektor beresolusi tinggi.
Pendekatan ini bekerja sangat baik. Di area padat yang sama pada zoom 300%, waktu GPU turun menjadi 0.84–0.85 ms dan tidak lagi bertambah mengikuti jumlah goresan kapur yang sudah ada.
Masalah pada perangkat sebenarnya sama jelasnya. Pada zoom 300%, diperlukan kira-kira enam piksel layar per titik, sedangkan cache hanya menyediakan dua. Begitu Apple Pencil menyentuh layar, semua goresan yang sudah ada berubah menjadi gambar lembut beresolusi rendah; ketika Pencil diangkat, goresan itu kembali tajam sepenuhnya.
Tester mengatakan satu hal: “Saat saya menulis, seluruh kanvas menjadi buram. Setelah saya mengangkatnya, semuanya kembali jelas.”
Optimisasi itu dihapus. 0.85 ms adalah hasil terendah yang diukur, tetapi bukan alat kapur yang dapat diterima. Goresan yang sudah ada merupakan bagian dari umpan balik menulis; kejernihannya tidak boleh berubah saat pen-down.
Membatasi setiap goresan kapur pada perseginya sendiri
Perbaikan akhir mempertahankan scratch per goresan dan komposit per goresan, lalu hanya mengurangi cakupan kerja pikselnya. Setiap goresan sudah memiliki bounding box canvas yang diturunkan dari gabungan semua radius cap-nya. Renderer mengubah bounding box ini ke koordinat drawable viewport saat ini dan menambahkan padding dua piksel untuk margin antialiasing:
Persegi panjang scissor yang sama kemudian digunakan untuk tiga hal: mengosongkan scratch, menggambar goresan, dan mengompositkan hasilnya kembali ke permukaan utama.
let rect = displayScissorRect(for: stroke.bounds, viewport: viewport)
scratchEncoder.setScissorRect(rect)
clearScratchExplicitly()
drawStrokeIntoScratch(stroke)
mainEncoder.setScissorRect(rect)
compositeChalkFromScratch(stroke)
mainEncoder.setScissorRect(fullDrawable)
Logika yang sama juga digunakan untuk baking dan partial replay pada tekstur tinta 4096², sehingga tampilan zoom tinggi dan lapisan tinta yang sudah selesai tidak menghasilkan perilaku kapur yang berbeda.
Dua detail mudah terlewat di sini.
Pertama, loadAction = .clear pada render pass terjadi selama tahap attachment load dan tidak dibatasi oleh rasterization scissor. Jika terus menggunakannya, seluruh tekstur scratch tetap akan di-clear. Pass yang diperbaiki menggunakan .dontCare, lalu menggambar clear_fragment di dalam scissor. Persegi panjang ini selanjutnya ditulis penuh, dan komposit hanya membaca persegi panjang yang sama, sehingga isi attachment lama tidak perlu dimuat.
Kedua, setelah komposit setiap goresan kapur selesai, scissor luar harus dipulihkan. Jika baris pemulihan state ini dihilangkan, pena, gambar, atau seleksi berikutnya akan terus terpotong oleh batas goresan kapur sebelumnya, sehingga tampak seperti goresan atau gambar yang hilang.
Grain kapur tetap mengambil sampel dari koordinat canvas absolut, bukan UV lokal di dalam persegi panjang. Memindahkan scissor hanya mengubah piksel mana yang diproses GPU; itu tidak mengubah lokasi tekstur grain yang dibaca setiap piksel. Karena itu persegi panjang yang bersebelahan tidak menghasilkan sambungan tekstur, dan menyeret canvas tidak membuat grain bergeser.
Jika hanya mempertimbangkan beban kerja piksel, cakupan kerja baru mendekati
di mana
Mengapa goresan kapur dengan warna sama tidak diproses dalam batch
Sebagian besar goresan kapur di halaman menggunakan warna dan kepadatan yang sama, dan menggambar puluhan goresan sekaligus ke scratch lalu hanya mengomposit sekali memang terlihat menggoda. Ini akan lebih mengurangi render pass, tetapi mengubah semantik warna dan grain pada area yang tumpang tindih.
Pertimbangkan kasus yang sengaja disederhanakan: dua goresan berbagi nilai grain-gate
sedangkan menggabungkan body terlebih dahulu lalu menerapkan satu gate menghasilkan
Perbedaannya adalah
Batching yang tepat memerlukan pembuktian bahwa piksel goresan saling tidak beririsan, atau mengalokasikan region atlas independen untuk setiap goresan dan mengompositkannya dalam urutan asli. Penerimaan final di perangkat mempertahankan pendekatan scissor, sehingga putaran ini tidak memperkenalkan atlas atau kompleksitas pengelolaannya.
Dari satu miliar piksel kembali menjadi beberapa juta
Pengukuran perangkat final:
| Skenario | Sebelum perbaikan | Scissor presisi |
|---|---|---|
| 70 goresan kapur terlihat, zoom 300%, menulis | GPU 52–60 ms | ≈ 9–10 ms |
| ≈ 121 goresan kapur terlihat, zoom 300% | GPU 77–80 ms | 13.7–15.6 ms |
| Cakupan persegi panjang teoretis per frame (scratch + komposit) | 783 M–1.34 B piksel | ≈ 1.7 M–3 M piksel |
Kami juga memverifikasi batas clipping menggunakan 3,452 goresan dan 202,710 titik sampel dari sebuah dokumen perangkat. Pada zoom 0.5×, 1×, 2×, 3×, 5×, dan 8×, dihasilkan 186,408 kasus viewport; setiap point sprite yang dapat menghasilkan coverage non-zero berada di dalam scissor yang dihitung. Pemeriksaan ini mencakup tepi canvas, tepi viewport, dan berbagai kombinasi offset.
Kode final tidak beralih ke LOD beresolusi rendah berdasarkan status interaksi. Tingkat zoom rendah tetap menampilkan tekstur tinta seluruh halaman; tingkat zoom tinggi tetap menggambar ulang goresan yang terlihat sebagai vektor. Pada sisi threshold yang sama, pen-down dan panning tidak mengganti goresan yang sudah ada dengan tingkat kejernihan yang berbeda. Selama penggambaran ulang vektor pada zoom tinggi, clear scratch dan komposit setiap goresan kapur hanya mencakup bounding box layarnya sendiri.
Waktu GPU tidak menangkap kejernihan
Masalah performa GPU tidak selalu berskala dengan jumlah yang paling terlihat dalam struktur data. Dalam kasus ini, 3,571 titik input adalah tersangka yang mudah; yang menentukan waktu frame adalah pekerjaan layar penuh yang dipicu oleh setiap 70 goresan kapur, bersama dengan pergantian render-pass-nya.
Semantik visual juga membatasi optimisasi yang tersedia. Scratch per goresan, urutan komposit asli, dan koordinat grain canvas absolut tidak dapat dihilangkan begitu saja. Warna dan kepadatan yang sama hanya berarti parameternya cocok — itu tidak membuktikan bahwa hasil yang tumpang tindih dapat digabungkan.
Feedback perangkat nyata menolak versi dengan waktu GPU terendah. Pernyataan “seluruh kanvas menjadi buram” memberikan batasan produk yang tidak diungkapkan oleh pengukuran saja: ketika Apple Pencil menyentuh layar, pengguna juga sedang mengamati goresan yang sudah ada.
Versi final tidak memperkenalkan lapisan cache interaksi baru dan tidak mengurangi kejernihan. Penggambaran ulang vektor pada zoom tinggi hanya membatasi pekerjaan setiap goresan kapur pada bounding box layarnya sendiri. Setelah dimuat kembali ke LucasPad, feedback-nya menjadi: “Terlihat bagus.”