Ghostty Devlog 002

Mitchell Hashimoto

Ghostty 개발 일지 002

원문은 Mitchell Hashimoto님이 에 게재했습니다. 이 블로그 구독하기

안녕하세요! Ghostty 👻의 두 번째 공식 개발 일지에 오신 것을 환영합니다!

첫 번째 개발 일지를 놓쳤거나 Ghostty가 무엇인지 더 알고 싶다면, 이 웹사이트의 Ghostty 페이지를 참고해 주세요.


비기술 소식: 공개 발표와 커뮤니티 만들기

곧 기술적인 이야기를 본격적으로 다루겠지만, 먼저 기술 외적인 소식부터 전하고 싶습니다. 공유하고 싶은 정말 흥미로운 일들이 몇 가지 있거든요!

먼저, Handmade Boston에서 Ghostty에 대해 공개적으로 이야기하는 첫 번째 '토크'를 진행했습니다. Twitch 다시보기 영상(2:45:30부터 시작)은 여기에서 볼 수 있습니다. 이 토크는 Ghostty 자체보다는 터미널 전반에 대한 이야기에 가깝지만, 터미널에 대한 제 철학과 Ghostty를 시작하고 지금까지 계속 만드는 이유가 궁금하다면 한번 들어보세요.

그리고 몇몇 커뮤니티 멤버들이 Ghostty 웹사이트와 공개 Discord(베타에 참여하지 않아도 들어갈 수 있습니다), 그리고 베타 참여 대기자 명단을 만들기 시작했습니다. 아직 초기 단계지만, 이 모든 것이 프로젝트를 정식 출시하는 과정에서 중요한 역할을 하게 될 겁니다. 그때까지 여러분 중 몇 분과 만나 이야기를 나눌 수 있으면 좋겠네요!

베타 접근과 관련해서는... 아마 다음 개발 일지에서 더 자세히 공유할 수 있을 것 같습니다. 열심히 준비 중입니다. '저희'라고 한 이유는 Ghostty 커뮤니티가 점점 커지면서, 더 많은 분들을 맞이할 준비를 도와주는 커뮤니티 멤버들이 생겼기 때문입니다. 정말 즐거운 일이죠!

Ghostty 출시 계획에 대해서도 자주 질문을 받습니다. 무료인가요, 오픈소스인가요, 같은 질문들이요. 네, 맞습니다! Ghostty는 무료이자 오픈소스로 공개될 예정입니다(자유로서의 무료, 공짜로서의 무료 모두 해당합니다). 아직 라이선스는 정하지 않았지만, 베타 그룹에서 의견을 제안할 수 있도록 하고 있습니다. 현재로서는 GPLv3 쪽으로 기울고 있지만, 아직 확정된 것은 없습니다.


macOS의 비네이티브 전체 화면(#215)

이 기능은 Thorsten Ball이 기여해 주었습니다.

다른 macOS 터미널 에뮬레이터들 사이에서 흔히 '비네이티브 전체 화면'이라고 불리는 기능이 있습니다. 이 기능은 영상으로 보는 것이 가장 이해하기 쉽습니다. 아래는 네이티브, 즉 전통적인 macOS 전체 화면입니다:

그리고 다음은 새로운 비네이티브 전체 화면입니다:

두 방식 모두 장단점이 있기 때문에 Ghostty에서는 설정으로 선택할 수 있게 했습니다. 특히 비네이티브 전체 화면은 매우 빠르고, 다른 프로그램을 위에 띄울 수 있으며, 새로운 '데스크탑'을 만들지 않습니다. 다만 전체 화면 상태에서는 탭에 접근할 수 없고 분할 화면만 사용할 수 있습니다(커스텀 탭 바 컨트롤러를 만들기 전까지는요. 도움 환영합니다!).

알고 보니 이걸 구현하는 건 보통 일이 아니었습니다. 겉으로 보기에는 정말 단순합니다. 프로그래밍적으로 윈도우 속성 몇 가지를 수정하면 되니까요. 구글에서 찾아보면 이 동작을 만드는 코드 예제는 열 줄 남짓입니다. 그런데 Ghostty의 PR은 +802/-239였습니다. 어떻게 된 걸까요?

Ghostty를 계속 지켜보셨다면 제가 Ghostty가 Zig로 작성되었지만 macOS GUI에는 SwiftUI를 사용한다고 자랑스럽게 이야기해 온 것을 아실 겁니다. Ghostty의 GUI는 100% SwiftUI였습니다. Ghostty.app의 메인 진입점이 SwiftUI App 객체였죠. 문제는 비네이티브 전체 화면을 구현하려면 NSWindow를 서브클래싱해야 하는데, SwiftUI에서는 그게 불가능하다는 점입니다(혹은 아직 공개적으로 방법을 찾아낸 사람이 없다는 점이죠).

그래서 비네이티브 전체 화면을 동작시키기 위해 SwiftUI 앱과 윈도우 라이프사이클 관리를 전부 들어내고, 좋은 옛 AppKit을 이용해 직접 다시 작성해야 했습니다. 참고로 뷰 자체에는 여전히 SwiftUI를 사용합니다. 바뀐 것은 윈도우/앱 라이프사이클 관리뿐입니다. 여기에는 시작 과정, 다중 윈도우 생성, 탭 바, 메뉴 바 등이 포함됩니다.

비네이티브 전체 화면 하나만을 위해서였다면 아마 이 정도 수고는 할 가치가 없었을 겁니다. 하지만 이미 SwiftUI 때문에 남아 있는 버그나 아직 구현되지 않은 기능들이 있었고, 이번 작업이 그 모든 문제를 해결할 실마리를 제공했기 때문에 충분히 가치 있는 결정이었습니다. 그 개선 사항들에 대해서는 나중에 더 공유하겠지만, Thorsten이 CursedMenuManager.swift라는 파일을 삭제할 수 있었다고만 말해 두겠습니다. 이 변화가 왜 더 큰 의미에서 좋은 결정이었는지를 잘 보여주는 힌트죠.

이제 비네이티브 전체 화면 기능이 생겼습니다! 멋지죠! 하지만 그뿐만 아니라 앞으로 새로운 macOS 기능을 만들 수 있는 훨씬 더 견고한 기반도 갖추게 되었으니, 겉으로 보이는 것보다 훨씬 더 큰 성과입니다. 고마워요, Thorsten!

아, 그리고 +802/-239 중 97%는 Swift였습니다. Swift를 정말 좋아하는 분들이 많다는 건 존중하지만, 저는 개인적으로 Zig로 작업하는 걸 훨씬 더 선호합니다. 그리고 Xcode도 그다지 좋지 않고요. Thorsten도 동의합니다. 그래서 그런 면에서는 이 기능 작업이 그리 즐겁지는 않았지만, 우리는 네이티브 macOS 경험을 멋지게 만들고 싶기 때문에 해야 할 일을 하는 겁니다.


Linux에서 글자가 흐릿해지고 박스 아티팩트가 생기는 문제(#178, #204)

거의 6개월 전, Ghostty의 아주 초기 테스터 중 한 분이 Linux에서 글자가 약간 흐릿하게 보이고 아티팩트가 나타난다고 제보했습니다:

위 스크린샷에는 두 가지 문제가 있습니다. 첫째, 글자가 흐릿합니다. 둘째, NixOS 로고의 박스 글리프 아래에 빈 줄이 있어서는 안 됩니다. 아래는 이런 문제가 나타나지 않았던 제 머신에서의 스크린샷입니다.

저는 Linux를 많이 사용합니다만 전혀 흐릿함을 보지 못했습니다. 여러 하드웨어에 여러 디스트로를 설치해도 문제를 재현할 수 없었습니다. 제보자의 OS 설정을 그대로 받아 실행해 봐도 마찬가지였습니다. DPI 문제일지도 모른다고 짐작은 했지만 거기서 더 나아가지는 못했습니다. 그 뒤 몇 달 동안 몇 번 다시 살펴봤지만 끝내 원인을 찾지 못했습니다.

그러다 마침내 원인을 찾았습니다. 정확히는 Thorsten이 찾아냈죠. 자, 캠프파이어 주변에 모여 보세요. 이건 부동소수점 연산과 부동소수점/정수 변환이 얼마나 교묘하게 문제를 일으킬 수 있는지를 보여주는 아주 중요한 소프트웨어 엔지니어링 교훈이니까요.

DPI 처리는 현대 소프트웨어에서 골치 아픈 일입니다. 수십 년 동안 디스플레이의 DPI는 몇 가지로 한정되어 있었고 모든 것이 스케일링 없이('1배') 렌더링되었습니다. 오늘날에는 디스플레이의 DPI가 매우 다양하고 해상도도 높아서 보통 스케일링을 적용해('2배') UI를 렌더링합니다. 더 복잡하게도 요즘은 분수 스케일링('1.25배')도 매우 흔합니다.

이런 이유로 이식성이 필요한 소프트웨어는 보통 크기 설정(폰트, 패딩 등)을 포인트 단위로 받습니다. 포인트는 일반적으로 1인치의 1/72입니다. DPI는 인치당 도트 수입니다. GPU 렌더링은 픽셀 단위로 동작합니다. 포인트를 픽셀로 변환하려면 계산이 필요합니다: pixels = (points * 72) / dpi.

결과적으로 몇 달 전에(앞서 보여드린 스크린샷처럼) 이런 예감이 들어 DPI 기반 계산을 전부 감사했지만, 전부 올바르다고 결론을 내렸습니다. 그리고 실제로 거의 다 맞았습니다. 폰트 크기와 GPU(셰이더) 파라미터에 대해 제가 했던 모든 계산은 정확했습니다. 반올림, 정밀도, 필요한 경우 정수 변환까지 모두 올바르게 처리하고 있었죠.

하지만 포인트를 사용하는 기능 중 감사를 빠뜨린 게 하나 있었습니다. 바로 윈도우 패딩이었죠. 그리고 윈도우 패딩 계산이 잘못되어 있었습니다. 이 잘못된 계산 때문에 패딩이 0 또는 1픽셀 어긋나게 되었습니다(그 이상 어긋나지는 않았습니다). 단 1픽셀만 어긋나도 글자가 흐릿해졌습니다. 세상에.

어떻게 잘못되었는지 살펴보겠습니다. 이전 패딩 계산은 다음과 같았습니다(너비만 표시했으며, 높이 계산도 y를 쓰는 것 외에는 동일했습니다):

const padding_x: f32 = (config.@"window-padding-x"* x_dpi) / 72;
const screen_width: u32 = self.width -| @as(u32, @intFromFloat(padding_x * 2));

문제가 보이시나요? 해결책을 보여드리면 좀 더 잘 보일지도 모릅니다:

const padding_x: u32 = @intFromFloat(@floor(config.@"window-padding-x"* x_dpi / 72));
const screen_width: u32 = self.width -| (padding_x * 2);

문제가 발생한 순서는 다음과 같습니다:

  1. 패딩이 부동소수점으로 계산되었습니다. DPI가 72로 깔끔하게 나누어떨어지지 않으면 패딩이 분수가 됩니다. 예를 들어 DPI가 125이고 패딩이 2라면 최종 픽셀 값은 3.333...이 됩니다.

  2. 화면 너비 계산에서는 부동소수점을 반올림에 대한 명확한 기준 없이 정수로 변환했습니다. 그 결과 내림이 되었고, 위 예시를 따르면 width - 3이 됩니다. 0.333...만큼의 패딩이 사라진 셈이죠.

  3. 화면 너비와 패딩은 모두 렌더링을 위해 GPU로 전달됩니다. GPU는 부동소수점 값으로 동작하므로 패딩에 대해 3.333...이라는 분수 픽셀 값을 그대로 반영합니다.

  4. 물론 그 값은 분수가 아닌 실제 픽셀로 매핑되어야 하고, GPU는 데이터를 잃지 않으려고 올림합니다. 하지만 이제 렌더링 결과가 화면보다 1픽셀 더 넓어지므로(0.3이 1로 반올림되어) 다운스케일 연산이 적용됩니다.

  5. 다운스케일은 안티에일리어싱을 유발하고, 이는 정의상 가장자리를 흐릿하게 만듭니다. 그래서 글자가 흐릿해진 것이죠.

대신 패딩을 내림으로 반올림하고 전체 과정에서 정수 타입만 사용함으로써, 우리는 이런 상황에 빠지지 않고 안티에일리어싱을 피하며 픽셀 퍼펙트한 렌더링을 얻을 수 있게 되었습니다. 😅 다음은 수정 전(왼쪽)과 수정 후(오른쪽)를 크게 확대한 이미지입니다:

이 일을 계기로 코드베이스 전체에서 @intFromFloat 빌트인을 사용하는 부분을 모두 감사했고, 실제로 반올림 오류로 미세한 아티팩트를 일으키던 사례를 하나 더 찾았습니다. 값비싼 교훈을 얻었죠.

이론상으로는 이 문제가 Linux 사용자만에게 영향을 미치는 것은 아니었습니다. 실제로는 Linux 하드웨어가 더 다양하고 테스터들이 패딩과 깔끔하게 나누어떨어지지 않는 DPI를 더 자주 사용했기 때문에 더 자주 드러났을 뿐입니다. 하지만 macOS 사용자라도 패딩 설정을 특정 값으로 조정했다면 같은 현상을 겪을 수 있었습니다.

원래의 '수정'은 패딩 계산에 @floor를 추가하는 2줄짜리 변경이었습니다. 위에서 설명한 문제의 순서를 철저히 이해한 뒤, 우리는 올바른 해결책은 모든 크기 관련 자료구조를 부동소수점 대신 정수를 사용하도록 바꾸고, GPU에 넘길 때만 정수를 부동소수점으로 변환하여 분수 값이 사용되지 않도록 하는 것이라고 판단했습니다. 화면 크기는 분수가 아니고, 패딩도 분수가 아니며, 그리드 크기도 분수가 아닙니다. 그래서 이번 일은 올바른 자료형을 사용하는 것에 대한 교훈이기도 합니다. Thorsten은 버그를 진정으로 이해하는 것의 가치에 대한 뉴스레터 글에서 이 경험을 다루었습니다.


마무리

이것으로 Ghostty 개발 일지 002를 마칩니다. 이번 개발 일지는 Thorsten 쇼였습니다! 정말 기쁘고 감사한 마음입니다! 베타 그룹이 아직 수십 명에 불과하지만, Ghostty 커뮤니티가 성장하는 모습을 보니 정말 기대됩니다.

최신 소식을 받아보고 싶다면 Twitter나 Mastodon에서 저를 팔로우해 주세요(링크는 푸터에 있습니다). 이 블로그는 RSS 피드도 제공합니다.

Boo. 👻

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글