Lulucat

Membangun Kembali Pipeline Rendering Lulucat Notes dengan Kimi K3

Gaoge ZhangGaoge Zhang

Pipeline tile Core Graphics kami berubah menjadi pipeline point-sprite Metal dalam enam langkah kecil, masing-masing diverifikasi di iPad sungguhan. Kodenya diprogram berpasangan dengan Kimi K3 di Fireworks.

Diperbarui

Minggu lalu, saat menyeret seleksi laso melintasi Lulucat Notes, muncul gelombang yang terlihat: beberapa tile layar menampilkan seleksi di posisi barunya, sementara tile lain masih menampilkan posisi lama, dalam frame yang sama. Kami mengganti seluruh pipeline rendering — bitmap Core Graphics plus CATiledLayer — dengan Metal dalam enam langkah kecil, dan setiap langkah diverifikasi di iPad sungguhan sebelum langkah berikutnya dimulai.

Kodenya diprogram berpasangan dengan Kimi K3, model terbuka milik Moonshot yang berjalan di Fireworks. Manusialah yang mengarahkan, memutuskan, dan menguji; model menulis hampir setiap baris.

Pipeline

Pipeline lama memiliki dua jenis penggambaran yang perlahan makin tidak selaras: stroke dibakar ke dalam bitmap agar murah ditampilkan, lalu digambar ulang sebagai vektor setiap kali tile membutuhkan detail lebih tinggi. Pipeline baru memiliki tepat satu gagasan: semua yang menyerupai tinta adalah point sprite. Goresan pena, sapuan stabilo, dan dab penghapus adalah vertex 32-byte yang sama — posisi, diameter, warna — yang digambar oleh pasangan shader yang sama sebagai lingkaran yang dirasterisasi GPU, dengan jarak satu point sepanjang panjang busur stroke.

Tiga panel dari goresan melengkung yang sama: titik sentuh input, cap lingkaran yang diberi jarak di sepanjang kurva berdasarkan panjang busur, dan goresan solid hasil komposit.

Dengan jarak satu point, rangkaian lingkaran menyimpang dari kapsul yang sempurna secara matematis sekitar 0.075 point — seperlima piksel pada kepadatan kanvas kami. Sebagai gantinya, tiga alat menyatu menjadi satu jalur kode, dan GPU melakukan hal yang paling dikuasainya.

Di sekitar gagasan itu, arsitekturnya sederhana. Tinta yang sudah dikomit berada dalam satu tekstur 4096². UIScrollView tetap ada, tetapi diturunkan perannya menjadi mesin gestur murni: contentOffset dan zoomScale-nya mengisi uniform viewport setiap frame, sehingga panning dan zooming tidak menulis apa pun. Setiap frame terdiri dari lima draw:

flowchart TB
    subgraph frame["Every frame: five draws"]
        direction TB
        paper["1 · paper blit"] --> ink["2 · committed ink"]
        ink --> live["3 · live stroke"]
        live --> sel["4 · selection"]
        sel --> dash["5 · lasso dashes"]
    end
    commit["stroke commit<br/>append stamps"] --> tex[("ink texture<br/>4096² render target")]
    replay["regional replay<br/>erase · delete · move · undo"] --> tex
    tex -. "sampled or re-drawn" .-> ink

Pengeditan menulis langsung ke tekstur. Commit sebuah stroke menambahkan cap-capnya. Menghapus tinta, menghapus, memindahkan, dan membatalkan memutar ulang region yang terdampak di balik scissor rectangle: bersihkan region, gambar ulang stroke yang beririsan dengannya, selesai. Penghapusan parsial mempertahankan semantik kepemilikan dari postingan sebelumnya — sebuah penghapusan menjadi milik stroke yang tintanya dihapus — dengan menggambar setiap stroke yang dihapus ke dalam tekstur scratch, mengurangkan jalur penghapusnya sendiri memakai destination-out blending (), lalu mengompositkan hasilnya kembali. Isolasi scratch mencegah penghapus menembus kertas atau stroke di sebelahnya.

Seleksi yang memulai semua ini kini juga digambar sebagai point sprite. Menyeretnya memperbarui satu offset uniform. Tidak ada penulisan tekstur, tidak ada invalidasi tile — gelombangnya hilang secara struktural, bukan sekadar diredam.

Penjaga gerbang: pixel diff

Kami tidak menghapus renderer Core Graphics. Kami menurunkan perannya menjadi implementasi referensi offline, dan setiap perubahan Metal harus lolos perbandingan piksel terhadapnya menggunakan data stroke nyata yang ditangkap di perangkat. Kriteria penerimaannya bukan “piksel identik” — dua rasterizer yang benar secara sah dapat berbeda beberapa tingkat abu-abu di sepanjang tepi anti-alias. Gerbangnya bersifat struktural: tidak ada tinta yang hilang, tidak ada offset, tidak ada perubahan warna, dan tidak ada perbedaan besar di mana pun yang jauh dari tinta.

Tiga crop dari catatan tulisan tangan yang sama: dirender oleh Core Graphics, dirender oleh point sprite Metal, dan perbedaan pikselnya diperkuat enam kali, hanya menampilkan garis samar di sepanjang tepi goresan.

Harness ini menemukan empat dari lima bug yang kami temui saat membangun renderer offline, semuanya didiagnosis dengan melakukan crop pada output hingga level piksel: ketidakcocokan stride struct Swift/Metal (28 byte versus 32, karena Metal menyelaraskan float4 ke 16 — layar dipenuhi blok warna), [[point_size]] tidak dapat dibaca sebagai varying di fragment shader (setiap cap menjadi persegi), dua render encoder hidup bersamaan pada satu command buffer (semuanya hitam), dan dab awal yang hilang membuat milimeter pertama goresan cepat tidak terlihat.

Enam langkah, bukan satu penulisan ulang

Rencana migrasinya terdiri dari enam langkah yang dapat dikirim secara independen: renderer offline yang lolos pixel diff; display shell tanpa perubahan visual; live stroke di GPU; seleksi di GPU; mutasi yang menulis langsung ke tekstur; penggambaran ulang vektor pada zoom tinggi (dilipat ke langkah kedua, karena “tanpa perubahan visual” menuntutnya). Setiap langkah diakhiri dengan manusia — bukan simulator, bukan screenshot diff — yang menulis, menghapus, memperbesar, dan menyeret pada iPad Pro di atas meja.

Perangkat menemukan tiga bug yang luput dari setiap pemeriksaan otomatis. Di atas zoom 100%, stroke digambar dua kali — tekstur lembut di bawah, sprite tajam di atas — yang terbaca sebagai blur samar dan disadari manusia dalam hitungan detik. Commit sebuah stroke berkedip selama satu frame karena overlay lama melakukan cross-fade keluar tidak selaras dengan pembaruan tekstur. Dan di atas zoom 170%, semua catatan menghilang: persegi panjang visibility-culling menggunakan contentOffset dalam ruang koordinat yang diskalakan, sehingga bergeser menjauh dari stroke saat Anda memperbesar. Ketiganya dapat diperbaiki dengan perubahan satu baris pada satu fungsi, dan tidak satu pun ada dalam tes yang bisa kami tulis sebelumnya, karena kami tidak tahu apa yang harus dicari. Untuk aplikasi konsumen yang UI-first, itulah alasan manusia tetap berada dalam loop.

Seperti apa bekerja dengan Kimi K3

Pertama-tama, cepat. Loop “diskusikan, tulis, build, install, lihat” berlangsung dalam hitungan menit, dan model yang menjawab cepat mengubah berapa banyak loop yang dapat Anda jalankan dalam sehari.

Kedua, ia tidak melakukan over-engineering. Codebase ini berjalan berdasarkan aturan internal yang eksplisit — tidak ada scaffolding backward-compatibility sebelum launch, kompleksitas hanya ditambahkan ketika perangkat membuktikan bahwa itu diperlukan — dan K3 mengikutinya tanpa perlu diingatkan. Ia tidak menambahkan spatial index “untuk nanti”, tidak membungkus setiap pemanggilan dengan pemeriksaan defensif, dan tidak membuat abstraksi spekulatif. Memberinya prompt terasa seperti bekerja dengan rekan yang kompeten, yang telah membaca aturan internal dan benar-benar mempercayainya.

Ketiga, beri ia alat dan ia akan menggunakannya dengan antusias. Kami merangkai utilitas gambar — view, crop ke region piksel, resize — dan model mulai secara proaktif melakukan crop pada output renderernya sendiri untuk mendiagnosis lima bug harness di atas. Adanya alat itu mengingatkannya untuk melihat.

Separuh lainnya: K3 menulis sebagian besar bug dalam cerita ini, termasuk bug ruang koordinat yang membuat catatan menghilang. Batasannya nyata. Yang membuat pekerjaan ini aman bukanlah model yang selalu benar; melainkan harness yang menangkap drift rendering dan manusia yang menangkap rasa. Namun, dari hari ke hari, saya tidak dapat dengan andal membedakannya dari model frontier tertutup yang juga kami gunakan — sistem kelas Opus. Pada beberapa sumbu, ia jelas lebih baik: lebih cepat, dan jauh lebih tidak cenderung memenuhi codebase dengan desain defensif.

Cara kami ingin membangun mulai sekarang

Kami sudah selesai dengan pengembangan agentik berbasis spesifikasi besar — gaya ketika Anda menyerahkan spesifikasi besar kepada model dan menerima apa pun yang dihasilkannya. Mode kegagalannya bukan kode yang buruk; melainkan kode yang tidak dipahami siapa pun.

Yang berhasil di sini, dan akan kami pertahankan: langkah-langkah kecil, masing-masing didiskusikan sebelum dimulai, dipahami manusia sebelum dibangun, dan diverifikasi pada perangkat tempatnya akan digunakan. Tugas model adalah menjadi cepat, presisi, dan jujur tentang ketidakpastian. Tugas manusia adalah penilaian, selera, dan verifikasi e2e — terutama untuk perangkat lunak konsumen yang UI-first, ketika spesifikasi tidak dapat menjelaskan seperti apa “yang benar” itu terasa. Kimi K3 di Fireworks ternyata cocok untuk loop ini: cukup cepat untuk menjaga loop tetap rapat, cukup cerdas untuk menjaga langkah-langkahnya tetap kecil dan bersih.

Gelombangnya hilang, pipeline-nya menjadi satu gagasan alih-alih dua, dan proses yang membawa kami ke sana akan tetap dipakai.