플레이스테이션 2 GS 에뮬레이션 – Vulkan 컴퓨트 에뮬레이션의 최종 경계
(themaister.net)- paraLLEl-GS는 PS2의 GS(Graphics Synthesizer)를 Vulkan 컴퓨트로 재현해, 20년 가까이 사실상 표준이던 GSdx가 남긴 정확도와 업스케일링 한계를 다룸
- GS는 4MiB VRAM과 높은 필레이트를 바탕으로 동작하지만, 목적지 알파 테스트·조건부 블렌딩·1.0을 넘는 알파/색상 같은 픽셀 파이프라인 특성 때문에 일반 그래픽 API로 맞추기 어려움
- 구현은 VRAM을 페이지와 256바이트 블록 단위로 추적하고, CLUT 스냅샷·텍스처 언스위즐·렌더 패스 배치를 조합해 프레임버퍼/텍스처 피드백을 처리함
- Tales of the Abyss, Final Fantasy X, MGS2, Valkyrie Profile 2, Shadow of the Colossus 같은 사례에서 업스케일링, UI, 고정밀 블렌딩, 텍스처 피드백 문제가 비교됐고 8x·16x SSAA 장면도 다룸
- 현재 검증은 주로 GS dump 재생에 의존하며, PCSX2 해킹 패치와 mkfifo 실시간 테스트는 있지만 실제 사용자에게 닿으려면 에뮬레이터 통합이 필요함
paraLLEl-GS의 목표와 출발점
- paraLLEl-GS는 PlayStation 2 GS(Graphics Synthesizer) 를 Vulkan 컴퓨트로 에뮬레이션하는 프로젝트임
- 같은 작성자의 2020년 작업인 paraLLEl-RDP는 N64 RDP를 Vulkan 컴퓨트로 구현했고, Angrylion을 기준으로 비트 정확성에 가까운 결과와 업스케일링을 목표로 삼았음
- PS2에서는 GSdx가 약 20년 동안 사실상 최첨단 구현으로 남아 있었음
- 2014년경 OpenCL 기반 PS2 GS 컴퓨트 구현 시도가 있었지만 완료되지 않았고, 현재 upstream 저장소에서도 찾기 어려움
- PS2에서 컴퓨트 셰이더 래스터를 써야 할 이유는 N64보다 약함
- PCSX2에는 잘 최적화된 소프트웨어 렌더러와 비교적 견고한 그래픽 기반 렌더러가 있음
- 소프트웨어 렌더러는 업스케일링을 지원하지 않음
- 그래픽 기반 렌더러는 특히 업스케일링에서 여러 버그와 글리치를 보임
- paraLLEl-GS는 하드웨어에 대한 비트 정확성보다는 명백한 정확도 문제를 피하는 쪽에 초점을 둠
- GSdx 소프트웨어 렌더러도 하드웨어 비트 정확 구현으로 보이지 않아, 직접 비교 기반 테스트는 빠르게 한계에 닿음
PS2 GS가 까다로운 이유
- GS는 2000년 당시 이론상 초당 10억 픽셀 이상을 처리할 수 있는 필레이트와 대역폭을 가진 장치였음
- VRAM은 4MiB로 작지만, 여러 DMA 엔진을 통해 계속 스트리밍되도록 설계됨
- 픽셀 파이프라인 자체는 N64 RDP보다 단순한 면이 있음
- 단일 텍스처
- 단일 사이클 컴바이너
- 매우 기본적인 안티앨리어싱
- 일반 그래픽 API로 구현하기 어려운 기능이 많음
- 1.0을 넘는 블렌딩: PS1에서 이어진 동작으로 0x80이 1.0처럼 취급되고 0xff까지 표현 가능함
- 목적지 알파 테스트: 목적지 알파를 유사 스텐실처럼 사용할 수 있음
- 조건부 블렌딩: 알파에 따라 블렌딩을 조건부로 끌 수 있음
- 알파 보정: 알파를 기록하기 전 MSB를 OR해 강제로 1에 가깝게 만들 수 있음
- 알파 테스트의 부분 폐기: 색상만 버리고 깊이 기록은 유지하는 식의 동작이 가능함
- AA1: coverage-to-alpha 방식이며 픽셀별 깊이 기록 제어와 얽힘
- 32비트 고정소수점 Z: D32_UINT 지원이 기술적으로 존재하지만 실제 사용 사례는 아직 보지 못함
- 프로그래머블 블렌딩이 없으면 즉시 모드 데스크톱 GPU에서는 ROV나 픽셀별 배리어가 필요해 성능이 크게 나빠짐
- 컴퓨트 구현은 자체 타일 기반 지연 렌더러(TBDR) 를 만들어 이런 제약을 우회함
래스터 규칙, 정점 큐, 메모리 배치
- GS의 프리미티브는 클립 공간에서 비교적 평범하게 제공됨
- VU1이 변환과 클리핑을 수행하고 GS에 여러 정점 속성을 내보냄
- 좌표와 속성은 GS 특유의 형식을 가짐
- X/Y: 12.4 unsigned fixed-point
- Z: 24비트 또는 32비트 uint
- FOG: 8비트 uint
- RGBA: 정점별 조명용 8비트 값
- STQ: 원근 보정 텍스처 좌표
- UV: 원근 보정이 없는 12.4 고정소수점 비정규화 좌표
- 래스터 규칙은 D3D9 스타일에 가까움
- 삼각형은 현대 GPU처럼 top-left raster 규칙을 사용함
- 픽셀 중심은 D3D9처럼 정수 좌표에 있음
- 선은 Bresenham 알고리듬을 사용해 업스케일링이 어렵고, rect나 parallelogram으로 근사해야 함
- 점은 가장 가까운 픽셀에 스냅됨
- 스프라이트는 두 좌표를 가진 단순 쿼드임
- GS 정점 큐는 OpenGL 1.0의 즉시 모드와 비슷함
- RGBA, STQ, 여러 레지스터를 설정하고 XYZ 레지스터 쓰기가 정점 “kick”을 형성함
- TRIANGLE_FAN도 지원함
- PS2 픽셀 좌표는 페이지 단위로 배치됨
- 한 페이지는 8KiB
- 페이지는 32개 블록으로 나뉨
- 32비트 RGBA 기준 페이지는 64×32 픽셀이고, 32개의 8×8 블록이 Z-order로 스위즐됨
- 24비트 색상이나 24비트 깊이에 렌더링할 때 남는 상위 8비트에 텍스처를 둘 수 있음
- 8H, 4HL, 4HH 형식은 8비트·4비트 팔레트에 유용함
텍스처, CLUT, TEXFLUSH
- GS 텍스처링은 현대 API와 비슷한 부분과 특이한 부분이 섞여 있음
- 텍셀 중심은 현대 API처럼 half-pixel에 있음
- 서브텍셀 정밀도는 8비트가 아니라 4비트로 보임
- 바이리니어 필터는 일반적인 바이리니어이며 N64의 3-point 필터 같은 특수 구조는 아님
- 특수 주소 지정 모드가 구현 난도를 높임
- REGION_CLAMP는 텍스처 아틀라스 내부의 임의 영역에 clamp를 적용할 수 있음
- REGION_REPEAT는 좌표마다
(u & MASK) | FIX같은 비트 연산을 적용할 수 있어 구현이 더 까다로움
- 밉매핑은 파생값 대신 보간된 Q factor의 log2와 스케일 계수로 LOD를 계산함
- 컴퓨트 구현에서는 파생값에 의존하지 않는 점이 큰 장점임
- 이 방식은 anisotropic filtering 같은 기능을 지원할 수 없음
- CLUT는 현재 팔레트를 담는 1KiB 캐시임
- VRAM에서 CLUT 캐시로 명시적 복사를 해야 사용할 수 있음
- 32비트 색상 기준 256색 팔레트 하나를 담을 수 있음
- 16bpp 기준으로는 16색 팔레트 32개를 담을 수 있음
- TEXFLUSH는 텍스처 캐시 동기화와 무효화에 가까운 명시적 명령임
- 구현 초기에는 TEXFLUSH를 위험 추적의 기준으로 삼으려 했지만 결국 무시해야 했음
- 게임이 TEXFLUSH를 잊거나 너무 많이 호출하는 문제가 있었음
- 최종 구현은 minimal caching 접근을 택함
- 캐시가 없다고 보고 위험을 직접 추적함
- 피드백 루프에는 별도 예외 처리를 고려함
- GSdx도 같은 방향으로 보임
Vulkan 컴퓨트 렌더링 파이프라인
- 구현 파이프라인은 각 단계 사이의 동기화를 전제로 구성됨
- CPU의 VRAM 복사본을 GPU와 동기화
- VRAM 업로드 또는 local-to-local copy 수행
- VRAM에서 CLUT 캐시 업데이트
- VRAM을 VkImage로 언스위즐해 직접 샘플링 가능하게 변환
- 렌더링 수행
- GPU VRAM 복사본을 CPU로 다시 동기화
- 일반적인 게임 동작은 이 파이프라인과 잘 맞음
- 텍스처를 VRAM에 업로드
- 팔레트를 VRAM에 업로드
- CLUT 캐시 업데이트
- 텍스처로 드로우
- 필요하면 VRAM에서 VkImage로 언스위즐
- 프리미티브 배치를 렌더 패스로 구성
- 뒤로 향하는 위험이 없으면 배치와 동기화 지연이 가능함
- 이런 방식의 렌더러에서 성능을 내려면 배치 유지가 중요함
- 주요 위험 사례는 별도로 처리됨
- 이미 copy가 쓴 VRAM에 다시 copy
- 샘플링된 텍스처나 CLUT가 읽은 VRAM에 copy
- 렌더링된 영역을 텍스처로 샘플링
- 렌더링된 VRAM에 copy
페이지 추적과 텍스처 캐시
- GS 에뮬레이션에서 가장 어려운 부분은 VRAM의 read-after-write와 write-after-write 위험을 다루는 일임
- 4MiB VRAM은 페이지 단위로 먼저 나뉨
- 페이지는 프레임버퍼와 깊이 버퍼의 단위라 가장 의미 있는 추적 기준임
- 페이지 단위로 추적하는 상태는 다음과 같음
- pending frame buffer write
- pending frame buffer read
- 텍스처와 VRAM copy는 256바이트 정렬을 가지므로 32개 블록에 대한 u32 비트마스크를 사용함
- VRAM copy write
- VRAM copy read
- CLUT 캐시 또는 VkImage로의 pending read
- 어떤 write로든 덮인 블록
- 24비트 색상에 렌더링하면서 상위 8비트를 텍스처로 샘플링하는 경우에는 위험이 없을 수 있음
- 이를 위해 프레임버퍼 write mask와 텍스처 read mask를 따로 추적함
- 각 페이지는 연결된 VkImage 목록을 가짐
- 페이지 텍스처가 무효화되면 이미지는 파괴되고 VRAM에서 다시 언스위즐해야 함
- 텍스처 하나가 여러 페이지에 걸칠 수 있으며, 그중 하나만 덮여도 텍스처가 무효화됨
- 단순하고 보수적인 추적만으로는 PS2 게임에서 작동하지 않음
- 256바이트 블록 수준 추적과 write/read mask 고려가 중요함
- POT 텍스처와 REGION_CLAMP 미사용 때문에 false positive가 생길 수 있음
- 예를 들어 512×448 렌더 타깃을 512×512 텍스처로 설정하면 사용하지 않는 영역이 위험처럼 보일 수 있음
- 구현은 그 “red zone”의 잠재 위험을 무시하는 우회책을 사용함
CLUT 배치와 텍스처 언스위즐
- 텍스처 업로드를 배치하려면 CLUT 업로드도 함께 배치해야 함
- 구현은 CLUT의 1024개 복사본을 스냅샷 링 버퍼로 가짐
- 하나의 워크그룹이 업데이트를 순회하며 SSBO에 기록함
- N64 RDP의 TMEM 업데이트와 비슷하지만 CLUT 업데이트는 훨씬 단순함
- Vulkan에서는 새 VkImage를 할당하고 VkDeviceMemory에서 서브할당한 뒤 컴퓨트 셰이더로 언스위즐함
- Vulkan specialization constants를 사용해 텍스처 형식과 스위즐 로직을 특화함
- REGION_REPEAT의 특수 동작도 언스위즐 단계에서 처리함
- 이후 ubershader가 이 경우를 고려해 수동 바이리니어 필터링을 할 필요가 줄어듦
- 렌더 타깃도 VRAM SSBO를 거쳐 텍스처로 왕복함
- 렌더 타깃을 텍스처로 직접 forward하려는 시도는 버그와 예외가 너무 많다고 판단됨
삼각형 셋업, 비닝, ubershader
- paraLLEl-GS는 paraLLEl-RDP처럼 타일 기반 렌더러임
- 비닝 전에 삼각형 셋업을 수행하며, 입력은 세 배열로 나뉨
- 위치
- 정점별 속성
- 프리미티브별 속성
- 래스터라이저는 barycentric 기반이며 Fabian Giesen의 그래픽 파이프라인 글과 Pineda 1988 논문에서 설명하는 병렬 래스터화 방식의 영향을 크게 받음
- 실제 PS2 GS는 DDA, 즉 scanline rasterizer지만, GS DDA의 비트 정확한 설명을 알지 못해 barycentric 방식을 사용함
- wide line과 sprite 구현을 위해 parallelogram도 지원함
- inv_area는 커스텀 고정소수점 RCP로 계산됨
- 표준 GPU RCP는 구현별 일관성이 낮고 약 22.5비트 정확도라 피함
- 커스텀 RCP는 약 24.0비트 정확도를 목표로 함
- 비닝은 보통 32×32 픽셀 블록을 사용함
- 렌더 패스당 최대 프리미티브 수는 u16 인덱스 때문에 64k임
- 관찰한 주요 렌더 패스는 대체로 10k~30k 프리미티브 범위였음
- PS2 GS는 필레이트가 높고 픽셀당 복잡도가 낮아 순수 ubershader가 가능함
- N64와 달리 bindless를 활용할 수 있어 텍스처링 복잡도도 줄어듦
- ubershader는 early Z, deferred on-tile shading, lazy pixel shading을 사용함
- 픽셀이 이전 결과에 의존할 때만 실제 셰이딩을 수행함
- alpha test, color write mask, alpha blending 등이 이런 의존성을 만듦
- 최종 프레임버퍼 색상과 깊이는 SSBO에 기록돼 GPU 대역폭 사용을 줄임
슈퍼샘플링과 업스케일링 아티팩트 완화
- 단일 샘플 렌더링만으로는 이 렌더러의 활용도가 충분하지 않음
- 예를 들어 8x SSAA에서는 GPU에 VRAM 10개 버전을 유지함
- 단일 샘플 VRAM 1개
- 단일 샘플 VRAM의 reference 값 1개
- 슈퍼샘플 8개
- 렌더링 시 단일 샘플 VRAM과 reference가 일치하면 슈퍼샘플 버전을 로드함
- 증분 렌더링에서 중요함
- 타일 완료 시 clustered subgroup 연산으로 멀티샘플 resolve를 수행하고 슈퍼샘플과 단일 샘플 복사본을 기록함
- 슈퍼샘플링은 단순 업스케일링보다 톱니가 덜하고, 3D 요소와 UI 요소의 해상도 느낌을 더 일관되게 맞춤
- 스프라이트 프리미티브는 항상 single-rate로 렌더링해야 함
- UI나 유사 요소가 대부분이며, 업스케일하면 의도한 rect 밖을 샘플링하거나 바이리니어 필터링으로 과도하게 흐려질 수 있음
- 많은 UI가 일반 삼각형으로 그려지기 때문에, 일부 flat primitive는 속성 보간을 single-pixel 좌표로 낮춤
- perspective를 사용하더라도 모든 정점의 Q가 같고 Z도 같으면 flat UI primitive로 추정함
- false positive가 있을 수 있지만 테스트한 게임에서는 충분히 잘 작동함
게임별 결과와 어려운 사례
- Tales of the Abyss에서는 PCSX2 Vulkan backend 업스케일링에서 유리의 bloom 정렬 불일치와 사각 패턴이 보임
- paraLLEl-GS의 8x SSAA에서는 나쁜 업스케일링의 전형적 문제가 잘 드러나지 않음
- 해당 스크린샷에는 FSR1 후처리 업스케일도 적용됨
- Final Fantasy X의 UI는 native resolution과 4x upscale 비교에서 업스케일링 문제가 드러남
- MSAA snap trick이 아티팩트를 피하는 데 효과적임
- 핵심 원칙은 UI를 nearest neighbor 정수 배율 이상으로 업스케일하지 않는 것임
- MGS2는 PCSX2에서 높은 blending accuracy를 요구하는 사례가 있음
- PCSX2는 programmable blending 경로에서 프리미티브마다 배리어가 들어가 성능이 크게 떨어짐
- paraLLEl-GS는 항상 100% blend accuracy로 동작하는 구조이며, RX 7600에서 16x SSAA 장면이 25W와 17% GPU utilization으로 제시됨
- Valkyrie Profile 2에는 자기 픽셀의 알파를 팔레트 인덱스로 샘플링하는 사례가 있음
- paraLLEl-GS는 이를 감지해 텍스처 인덱스를 특수 값으로 만들고, in-register framebuffer color를 참조함
- 이 최적화로 render pass barrier가 500개 이상에서 18개로 줄어듦
- MGS2 intro의 camo 효과는 framebuffer를 텍스처로 샘플링하지만 픽셀 정렬이 맞지 않는 겹친 좌표를 사용함
- PCSX2도 여기서 배리어를 추가하지 않는 것으로 보이며, paraLLEl-GS도 같은 방식으로 처리함
- Shadow of the Colossus는 강한 스트레스 테스트 역할을 함
- PCSX2 최대 blend accuracy에서는 intro에서 2x upscale만으로 GPU가 24 FPS까지 내려감
- paraLLEl-GS는 8x SSAA에서도 성능이 괜찮지만, 해당 장면에서는 부담이 커짐
- 이 경우 병목은 GPU보다 CPU의 geometry processing에 있음
현재 상태와 다음 단계
- 현재 테스트의 실질적인 방법은 GS dump를 사용하는 것임
- PCSX2에서 raw GS trace를 덤프할 수 있게 하는 hack-patch가 있음
- mkfifo를 통한 조잡한 실시간 테스트도 가능함
- 최종 사용자에게 유용해지려면 어떤 형태로든 에뮬레이터 통합이 필요함
- PS2 라이브러리가 매우 크기 때문에 아직 많은 버그가 숨어 있을 가능성이 큼
- standalone library 성격 때문에 오래된 스타일의 렌더링 API처럼 쓰는 잠재적 사용 사례도 있음
댓글과 토론
Hacker News 의견들
-
이 글에서 GS 약어가 언제 풀어 쓰이는지 한참 찾아야 하나 싶음. 내용은 흥미로운데 여기서 좀 뒤처지는 느낌임
- Graphics Synthesizer의 약자이고, Sony가 PS2의 “GPU”에 붙인 이름임
-
“프로그래머블 블렌딩이 있기를 기도하라”라니, 2000년대 초 픽셀 셰이더를 처음 배운 뒤로 “블렌딩 셰이더”를 통한 프로그래머블 블렌딩을 계속 바라왔음
참고로 커스텀 텍스처 형식/압축, 텍스처 합성 등에 유용한 “텍스처 셰이더”를 통한 프로그래머블 텍스처 디코딩도 원했음
어쩌다 보니 GPU는 프로그래머블 블렌딩보다 레이 트레이싱을 먼저 얻었는데, 전자는 여름밤 꿈같았고 후자는 또 하나의 고정 기능 블록을 프로그래머블 블록으로 바꾸는 정도처럼 느껴졌음. 텍스처 셰이더는 아직도 기다리는 중임- PowerVR 계열을 물려받은 모바일 GPU에는 그런 기능이 있음
https://medium.com/pocket-gems/programmable-blending-on-ios-...
https://developer.apple.com/videos/play/tech-talks/605 - 마지막으로 만져봤을 때 모바일 PowerVR GPU에는 프로그래머블 블렌딩이 있었고, 사실 블렌딩 방식이 그것뿐이었음. PS Vita에서 블렌드 상태를 바꾸는 데 약 1ms가 걸려서 보기 좋진 않았음
- VK_EXT_fragment_shader_interlock가 일종의 프로그래머블 블렌딩 아닌가 싶음. DirectX 쪽의 Raster-order-views도 그렇고
이걸 활용한 좋은 예시가 있음: https://vulkan.org/user/pages/09.events/vulkanised-2024/vulk... - 요즘은 메시 셰이더, work graphs, CUDA, 일반 C++ 셰이더도 있음. OTOY는 이제 렌더링을 전부 컴퓨트로 처리함
- 그 대부분은 Vulkan이나 DX12에서 에뮬레이션할 수 있을 것 같고, 다른 API는 잘 모르겠음. 다만 실제 사용처가 궁금함
어느 정도는 구현 가능하다고 보지만, 설득력 있는 사용 사례가 없으면 구현 작업을 정당화하기 어렵다고 봄
- PowerVR 계열을 물려받은 모바일 GPU에는 그런 기능이 있음
-
GS에서 제일 좋아하는 부분은 버스 구조의 말도 안 되는 규모였음. 총 2560비트 폭에 캐시 분할도 영리했음
PS3는 어떤 면에서는 후퇴처럼 느껴졌고, 특히 블렌딩이 그랬음 -
이 접근 방식은 Dolphin의 ubershader와 어떻게 비교되는지 궁금함
- 기본적으로는 거의 비교 대상이 아님. Dolphin의 ubershader는 현대의 유연한 하드웨어로 고정 기능 블렌딩/텍스처링을 흉내 내는 한 가지 일을 함
사실 Dolphin이 도입했을 때도 이미 오래된 기법이었음. 이 프로젝트는 래스터라이저까지 포함한 완전한 렌더러이고, 본문에서 보이듯 블렌딩용 ubershader도 포함
셰이더가 삼각형을 그리는 게 아니라, 삼각형 안의 각 지점마다 호출되어 몇 가지 입력을 받고 그 점의 색을 결정함. 전체 CPU 에뮬레이터와 ADD/MUL 명령만 구현한 것을 비교하는 것과 어렴풋이 비슷함
- 기본적으로는 거의 비교 대상이 아님. Dolphin의 ubershader는 현대의 유연한 하드웨어로 고정 기능 블렌딩/텍스처링을 흉내 내는 한 가지 일을 함
-
top-left raster가 무슨 뜻인지 궁금함
- 삼각형 래스터화에서 쓰는 top-left 규칙을 말함 (https://en.wikipedia.org/wiki/Rasterisation#Triangle_rasteri...)