덧칠

겉모습을 새로 칠하는 일은 더 보기 좋으라고 하는 작업입니다. 그런데 이번 주에 새로 칠한 면마다 그 아래 오래 숨어 있던 결함이 나타났습니다. 겉을 바꾸는 일이 어떻게 옛 침묵을 깨는 계측기가 되는지 정리해봤습니다.

덧칠

아직 만드는 중인 태양계 안에서 화물을 나르는 우주 트럭 게임의 우주선을 이번 주에 캡슐에서 트럭 차체로 바꿨습니다. 겉모습만 바꾼 작업이었습니다. 그런데 새 트럭이 화면에 처음 나타난 순간 바퀴가 아래가 아니라 옆을 향한 채 옆으로 누워 있었습니다. 판정을 담당하는 서버 코드는 한 줄도 건드리지 않았고 대표 시나리오의 상태를 해시로 고정해 둔 골든 테스트도 그대로 통과했습니다. 그러니 새로 그린 트럭 모델에 문제가 있다고 보는 것이 자연스러웠습니다. 원인은 트럭이 아니었습니다.

우주 트럭 게임의 트럭 차체를 바깥에서 본 화면입니다. 별밭을 배경으로 운전실과 바퀴가 달린 트럭 본체가 떠 있고 오른쪽에 행성 가장자리가 보이며, 아래 콘솔에 목적지 선택과 속도 109,610km/h 표시, 루나 기지에 화물을 배달하는 버튼과 거래 버튼이 있습니다.

낡은 벽에 페인트를 칠해보면 매끄러운 새 칠은 벽을 예쁘게 덮습니다. 그러면서 그 아래 울퉁불퉁함과 잔금이 오히려 도드라집니다. 낡은 표면이 눈감아 주던 것을 새 칠이 정면으로 보여 줍니다. 이번 주에 덧칠한 것은 트럭만이 아닙니다. 이전에 공개한 여러 게임과 도구의 겉을 새로 칠했고 그때마다 같은 일이 나타났습니다. 지난 '모형'에서 작지만 온전히 도는 게임을 혼자 완성하는 이야기를 했고 지난 '리허설'에서는 완성한 것을 어떻게 믿을 것인가를 이야기했습니다. 이번 주에는 겉을 바꾸는 일이 골든 테스트를 한 줄도 건드리지 않으면서 오래 숨어 있던 진실을 드러냈습니다. 골든 테스트는 대표 시나리오를 한 번 돌려 나온 상태를 정답으로 삼아 해시로 박아 두고 다음부터 그 값이 한 글자라도 달라지면 실패로 잡는 테스트입니다.

트럭이 옆으로 누운 원인은 자세를 나타내는 네 숫자짜리 값 하나였습니다. 1 뒤에 0이 셋 오는 이 값은 아무 회전도 없는 기본 자세처럼 보이지만 이 좌표계의 숫자 순서에서는 x축으로 180도 돌린 자세입니다. 캡슐 우주선은 위아래가 대칭이라 180도 뒤집혀 있어도 똑같아 보였고 그래서 이 결함은 이전 Space 게임을 공개한 날부터 지금까지 조용히 살아 있었습니다. 트럭에는 지붕이 있고 바퀴가 있습니다. 대칭이 깨진 그 겉모습이 처음으로 이 값을 화면에 보여 준 것입니다. 여기서 배운 것은 겉을 바꾸는 일이 새 기능을 더하는 일이 아닐 수도 있다는 점입니다. 그것은 옛 침묵을 깨는 계측기를 다는 일이고 그 계측기가 처음 낸 신호를 새로 만든 것의 버그로 읽으면 진짜 원인을 지나칩니다.

우주 트럭 게임의 새 트럭 차체가 달로 향하는 화면입니다. 붉은 궤적선을 따라 트럭 본체가 배기를 내면서 순항하고 유리 콘솔에 거래·궤도 진입·착륙 버튼이 있습니다.

같은 덧칠을 실측 천체를 항해하는 우주 시뮬레이션에서도 했습니다. 절차로 생성한 외계행성의 겉모습을 이번 주에 물리로 다시 그렸습니다. 모항성과의 거리에서 나오는 평형온도로 외형을 정하게 해서 별에 바싹 붙은 암석행성은 갈라진 지각에서 용암이 빛나는 세계로 온난하고 대기가 있는 세계는 대륙과 바다와 구름을 가진 세계로 나뉩니다. 이 덧칠이 그 아래 오래된 결함을 드러냈습니다. 절차생성을 통해 만든 외계행성 122개가 지금까지 전부 같은 목성으로 그려지고 있었습니다. 행성의 색을 고르는 코드가 행성의 종류가 아니라 이름으로 팔레트를 찾고 있었고 태양계 천체만 이름이 맞아떨어져 나머지 외계 거대행성은 조회에 실패해 전부 목성 팔레트로 그려졌습니다. 파란 얼음거인조차 갈색 목성으로 나왔습니다. 같은 항성계의 두 행성이 픽셀 단위로 똑같은 그림이었던 적도 있어서 스크린샷 한 장으로는 무엇이 바뀌었는지 보여 줄 수 없었습니다. 대표 시나리오의 골든 테스트 13개는 전부 그대로였습니다.

space 프로젝트의 절차 생성 외계행성. 별에 바싹 붙어 평형온도가 높은 암석행성이라 갈라진 지각 틈으로 용암이 빛나는 용암 세계로 그려집니다.
같은 생성기가 평형온도 하나로 나눈 해양 세계. 온난하고 대기가 있는 암석행성이라 대륙과 바다와 구름 띠를 가진 모습입니다.
같은 생성기가 그린 고리를 두른 외계 거대행성입니다. 청록빛 몸체를 기울어진 고리가 감싸고 뒤로 별밭이 펼쳐집니다.

미드웨이 해전을 재생하는 시뮬레이션에서도 같은 일이 나타났습니다. 함선과 항공기를 이번 주에 역사 고증에 맞춰 다시 그렸습니다. 일본 항모 네 척과 미국 항모 세 척을 저마다 다른 모습으로 모델링하고 항공기 종류를 여섯에서 열넷으로 늘렸습니다. 이 덧칠 전까지 항공기는 실제 기종이 아니라 그 항공기가 수행하는 임무로 그려지고 있었습니다. 그래서 31.7미터짜리 PBY-5 카탈리나와 B-26 폭격기가 어뢰 임무를 날았다는 이유만으로 단발 함상 뇌격기로 화면에 나왔습니다. 무개입 재생을 고정한 해시는 그대로였습니다. 해시는 재생이 만들어 낸 상태들을 하나의 지문 숫자로 압축한 값이라 어느 한 단계라도 달라지면 그 값이 함께 달라지므로, 값이 그대로라는 것은 무개입 재생이 이전과 한 치도 어긋나지 않았다는 뜻입니다.

midway 시뮬레이션의 6월 4일 정오. 개별 모델링된 일본 항모 아카기가 갑판에 함재기를 실은 채 떠 있고 멀리 미국 항모 세 척이 보입니다. 왼쪽 아래에 정상·기왕손상·피격·대화재 같은 함선 상태 범례가 있습니다.

이 세 덧칠은 전부 렌더만 바꾸는 작업이라 골든이나 재생 해시를 건드리지 않았습니다. 그런데도 저마다 오래된 침묵을 하나씩 깼습니다. 핵심은 잡은 결함들이 아니라 겉을 바꾸는 일이 곧 계측기를 다는 일이라는 점입니다. 그 옆에 짝을 이루는 것이 하나 있습니다. 테스트 커버리지를 채우는 일도 결함 탐지기입니다. 우주 트럭 게임에서 자동 검증의 커버리지를 100%까지 채우자 게임을 내놓은 날부터 죽어 있던 기능이 나타났습니다. 계약 보드에 뜨는 구호 물자 계약을 수락하면 언제나 실패했습니다. 검증 코드가 바로 그 구호 계약을 예외로 빼 두고 있었기 때문에 아무도 그 버튼을 끝까지 눌러 본 적이 없었습니다. 테스트에서 예외로 빼 둔 것은 테스트되지 않습니다.

이번 주에 가장 크게 덧칠한 것은 터미널 멀티플렉서 pytmux입니다. 터미널 멀티플렉서가 낯선 분을 위해 잠깐 설명하면 한 터미널 창을 여러 칸으로 나누고 여러 원격 기계의 세션을 한 터미널에서 다룰 수 있게 해 주는 도구입니다. 지금까지 화면을 그리는 클라이언트는 파이썬 Textual로 만든 TUI 하나였습니다. 사실 이 TUI 하나로 할 수 있는 일은 이미 전부 됩니다. 그런데 같은 TUI라도 어느 터미널 위에서 돌리느냐에 따라 경험이 조금씩 달라집니다. 예를 들어 구버전 윈도우 터미널은 Shift+Enter를 언제나 그냥 Enter로 보냅니다. 그래서 그 안에서는 줄을 바꾸려면 Ctrl+J를 눌러야 합니다. 이런 차이는 터미널이 키를 pytmux에 넘기기 전에 이미 정해 버리는 것이라 pytmux TUI 수준에서는 고칠 수 없습니다. TUI는 자기 화면을 남의 터미널 위에 그리고 그 터미널이 정한 규칙을 그대로 물려받기 때문입니다.

마침 얼마 전 Warp 터미널이 오픈소스로 전환하면서[^Warp is now open-source] 그 GUI 부분을 MIT 라이선스로 쓸 수 있게 됐습니다. 그래서 이참에 TUI와 거의 똑같이 동작하지만 Warp의 GUI를 바탕으로 하는 pytmux GUI 클라이언트를 만들기 시작했습니다. 목표는 기존 TUI와 완전히 똑같이 동작하는 GUI 클라이언트입니다. 이 작업이 가능한 이유는 pytmux의 구조에 있습니다. 판정과 상태는 전부 서버가 맡고 클라이언트는 서버가 보낸 것을 화면에 그리기만 합니다. 그래서 클라이언트를 하나 더 만드는 일은 같은 서버 위에 겉면을 하나 더 칠하는 일에 가깝습니다. 러스트로 작성된 Warp GUI 위에서 pytmux TUI와 같은 동작을 구현하면 됩니다.

먼저 정리해야 했던 것은 라이선스였습니다. Warp의 GUI 코드는 MIT지만 그 아래 지원 라이브러리 일부가 더 엄격한 라이선스였습니다. 실제로 쓰는 함수를 세어 보니 여섯 개였고 그 여섯 개만 따로 다시 구현해서 엄격한 라이선스 코드가 최종 결과물에 들어오지 않도록 경계를 만들었습니다. 그다음은 오리지널 TUI가 가진 조작면을 하나씩 GUI로 옮기는 맞춤 작업이었습니다. 명령 87개와 단축키와 설정 34개와 팝업 화면 17개를 더해 조작면 187개를 파악해 두고 GUI가 그중 몇 개를 오리지널과 똑같이 해내는지 점수판으로 만들어 관리했습니다. 처음에는 187개 중 다섯 개에서 시작해 이번 주에 187개 전부로 맞췄습니다.

왼쪽은 파이썬 Textual로 만든 pytmux 오리지널 TUI이고 오른쪽은 이번 주에 만들기 시작한 러스트 GUI 클라이언트입니다. 둘이 같은 설정 화면을 같은 항목과 같은 값으로 그려 조작면이 똑같이 동작함을 보여 줍니다.

오리지널 TUI와 새 GUI의 차이는 결국 누가 화면을 그리느냐입니다. TUI는 남의 터미널이 주는 글자 칸에 그리고 GUI는 자기 창에 픽셀을 직접 그립니다. 그래서 GUI에서는 Shift+Enter가 그대로 Shift+Enter로 오고 칸을 나누는 테두리도 아스키 선문자로 그린 선이 아니라 진짜 선으로 그려지고 마우스 휠이나 모호폭 글자처럼 터미널마다 다르던 것들을 정확히 의도한 대로 그릴 수 있습니다. 남의 터미널의 규칙을 물려받지 않는다는 것이 GUI의 핵심 장점입니다. 그리고 새 GUI를 오리지널에 맞추는 이 덧칠이 그 아래 숨겨져 있던 결함을 드러냈습니다. 위아래로 화면을 나누는 분할이 이 작업을 시작한 이래 한 번도 동작한 적이 없었습니다. 명령이 제대로 갔는지 검사하는 게이트가 명령의 이름만 보고 그 안에 실린 값의 이름은 보지 않았기 때문입니다. 클라이언트는 가로 방향이라고 보냈고 서버는 다른 이름의 칸을 읽고 있었습니다. 테스트 1,400개가 전부 통과인 채로 상하 분할은 한 번도 일어나지 않았습니다. 오리지널과 새 GUI를 나란히 놓고 같은 화면을 비교한 뒤에야 이 차이를 찾아냈습니다.

GUI가 좋기만 한 것은 아닙니다. TUI는 이미 열려 있는 터미널이나 ssh 세션 안에서 그대로 돕니다. 반면 GUI는 별도의 창을 띄우는 무거운 프로그램입니다. GPU로 그리고 실행 파일도 큽니다. 아직 오래 다듬은 오리지널만큼 안정적이지 않습니다. 그리고 클라이언트가 하나에서 둘로 늘어난 만큼 오리지널이 바뀔 때마다 두 곳을 똑같이 맞춰야 하는 짐도 함께 늘었습니다. 이번 주 마지막에는 러스트 GUI와 파이썬 오리지널 TUI의 두 클라이언트를 같은 서버에 동시 접속시켜 둘이 똑같은 두 탭 상태를 그리는지 확인했습니다. 앞으로의 로드맵은 윈도우 쪽 다듬기입니다.

겉을 바꾸는 일이 곧 색을 바꾸는 일이었던 도구도 있습니다. 윈도우용 화면 돋보기입니다. 저시력 사용자를 위해 화면 일부를 확대해 보여 주는 이 작은 도구를 직접 만들어 매일 쓰고 있습니다. 윈도우에는 이미 내장 돋보기가 있습니다. 그런데도 왜 따로 만들었는지부터 설명하면 내장 돋보기는 보안 로그인 시퀀스와 상위 권한과 시스템 추적 훅에 결합돼 있어서 보안 소프트웨어가 겹겹이 상주하는 업무용 PC에서 자주 고장납니다. 실행 명령을 내려도 창이 뜨기까지 몇 초에서 십수 초가 걸리고 끄는 명령을 내려도 곧바로 꺼지지 않습니다. 어떤 환경에서는 아예 뜨지 않고 켜져 있는 동안 시스템 전체가 느려집니다. 정작 돋보기가 가장 필요한 환경에서 내장 돋보기가 가장 불안정합니다.

이것이 제 사정만은 아닙니다. 저도 예전에 '시력이 약한 사람의 컴퓨터 사용'에서 저시력 사용자가 컴퓨터를 쓰며 겪는 어려움을 다룬 적이 있습니다. 저시력 커뮤니티와 접근성 기관은 오래전부터 내장 돋보기의 한계를 지적해 왔습니다. 확대된 글자가 각지고 흐릿해지는 것이 대표적입니다. 확대한 글자는 큰 크기로 직접 그려지는 것이 아니라 작게 그려진 것을 이미지째 늘린 것이라 흐릿하고 픽셀이 도드라집니다. ClearType 방식의 글자 가장자리 색까지 함께 늘어나 비수평 획의 가장자리에 예상치 못한 색이 생깁니다[^Magnifier (Windows)]. 미국맹인재단의 한 리뷰는 내장 돋보기가 폰트 스무딩을 쓰지 않아 글자가 매우 각지고 읽기 어렵게 된다고 지적하고 어떤 시각장애 사용자에게도 추천하기 어렵다고 적었습니다[^A Review of WinZoom 4 and the Windows 7 Magnifier]. 마이크로소프트 공식 문답에는 마우스를 움직이면 화면이 깜빡이고 검게 되어 눈과 머리에 무리가 간다는 저시력 사용자 신고가 쌓여 있습니다[^Windows Magnifier BUG (After 20h2)]. 그래서 많은 저시력 사용자가 상용 확대 소프트웨어인 줌텍스트로 넘어갑니다. 한 저시력자 단체는 내장 돋보기를 제한적이라고 보고 대다수 사용자에게 줌텍스트를 권합니다[^Built-In Versus Third-Party Magnifiers: Finding the Program that Works Best for You]. 품질은 좋지만 기본 가격이 693달러인 유료 소프트웨어입니다[^ZoomText (Freedom Scientific)]. 저시력 사용자의 선택지가 무료지만 글자가 뭉개지고 불안정한 내장 돋보기와 품질은 좋지만 수백 달러짜리 상용 소프트웨어로 나뉩니다.

직접 만든 돋보기가 겨냥하는 지점이 그 사이입니다. 무료이고 설치가 필요 없는 단일 실행 파일이면서 글자 품질과 안정성을 챙기는 제3의 선택지입니다. 만드는 방법의 핵심은 후킹 소프트웨어가 끼어드는 통로를 처음부터 피하는 것입니다. 화면을 GPU에서 직접 받아 오는 방식을 써서 보안 도구가 자주 끼어드는 옛 그리기 통로를 지나지 않고 상위 권한이나 내장 돋보기용 시스템 라이브러리에도 기대지 않습니다. 그 결과가 외부 의존이 없는 54킬로바이트짜리 실행 파일 하나입니다.

저시력 관점에서 이 도구가 가장 공들이는 것은 확대해도 글자가 또렷하고 밝게 보이는 것입니다. 그리고 이번 주 이 도구의 덧칠이 바로 색이었습니다. 검은 바탕의 흰 글자를 확대하면 원본보다 어두워지는 결함을 GPU 실측으로 찾아냈습니다. 원인은 확대할 때 이웃한 픽셀을 절반씩 섞는 처리가 감마 공간에서 일어난 것입니다. 흰색 255와 검정 0을 절반씩 섞으면 값은 127이 됩니다. 이 127의 실제 밝기는 절반인 50%가 아니라 21%입니다. 그래서 섞이는 곳마다 빛이 사라지고 2배로 확대한 흰 글자의 평균 밝기가 24% 어두워졌습니다. 이미지를 처리하기 전에 색을 선형 공간으로 되돌려 섞는 방식으로 이 손실을 0으로 되돌렸습니다. 이미지 알고리즘이 선형 광량을 전제하므로 감마 인코딩 값 위에서 섞으면 결과가 훨씬 어두워지고 글자가 필요 이상으로 굵어진다는 것은 잘 알려진 문제입니다[^What every coder should know about gamma]. 색을 새로 칠한 이 덧칠이 그 색 자체의 오래된 오차를 드러냈습니다. 글자 품질 말고도 저시력 사용에 필요한 것을 몇 가지 더 챙겼습니다. 커서를 부드럽게 따라가게 해서 좁은 확대 영역에서 마우스가 떨릴 때 그 떨림을 흡수합니다. 확대 화면에서 손떨림이 그대로 증폭되면 어지럼을 일으킵니다. 이 흡수가 그것을 줄입니다. 색을 반전하는 기능도 있어서 밝은 화면에 예민한 사용자를 돕습니다. 이 도구는 원래 내부 도구로 만들었고 지금 MIT 라이선스로 공개할 준비를 하고 있습니다. 아직 공개 상태는 아닙니다.

덧칠은 더 보기 좋으라고 하는 일입니다. 그런데 이번 주에 새로 칠한 면마다 그 아래 오래 숨어 있던 것이 나타났습니다. 트럭의 자세도 행성의 색도 함선의 기종도 클라이언트의 화면도 돋보기의 흰 글자도 겉을 바꾸는 순간 그동안의 침묵을 깼습니다. 지난주 '리허설'이 완성한 것을 어떻게 믿을 것인가였다면 이번 주는 겉을 바꾸는 일 자체가 그 아래를 재는 계측기였다는 이야기입니다. 새 칠이 아름다우려고 한 일이 오래된 결함을 보여 준다는 것은 다음 주에도 다른 겉모습으로 또 만날 것 같습니다. 그럼 다음에도 기계와 함께 겉을 새로 칠하고 그 아래에 무엇이 있었는지 살펴보겠습니다.