Kimi K3와 함께 Lulucat Notes 렌더링 파이프라인 다시 구축하기
Core Graphics 타일 파이프라인을 여섯 개의 작은 단계로 Metal 포인트 스프라이트 파이프라인으로 바꾸고, 각 단계를 실제 iPad에서 검증했다. 코드는 Fireworks의 Kimi K3와 페어 프로그래밍으로 작성했다.
지난주 Lulucat Notes에서 올가미 선택을 드래그하면 눈에 보이는 파도가 생겼다. 같은 프레임 안에서 어떤 화면 타일은 선택을 새 위치에 보여 주는 반면, 다른 타일은 여전히 이전 위치를 보여 줬다. 렌더링 파이프라인 전체 — Core Graphics 비트맵과 CATiledLayer — 를 Metal로 교체했다. 여섯 개의 작은 단계로 진행했고, 다음 단계로 넘어가기 전에 각 단계를 실제 iPad에서 검증했다.
코드는 Fireworks에서 실행되는 Moonshot의 오픈 모델 Kimi K3와 페어 프로그래밍으로 작성했다. 이끌고, 결정하고, 테스트한 것은 사람이었고, 코드의 거의 모든 줄을 쓴 것은 모델이었다.
파이프라인
옛 파이프라인에는 서서히 갈라지는 두 종류의 그리기가 있었다. 스트로크는 저렴하게 표시하려고 비트맵에 구워 넣었고, 타일이 더 많은 디테일을 필요로 할 때마다 벡터로 다시 그렸다. 새 파이프라인에는 정확히 하나의 생각이 있다. 잉크처럼 보이는 모든 것은 포인트 스프라이트다. 펜 스트로크, 형광펜 쓸기, 지우개 찍기는 모두 같은 32바이트 정점 — 위치, 지름, 색 — 이며, 같은 셰이더 쌍이 스트로크의 호 길이를 따라 1포인트 간격으로 배치한 GPU 래스터화 원으로 그린다.
![]()
1포인트 간격에서 원의 사슬은 수학적으로 완벽한 캡슐에서 약 0.075포인트 벗어난다. 우리 캔버스 밀도에서 픽셀의 5분의 1이다. 그 대가로 세 도구가 하나의 코드 경로로 합쳐지고, GPU는 자기가 가장 잘하는 일을 한다.
그 생각을 중심으로 아키텍처는 단순하다. 커밋된 잉크는 하나의 4096² 텍스처에 산다. UIScrollView는 남겨 두되 순수한 제스처 엔진으로 강등했다. 매 프레임 contentOffset과 zoomScale이 뷰포트 유니폼을 채우므로 패닝과 줌은 아무것도 쓰지 않는다. 각 프레임은 다섯 번의 그리기다.
flowchart TB
subgraph frame["Every frame: five draws"]
direction TB
paper["1 · paper blit"] --> ink["2 · committed ink"]
ink --> live["3 · live stroke"]
live --> sel["4 · selection"]
sel --> dash["5 · lasso dashes"]
end
commit["stroke commit<br/>append stamps"] --> tex[("ink texture<br/>4096² render target")]
replay["regional replay<br/>erase · delete · move · undo"] --> tex
tex -. "sampled or re-drawn" .-> ink
편집은 텍스처에 직접 쓴다. 스트로크를 커밋하면 스탬프를 덧붙인다. 지우기, 삭제, 이동, 실행 취소는 시저 사각형을 기준으로 영향을 받은 영역을 다시 재생한다. 영역을 지우고, 그 영역과 교차하는 스트로크를 다시 그리면 끝이다. 부분 지우기는 이전 글의 소유권 의미를 유지한다 — 지우기는 잉크를 제거한 스트로크에 속한다 — 각 지워진 스트로크를 스크래치 텍스처에 그리고, destination-out 블렌딩(
이 모든 것을 시작한 선택도 이제 포인트 스프라이트로 그려진다. 끌면 유니폼 오프셋 하나가 바뀐다. 텍스처 쓰기 0, 타일 무효화 0 — 파도는 완화된 게 아니라 구조적으로 사라졌다.
문지기: 픽셀 차이
Core Graphics 렌더러를 삭제하지는 않았다. 오프라인 참조 구현으로 강등했고, 모든 Metal 변경은 기기에서 캡처한 실제 스트로크 데이터 위에서 이것과의 픽셀 비교를 통과해야 한다. 허용 기준은 “동일한 픽셀”이 아니다. 올바른 래스터라이저 두 개도 앤티에일리어싱된 가장자리를 따라 몇 단계의 회색에서 정당하게 어긋날 수 있다. 판정 기준은 구조적이다. 빠진 잉크도, 오프셋도, 색 드리프트도 없고, 잉크에서 벗어난 어느 곳에서도 큰 차이가 없어야 한다.
![]()
이 하네스는 오프라인 렌더러를 만드는 동안 마주친 버그 다섯 개 중 네 개를 잡았고, 모두 출력을 픽셀 단위로 크롭해 진단했다. Swift/Metal 구조체 스트라이드 불일치(28바이트 대 32바이트, Metal이 float4를 16에 정렬하므로 — 화면이 색 블록으로 가득 찼다), 프래그먼트 셰이더에서 [[point_size]]를 varying으로 읽지 못하는 문제(모든 스탬프가 정사각형으로 나왔다), 하나의 커맨드 버퍼 위에 두 렌더 인코더가 공존한 문제(모두 검정), 빠른 스트로크의 시작 스탬프가 빠져 첫 1밀리미터가 보이지 않은 문제다.
한 번의 재작성이 아니라 여섯 단계
이행 계획은 독립적으로 배포할 수 있는 여섯 단계였다. 픽셀 diff를 통과하는 오프라인 렌더러, 시각적 변화가 없는 디스플레이 셸, GPU 위의 라이브 스트로크, GPU 위의 선택, 텍스처에 직접 쓰는 변경, 고배율 벡터 재그리기(2단계에 접어 넣었다. “시각적 변화 없음”이 그것을 요구했으므로). 각 단계는 시뮬레이터나 스크린샷 diff가 아니라, 책상 위 iPad Pro에서 사람이 쓰고, 지우고, 줌하고, 드래그하는 것으로 끝났다.
기기는 모든 자동화 검사가 놓친 버그 세 개를 잡았다. 100% 줌 너머에서 스트로크가 두 번 그려졌다 — 아래에 부드러운 텍스처, 위에 날카로운 스프라이트 — 이것이 희미한 블러처럼 보여 사람은 몇 초 만에 알아챘다. 스트로크를 커밋할 때 한 프레임 동안 깜빡였는데, 옛 오버레이가 텍스처 갱신과 어긋나 크로스페이드 아웃됐기 때문이다. 그리고 170% 줌 너머에서 모든 노트가 사라졌다. 가시성 컬링 사각형이 스케일된 좌표 공간에서 contentOffset을 썼기 때문에, 줌할수록 스트로크에서 멀어졌다. 세 버그 모두 함수 하나당 한 줄을 고치는 정도의 수정이었고, 미리 작성할 수 있었을 어떤 테스트로도 잡아낼 수 없었다. 우리가 그것을 찾아야 한다는 것을 몰랐기 때문이다. UI 우선 소비자 앱에서 사람을 루프 안에 두는 이유가 바로 이것이다.
Kimi K3와 함께 일하는 느낌
무엇보다 빠르다. “논의하고, 쓰고, 빌드하고, 설치하고, 살펴보는” 루프가 몇 분 안에 돌았고, 빠르게 답하는 모델은 하루에 감당할 수 있는 루프 수를 바꿔 놓는다.
둘째, 과하게 설계하지 않는다. 이 코드베이스는 명시적인 하우스 룰 위에서 돈다 — 출시 전 하위 호환성 발판은 없고, 기기가 필요성을 증명할 때만 복잡성을 더한다 — K3는 일깨워 주지 않아도 이를 따른다. “나중을 위한” 공간 인덱스를 추가하지도, 모든 호출을 방어적 검사로 감싸지도, 추측성으로 추상화하지도 않았다. 프롬프트를 작성하는 느낌은 하우스 룰을 읽고 실제로 믿는 유능한 동료와 일하는 것과 같다.
셋째, 도구를 주면 열심히 쓴다. 이미지 유틸리티 — 보기, 픽셀 영역으로 크롭, 리사이즈 — 를 연결하자, 모델은 스스로 자기 렌더러 출력을 크롭해 위의 다섯 하네스 버그를 진단하기 시작했다. 도구가 모델에게 살펴보라고 상기시켰다.
나머지 절반: 이 이야기의 버그 대부분도 K3가 썼고, 노트를 사라지게 한 좌표 공간 버그도 그중 하나다. 한계는 실재한다. 작업을 안전하게 만든 것은 결코 모델이 옳다는 것이 아니었다. 렌더링 드리프트를 잡는 하네스와 사용감을 포착하는 사람이었다. 그래도 일상적으로는 우리가 함께 쓰는 최첨단 폐쇄형 모델 — Opus 급 시스템 — 과 안정적으로 구분할 수 없었다. 어떤 면에서는 분명 더 나았다. 더 빠르고, 코드베이스를 방어적 설계로 채우려는 경향이 훨씬 적었다.
앞으로 우리가 만들고 싶은 방식
대규모 명세 기반 에이전트 개발은 이제 그만한다 — 모델에게 큰 명세를 넘기고 무엇이 나와도 받아들이는 방식이다. 실패 양상은 나쁜 코드가 아니다. 아무도 이해하지 못하는 코드다.
여기서 통했고 우리가 계속할 것: 작은 단계, 각 단계는 시작하기 전에 논의하고, 만들기 전에 사람이 이해하고, 실제로 실행될 기기에서 검증한다. 모델의 역할은 빠르고, 정확하고, 불확실성에 정직한 것이다. 사람의 역할은 판단과 취향, 그리고 e2e 검증이다 — 특히 스펙이 “옳음”의 느낌을 서술할 수 없는 UI 우선 소비자 소프트웨어에서. Fireworks 위의 Kimi K3는 정확히 이 루프에 잘 맞는 것으로 드러났다. 루프를 타이트하게 유지할 만큼 빠르고, 단계를 작고 깔끔하게 유지할 만큼 똑똑했다.
파도는 사라졌고, 파이프라인은 두 개가 아니라 하나의 생각이 됐고, 우리를 그곳으로 이끈 과정은 계속된다.