Liitutyökalun suorituskyky: koko näytön pyyhkäisyistä scissor-suorakulmioihin
Lulucat Notesin liitutyökalu hidastui tiheän käsinkirjoituksen alueilla. Pullonkaula eivät olleet 3,571 syötenäytettä, vaan 70 koko näytön scratch-pyyhkäisyä ruutua kohti. Hylätty matalan resoluution välimuisti ja vedon oma scissor-suorakulmio kertovat loput.

Punaista ja sinistä liitua laitteen lopullisessa buildissa, 155 vetoa 255%:n zoomilla.
Lulucat Notesin liitutyökalussa oli tietynlainen suorituskykyongelma: tyhjässä kohdassa kirjoittaminen tuntui sujuvalta, mutta kun siirryttiin alueelle, joka oli jo täynnä liitu-uria, kynän kärki alkoi jäädä jälkeen. Kun samalla alueella jatkoi kirjoittamista, myös kanvaasin panorointi hidastui vähitellen.
Yksi tavallinen käsinkirjoitussivu riitti laukaisemaan ongelman: 300%:n zoom, paikallisella alueella näkyvät 70 liituvetoa ja yhteensä 3,571 syötteen näytepistettä. Tyhjät alueet pysyivät sulavina; hitaaksi muuttui vain alue, johon vedot keskittyivät.
Korjauksen jälkeen samalle sivulle voi kirjoittaa edelleen uutta tekstiä 255%:n zoomilla, ja olemassa olevat vedot säilyttävät täyden terävyytensä sekä kynän painamisen aikana että panoroinnissa.

Lopullinen laitekuvakaappaus, yhteensä 155 vetoa. Tällä zoomaustasolla olemassa olevien vetojen terävyys ei vaihdu hetkellisesti kynää painettaessa eikä panoroinnin aikana.
Miksi liitu tarvitsee scratch-tekstuurin
Tavallinen kynä voi yhdistää jokaisen pyöreän leiman suoraan mustetekstuuriin source-over-sekoituksella. Liitu lisää rakeisuutta ohjaavan kerroksen: renderöijä kerää ensin kokonaisen vedon rungon peiton ja syvyyden, käyttää sitten kiinteää grain-tekstuuria määrittämään, mitkä kohdat saavat liitupölyä, ja yhdistää lopuksi tuloksen olemassa olevaan musteeseen.
Tämä scratch-tekstuuri eristää yhden liituvedon. Eristys on tärkeää, koska saman vedon leimat menevät voimakkaasti päällekkäin; jos jokainen leima käsiteltäisiin grain-ohjauksella erikseen, vedon keskilinjaan kertyisi väriä toistuvasti ja liidun huokoset siirtyisivät näytteenottotiheyden mukana.
Suurella zoomilla Lulucat Notes piirtää nykyisessä viewportissa näkyvät vektorivedot uudelleen. Vanha toteutus teki jokaiselle näkyvälle liituvedolle seuraavat vaiheet:
- Pääasiallisen render encoderin lopettaminen;
- Scratch-tekstuurin tyhjentäminen;
- Tämän yhden liituvedon piirtäminen scratchiin;
- Pääasiallisen render encoderin avaaminen uudelleen;
- Scratchin yhdistäminen takaisin drawableen koko näytön kolmion avulla.
iPadin drawable oli 2732×2048 — noin 5.6 miljoonaa pikseliä. Jokainen liituveto käynnisti yhden scratch-pyyhkäisyn ja yhden koko näytön kompositoinnin. Seitsemänkymmentä liituvetoa tarkoitti noin 141 render encoderia ja 70 koko näytön kompositointia.
Olkoon näkyvien liituvetojen määrä
Jokaiseen liituvetoon liittyi myös kiinteä render-pass-ylikustannus, joten tämäkin kustannus kasvoi lineaarisesti
Mittaukset tehtiin 12.9 tuuman iPad Prolla (5. sukupolvi, M1), jossa oli iPadOS 18.6.2. Vertasimme saman viewportin GPU-aikaleimoja ennen muutosta ja sen jälkeen käyttäen command bufferin aikaleimoja samassa Debug-laitebuildissa tällä nimenomaisella iPadilla — kutsumme sitä jäljempänä nimellä LucasPad. Alla olevat vaihteluvälit ovat usean ruudun lokien tyypillisiä vaihteluita, eivät toimitettavan version ruutunopeuslupauksia. Kun näkyvissä oli 70 liituvetoa, yksi ruutu vaati tyypillisesti 52–60 ms GPU-aikaa; noin 120 vedon alueella GPU-aika nousi lukemiin 77–80 ms.
Kun scratch- ja composite-pyyhkäisyjen koko näytön suorakulmioiden pinta-alasta arvioidaan, ruudun teoreettinen työmäärä kasvoi noin 783 miljoonasta pikselistä 1.34 miljardiin pikseliin. Luku on suorakulmioiden pinta-alojen summa, eikä se vastaa fragmenttikutsujen määrää, videomuistin luku-/kirjoitustavuja tai GPU:n laitteistolaskureita. Metalin fast clear, attachment load/store ja passien vaihto pysyvät GPU:n ja ajurin hallinnassa.
Tämä selittää myös, miksi tyhjät alueet pysyivät sulavina. Näkyvyyden perusteella tehtävä rajaus ohittaa viewportin ulkopuoliset vedot; tyhjällä alueella
Väärä vastaus lukemassa 0.85 ms
Sovelluksella oli jo koko sivun mustetekstuuri, joka oli leivottu kaksi pikseliä pistettä kohti. Kokeilimme näyttää tätä tekstuuria suoraan kirjoittamisen, panoroinnin ja zoomauksen aikana ja pitää vain kulloisenkin Apple Pencil -vedon elävänä vektorina; vuorovaikutuksen päätyttyä yksi lisäruutu renderöisi korkean resoluution vektorituloksen uudelleen.
Tämä lähestymistapa toimi erittäin hyvin. Samalla tiheällä alueella 300%:n zoomilla GPU-aika putosi lukemaan 0.84–0.85 ms eikä enää kasvanut olemassa olevien liituvetojen määrän mukana.
Todellisen laitteen ongelma oli yhtä selvä. 300%:n zoomilla tarvittiin noin kuusi näyttöpikseliä pistettä kohti, mutta välimuisti tarjosi vain kaksi. Heti kun Apple Pencil kosketti näyttöä, kaikki olemassa olevat vedot muuttuivat pehmeäksi, matalan resoluution kuvaksi; kun Pencil nostettiin, ne palautuivat napsahtaen täyteen terävyyteen.
Testaaja sanoi yhden asian: ”Kun kirjoitan, koko kanvas muuttuu sumeaksi. Se kirkastuu heti, kun nostan kynän.”
Poistimme optimoinnin. 0.85 ms oli mitatuista tuloksista pienin, mutta liitutyökalu ei sen perusteella ollut hyväksyttävä. Olemassa olevat vedot ovat osa kirjoittamisen palautetta; niiden terävyys ei saa muuttua kynän painamisen hetkellä.
Kunkin liituvedon rajaaminen omaan suorakulmioonsa
Lopullinen korjaus säilytti vedon oman scratchin ja vedon oman kompositoinnin ja pienensi vain niiden pikselityön laajuutta. Jokaisella vedolla oli jo kanvaasin bounding box, joka oli johdettu kaikkien sen leimasäteiden yhdisteestä. Renderöijä muuntaa tämän bounding boxin nykyisen viewportin drawable-koordinaateiksi ja lisää siihen kaksi pikseliä antialiasing-marginaalia varten:
Samaa scissor-suorakulmiota käytetään sitten kolmeen asiaan: scratchin tyhjentämiseen, vedon piirtämiseen ja tuloksen yhdistämiseen takaisin pääpinnalle.
let rect = displayScissorRect(for: stroke.bounds, viewport: viewport)
scratchEncoder.setScissorRect(rect)
clearScratchExplicitly()
drawStrokeIntoScratch(stroke)
mainEncoder.setScissorRect(rect)
compositeChalkFromScratch(stroke)
mainEncoder.setScissorRect(fullDrawable)
Samaa logiikkaa käytetään myös 4096²-mustetekstuuriin leipomiseen ja osittaiseen uudelleentoistoon, joten suurennetun näkymän ja vakiintuneen mustekerroksen liidukäytös ei eroa toisistaan.
Tässä on kaksi helposti unohtuvaa yksityiskohtaa.
Ensinnäkin render passin loadAction = .clear tapahtuu attachmentin latausvaiheessa, eikä rasterointiscissor rajoita sitä. Sen käyttäminen edelleen tyhjentäisi koko scratch-tekstuurin. Korjattu pass käyttää .dontCare-asetusta ja piirtää sen jälkeen clear_fragment-fragmentin scissorin sisällä. Tämä suorakulmio kirjoitetaan kokonaan, ja composite lukee vain saman suorakulmion, joten attachmentin vanhaa sisältöä ei tarvitse ladata.
Toiseksi jokaisen liituvedon kompositoinnin valmistuttua ulompi scissor on palautettava. Jos tämä tilan palautusrivi jätetään pois, seuraavat kynät, kuvat tai valinnat leikkautuvat edelleen edellisen liituvedon rajojen mukaan ja näyttävät puuttuvilta vedoilta tai kuvilta.
Liidun grain näytteistetään edelleen absoluuttisista kanvasikoordinaateista eikä suorakulmion paikallisista UV-koordinaateista. Scissorin siirtäminen muuttaa vain sitä, mitä pikseleitä GPU käsittelee; se ei muuta sitä grain-tekstuurin kohtaa, josta pikseli lukee. Vierekkäiset suorakulmiot eivät siksi tuota tekstuurisaumoja, eikä kanvaasin raahaaminen saa grainia vaeltamaan.
Kun tarkastellaan vain pikselityömäärää, uusi työmäärä on lähellä muotoa
missä
Miksi samanvärisiä liituvetoja ei yhdistetty eräajoksi
Useimmilla sivun liiduvetoilla on sama väri ja tiheys, joten olisi houkuttelevaa piirtää kymmeniä vetoja scratchiin kerralla ja tehdä composite vain kerran. Se vähentäisi render-passeja entisestään, mutta muuttaisi värin ja grainin semantiikkaa päällekkäisillä alueilla.
Tarkastellaan tarkoituksella yksinkertaistettua tapausta: kaksi vetoa jakaa tietyssä pikselissä täsmälleen saman grain-gate-arvon
kun taas runkojen yhdistäminen ensin ja yhden gaten käyttäminen sen jälkeen tuottaa
Ero on
Tarkka eräajo edellyttäisi, että todistetaan vetojen pikselien olevan toisensa poissulkevia, tai että jokaiselle vedolle varataan oma atlas-alue ja tulokset yhdistetään alkuperäisessä järjestyksessä. Lopullinen laitehyväksyntä säilytti scissor-lähestymistavan, joten tähän kierrokseen ei lisätty atlasia eikä sen hallinnan tuomaa monimutkaisuutta.
Miljardista pikselistä muutamaan miljoonaan
Lopulliset laitemittaukset:
| Skenaario | Ennen korjausta | Tarkka scissor |
|---|---|---|
| 70 näkyvää liituvetoa, 300%:n zoom, kirjoittaminen | GPU 52–60 ms | ≈ 9–10 ms |
| ≈ 121 näkyvää liituvetoa, 300%:n zoom | GPU 77–80 ms | 13.7–15.6 ms |
| Teoreettinen suorakulmioalue ruutua kohti (scratch + composite) | 783 M–1.34 B pikseliä | ≈ 1.7 M–3 M pikseliä |
Varmistimme leikkausrajat myös käyttämällä laitedokumentista peräisin olevia 3,452 vetoa ja 202,710 näytepistettä. Zoomauksilla 0.5×, 1×, 2×, 3×, 5× ja 8× luotiin 186,408 viewport-tapausta; jokainen point sprite, joka saattoi tuottaa nollasta poikkeavaa peittoa, jäi lasketun scissorin sisään. Tarkistus kattoi kanvaasin reunat, viewportin reunat ja erilaiset offset-yhdistelmät.
Lopullinen koodi ei vaihda matalan resoluution LOD-tasolle vuorovaikutustilan perusteella. Pienillä zoomaustasoilla näytetään edelleen koko sivun mustetekstuuri; suurilla zoomaustasoilla näkyvät vedot piirretään edelleen vektoreina. Kynnyskohdan samalla puolella kynän painaminen ja panorointi eivät korvaa olemassa olevia vetoja eri terävyystasolla. Suuren zoomin vektoripiirrossa kunkin liituvedon scratch-tyhjennys ja composite kattavat vain sen oman näytön bounding boxin.
GPU-aika ei mitannut terävyyttä
GPU-suorituskykyongelmat eivät välttämättä skaalaudu tietorakenteen näkyvimmän määrän mukana. Tässä tapauksessa 3,571 syöttöpistettä oli helppo epäilty; ruutuajan määritti kunkin 70 liituvedon käynnistämä koko näytön työ yhdessä niiden render-pass-vaihtojen kanssa.
Visuaalinen semantiikka rajoitti myös käytettävissä olevia optimointeja. Vedon omaa scratchia, alkuperäistä kompositointijärjestystä ja absoluuttisia kanvaasin grain-koordinaatteja ei voinut poistaa mielivaltaisesti. Sama väri ja sama tiheys tarkoittavat vain sitä, että parametrit täsmäävät — ne eivät todista, että päällekkäiset tulokset voi yhdistää.
Todellisen laitteen palaute hylkäsi version, jossa GPU-aika oli pienin. Huomio ”koko kanvas muuttuu sumeaksi” toi esiin tuotteen rajoitteen, jota mittaus yksin ei ollut ilmaissut: kun Apple Pencil koskettaa näyttöä, käyttäjä tarkkailee samalla myös olemassa olevia vetoja.
Lopullinen toteutus ei lisää uutta vuorovaikutuksen välimuistikerrosta eikä vähennä terävyyttä. Suuren zoomin vektoripiirto rajaa yksinkertaisesti kunkin liituvedon työn sen omaan näytön bounding boxiin. Kun se ladattiin jälleen LucasPadiin, palaute muuttui muotoon: ”Näyttää hyvältä.”