Performanse alata za kredu: od prolaza preko cijelog zaslona do scissor pravokutnika
Alat za kredu u Lulucat Notesu usporavao je u područjima s gustom rukom pisanom bilješkom. Usko grlo nisu bile 3,571 ulazne uzorkovne točke, nego 70 scratch prolaza preko cijelog zaslona po okviru. Odbijena predmemorija niske razlučivosti i scissor pravokutnik za svaki potez objašnjavaju ostatak.

Crvena i plava kreda u konačnoj verziji za uređaj, 155 poteza pri zumu od 255%.
Alat za kredu u Lulucat Notesu imao je vrlo specifičan problem s performansama: pisanje na praznom području djelovalo je glatko, ali pri ulasku u područje već ispunjeno potezima krede vrh olovke počeo bi zaostajati. Nastavak pisanja na istom području postupno je usporavao i pomicanje platna.
Za pokretanje problema bila je dovoljna jedna stranica običnog rukopisa: zum od 300%, 70 vidljivih poteza krede u lokalnom području, ukupno 3,571 ulazna uzorkovna točka. Prazna su područja ostala glatka; usporilo se samo područje u kojem su potezi bili koncentrirani.
Nakon popravka na isto se područje može nastaviti pisati novim potezima pri zumu od 255%, a postojeći potezi zadržavaju punu jasnoću i tijekom spuštene olovke i tijekom pomicanja.

Konačna snimka zaslona uređaja, ukupno 155 poteza. Na ovoj razini zuma jasnoća postojećih poteza privremeno se ne mijenja ni pri spuštanju olovke ni tijekom pomicanja.
Zašto kredi treba scratch tekstura
Obična olovka može svaki kružni otisak izravno komponirati na teksturu tinte source-over blendanjem. Kreda dodaje sloj za upravljanje zrnatosti: renderer najprije akumulira pokrivenost tijela i dubinu cijelog poteza, zatim fiksnom teksturom zrna određuje koje će pozicije dobiti prašinu krede, a naposljetku komponira rezultat na postojeću tintu.
Ova scratch tekstura izolira jedan potez krede. Izolacija je važna jer se otisci unutar istog poteza snažno preklapaju; kada bi se svaki otisak zasebno obrađivao zrnatim upravljanjem, središnja bi linija poteza akumulirala ponovljenu boju, a pore krede pomicale bi se s gustoćom uzorkovanja.
Pri visokim razinama zuma Lulucat Notes ponovno iscrtava vektorske poteze vidljive u trenutačnom viewportu. Stara je implementacija za svaki vidljivi potez krede izvodila ove korake:
- Završiti glavni render encoder;
- Očistiti scratch teksturu;
- Iscrtati ovaj jedan potez krede u scratch;
- Ponovno otvoriti glavni render encoder;
- Komponirati scratch natrag na drawable trokutom preko cijelog zaslona.
iPadov drawable bio je veličine 2732×2048 — približno 5.6 milijuna piksela. Svaki je potez krede pokretao jedan scratch prolaz i jednu kompoziciju preko cijelog zaslona. Sedamdeset poteza krede značilo je približno 141 render encodera i 70 kompozicija preko cijelog zaslona.
Neka je
Svaki je potez krede imao i fiksni trošak render-passa, pa je i taj trošak linearno rastao s
Mjerenja su obavljena na iPadu Pro od 12.9 inča (5. generacija, M1) s iPadOS-om 18.6.2. Usporedili smo GPU vremenske oznake istog viewporta prije i nakon promjene, koristeći vremenske oznake command buffera u istoj Debug verziji za uređaj na tom konkretnom iPadu — u nastavku nazvanom LucasPad. Rasponi u nastavku tipična su odstupanja iz zapisa s više okvira, a ne obećanja broja sličica u sekundi za isporučenu verziju. Pri 70 vidljivih poteza krede jedan je okvir obično zahtijevao 52–60 ms GPU vremena; u području s približno 120 poteza GPU vrijeme poraslo je na 77–80 ms.
Procjenom prema površini pravokutnika preko cijelog zaslona u scratch i composite prolazima, teorijski opseg rada po okviru porastao je s približno 783 milijuna piksela na 1.34 milijarde piksela. Ta je brojka zbroj površina pravokutnika i nije jednaka broju poziva fragmenata, bajtovima čitanja/pisanja videomemorije ni hardverskim brojačima GPU-a. Metalov fast clear, učitavanje/spremanje attachmenta i prebacivanje prolaza i dalje su pod nadzorom GPU-a i upravljačkog programa.
To objašnjava i zašto su prazna područja ostala glatka. Odabir vidljivosti preskače poteze izvan viewporta; u praznom je području
Pogrešan odgovor na 0.85 ms
Aplikacija je već imala teksturu tinte cijele stranice, pečenu na dva piksela po točki. Pokušali smo tu teksturu prikazivati izravno tijekom pisanja, pomicanja i zumiranja, uz samo trenutačni potez Apple Pencila kao živi vektor; nakon završetka interakcije jedan bi dodatni okvir ponovno iscrtao vektorski rezultat visoke razlučivosti.
Ovaj je pristup radio vrlo dobro. U istom gustom području pri zumu od 300% GPU vrijeme palo je na 0.84–0.85 ms i više nije raslo s brojem postojećih poteza krede.
Problem na stvarnom uređaju bio je jednako jasan. Pri zumu od 300% bilo je potrebno približno šest zaslonskih piksela po točki, ali predmemorija je pružala samo dva. Čim bi Apple Pencil dotaknuo zaslon, svi bi postojeći potezi postali meka slika niske razlučivosti; podizanjem Pencila naglo bi se vratili u punu jasnoću.
Tester je rekao jednu stvar: „Kad pišem, cijelo se platno zamuti. Razbistri se čim podignem olovku.”
Uklonili smo optimizaciju. 0.85 ms bio je najniži izmjereni rezultat, ali to nije bio prihvatljiv alat za kredu. Postojeći su potezi dio povratne informacije pri pisanju; njihova se jasnoća ne smije promijeniti pri spuštanju olovke.
Ograničavanje svakog poteza krede na vlastiti pravokutnik
Konačni je popravak zadržao scratch po potezu i kompoziciju po potezu te smanjio samo opseg njihova rada s pikselima. Svaki je potez već imao okvir omeđenja platna izveden iz unije svih polumjera svojih otisaka. Renderer taj okvir pretvara u drawable koordinate trenutačnog viewporta i proširuje ga za dva piksela kao marginu za antialiasing:
Isti se scissor pravokutnik zatim koristi za tri stvari: čišćenje scratcha, iscrtavanje poteza i komponiranje rezultata natrag na glavnu površinu.
let rect = displayScissorRect(for: stroke.bounds, viewport: viewport)
scratchEncoder.setScissorRect(rect)
clearScratchExplicitly()
drawStrokeIntoScratch(stroke)
mainEncoder.setScissorRect(rect)
compositeChalkFromScratch(stroke)
mainEncoder.setScissorRect(fullDrawable)
Ista se logika koristi i za pečenje i djelomični replay na teksturi tinte 4096², pa prikaz pri visokom zumu i sloj stabilizirane tinte ne stvaraju dva različita ponašanja krede.
Ovdje se lako previde dvije pojedinosti.
Prvo, loadAction = .clear render passa događa se tijekom faze učitavanja attachmenta i nije ograničen rasterizacijskim scissorom. Nastavak njegove upotrebe i dalje bi čistio cijelu scratch teksturu. Fiksni pass koristi .dontCare, a zatim unutar scissora iscrtava clear_fragment. Taj se pravokutnik naknadno u cijelosti zapisuje, a composite čita samo isti pravokutnik, pa prethodni sadržaj attachmenta nije potrebno učitati.
Drugo, nakon završetka kompozicije svakog poteza krede treba vratiti vanjski scissor. Ako se izostavi taj redak za vraćanje stanja, sljedeće olovke, slike ili selekcije i dalje će biti odrezane granicama prethodnog poteza krede i izgledat će kao nestali potezi ili slike.
Zrno krede i dalje se uzorkuje iz apsolutnih koordinata platna, a ne iz lokalnih UV-ova unutar pravokutnika. Pomicanje scissora mijenja samo piksele koje GPU obrađuje; ne mijenja mjesto u teksturi zrna koje svaki piksel čita. Susjedni pravokutnici zato ne stvaraju šavove teksture, a povlačenje platna ne uzrokuje pomicanje zrna.
Ako uzmemo u obzir samo opterećenje pikselima, novi je opseg rada blizu izraza
gdje je
Zašto potezi krede iste boje nisu grupirani
Većina poteza krede na stranici dijeli istu boju i gustoću, pa je primamljivo desetke poteza odjednom iscrtati u scratch i komponirati samo jednom. To bi dodatno smanjilo broj render passova, ali bi promijenilo semantiku boje i zrna u područjima preklapanja.
Razmotrimo namjerno pojednostavljeni slučaj: dva poteza dijele potpuno istu vrijednost grain-gatea
dok spajanje tijela prije primjene jednog gatea daje
Razlika je
Točno grupiranje zahtijeva dokaz da su pikseli poteza međusobno disjunktni ili dodjelu neovisnog atlas područja svakom potezu i kompoziciju izvornim redoslijedom. Konačno prihvaćanje na uređaju zadržalo je scissor pristup, pa ova iteracija nije uvela atlas ni složenost njegova upravljanja.
Od milijarde piksela natrag na nekoliko milijuna
Konačna mjerenja na uređaju:
| Scenarij | Prije popravka | Precizni scissor |
|---|---|---|
| 70 vidljivih poteza krede, zum od 300%, pisanje | GPU 52–60 ms | ≈ 9–10 ms |
| ≈ 121 vidljivih poteza krede, zum od 300% | GPU 77–80 ms | 13.7–15.6 ms |
| Teorijski opseg pravokutnika po okviru (scratch + composite) | 783 M–1.34 B piksela | ≈ 1.7 M–3 M piksela |
Granice izrezivanja provjerili smo i s 3,452 poteza i 202,710 uzorkovnih točaka iz dokumenta na uređaju. Pri zumovima 0.5×, 1×, 2×, 3×, 5× i 8× generirano je 186,408 viewport slučajeva; svaki point sprite koji je mogao proizvesti pokrivenost različitu od nule nalazio se unutar izračunatog scissora. Provjera je obuhvatila rubove platna, rubove viewporta i različite kombinacije offseta.
Konačni kod ne prebacuje se na LOD niske razlučivosti ovisno o stanju interakcije. Niske razine zuma i dalje prikazuju teksturu tinte cijele stranice; visoke razine zuma i dalje ponovno iscrtavaju vidljive poteze kao vektore. Na istoj strani praga spuštanje olovke i pomicanje ne zamjenjuju postojeće poteze drugom razinom jasnoće. Tijekom vektorskog ponovnog iscrtavanja pri visokom zumu, scratch čišćenje i composite svakog poteza krede pokrivaju samo njegov vlastiti okvir omeđenja na zaslonu.
GPU vrijeme nije uhvatilo jasnoću
Problemi s performansama GPU-a ne moraju se skalirati s najvidljivijom količinom u strukturi podataka. U ovom je slučaju 3,571 ulaznih točaka bilo lako osumnjičiti; vrijeme okvira određivali su rad preko cijelog zaslona koji je pokretao svaki od 70 poteza krede i njihova prebacivanja render-passa.
Vizualna semantika također je ograničila dostupne optimizacije. Scratch po potezu, izvorni redoslijed kompozicije i apsolutne koordinate zrna na platnu nisu se mogli proizvoljno ukloniti. Ista boja i ista gustoća samo znače da se parametri podudaraju — ne dokazuju da se preklapajući rezultati mogu spojiti.
Povratne informacije sa stvarnog uređaja odbile su verziju s najkraćim GPU vremenom. Napomena “cijelo se platno zamuti” izrazila je proizvodno ograničenje koje samo mjerenje nije pokazalo: kada Apple Pencil dotakne zaslon, korisnici promatraju i postojeće poteze.
Konačna provjerena implementacija ne uvodi novi sloj predmemorije interakcije i ne smanjuje jasnoću. Vektorsko ponovno iscrtavanje pri visokom zumu jednostavno ograničava rad svakog poteza krede na njegov vlastiti okvir omeđenja na zaslonu. Nakon ponovnog učitavanja na LucasPad, povratna je informacija postala: „Izgleda odlično.”