분석 대상: zed/crates/gpui/ · metal_renderer.rs · wgpu_renderer.rs · scene.rs · atlas.rs

Zed 그래픽 시스템 하나의 코드, 네 가지 GPU API

Zed는 macOS에서 Metal, Linux에서 Vulkan(wgpu 경유), Windows에서 DirectX, 웹에서 WebGPU를 사용한다. 플랫폼마다 GPU API가 달라도 UI 코드는 하나인 이유는 Scene이라는 플랫폼 독립적인 중간 표현 덕분이다. 텍스트 렌더링만큼은 OS별로 완전히 다른 엔진을 쓴다.

1. 전체 아키텍처 — 세 계층

Zed의 그래픽 파이프라인은 크게 세 계층으로 나뉜다. 가장 위에 플랫폼을 전혀 모르는 Element 트리, 중간에 공통 draw 명령을 담는 Scene, 가장 아래에 실제 GPU 명령으로 변환하는 PlatformRenderer.

① Element 트리 (CPU) Div · Text · Image · Button … 각 Element::paint() 호출 cx.paint_quad() · cx.paint_glyph() · cx.paint_path() 등으로 Scene에 명령 추가 paint 완료 ② Scene — 플랫폼 독립 중간 표현 Quad · Shadow · Path · MonochromeSprite · PolychromeSprite · Underline · PaintSurface 모든 primitive는 #[repr(C)] — GPU 버퍼에 zero-copy로 복사됨 draw(scene) «trait» PlatformRenderer fn draw(&mut self, scene: &Scene) macOS MetalRenderer CAMetalLayer Linux WgpuRenderer Vulkan / OpenGL Windows DirectXRenderer DirectX 11 Web / WASM WgpuRenderer WebGPU / WebGL .metal (MSL) .wgsl (WGSL) .hlsl (HLSL) .wgsl (WGSL) 실제 GPU 하드웨어
왜 Scene이라는 중간 계층이 필요한가?

UI 코드(버튼, 텍스트박스 등)가 Metal API를 직접 호출하면, Linux나 Web 버전을 만들 때 UI 코드를 전부 다시 작성해야 한다. Scene은 "무엇을 그릴지"만 담고, "어떻게 그릴지"는 플랫폼 렌더러에 맡긴다. UI 코드는 플랫폼을 전혀 몰라도 된다.

2. Scene이 담는 Primitive 목록

Scene에는 여덟 종류의 primitive가 있다. 모두 #[repr(C)]로 선언되어 있어 Rust 구조체를 그대로 GPU 버퍼에 복사(memcpy)할 수 있다. 그려지는 순서는 DrawOrder(u32) 값으로 결정하고, 같은 타입끼리 묶어서(batch) draw call 횟수를 최소화한다.

Primitive용도렌더링 방식
Quad 사각형 배경, 버튼, 패널, 테두리 SDF(Signed Distance Field) — 셰이더가 각 픽셀에서 거리 함수를 계산해 둥근 모서리와 AA를 GPU에서 처리. 텍스처 없이 계산만으로 렌더링.
Shadow 드롭 섀도, 인셋 섀도 Gaussian blur를 SDF로 근사. 역시 텍스처 불필요. 셰이더 계산만으로 부드러운 그림자 표현.
Path 베지어 커브, SVG 경로, 삼각형 MSAA 4× — 별도 임시 텍스처에 4배 해상도로 래스터라이즈 후 resolve. Quad와 달리 임의 모양이라 SDF로 표현 불가.
Underline 텍스트 밑줄, 빨간 물결(오류 표시) wavy 플래그로 직선/물결 구분. 셰이더에서 물결 곡선을 수학적으로 계산.
MonochromeSprite 일반 텍스트 글리프 글리프 Atlas의 회색조(1채널) 타일 참조. 텍스트 색상은 셰이더에서 덧씌움.
PolychromeSprite 이모지, 컬러 아이콘, 이미지 Atlas의 RGBA(4채널) 타일 참조. 이모지는 OS 텍스트 엔진이 래스터라이즈 후 Atlas에 업로드.
SubpixelSprite 서브픽셀 AA 텍스트 Linux/Windows 전용. macOS에서는 이 분기가 unreachable!().
PaintSurface 비디오/카메라 프레임 macOS 전용. CVPixelBuffer → CVMetalTextureCache 경로로 GPU 복사 없이 직접 표시.

3. 렌더 파이프라인 — 한 프레임의 여정

화면을 한 번 그리는 데 아래 단계를 거친다. 초록색 단계는 CPU(Rust), 보라색 단계는 GPU(Metal/Vulkan/DX)에서 실행된다.

1

Element::paint()

각 UI 요소(Div, Text, Button 등)가 cx.paint_quad(), cx.paint_glyph() 등을 호출한다. BoundsTree로 레이어 Z-순서를 결정하고, 각 draw 명령을 Scene 버퍼에 적재한다.

2

Scene::finish() — 정렬 & 배치

모든 primitive를 DrawOrder(u32) 기준으로 정렬한다. 같은 타입 + 같은 텍스처 ID끼리 묶어 배치 이터레이터를 만든다. draw call 횟수를 최소화하는 핵심 단계.

3

PlatformRenderer::draw(scene)

완성된 Scene을 플랫폼 렌더러에 넘긴다. 이 경계에서 CPU → GPU 영역으로 전환된다.

4

Command Buffer 생성

Metal command_queue.new_command_buffer()로 이번 프레임의 GPU 명령 기록 시작. InstanceBufferPool에서 이전 프레임 버퍼를 재사용(재할당 없음).

5

배치별 Draw Call

Shadows → Quads → Path MSAA 중간 패스 → Path resolve → Underlines → MonochromeSprites → PolychromeSprites 순으로 draw call을 발행한다. 각 배치는 GPU에서 병렬로 인스턴스 렌더링된다.

6

Present & Commit

command_buffer.present_drawable() + commit()으로 화면에 표시. CAMetalLayer가 vertical sync에 맞춰 swap한다.

InstanceBufferPool: Metal 버퍼를 프레임마다 새로 할당하면 매 프레임 수천 번의 malloc/free가 발생한다. Zed는 InstanceBufferPool로 버퍼를 풀링해 같은 크기면 재사용한다. Apple Silicon에서는 CPU/GPU가 메모리를 공유(StorageModeShared)해 복사 자체가 없다. 외장 GPU에서는 StorageModeManaged로 명시적 동기화.

4. 글리프 Atlas — 텍스트 렌더링 캐시

화면에 글자를 그릴 때마다 OS 텍스트 엔진에 "이 글자를 픽셀로 그려줘"를 요청하면 너무 느리다. 한 번 래스터라이즈한 글리프를 큰 GPU 텍스처(Atlas)에 저장해두고, 이후에는 "Atlas의 이 좌표에 있는 타일을 거기에 붙여라"만 전달한다. 이것이 글리프 아틀라스 패턴이다.

✓ 캐시 히트 (대부분)

1
AtlasKey 조회
font_id + glyph_id + pixel_size + subpixel_offset
2
AtlasTile 반환
texture_id + 좌표(x,y,w,h)
3
MonochromeSprite로 Scene 추가
GPU 업로드 없음, 좌표만 전달

✗ 캐시 미스 (첫 등장)

1
AtlasKey 조회 → 없음
2
플랫폼 텍스트 엔진 래스터라이즈
CoreText / cosmic-text / DirectWrite
3
etagere로 Atlas 슬롯 할당
4
GPU 텍스처 업로드
이후 요청부터는 히트
GPU 텍스처 Atlas (개념도) Mono A B C D E Poly 😀 🦀 🎉 Monochrome (텍스트 글리프, 회색조) Polychrome (이모지·아이콘, RGBA) 할당: etagere::BucketedAtlasAllocator 크기별로 버킷 분류 → 공간 낭비 최소화 Atlas 가득 차면 새 텍스처 추가 할당 미할당 공간

5. 플랫폼별 상세 비교

각 플랫폼이 다른 API를 쓰는 이유는 단순하다 — Metal은 Apple이 macOS/iOS 전용으로 만든 API라 Linux에서 쓸 수 없고, DirectX는 Microsoft가 Windows 전용으로 만든 API라 macOS에서 쓸 수 없다. Linux/Web은 크로스 플랫폼 추상화 레이어인 wgpu를 통해 Vulkan 또는 WebGPU를 사용한다.

항목 macOS Linux Windows Web/WASM
GPU API Metal (Apple 네이티브) wgpu → Vulkan
OpenGL fallback
DirectX 11 (MS 네이티브) wgpu → WebGPU
WebGL fallback
셰이더 언어 .metal (MSL) .wgsl (WGSL) .hlsl (HLSL) .wgsl (WGSL)
윈도우 시스템 AppKit (NSWindow) Wayland / X11 Win32 (HWND) wasm-bindgen Canvas
텍스트 엔진 CoreText cosmic-text DirectWrite cosmic-text
서브픽셀 AA 비활성화
Retina에서 불필요
지원 지원 (ClearType) 비활성화
글리프 Atlas MetalAtlas WgpuAtlas DirectXAtlas WgpuAtlas
비디오 서피스 CVPixelBuffer →
CVMetalTextureCache
(zero-copy)
미지원 미지원 미지원
Path AA MSAA 4× (Metal) wgpu MSAA MSAA 4× (DX11) wgpu MSAA
macOS — Metal

Apple이 OpenGL을 deprecated하고 밀어붙인 차세대 API. CPU 오버헤드가 적고 Apple Silicon에서 CPU/GPU 통합 메모리를 활용해 복사 없이 데이터를 공유할 수 있다. CoreText가 OS에서 제공하는 고품질 텍스트 렌더링을 담당한다.

Linux — wgpu + Vulkan

Vulkan은 강력하지만 코드가 매우 장황하다. wgpu가 Vulkan 위에 WebGPU 표준 API를 제공해 코드를 단순화한다. cosmic-text는 순수 Rust로 작성된 텍스트 레이아웃 엔진으로 Wayland/X11 모두에서 동작한다.

Windows — DirectX 11

DirectX 12보다 구형이지만 더 넓은 호환성을 위해 DX11을 선택했다. DirectWrite는 ClearType 힌팅을 포함한 Windows 네이티브 텍스트 렌더링 API다.

Web/WASM — WebGPU

브라우저에서 동작하는 버전. wgpu가 WebGPU 백엔드를 지원하므로 Linux와 거의 같은 렌더러 코드를 재사용한다. WebGPU가 없는 브라우저에서는 WebGL로 fallback한다.

macOS에서 서브픽셀 AA가 없는 이유: Apple은 macOS 10.14(Mojave)부터 서브픽셀 AA를 기본 비활성화했다. Retina 디스플레이(2× 이상 픽셀 밀도)에서는 픽셀이 워낙 촘촘해서 서브픽셀 AA의 효과가 눈에 띄지 않고, 다크 모드에서 RGB stripe 방식이 오히려 컬러 프린징(색수차)을 유발하기 때문이다. Zed 소스에서 macOS 구현의 SubpixelSprite 분기가 unreachable!()인 이유가 바로 이것이다.