Kridtværktøjets ydeevne: fra fuldskærmspassager til scissor-rektangler
Lulucat Notes' kridt-værktøj blev langsommere i tætte håndskriftsområder. Flaskehalsen var ikke de 3,571 inputprøver — det var 70 fuldskærms-scratchpassager pr. frame. En afvist cache i lav opløsning og et scissor-rektangel pr. streg fortæller resten af historien.

Rødt og blåt kridt i test-buildet til enheden, 155 streger ved 255% zoom.
Kridt-værktøjet i Lulucat Notes havde et meget bestemt ydeevneproblem: Det føltes glat at skrive i et tomt område, men når man bevægede sig ind i et område, der allerede var fyldt med kridtstreger, sakkede pennespidsen bagud. Hvis man fortsatte med at skrive i det samme område, blev panorering af lærredet også gradvist langsommere.
En enkelt side med almindelig håndskrift var nok til at udløse problemet: 300% zoom, 70 synlige kridtstreger i lokalområdet og i alt 3,571 inputpunkter. Tomme områder forblev flydende; det var kun området med de koncentrerede streger, der blev langsomt.
Efter rettelsen kan den samme side fortsat modtage ny skrift ved 255% zoom, og eksisterende streger bevarer deres fulde klarhed både under pen-down og panorering.

Skærmbilledet fra enhedstesten, 155 streger i alt. Ved dette zoomniveau skifter klarheden i de eksisterende streger ikke midlertidigt, hverken under pen-down eller panorering.
Hvorfor kridt har brug for en scratch-tekstur
En almindelig pen kan compositere hvert cirkulært stempel direkte på blæktexturet med source-over-blanding. Kridt tilføjer et lag til kornstyring: Rendereren akkumulerer først kropsdækning og dybde for en hel streg, bruger derefter en fast korntekstur til at afgøre, hvilke positioner der modtager kridtstøv, og compositer til sidst resultatet på den eksisterende blæktexture.
Denne scratch-tekstur isolerer en enkelt kridtstreg. Isolationen er vigtig, fordi stemplerne i den samme streg overlapper kraftigt; hvis hvert stempel blev kornstyret individuelt, ville stregens centerlinje samle farve gentagne gange, og kridtporerne ville flytte sig med sample-tætheden.
Ved høje zoomniveauer tegner Lulucat Notes de vektortegninger, der er synlige i den aktuelle viewport, igen. Den gamle implementering udførte disse trin for hver synlig kridtstreg:
- Afslut den primære render encoder;
- Ryd scratch-texturen;
- Tegn denne kridtstreg ind i scratch;
- Åbn den primære render encoder igen;
- Composit scratch tilbage på drawable med en fuldskærmstrekant.
Semantikken for én streg var korrekt, men arbejdsområdet var langt større. iPadens drawable var 2732×2048 — omtrent 5.6 millioner pixels. Hver kridtstreg udløste én scratch-pass og én fuldskærmscomposite. 70 kridtstreger betød cirka 141 render encodere og 70 fuldskærmscomposites.
Lad antallet af synlige kridtstreger være
Hver kridtstreg havde også en fast overhead for renderpasset, så den omkostning skalerede ligeledes lineært med
Målingerne blev taget på en 12.9-tommers iPad Pro (5. generation, M1) med iPadOS 18.6.2. Vi sammenlignede GPU-tidsstempler fra den samme viewport før og efter ændringen ved hjælp af command-buffer-tidsstempler i den samme Debug-device-build på netop denne iPad — nedenfor kalder vi den LucasPad. Intervallerne herunder er typiske udsving fra logs over flere frames, ikke et løfte om frame rate i en build til distribution. Ved 70 synlige kridtstreger krævede en enkelt frame typisk 52–60 ms GPU-tid; i et område med omkring 120 streger steg GPU-tiden til 77–80 ms.
Hvis man estimerer arbejdsområdet pr. frame ud fra rektangelarealet for scratch- og composite-passagerne på hele skærmen, voksede det teoretiske område fra cirka 783 millioner pixels til 1.34 milliarder pixels. Tallet er summen af rektangelarealer og svarer ikke til antal fragmentinvokationer, bytes læst/skrevet i videohukommelsen eller GPU-hardwaretællere. Metals fast clear, attachment load/store og passkift styres stadig af GPU’en og driveren.
Det forklarer også, hvorfor tomme områder forblev glatte. Visibility culling springer streger uden for viewporten over;
Et forkert svar ved 0.85 ms
Appen havde allerede en inktexture for hele siden, forbagt ved to pixels pr. point. Vi prøvede at vise denne texture direkte under skrivning, panorering og zoom og kun beholde den aktuelle Apple Pencil-streg som live-vektor; når interaktionen sluttede, ville én ekstra frame gengive vektorresultatet i høj opløsning igen.
Denne tilgang fungerede meget godt. I det samme tætte område ved 300% zoom faldt GPU-tiden til 0.84–0.85 ms og voksede ikke længere med antallet af eksisterende kridtstreger.
Problemet på den faktiske enhed var lige så tydeligt. Ved 300% zoom var der brug for omtrent seks skærmpixels pr. point, men cachen leverede kun to. I det øjeblik Apple Pencil rørte overfladen, blev alle eksisterende streger til et blødt billede i lav opløsning; når Apple Pencil blev løftet, sprang de tilbage til fuld klarhed.
Testeren sagde én ting: “Når jeg skriver, bliver hele lærredet sløret. Det bliver klart igen, så snart jeg slipper.”
Optimeringen blev fjernet. 0.85 ms var det laveste målte resultat, men det var ikke et acceptabelt kridt-værktøj. Eksisterende streger er en del af skrivefeedbacken; deres klarhed må ikke ændre sig ved pen-down.
Hver kridtstreg begrænses til sit eget rektangel
Den implementerede rettelse bevarede scratch pr. streg og compositing pr. streg og reducerede kun deres pixelarbejdsområde. Hver streg havde allerede en afgrænsningsboks for lærredet, der var afledt af unionen af alle dens stempelradier. Rendereren transformerer denne afgrænsningsboks til drawable-koordinater i den aktuelle viewport og paddinger den med to pixels som antialiasing-margin:
Det samme scissor-rektangel bruges derefter til tre ting: at rydde scratch, tegne stregen og compositere resultatet tilbage på hovedoverfladen.
let rect = displayScissorRect(for: stroke.bounds, viewport: viewport)
scratchEncoder.setScissorRect(rect)
clearScratchExplicitly()
drawStrokeIntoScratch(stroke)
mainEncoder.setScissorRect(rect)
compositeChalkFromScratch(stroke)
mainEncoder.setScissorRect(fullDrawable)
Den samme logik bruges også til forbagning og delvis afspilning på 4096²-inktexturet, så high-zoom-visningen og det færdige ink-lag ikke giver to forskellige kridtadfærd.
Der er to detaljer, som er nemme at overse.
For det første sker loadAction = .clear for et renderpass under attachment-loadfasen og er ikke begrænset af rasteriserings-scissoren. Hvis vi fortsatte med at bruge det, ville hele scratch-texturen stadig blive ryddet. Det faste pass bruger .dontCare og tegner derefter en clear_fragment inden for scissoren. Dette rektangel skrives efterfølgende helt, og compositen læser kun det samme rektangel, så det gamle attachment-indhold behøver ikke indlæses.
For det andet skal den ydre scissor gendannes, når compositen af hver kridtstreg er færdig. Hvis denne linje til gendannelse af tilstanden udelades, vil efterfølgende penne, billeder eller markeringer fortsat blive klippet af den forrige kridtstregs grænser og se ud som manglende streger eller billeder.
Kridtkornet samples stadig fra absolutte canvas-koordinater i stedet for lokale UV’er i rektanglet. Når scissoren flyttes, ændres kun de pixels, GPU’en behandler; den ændrer ikke, hvilken kornteksturposition den enkelte pixel læser fra. Derfor giver naborektangler ingen tekstursømme, og kornet driver ikke, når lærredet trækkes.
Hvis vi kun ser på pixelarbejdet, ligger det nye arbejdsområde tæt på
hvor
Hvorfor kridtstreger med samme farve ikke blev batchet
De fleste kridtstreger på siden har samme farve og tæthed, og det er fristende at tegne snesevis af streger ind i scratch på én gang og kun compositere én gang. Det ville reducere antallet af renderpassager yderligere, men ændre farve- og kornsemantikken i overlappende områder.
Se på et bevidst forenklet tilfælde: To streger deler den samme grain-gate-værdi
mens en sammenlægning af kroppene først og derefter én gate giver
Forskellen er
Præcis batching kræver, at man beviser, at stregpixels er indbyrdes disjunkte, eller tildeler et uafhængigt atlasområde til hver streg og compositerer dem i den oprindelige rækkefølge. Accepttesten på enheden beholdt scissor-tilgangen, så denne runde tilføjede hverken et atlas eller kompleksiteten ved at administrere det.
Fra en milliard pixels tilbage til få millioner
Målingerne på enheden:
| Scenarie | Før rettelsen | Præcis scissor |
|---|---|---|
| 70 synlige kridtstreger, 300% zoom, skrivning | GPU 52–60 ms | ≈ 9–10 ms |
| ≈ 121 synlige kridtstreger, 300% zoom | GPU 77–80 ms | 13.7–15.6 ms |
| Teoretisk rektangelområde pr. frame (scratch + composite) | 783 M–1.34 B pixels | ≈ 1.7 M–3 M pixels |
Vi kontrollerede også klipningsgrænserne med 3,452 streger og 202,710 målepunkter fra et device-dokument. Ved 0.5×, 1×, 2×, 3×, 5× og 8× zoom blev der genereret 186,408 viewport-tilfælde; hvert point sprite, der kunne give non-zero coverage, lå inden for den beregnede scissor. Kontrollen dækkede lærredskanter, viewport-kanter og forskellige kombinationer af offsets.
Den valgte implementering skifter ikke til en LOD i lav opløsning baseret på interaktionstilstand. Lave zoomniveauer viser stadig inktexturet for hele siden; høje zoomniveauer tegner stadig synlige streger igen som vektorer. På samme side af tærsklen erstatter pen-down og panorering ikke eksisterende streger med et andet klarhedsniveau. Under vektorgentegning ved højt zoom dækker scratch-clear og composite for hver kridtstreg kun dens egen afgrænsningsboks på skærmen.
GPU-tid fangede ikke klarheden
GPU-ydeevneproblemer skalerer ikke nødvendigvis med den mest synlige størrelse i datastrukturen. I dette tilfælde var de 3,571 inputpunkter en oplagt mistænkt; det, der bestemte frametiden, var fuldskærmsarbejdet udløst af hver af de 70 kridtstreger sammen med skift mellem renderpassager.
Visuel semantik begrænsede også de mulige optimeringer. Scratch pr. streg, den oprindelige compositing-rækkefølge og absolutte canvas-koordinater for korn kunne ikke fjernes efter behag. Samme farve og samme tæthed betyder kun, at parametrene matcher — det beviser ikke, at overlappende resultater kan flettes.
Feedback fra den faktiske enhed afviste versionen med den laveste GPU-tid. Bemærkningen “hele lærredet bliver sløret” gav den produktbegrænsning, som målingen alene ikke havde udtrykt: Når Apple Pencil rører overfladen, observerer brugerne også de eksisterende streger.
Den valgte implementering indfører ikke et nyt interaktions-cachelag og reducerer ikke klarheden. Vektorgentegning ved højt zoom begrænser blot hver kridtstregs arbejde til dens egen afgrænsningsboks på skærmen. Efter at den igen blev indlæst på LucasPad, lød feedbacken: “Det ser godt ud.”