Lulucat

Lulucat Notes Renderlama İşlem Hattını Kimi K3 ile Yeniden Kurmak

Gaoge ZhangGaoge Zhang

Core Graphics döşeme işlem hattımız altı küçük adımda Metal point-sprite işlem hattına dönüştü; her adım gerçek bir iPad üzerinde doğrulandı. Kod, Fireworks üzerindeki Kimi K3 ile eşli programlandı.

Güncellendi

Geçen hafta Lulucat Notes’ta bir lasso seçimini sürüklemek görünür bir dalga oluşturdu: bazı ekran döşemeleri seçimi yeni konumunda gösterirken diğerleri aynı karede hâlâ eski konumu gösteriyordu. Core Graphics bitmap’i ve CATiledLayer’dan oluşan tüm renderlama işlem hattını Metal ile değiştirdik; bunu altı küçük adımda yaptık ve bir sonraki adıma geçmeden önce her adımı gerçek bir iPad’de doğruladık.

Yazarlık hakkında bir şey daha, çünkü bu yazının ikinci yarısı için önemli: Kod, Fireworks üzerinde çalışan Moonshot’ın açık modeli Kimi K3 ile eşli programlandı. İnsan yönetti, karar verdi ve test etti; model neredeyse her satırı yazdı.

İşlem hattı

Eski işlem hattında yavaş yavaş birbirinden uzaklaşan iki çizim türü vardı: stroke’lar ucuz görüntüleme için bir biteşleme pişiriliyor, döşemelerin daha fazla ayrıntıya ihtiyaç duyduğu her seferde vektör olarak yeniden çiziliyordu. Yeni işlem hattının tam olarak tek bir fikri var: mürekkep benzeri her şey bir point sprite’tır. Bir kalem stroke’u, bir fosforlu kalem sweep’i ve bir silgi dokunuşu aynı 32-byte vertex’tir — position, diameter, color —; stroke’un yay uzunluğu boyunca bir point aralıkla yerleştirilmiş, GPU’da rasterize edilen daireler olarak aynı shader çifti tarafından çizilir.

Aynı kıvrımlı stroke'un üç paneli: giriş dokunma noktaları, yay uzunluğuna göre eğri boyunca aralıklanmış dairesel damgalar ve birleştirilmiş katı stroke.

Bir point aralıkla yerleştirildiğinde daireler zinciri, matematiksel olarak kusursuz bir kapsülden yaklaşık 0.075 points sapar — tuval yoğunluğumuzda bir pikselin beşte biri. Karşılığında üç araç tek bir kod yolunda birleşir ve GPU en iyi yaptığı işi yapar.

Bu fikrin çevresindeki mimari basittir. Commit edilmiş mürekkep tek bir 4096² texture’da tutulur. UIScrollView kalır, ancak saf bir gesture engine’e indirgenir: contentOffset ve zoomScale her karede bir viewport uniform’u besler, bu nedenle pan ve zoom hiçbir şey yazmaz. Her kare beş çizimden oluşur:

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

Düzenlemeler texture’a doğrudan yazar. Bir stroke’u commit etmek damgalarını ekler. Silme, kaldırma, taşıma ve geri alma, bir scissor rectangle’ın arkasında etkilenen bölgeyi yeniden oynatır: bölgeyi temizle, onunla kesişen stroke’ları yeniden çiz, bitti. Kısmi silme, önceki yazıdaki sahiplik semantiğini korur — bir silme, mürekkebini kaldırdığı stroke’a aittir — bunu her silinen stroke’u bir scratch texture’a çizerek, kendi silme yollarını destination-out blending ile () çıkararak ve sonucu geri birleştirerek yapar. Scratch yalıtımı, bir silginin kâğıdı veya komşu stroke’ları delip geçmesini engeller.

Her şeyi başlatan seçim de artık point sprite olarak çiziliyor. Onu sürüklemek tek bir uniform ofseti günceller. Sıfır texture yazımı, sıfır tile invalidation — dalga yapısal olarak ortadan kalktı; hafifletilmiş değil.

Bekçi: bir pixel diff

Core Graphics renderer’ını silmedik. Onu çevrimdışı bir reference implementation’a indirgedik ve her Metal değişikliği, cihazda yakalanan gerçek stroke verileri üzerinde onunla yapılan bir pixel karşılaştırmasını geçmek zorunda. Kabul ölçütü “identical pixels” değildir — iki doğru rasterizer, anti-aliased kenarlar boyunca birkaç gray level ile meşru biçimde ayrışabilir. Kapı yapısaldır: eksik mürekkep yok, offset yok, renk kayması yok ve mürekkepten uzakta hiçbir yerde büyük fark yok.

Aynı el yazısı notların üç kırpımı: Core Graphics ile render edilen, Metal point sprite ile render edilen ve altı kat büyütülmüş pixel farkı; yalnızca stroke kenarları boyunca soluk dış çizgiler görülüyor.

Bu harness, çevrimdışı renderer’ı kurarken karşılaştığımız beş bug’dan dördünü yakaladı; hepsi çıktıyı pixel seviyesine kırparak teşhis edildi: bir Swift/Metal struct stride uyumsuzluğu (Metal float4’ü 16’ya hizaladığı için 28 bytes ile 32 arasındaki fark — ekran renk bloklarıyla doldu), [[point_size]]’ın fragment shader’da varying olarak okunamaması (her damga kare çıktı), tek bir command buffer’da iki render encoder’ın birlikte bulunması (her şey siyah) ve hızlı stroke’ların ilk millimetresini görünmez bırakan eksik bir start dab.

Altı adım, tek bir yeniden yazım değil

Geçiş planı bağımsız olarak gönderilebilen altı adımdı: pixel diff’i geçen çevrimdışı renderer; sıfır görsel değişiklik içeren display shell; GPU üzerindeki canlı stroke; GPU üzerindeki seçim; texture’a doğrudan yazan mutation’lar; yüksek zoom’da vektörün yeniden çizilmesi (“zero visual change” bunu gerektirdiği için ikinci adıma katlandı). Her adım, masadaki iPad Pro’da bir insanın yazması, silmesi, zoom yapması ve sürüklemesiyle sona erdi — bir simülatörle ya da screenshot diff’iyle değil.

Cihaz, her otomatik kontrolün kaçırdığı üç bug’ı yakaladı. 100% yakınlaştırmanın üzerinde stroke’lar iki kez çiziliyordu — altta yumuşak texture, üstte keskin sprite’lar — ve insanın saniyeler içinde fark ettiği soluk bir bulanıklık oluşturuyordu. Bir stroke’u commit etmek bir kare boyunca titredi, çünkü eski overlay texture güncellemesiyle eşzamanlı olmayan bir cross-fade ile sönümlendi. 170% yakınlaştırmanın üzerinde her note kayboldu: visibility-culling dikdörtgeni ölçeklenmiş koordinat uzayında contentOffset kullandığı için siz zoom yaptıkça stroke’lardan uzaklaştı. Üçünün de çözümü bir satırdan tek bir işleve kadar uzanıyordu ve hiçbirisi önceden yazabileceğimiz hiçbir testte yoktu; çünkü onları aramamız gerektiğini bilmiyorduk. UI-öncelikli bir tüketici uygulamasında insanın döngüde kalmasının nedeni budur.

Kimi K3 ile çalışmak nasıldır

Her şeyden önce hızlı. “discuss, write, build, install, look” döngüsü dakikalar içinde çalıştı ve hızlı yanıt veren bir model, bir günde kaç döngüyü karşılayabileceğinizi değiştiriyor.

İkincisi, aşırı mühendislik yapmıyor. Bu kod tabanı açık ev kurallarıyla çalışıyor — lansmandan önce geriye dönük uyumluluk scaffolding’i yok, karmaşıklık yalnızca bir cihaz gerekli olduğunu kanıtladığında ekleniyor — ve K3 bunlara hatırlatılmadan uyuyor. “Sonrası için” uzamsal dizinler eklemedi, her çağrıyı savunmacı denetimlere sarmadı, spekülatif biçimde soyutlamadı. Ona prompt vermek, ev kurallarını okumuş ve onlara gerçekten inanan yetkin bir meslektaşla çalışmak gibi hissettiriyor.

Üçüncüsü, ona araçlar verin, onları hevesle kullanır. Görüntü yardımcı programlarını bağladık — görüntüle, bir pixel bölgesine kırp, yeniden boyutlandır — ve model yukarıdaki beş harness bug’ını teşhis etmek için kendi renderer çıktısını proaktif biçimde kırpmaya başladı. Araca sahip olmak ona bakmayı hatırlattı.

Öteki yarısı şu: K3, notların kaybolmasına neden olan koordinat-uzayı bug’ı da dahil olmak üzere bu hikâyedeki bug’ların çoğunu yazdı. Sınırları gerçek. Çalışmayı güvenli kılan şey modelin haklı olması değildi; rendering drift’ini yakalayan harness ile hissi yakalayan insandı. Yine de günlük kullanımda, onu bizim de kullandığımız frontier closed model’lerden — Opus-class sistemlerden — güvenilir biçimde ayırt edemedim. Bazı açılarda açıkça daha iyiydi: daha hızlıydı ve kod tabanını savunmacı tasarımla doldurmaya çok daha az meyilliydi.

Bundan sonra nasıl geliştirmek istiyoruz

Büyük spesifikasyonlarla yürütülen agentic development’ı bıraktık — bir modele büyük bir specification verip ortaya ne çıkarsa kabul ettiğiniz tarzı. Başarısızlık biçimi kötü kod değil; kimsenin anlamadığı kod.

Burada işe yarayan ve koruyacağımız şey şu: küçük adımlar; her biri başlamadan önce tartışılıyor, inşa edilmeden önce insan tarafından anlaşılıyor ve yaşayacağı cihazda doğrulanıyor. Modelin işi hızlı, kesin ve belirsizlik konusunda dürüst olmak. İnsanın işi yargı, zevk ve uçtan uca doğrulama — özellikle specification’ın “doğru”nun nasıl hissettirdiğini tanımlayamadığı UI-öncelikli tüketici yazılımında. Fireworks üzerindeki Kimi K3’ün tam olarak bu döngüye uygun olduğu ortaya çıktı: döngüyü sıkı tutacak kadar hızlı, adımları küçük ve temiz tutacak kadar akıllı.

Dalga gitti, işlem hattı iki fikir yerine tek bir fikir ve bizi oraya getiren süreç kalıyor.