1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
# 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`에 쓰기).
## 조판: 격자가 놓고, 글꼴이 짠다
칸은 터미널의 단위다. 사람이 보는 화면에서 고정폭 칸에 한 글자씩 두면 아랍 문자는 이어지지 않고, 데바나가리는 결합자를 못
만들고, 라틴은 커닝이 없다. 칸마다 그리는 길은 셰이핑([004](004-text.md))을 쓸 자리가 없다. 그렇다고 칸의 레이아웃을 버리고
박스 레이아웃을 새로 두면 TUI와 GUI가 두 레이아웃이 된다. 그래서 레이아웃은 칸의 것을 그대로 쓰고, 글을 짜는 일만 픽셀이 한다.
- `vega_draw`는 칸을 채우면서 쓴 글을 스팬으로도 남긴다(`VegaSpan`: 행, 받은 열 [column, end), 칸이 닿은 곳, UTF-8, 톤).
같은 펜이 멈춘 자리에서 이어 쓴 스팬은 앞 스팬을 따른다(필드 이름과 값, 도움말 줄, 입력 줄). 펜을 옮겨 쓴 스팬(표의 열,
목록의 라벨, 배지)은 제 열에서 새로 시작한다. 배지는 끝에 붙인다.
- 첫 글꼴이 셰이핑을 가지면(`--shape`로 만든 심볼 파일) 픽셀은 글을 짠다. 스팬과 그것을 따르는 스팬들이 한 줄의 글이
된다. 한 문단으로 짜므로(`vega_layout`, 방향은 첫 강한 문자에서) 양방향과 커닝이 스팬 경계를 넘는다. 글리프의 색은 그
문자가 온 스팬의 톤이다. 줄은 제 열에서 시작해, 스팬들의 끝과 행의 다음 스팬의 열 중 가까운 곳까지 쓸 수 있다. 넘치면 자소
클러스터 경계에서 자르고 말줄임표를 붙인다(이분 탐색, 들어가는 가장 긴 앞부분). 입력 줄에는 글 뒤에 커서를 그린다.
- 글꼴은 `--symbols`를 되풀이해 선호 순서로 준다(최대 8개). 클러스터마다 그것을 가진 첫 글꼴로 간다. 칸의 크기는 첫 글꼴의
숫자와 줄이다. 다른 글꼴의 글리프도 같은 기준선에 놓이고, 줄 밖으로 나가는 잉크(데바나가리의 아래 모음 부호 등)는 자르지
않는다. 첫 글꼴이 셰이핑을 갖지 않으면 전처럼 칸마다 그리되, 칸의 글자도 그것을 가진 첫 글꼴에서 가져온다.
- 셰이핑할 수 없는 줄(어느 글꼴도 그 문자 체계의 계획이 없다)은 그 스팬들의 칸으로 그린다.
DejaVu Sans(라틴·아랍), Noto Sans CJK KR(한글), Noto Sans Devanagari를 20px로 쌓은 todo 화면에서 한글 항목, 결합한 아랍
낱말, 데바나가리 결합자(स्त), 커닝(AV, Wa), 한 줄 안의 양방향("hello 123 مرحبا world"), 끝에 붙은 배지, 말줄임표, 커서를
눈으로 확인했다. 100×50칸(1200×1200)에 여러 문자 체계의 줄 50개를 짜는 데 프레임마다 약 2.9 ms가 더 든다(칸으로 그리기
대비, 40프레임 0.39 s 대 0.28 s, PPM 쓰기 포함).
## 디스플레이(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개).
조판은 11번의 둘째 부분이다. DejaVu Sans, Noto Sans CJK KR, FreeSans(데바나가리)를 쌓는다. 목록에 혼자 있는 한 문자 체계의
라벨(라틴 커닝, 한글, 아랍, 데바나가리)은 그것을 가진 글꼴 하나로 `vega-symbols paragraph`가 짠 그림과 픽셀마다 같다(커버리지
±4, 57,408픽셀). 그래서 클러스터마다 맞는 글꼴로 가서 레이아웃이 말하는 대로 셰이핑되고 놓였다. 클러스터를 늘 첫 글꼴로
보내는 변이를 넣으면 한글 라벨에서 잡는다. 여러 라벨에서는 행에 안 들어가는 라벨이 행 안에서 잘리고, 글이 없는 행은 비어
있고, 입력 줄에 커서가 있다.
## 다음
- 바뀐 칸만 다시 그리고 복사한다(TUI가 이미 칸마다 보인 것을 기억한다). 커밋을 기다리지 않고 페이지 넘김 이벤트로 맞춘다.
- 입력: 한글 입력기(지금은 US 배열의 글자만 친다), 제스처(끌기로 스크롤).
- 조판의 캐시: 바뀌지 않은 줄은 다시 짜지 않는다(지금은 프레임마다 모든 줄을 짠다). 합성은 GPU로(아틀라스,
[002](002-symbol-format.md)).
- 칸이 아닌 글자 폭으로 놓는 표와 여러 줄 문단(지금은 한 줄을 받은 열 안에 짠다).
|