Hacker News

Anatomy of a Texture

텍스처의 해부 — 플랫폼 간 변환에 숨겨진 메모리 레이아웃의 복잡성

505 Games의 그래픽스 프로그래머가 자체 PC 게임 엔진을 콘솔로 포팅하며 텍스처 변환 코드를 작성하면서 체감한 복잡성을 해부한 글입니다. BC7 블록 압축, 스위즐을 통한 텍셀 순서, 밉(mip), 타일, 밉 테일(mip tail), 비연속 타일 매핑까지, 텍스처 하나가 메모리에 놓이기까지 거치는 계층 구조와 그 이유를 DirectX 12 관점에서 차례로 설명합니다.

본문

저자는 505 Games에서 자체 PC 게임 엔진을 콘솔로 포팅하는 팀에서 일하며, 작년에 게이밍 플랫폼 간 텍스처 데이터를 변환하는 코드를 작성해야 했습니다. 개별 요소들은 각각 알고 있었지만, 이 모든 세부 사항을 동시에 다루는 코드를 직접 작성하고 나서야 전체 복잡성이 비로소 체감됐다고 말합니다. 글에서는 1024x1024 RGBA 텍스처에 BC7 압축을 적용한 예시를 따라가며 블록 압축, 텍셀 순서, 밉, 타일의 네 가지를 설명하고, 피치·깊이·배열 인덱스처럼 단순 오프셋만 필요한 요소는 생략합니다.

왜 메모리 위치 이야기인가

텍스처는 모든 플랫폼에서 동일한 계층 구조의 빌딩 블록으로 분해되지만, 계층의 각 단계에서 요소들이 메모리에 배치되는 순서는 플랫폼마다 다릅니다. 따라서 플랫폼 간 텍스처 변환은 결국 각 요소가 변환 후에 어떤 메모리 주소에 있어야 하는지 계산하는 문제로 환원됩니다. 플랫폼 구분이 필요한 부분에서는 DirectX 12의 관점을 따릅니다.

블록 압축과 BC7

단순한 RGBA 이미지라면 텍셀이 좌에서 우로, 행 단위로 이어지는 바이트 스트림이라고 생각하기 쉽지만, 현대 게임 텍스처는 대부분 블록 압축을 사용합니다. 가장 널리 쓰이는 BC7은 인접 텍셀의 유사성을 이용한 손실 압축으로, 텍셀을 4x4 블록으로 저장합니다. 각 블록은 엔드포인트(end point)라 불리는 기준 색상 쌍을 지정하고, 각 텍셀은 두 엔드포인트 사이를 보간한 값을 가리키는 인덱스로 색을 얻습니다. 블록 하나는 16바이트로 16개 텍셀을 표현하므로 텍셀당 1바이트, 즉 8비트 RGBA 대비 4배 압축입니다. 그런데도 육안으로는 나란히 놓은 스크린샷에서도 구분이 어려울 정도라서, 대용량 텍스처 스트리밍에 큰 이점이 됩니다. 다만 변환 코드 입장에서는 텍셀을 다룰지 블록을 다룰지 매번 분기해야 하고, 메모리 크기 계산이 해상도·채널 수·비트 깊이·압축 모드에 따라 달라집니다.

압축 텍스처 디버깅 요령

BC7의 각종 트릭 때문에 메모리 덤프는 툴링 없이 사실상 판독이 불가능하고, 바이트 정렬이 어긋나면 어디가 엔드포인트이고 어디가 인덱스인지가 뒤틀려 데이터 전체가 무의미한 색으로 보입니다. 저자가 쓴 요령은 두 가지입니다. 대부분의 경우 16바이트 블록을 32비트 정수 4개로 대체해 x, y, z 좌표와 밉 레벨 같은 값을 기록하면, 덤프의 값과 실제 좌표를 대조해 정확한 메모리 오프셋을 식별할 수 있습니다. 더 큰 데이터를 써야 할 때는 전부 0으로 채운 16바이트 블록이 기술적으로는 무효하지만 반드시 검은색으로 렌더링된다는 점을 이용해, 검은 블록들을 시각적 마커로 두고 그 사이에 디버그 데이터를 기록하는 방식을 씁니다.

텍셀 순서 — 스위즐

시각적으로 가까운 텍셀은 함께 샘플링될 가능성이 높으므로 메모리에서도 가깝게 두어야 합니다. 이를 위해 텍스처는 스위즐(swizzle)이라는 배치 패턴을 사용하며, 가장 유명한 것이 프랙탈 Z 패턴으로 진행하는 Morton order입니다. 어떤 순서가 필요한지는 텍스처와 플랫폼에 따라 달라지므로, 변환 시 소스와 타깃 양쪽의 순서를 모두 고려해 텍셀을 재배열해야 합니다. 정렬은 맞는데 스위즐이 틀리면 이미지가 4x4 퍼즐 조각처럼 뒤섞여 보입니다.

밉과 타일, 그리고 밉 테일

텍스처는 사실 단일 이미지가 아니라 절반씩 축소된 밉(mip)의 연속입니다. 카메라에서 먼 표면은 픽셀 하나가 여러 텍셀과 겹치므로, 미리 만들어둔 밉 중 렌더 타깃 해상도와 맞는 것을 골라 쉬머링과 모아레 같은 앨리어싱을 피합니다. 밉의 저장 순서가 오름차순인지 내림차순인지는 플랫폼마다 다릅니다. 타일은 캐시 지역성을 위한 또 다른 계층으로, 보통 64KiB 크기이며 각 타일이 밉의 정사각형 조각을 담습니다. 예시의 1024x1024 BC7 텍스처는 4x4 그리드의 타일 16개로 이뤄지고, 타일 하나에는 256x256 텍셀, 즉 4x4 텍셀 블록 4096개가 스위즐에 따라 배치됩니다. 타일보다 작은 밉에 타일 하나를 통째로 주면 낭비이므로, 대부분의 플랫폼은 작은 밉들을 최소 개수의 타일에 채워 넣는데 이를 밉 테일(mip tail)이라 부릅니다. 예시 텍스처는 마지막 8개 밉이 64KiB 타일 하나에 들어갑니다.

비연속 타일 — 마지막 함정

현대 엔진에서는 텍스처를 타일 단위로 공유 텍스처 메모리(DirectX 12의 힙)에 스트리밍하므로, 같은 텍스처의 타일들이 메모리에서 연속적이지 않을 수 있습니다. 따라서 텍스처 시작 주소에 전역 오프셋을 더하는 식의 계산은 다른 텍스처의 타일을 덮어쓰고 자기 타일 일부는 초기화하지 못하는 버그를 만듭니다. 타일 인덱스에서 실제 매핑 주소로 변환하는 테이블을 둔 뒤 타일 내 오프셋을 더해야 하는데, 저자는 이 잘못된 가정 때문에 실제 버그를 만들었고 원인을 깨닫기까지 꽤 시간이 걸렸다고 회고합니다.

Hacker News 반응

  • @doormatt — 훌륭한 글입니다! LLM 특유의 어조가 조금도 느껴지지 않네요.
  • @Agentlien — 감사합니다. 제 통찰을 제가 필요하다고 느낀 방식대로, 제 목소리로 표현하는 데 많은 정성과 노력을 들였습니다. 기계가 그 말들을 대신하게 했다면 그 전제 자체가 망가졌을 겁니다.
  • @taylor-tg — 정말 즐겁게 읽었습니다. 열정이 담긴, 직접 손으로 쓴 글을 읽으니 신선합니다. 플랫폼 간 텍스처 처리라는 얌전해 보이는 주제에 이토록 많은 복잡성이 있는 줄 몰랐습니다. 505가 개발사들의 게임을 더 많은 사람에게 닿게 하는 일을 엿볼 수 있어서도 좋았고요. 505가 그렇게 많은 유명 타이틀의 퍼블리셔인 줄은 몰랐네요.
  • @essai57 — 정말 좋은 글이네요, 써주셔서 감사합니다! 최근 게임 모딩과 관련해서 이 분야를 조금 건드려 봤는데, 글에서 설명하는 밉·타일·블록·텍셀의 계층을 실제로 마주쳤습니다. 정말 공감이 가네요. 훌륭한 정리입니다!
  • @augment_me — NTC, VAE-VQ 같은 뉴럴 코덱으로 텍스처를 일반화된 uint8 임베딩 공간에 압축한 뒤, 타깃 하드웨어 표준으로 전개해서 밉맵 등을 만드는 방식은 왜 안 되는 건가요?

원문: Hacker News / 번역·요약: Trawling