Ytelsen til krittverktøyet: Fra fullskjermspass til scissorrektangler
Krittverktøyet i Lulucat Notes ble tregt i tette håndskriftområder. Flaskehalsen var ikke de 3,571 inndatasamplene, men 70 fullskjerms scratch-pass per bilde. En avvist hurtigbuffer med lav oppløsning og et eget scissorrektangel per strøk forteller resten av historien.

Rødt og blått kritt på enhetsbygget som ble brukt til sluttkontrollen, 155 strøk ved 255% zoom.
Krittverktøyet i Lulucat Notes hadde et bestemt ytelsesproblem: Det føltes jevnt å skrive på et tomt område, men når pennen kom inn i et område som allerede var fylt med krittstrøk, begynte pennespissen å henge etter. Fortsatt skriving i det samme området gjorde også panorering av lerretet gradvis tregere.
Én vanlig håndskrevet side var nok til å utløse problemet: 300% zoom, 70 synlige krittstrøk i lokalområdet, til sammen 3,571 inndatasamplingspunkter. Tomme områder forble flytende; bare området der strøkene var konsentrert, ble tregt.
Etter rettingen kan den samme siden fortsatt ta imot ny skriving ved 255% zoom, og eksisterende strøk beholder full skarphet både under pen-down og panorering.

Skjermbildet fra enheten som ble brukt til sluttkontrollen, 155 strøk totalt. På dette zoomnivået bytter verken pen-down eller panorering midlertidig skarpheten til eksisterende strøk.
Hvorfor kritt trenger en scratch-tekstur
En vanlig penn kan komponere hvert sirkelformede stempel direkte på blekkteksturen med source-over-blanding. Kritt legger til et grain-gating-lag: Rendereren akkumulerer først kroppens coverage og depth for et helt strøk, bruker deretter en fast grain-tekstur til å avgjøre hvilke posisjoner som får krittpulver, og komponerer til slutt resultatet på det eksisterende blekket.
Denne scratch-teksturen isolerer ett krittstrøk. Isolasjonen er viktig fordi stemplene innenfor samme strøk overlapper kraftig; hvis hvert stempel ble grain-gatet separat, ville strøkets midtlinje samle opp gjentatt farge, og krittporene ville flytte seg med samplings-tettheten.
Ved høye zoomnivåer tegner Lulucat Notes vektorstrøkene som er synlige i gjeldende viewport, på nytt. Den gamle implementasjonen utførte disse trinnene for hvert synlige krittstrøk:
- Avslutt main render encoder;
- Tøm scratch-teksturen;
- Tegn dette ene krittstrøket inn i scratch;
- Åpne main render encoder på nytt;
- Komponer scratch tilbake til drawable med en fullskjermstrekant.
Semantikken for ett strøk var riktig, men arbeidsområdet var langt større. iPad-ens drawable var 2732×2048 — omtrent 5.6 millioner piksler. Hvert krittstrøk utløste ett scratch-pass og én fullskjermskomposisjon. 70 krittstrøk betydde omtrent 141 render encodere og 70 fullskjermskomposisjoner.
La antallet synlige krittstrøk være
Hvert krittstrøk hadde også en fast overhead for render-pass, så også den kostnaden skalerte lineært med
Målingene ble gjort på en 12.9-tommers iPad Pro (5. generasjon, M1) med iPadOS 18.6.2. Vi sammenlignet GPU-tidsstempler fra samme viewport før og etter endringen, ved å bruke tidsstempler fra command-bufferen i det samme Debug-enhetsbygget på denne iPad-en — heretter kalt LucasPad. Intervallene under er typiske variasjoner fra logger over flere bilder, ikke løfter om bildefrekvens i en kommende versjon. Med 70 synlige krittstrøk krevde ett bilde vanligvis 52–60 ms GPU-tid; i et område med rundt 120 strøk steg GPU-tiden til 77–80 ms.
Estimert ut fra arealet til fullskjermsrektangelet for scratch- og composite-passene vokste det teoretiske arbeidsområdet per bilde fra omtrent 783 millioner piksler til 1.34 milliarder piksler. Dette tallet er summen av rektangelarealene og tilsvarer ikke antallet fragmentkall, antall byte lest/skrevet i videominnet eller GPU-ens maskinvaretellere. Metals fast clear, attachment load/store og bytte av pass er fortsatt under GPU-ens og driverens kontroll.
Dette forklarer også hvorfor tomme områder forble jevne. Visibility culling hopper over strøk utenfor viewporten;
Et feil svar ved 0.85 ms
Appen hadde allerede en blekktekstur for hele siden, bakt ved to piksler per punkt. Vi prøvde å vise denne teksturen direkte under skriving, panorering og zooming, og bare beholde det aktuelle Apple Pencil-strøket som live-vektor; etter at interaksjonen var over, skulle ett ekstra bilde tegne vektorresultatet med høy oppløsning på nytt.
Denne tilnærmingen fungerte svært godt. I det samme tette området ved 300% zoom falt GPU-tiden til 0.84–0.85 ms og vokste ikke lenger med antallet eksisterende krittstrøk.
Problemet på den faktiske enheten var like tydelig. Ved 300% zoom trengtes omtrent seks skjermpiksler per punkt, men hurtigbufferen ga bare to. I det øyeblikket Apple Pencil berørte skjermen, ble alle eksisterende strøk til et mykt bilde med lav oppløsning; da Pencil ble løftet, hoppet de tilbake til full skarphet.
Testeren sa: «Når jeg skriver, blir hele lerretet uklart. Det blir skarpt igjen når jeg slipper.»
Optimaliseringen ble fjernet. 0.85 ms var det laveste målte resultatet, men det var ikke et akseptabelt krittverktøy. Eksisterende strøk er en del av tilbakemeldingen ved skriving; skarpheten deres kan ikke endres ved pen-down.
Begrense hvert krittstrøk til sitt eget rektangel
Den endelige rettingen beholdt scratch per strøk og komposisjon per strøk, og reduserte bare pikselarbeidsområdet. Hvert strøk hadde allerede en canvas bounding box utledet fra unionen av alle stempelenes radier. Rendereren transformerer denne bounding box-en til drawable-koordinatene for gjeldende viewport og legger til to piksler som antialiasing-margin.
Det samme sakserektangelet brukes deretter til tre ting: tømme scratch, tegne strøket og komponere resultatet tilbake på hovedflaten.
let rect = displayScissorRect(for: stroke.bounds, viewport: viewport)
scratchEncoder.setScissorRect(rect)
clearScratchExplicitly()
drawStrokeIntoScratch(stroke)
mainEncoder.setScissorRect(rect)
compositeChalkFromScratch(stroke)
mainEncoder.setScissorRect(fullDrawable)
Den samme logikken brukes også til baking og delvis replay på blekkteksturen 4096², slik at visningen ved høy zoom og det ferdigstilte blekklaget ikke produserer to forskjellige krittoppførsler.
To detaljer er lette å overse her.
For det første skjer en render-pass sin loadAction = .clear under attachment load-fasen og begrenses ikke av rasterization scissor. Hvis vi fortsatte å bruke den, ville den fortsatt tømme hele scratch-teksturen. Det faste passet bruker .dontCare og tegner deretter en clear_fragment innenfor scissor. Dette rektangelet skrives etterpå i sin helhet, og compositen leser bare det samme rektangelet, så det er ikke nødvendig å laste inn gammelt attachment-innhold.
For det andre må den ytre scissoren gjenopprettes etter at compositen for hvert krittstrøk er ferdig. Hvis denne linjen for gjenoppretting av tilstand fjernes, vil påfølgende penner, bilder eller utvalg fortsette å bli klippet av grensene til det forrige krittstrøket, slik at de ser ut som manglende strøk eller manglende bilder.
Krittets grain blir fortsatt samplet fra absolutte canvas-koordinater, ikke fra lokale UV-er i rektangelet. Å flytte scissor endrer bare hvilke piksler GPU-en behandler; det endrer ikke hvilket sted i grain-teksturen hver piksel leser fra. Derfor oppstår det ingen tekstursømmer mellom tilstøtende rektangler, og å dra lerretet får ikke grain til å flytte seg.
Hvis vi bare ser på pikselarbeidsmengden, ligger det nye arbeidsområdet nær
der
Hvorfor krittstrøk med samme farge ikke ble batch-et
De fleste krittstrøkene på siden deler samme farge og tetthet, og det er fristende å tegne dusinvis av strøk inn i scratch samtidig og bare komponere én gang. Det ville redusere antallet render pass ytterligere, men endre farge- og grain-semantikken i overlappende områder.
Tenk på et bevisst forenklet tilfelle: To strøk deler den samme grain-gate-verdien
mens det å slå sammen kroppene først og så bruke én gate gir
Forskjellen er
Nøyaktig batching krever at vi beviser at strøkpikslene er gjensidig disjunkte, eller at vi allokerer et uavhengig atlasområde for hvert strøk og komponerer i den opprinnelige rekkefølgen. Den endelige godkjenningen på enheten beholdt scissor-tilnærmingen, så denne runden innførte verken et atlas eller kompleksiteten ved å administrere det.
Fra en milliard piksler tilbake til noen få millioner
Endelige målinger på enheten:
| Scenario | Før rettingen | Presis scissor |
|---|---|---|
| 70 synlige krittstrøk, 300% zoom, skriving | GPU 52–60 ms | ≈ 9–10 ms |
| ≈ 121 synlige krittstrøk, 300% zoom | GPU 77–80 ms | 13.7–15.6 ms |
| Teoretisk rektangelomfang per bilde (scratch + composite) | 783 M–1.34 B piksler | ≈ 1.7 M–3 M piksler |
Vi verifiserte også klippegrensene med 3,452 strøk og 202,710 samplingspunkter fra et dokument på enheten. Ved 0.5×, 1×, 2×, 3×, 5× og 8× zoom ble 186,408 viewport-tilfeller generert; hvert point sprite som kunne produsere non-zero coverage, lå innenfor den beregnede scissoren. Kontrollen dekket lerretskanter, viewport-kanter og ulike kombinasjoner av forskyvninger.
Den endelige koden bytter ikke til en LOD med lav oppløsning basert på interaksjonstilstanden. Lave zoomnivåer viser fortsatt blekkteksturen for hele siden; høye zoomnivåer tegner fortsatt synlige strøk på nytt som vektorer. På samme side av terskelen erstatter ikke pen-down og panorering eksisterende strøk med en annen skarphetsgrad. Under vektorgjengivelse ved høy zoom dekker scratch clear og composite for hvert krittstrøk bare strøkets egen bounding box på skjermen.
GPU-tid fanget ikke opp skarphet
GPU-ytelsesproblemer skalerer ikke nødvendigvis med den mest synlige mengden i datastrukturen. I dette tilfellet var 3,571 inndatapunkter en lett mistenkt; det som bestemte bildetiden, var fullskjermsarbeidet som hvert av de 70 krittstrøkene utløste, sammen med byttingen av render pass.
Visuell semantikk begrenset også de tilgjengelige optimaliseringene. Scratch per strøk, den opprinnelige komposisjonsrekkefølgen og absolutte canvas-grain-koordinater kunne ikke fjernes etter ønske. Samme farge og samme tetthet betyr bare at parametrene samsvarer — det beviser ikke at overlappende resultater kan slås sammen.
Tilbakemeldinger fra den virkelige enheten avviste versjonen med lavest GPU-tid. Kommentaren «hele lerretet blir uklart» ga produktbegrensningen som målingen alene ikke hadde uttrykt: Når Apple Pencil berører skjermen, ser brukeren også på de eksisterende strøkene.
Den endelige versjonen introduserer ikke et nytt interaksjonsbufferlag og reduserer ikke skarpheten. Vektorgjengivelse ved høy zoom begrenser bare arbeidet for hvert krittstrøk til dets egen bounding box på skjermen. Etter at den ble lastet inn på LucasPad igjen, ble tilbakemeldingen: «Ser bra ut.»