# 003 — 픽셀 표면: 칸에서 픽셀로 곧장 2026-09-27. Pure [로드맵](/srv/git/pure.git) 장기 1 "Vega 종합 인터페이스"의 첫 단계. 착수 기준: - 바꿀 결정: 사람에게 보이는 그림(GUI)을 브라우저(웹 표면)에 맡길지, Vega가 픽셀까지 직접 그릴지. 직접 그린다면 뷰에서 픽셀까지 무엇을 거칠지(가상 DOM, 씬 그래프를 두지 않고). - 최소 확인: TUI가 이미 만드는 칸의 격자(`VegaGrid`, 보이는 칸만 평가한다)를 심볼 형식의 글리프로 그려, 같은 스크립트에서 TUI의 글과 같은 프레임이 나오고 글자가 옆 칸으로 번지지 않는지 본다. 화면이 없는 Pi에서 이미지 파일로 확인한다. ## 한 것 `vega pixels MODULE --symbols FONT.vsym [--size PX] --grid WxH --out PREFIX < script`: TUI의 dump와 같은 스크립트를 받아 `show`마다 그 글을 쓰고 `PREFIX-N.ppm`에 그림을 쓴다. 경로는 짧다. 앱의 뷰가 칸(`vega_draw`, 화면에 든 칸만 평가)이 되고, 칸이 글리프가 되어 픽셀에 합성된다(`surfaces/pixels.c`). 중간에 문서나 장면 트리가 없다. - 글리프는 크기마다 처음 쓸 때 한 번 정확 넓이로 굽는다(`vega_glyphs`, 심볼 파일의 곡선에서, [002](002-symbol-format.md)). - 칸은 숫자 하나만큼 넓고 글꼴의 줄만큼 높다. 줄은 심볼 파일의 올림과 내림(3판에서 더한 헤더의 두 값)이다. 넓은 글자(한글, CJK)는 두 칸을 쓴다. - 톤마다 색이 있고, 포커스 행은 배경이 다르다. 심볼에 없는 글자는 칸에 상자를 그린다. - 글꼴은 차례로 채운다(`vega-symbols build --font A --codes … --font B --codes …`). 앞 글꼴에 없는 코드만 뒤 글꼴에서 가져온다. 처음에는 한 CJK 글꼴(Noto Sans Mono CJK KR)만 썼는데, 동아시아 폭이 모호한 글자(화살표 등)를 그 글꼴은 전각으로 그려 한 칸인 격자에서 옆 칸으로 번졌다. 검사가 그것을 잡았다. 라틴 고정폭(DejaVu Sans Mono) 다음에 한글을 채우면 번지지 않는다. ## 장치가 말한 것 Raspberry Pi 5. todo 예제에 한글 항목 넷, 48×12칸, 20px: 실행 전체(3.6 MB 심볼 파일 열기, 글리프 약 40개 굽기, 그리기, PPM 쓰기)가 7 ms다. 192×45칸(1920×1350)은 프레임마다 12 ms다(그리기, 합성, 7.8 MB PPM을 `/dev/shm`에 쓰기). ## 디스플레이(KMS) `vega display MODULE --symbols FONT.vsym [--size PX] [--card PATH] [--writeback WxH --out PREFIX] < script`: 같은 프레임을 디스플레이에 낸다(`surfaces/display.c`). 라이브러리 없이 커널의 ioctl로 한다. 연결된 커넥터를 찾아 그 모드를 CRTC에 걸고, 주 평면(primary plane)에 덤 버퍼 둘 중 하나를 atomic 커밋으로 넘긴다. 격자는 화면에 드는 만큼의 칸이다. 프레임은 캐시되는 메모리에 그린 뒤 덤 버퍼로 복사한다. 덤 버퍼는 write-combine 메모리여서 글리프를 섞으려고 그 메모리를 읽으면 느리다. 처음에는 그 버퍼에 바로 그렸고, 1080p 한 장에 약 370 ms가 들었다. 이 Pi(vc4)는 HDMI가 비어 있어도 writeback 커넥터가 둘 있다. 디스플레이 컨트롤러가 내보낸 프레임을 메모리로 되돌려 쓰는 출력이다. `--writeback`은 그 커넥터에 모드를 걸고, 프레임마다 돌아온 그림을 그린 것과 대 보고 PPM으로 쓴다. 1920×1080에서 프레임 하나가 20.6 ms다(합성, 복사, 커밋과 수직 동기 한 번(60 Hz, 16.7 ms), 캡처를 읽어 대 보고 6 MB PPM을 쓰기까지). 돌아온 그림은 늘 그린 것과 같았다(다른 픽셀 0). ## 입력(evdev) `--live`는 스크립트 대신 사람의 입력을 받는다(`surfaces/input.c`, 라이브러리 없이 커널의 입력 사건을 읽는다). 키보드는 Pure의 키 이름(`KeyboardEvent.code`의 철자)과 US 배열에서 그 키가 치는 글자가 되고, TUI가 쓰는 것과 같은 처리(`handle_key`, `handle_text`: 입력 줄이면 글이, 아니면 키가 된다)로 간다. 마우스는 움직임과 왼쪽 단추와 휠이, 터치 화면은 장치의 범위를 화면에 맞춘 위치와 누름이 된다. 누른 픽셀의 줄이 곧 행이다(`vega_hit`). 마우스 포인터는 그 아래 칸의 테두리로 보인다. 신호(SIGINT, SIGTERM)에서 멈춘다. `--input FILE`은 파일에 기록된 사건(`struct input_event`, 위치는 화면 픽셀)을 끝까지 읽고 마지막 프레임을 글로 낸다. 화면도 키보드도 없는 Pi에서 이 경로로 확인한다(`/dev/uinput`은 root만 쓴다). ## 확인 `tests/check.py` 11번과 12번. 12번의 둘째 부분은 기록된 사건(Shift로 대문자를 치고 Enter, Tab, Space, 첫 항목 행을 터치)으로 디스플레이를 돌린다. 마지막 프레임이 같은 키와 클릭을 받은 TUI의 프레임과 줄마다 같고, writeback으로 돌아온 그림이 그린 것과 같다(사건 62개). 12번은 writeback 커넥터가 있으면 같은 스크립트를 디스플레이로 돌린다. 프레임마다 돌아온 그림이 그린 것과 같고, 같은 격자의 픽셀 표면 그림과 픽셀 하나까지 같다(640×480에서 프레임 둘, 589,512 픽셀). `tests/check.py` 11번. 같은 스크립트를 `tui --dump`와 `pixels`로 돌려 글이 같고, 그림에서 빈 칸은 그 행의 배경(포커스 행은 제 색)뿐이며 글자가 있는 칸에는 잉크가 있다(프레임 둘, 칸 1,024개). ## 다음 - 바뀐 칸만 다시 그리고 복사한다(TUI가 이미 칸마다 보인 것을 기억한다). 커밋을 기다리지 않고 페이지 넘김 이벤트로 맞춘다. - 입력: 한글 입력기(지금은 US 배열의 글자만 친다), 제스처(끌기로 스크롤). - 비례 글꼴의 줄: 칸이 아닌 글자 폭으로 놓는 표와 문단. 합성은 GPU로(아틀라스, [002](002-symbol-format.md)).