펜이 화면보다 빨라
iPad Pro 위의 Apple Pencil은 1초에 최대 240번 샘플링돼. UIKit은 뷰를 1초에 240번 깨우지 않아. 자기 박자대로 터치 이벤트를 보내고, 그 사이 샘플들은 그 이벤트 하나에 접어 넣어. 그림 앱이 다음에 뭘 해야 하는지 Apple 문서는 대놓고 말해. 이벤트에 coalescedTouches(for:)를 부르면, 지난번 전달 이후 보고된 샘플이 순서대로 전부 나와. 마지막 건 네가 받은 바로 그 터치의 사본이고. 이걸 안 부르면 이벤트마다 마지막 샘플 하나로만 칠하게 돼. 손은 곡선을 그렸는데 화면엔 다각형이 남지.
Ariandel 캔버스 뷰는 첫머리에 방침을 적어 둬. "every Pencil event's coalesced touches (240 Hz on iPad Pro) become StylusInputs in canvas space and go to the painter in one batch per event, so a frame renders exactly what the hand did." 코드는 첫 번째 코드 블록의 feed 함수야. 합쳐진 샘플을 받아서(물어볼 이벤트가 없으면 터치 하나만) 하나하나 1강의 그 유일한 문으로 바꿔. 마지막 건 획을 뗄 지점으로 따로 챙겨 두고, 배열은 통째로 continueStroke 한 번에 넘겨. Drawing Pad도 움직이는 터치를 똑같이 다루고, 손으로 그리는 선택 영역도 합쳐진 샘플마다 윤곽선을 늘려.
이벤트 하나, 배치 하나, 다시 그리기 한 번
"이벤트당 배치 하나"는 성능만이 아니라 구조를 정하는 선택이야. Painter.continueStroke 안에서 샘플들은 surface.begin()과 surface.end() 사이에서 libmypaint로 들어가. 원자적 배치 하나야. libmypaint가 댑을 줄 세우고, 서피스가 end()에서 그걸 타일에 그리고(2트랙), 그다음에야 Ariandel 자기 배치 단위 작업이 돌아. 클론과 듀얼 브러시 합치기, 그레인, 선택 영역 안에 물감 가두기(5트랙). 그러니까 합치기 덕분에 그 모든 작업이 샘플마다가 아니라 전달된 이벤트마다 한 번씩 돌아.
화면 쪽도 자기 속도를 솔직하게 말해. 캔버스는 preferredFramesPerSecond = 120인 MTKView인데, 멈춰 둔 채 enableSetNeedsDisplay를 켜 놨어. 필요할 때만 그린다는 뜻이야. continueStroke는 매번 화면 갱신을 요청하면서 끝나고, 캔버스를 만든 커밋(d24155e)은 이걸 "redrawing on demand at up to 120 Hz"라고 적어. 펜은 화면 최고 속도의 두 배로 샘플링하고, 그 둘을 맞춰 주는 게 배치야.
예측 터치: 검색 결과 0건, 일부러
UIKit은 같은 이벤트에 배열을 하나 더 줘. predictedTouches(for:). 펜이 다음에 어디 있을지 시스템이 추측한 값이야. 손보다 살짝 앞서 그려서 지연을 좀 숨기라는 거지. Ariandel 소스에서 predicted를 검색하면 터치 관련은 하나도 안 나와. 비슷하게 걸리는 건 Photoshop 리더가 ZIP "prediction"을 푸는 부분뿐인데, 그건 압축 용어야.
없다는 사실만으론 이유가 안 되니까, 이유를 말할게. 예측은 몇 밀리초 뒤 버려야 하는 초안이야. 획이 벡터 경로인 앱은 감당할 수 있어. 추측을 버릴 오버레이에 그리고, 다음 프레임에 지우면 돼. Ariandel 획은 경로가 아냐. 입력 하나하나가 libmypaint로 들어가고, libmypaint는 댑을 바로 타일에 박으면서 자기 상태도 앞으로 밀어. 다듬어진 위치, 다음 댑까지 이미 간 거리, 번짐 버킷, 랜덤 입력. 추측이 칠한 픽셀을 지우는 걸로는 모자라. 두 번째 코드 블록은 그 상태 중 딱 두 개만 가진 장난감이고, 컴파일해서 돌렸어. 예측 점 하나를 먹이고, 예측이 찍은 댑을 전부 지운 다음, 진짜 점들을 먹여. 그래도 획이 달라져. 댑이 줄고, 그중 둘은 선 밖에 떨어져. 예측을 정직하게 무르려면 매 프레임 타일과 붓 상태를 둘 다 떠 놨다가 되돌리거나, 추측 전용 엔진을 하나 더 돌려야 해.
그래서 Ariandel은 대신 지연을 치러. 그 지연이 손에 느껴질까? 아직 아무도 몰라. libmypaint를 고른 엔진 평가는 "latency under Apple Pencil"을 확인 안 한 항목에 올려 놨고, 2026-10-12 기준 cwkAriandel의 coop 기록 행 22개 중 지연을 언급한 건 하나도 없어. 결론 난 맞바꿈이 아니라 열린 질문이야.
추정값 갱신: 일부러 무시해
세 번째 흐름도 있어. 어떤 샘플은 UIKit이 힘이나 고도각, 방위각의 최종값을 제때 못 줘. 그래서 추정값을 먼저 보내고, 나중에 touchesEstimatedPropertiesUpdated(_:)로 다듬은 값을 보내. 짝은 estimationUpdateIndex로 맞추고. 벡터 앱이면 저장해 둔 점을 고치면 돼. Ariandel은 추정값 자체는 안 버려. 그 값을 실은 샘플은 다른 샘플처럼 배치에 들어가고, 힘과 기울기는 그때 UIKit이 준 그대로야. 버리는 건 나중에 오는 보정이야. 그래서 이 메서드를 빈 몸통으로 오버라이드하고, 방침을 주석으로 박아. "Force and tilt arrive refined a few milliseconds later; strokes use the coalesced values as delivered, which is what libmypaint expects." 다듬은 값이 도착할 즈음엔 그 값이 바꿨을 댑이 이미 타일에 있고, 붓 상태도 지나가 버렸어. 예측이랑 같은 제약이고 답도 같아. 커밋하는 엔진은 입력을 한 번, 순서대로 받아.