Výkon nástroja s kriedou: od celoplošných prechodov po scissor obdĺžniky
Nástroj s kriedou v Lulucat Notes sa spomaľoval v oblastiach s hustým rukopisom. Úzkym miestom nebolo 3,571 vstupných vzoriek — bolo ním 70 celoplošných scratch prechodov na snímku. Zavrhnutá vyrovnávacia pamäť s nízkym rozlíšením a scissor obdĺžnik pre každý ťah dopĺňajú príbeh.

Červená a modrá krieda na finálnej zostave zariadenia, 155 ťahov pri priblížení 255%.
Nástroj s kriedou v Lulucat Notes mal konkrétny problém s výkonom: písanie na prázdnej ploche pôsobilo plynulo, no po prechode do oblasti už vyplnenej ťahmi kriedy začal hrot pera zaostávať. Pokračovanie v písaní v rovnakej oblasti postupne spomaľovalo aj posúvanie plátna.
Na vyvolanie problému stačila jedna strana obyčajného rukopisu: priblíženie 300%, 70 viditeľných ťahov kriedy v miestnej oblasti, spolu 3,571 vstupných vzorkovacích bodov. Prázdne oblasti zostávali plynulé; spomalila sa iba oblasť, v ktorej sa ťahy sústredili.
Po oprave môže tá istá strana pri priblížení 255% ďalej prijímať nové písanie a existujúce ťahy si zachovávajú úplnú ostrosť pri položení pera aj počas posúvania.

Finálna snímka obrazovky zo zariadenia, spolu 155 ťahov. Pri tejto úrovni priblíženia sa ostrosť existujúcich ťahov dočasne nemení ani pri položení pera, ani pri posúvaní.
Prečo krieda potrebuje scratch textúru
Obyčajné pero môže každý kruhový odtlačok priamo kompozitovať na atramentovú textúru pomocou miešania source-over. Krieda pridáva vrstvu riadenia zrna: vykresľovač najprv nahromadí pokrytie tela a hĺbku celého ťahu, potom pomocou pevnej textúry zrna určí, ktoré pozície dostanú kriedový prach, a napokon výsledok skompozituje na existujúci atrament.
Táto scratch textúra izoluje jeden ťah kriedy. Izolácia je dôležitá, pretože odtlačky v rámci toho istého ťahu sa výrazne prekrývajú; ak by sa každý odtlačok spracoval cez zrno samostatne, stredová línia ťahu by opakovane hromadila farbu a póry kriedy by sa menili podľa hustoty vzorkovania.
Pri vysokých úrovniach priblíženia Lulucat Notes prekresľuje vektorové ťahy viditeľné v aktuálnom viewporte. Stará implementácia vykonávala pre každý viditeľný ťah kriedy tieto kroky:
- Ukončiť hlavný render encoder;
- Vymazať scratch textúru;
- Nakresliť tento jediný ťah kriedy do scratch;
- Znovu otvoriť hlavný render encoder;
- Skompozitovať scratch späť do drawable pomocou celoplošného trojuholníka.
Sémantika jedného ťahu bola správna, ale rozsah práce bol oveľa väčší. Drawable iPadu mal rozmery 2732×2048 — približne 5.6 million pixels. Každý ťah kriedy spustil jeden scratch prechod a jednu celoplošnú kompozíciu. Sedemdesiat ťahov kriedy znamenalo približne 141 render encoders a 70 celoplošných kompozícií.
Nech je počet viditeľných ťahov kriedy
Každý ťah kriedy mal aj pevnú réžiu render pass, takže aj táto cena rástla lineárne s
Merania sme vykonali na 12.9-inch iPad Pro (5th generation, M1) so systémom iPadOS 18.6.2. Porovnali sme GPU timestamps z toho istého viewportu pred zmenou a po nej, pričom sme v tej istej Debug device build na tomto konkrétnom iPade použili command-buffer timestamps — ďalej ho nazývame LucasPad. Uvedené rozsahy sú typické výkyvy z logov s viacerými snímkami, nie záväzok týkajúci sa frekvencie snímok vo verzii určenej používateľom. Pri 70 viditeľných ťahoch kriedy jedna snímka zvyčajne potrebovala 52–60 ms času GPU; v oblasti s približne 120 ťahmi stúpol čas GPU na 77–80 ms.
Pri odhade podľa plochy celoplošných obdĺžnikov scratch a composite prechodov vzrástol teoretický rozsah práce na snímku približne zo 783 million pixels na 1.34 billion pixels. Toto číslo je súčtom plôch obdĺžnikov a nerovná sa počtu volaní fragmentov, počtu bajtov čítania/zápisu videopamäte ani hardvérovým počítadlám GPU. Fast clear, attachment load/store a prepínanie passov v Metal zostávajú pod kontrolou GPU a ovládača.
To tiež vysvetľuje, prečo prázdne oblasti zostávali plynulé. Visibility culling preskakuje ťahy mimo viewportu; v prázdnej oblasti je
Nesprávna odpoveď za 0.85 ms
Aplikácia už mala baked textúru atramentu celej strany pri dvoch pixeloch na bod. Skúsili sme túto textúru zobrazovať priamo počas písania, posúvania a približovania, pričom iba aktuálny ťah Apple Pencil zostával ako živý vektor; po skončení interakcie by jedna ďalšia snímka znovu vykreslila vektorový výsledok vo vysokom rozlíšení.
Tento prístup fungoval veľmi dobre. V rovnakej hustej oblasti pri priblížení 300% klesol čas GPU na 0.84–0.85 ms a prestal rásť s počtom existujúcich ťahov kriedy.
Na skutočnom zariadení bol problém rovnako jasný. Pri priblížení 300% bolo potrebných približne šesť pixelov obrazovky na bod, no cache poskytovala iba dva. V okamihu, keď sa Apple Pencil dotkol povrchu, všetky existujúce ťahy sa zmenili na mäkký obraz s nízkym rozlíšením; po zdvihnutí Pencila sa vrátili k úplnej ostrosti.
Tester povedal jednu vec: „Keď píšem, celé plátno sa rozmaže. Keď pero pustím, znovu sa vyjasní.“
Optimalizáciu sme odstránili. 0.85 ms bol najnižší nameraný výsledok, ale nebol to prijateľný nástroj s kriedou. Existujúce ťahy sú súčasťou spätnej väzby pri písaní; ich ostrosť sa pri položení pera nesmie meniť.
Obmedzenie každého ťahu kriedy na vlastný obdĺžnik
Finálna oprava zachovala scratch pre každý ťah aj kompozíciu pre každý ťah a znížila iba rozsah práce s pixelmi. Každý ťah už mal canvas bounding box odvodený zo zjednotenia všetkých polomerov jeho odtlačkov. Vykresľovač transformuje tento bounding box do súradníc drawable aktuálneho viewportu a pridá dva pixely ako okraj pre antialiasing:
Ten istý scissor obdĺžnik sa používa na tri veci: vymazanie scratch, vykreslenie ťahu a kompozitovanie výsledku späť na hlavnú plochu.
let rect = displayScissorRect(for: stroke.bounds, viewport: viewport)
scratchEncoder.setScissorRect(rect)
clearScratchExplicitly()
drawStrokeIntoScratch(stroke)
mainEncoder.setScissorRect(rect)
compositeChalkFromScratch(stroke)
mainEncoder.setScissorRect(fullDrawable)
Rovnaká logika sa používa aj pri bakingu a čiastočnom prehrávaní na atramentovej textúre 4096², takže zobrazenie pri vysokom priblížení a ustálená vrstva atramentu nevytvárajú dve odlišné správania kriedy.
Ľahko sa tu prehliadnu dva detaily.
Po prvé, loadAction = .clear render passu prebieha vo fáze načítania attachmentu a rasterizačný scissor ho neobmedzuje. Ak by sme ho používali ďalej, stále by vymazal celú scratch textúru. Opravený pass používa .dontCare a potom v scissore vykreslí clear_fragment. Tento obdĺžnik sa následne zapíše celý a composite číta iba ten istý obdĺžnik, takže starý obsah attachmentu netreba načítať.
Po druhé, po dokončení kompozície každého ťahu kriedy treba obnoviť vonkajší scissor. Ak sa tento riadok obnovy stavu vynechá, nasledujúce perá, obrázky alebo výbery budú naďalej orezané hranicami predchádzajúceho ťahu kriedy a budú vyzerať ako chýbajúce ťahy alebo obrázky.
Zrno kriedy sa stále vzorkuje z absolútnych súradníc canvasu, nie z lokálnych UV. Posunutie scissoru mení iba pixely, ktoré GPU spracuje; nemení miesto v textúre zrna, z ktorého daný pixel číta. Susedné obdĺžniky preto nevytvárajú švy v textúre a posúvanie plátna nespôsobí drift zrna.
Ak zohľadníme iba pixelové zaťaženie, nový rozsah práce je blízko k
kde
Prečo sa ťahy kriedy rovnakej farby nespracovali v jednej dávke
Väčšina ťahov kriedy na stránke má rovnakú farbu a hustotu a je lákavé nakresliť desiatky ťahov do scratch naraz a kompozitovať iba raz. Ďalej by to znížilo počet render passov, ale zmenilo by to sémantiku farby a zrna v miestach prekrytia.
Uvažujme zámerne zjednodušený prípad: dva ťahy zdieľajú na danom pixeli rovnakú hodnotu grain-gate
zatiaľ čo pri prvom zlúčení tiel a následnom použití jedného gate dostaneme
Rozdiel je
Presné zoskupenie do dávok vyžaduje dokázať, že pixely ťahov sú navzájom disjunktné, alebo prideliť každému ťahu nezávislú oblasť atlasu a kompozitovať v pôvodnom poradí. Finálna akceptácia na zariadení ponechala scissor prístup, takže v tomto kole sme nepridali atlas ani zložitosť jeho správy.
Z miliardy pixelov späť k niekoľkým miliónom
Finálne merania na zariadení:
| Scenár | Pred opravou | Presný scissor |
|---|---|---|
| 70 viditeľných ťahov kriedy, priblíženie 300%, písanie | GPU 52–60 ms | ≈ 9–10 ms |
| ≈ 121 viditeľných ťahov kriedy, priblíženie 300% | GPU 77–80 ms | 13.7–15.6 ms |
| Teoretický rozsah obdĺžnikov na snímku (scratch + composite) | 783 M–1.34 B pixels | ≈ 1.7 M–3 M pixels |
Hranice orezania sme overili aj pomocou 3,452 ťahov a 202,710 vzorkovacích bodov z dokumentu na zariadení. Pri priblíženiach 0.5×, 1×, 2×, 3×, 5× a 8× sme vygenerovali 186,408 prípadov viewportu; každý point sprite, ktorý mohol vytvoriť nenulové pokrytie, sa nachádzal vo vypočítanom scissore. Kontrola pokryla okraje canvasu, okraje viewportu a rôzne kombinácie offsetov.
Finálny kód neprepína na LOD s nízkym rozlíšením podľa stavu interakcie. Pri nízkych úrovniach priblíženia sa stále zobrazuje atramentová textúra celej strany; pri vysokých úrovniach sa viditeľné ťahy stále prekresľujú ako vektory. Na tej istej strane prahu položenie pera a posúvanie nenahrádzajú existujúce ťahy inou úrovňou ostrosti. Počas vektorového prekresľovania pri vysokom priblížení pokrývajú scratch clear a composite každého ťahu kriedy iba jeho vlastný obrazovkový bounding box.
Čas GPU nezachytil ostrosť
Problémy s výkonom GPU sa nemusia škálovať s najviditeľnejšou veličinou v dátovej štruktúre. V tomto prípade bolo 3,571 vstupných bodov ľahkým podozrivým; čas snímky určovala celoplošná práca spúšťaná každým zo 70 ťahov kriedy spolu s prepínaním render-passov.
Vizuálna sémantika tiež obmedzila dostupné optimalizácie. Scratch pre každý ťah, pôvodné poradie kompozície a absolútne súradnice zrna na canvase nebolo možné ľubovoľne odstrániť. Rovnaká farba a rovnaká hustota znamenajú iba zhodu parametrov — nedokazujú, že prekrývajúce sa výsledky možno zlúčiť.
Spätná väzba zo skutočného zariadenia odmietla verziu s najnižším časom GPU. Poznámka „celé plátno sa rozmaže“ vyjadrila produktové obmedzenie, ktoré samotné meranie neukázalo: keď sa Apple Pencil dotkne povrchu, používateľ zároveň sleduje aj existujúce ťahy.
Finálna verzia nepridáva novú vrstvu interakčnej cache a neznižuje ostrosť. Vektorové prekresľovanie pri vysokom priblížení iba obmedzí prácu každého ťahu kriedy na jeho vlastný obrazovkový bounding box. Po opätovnom načítaní na LucasPad znela spätná väzba: „Vyzerá to skvele.“