God of War on PSP, recompiled to WebAssembly and running in the browser
PSP용 갓 오브 워를 WebAssembly로 재컴파일해 브라우저에서 실행합니다
PSP 게임의 MIPS 코드를 C++로 정적 재컴파일하고, PSP 운영체제와 그래픽 기능을 구현해 WebAssembly와 WebGL2로 실행하는 프로젝트입니다. 《God of War: Chains of Olympus》는 측정한 장면에서 초당 60프레임으로 동작하며, 성능 개선의 상당 부분은 렌더러 최적화보다 프레임 동기화와 브라우저별 병목을 찾아 해결한 결과입니다.
- 주제
AI 요약
PSP 게임을 브라우저에서 실행하는 프로젝트입니다. 게임의 MIPS 기계어를 미리 C++로 변환한 뒤 WebAssembly로 컴파일합니다. 변환된 코드는 PSP의 레지스터와 메모리 모델을 사용하며, 별도로 구현한 PSP 운영체제 기능과 그래픽 처리 코드에 연결됩니다. 그래픽은 WebGL2로 그립니다. 프로젝트는 일반적인 에뮬레이터를 쓰지 않는다고 설명하지만, PSP의 시스템 동작을 재현하는 코드가 포함됩니다.
실행 결과와 게임 데이터
첫 번째 대상은 《God of War: Chains of Olympus》입니다. 부팅부터 메뉴, 컷신, 전투까지 실행하며, 측정한 장면에서 Chrome과 Firefox를 사용하는 노트북 기준 초당 60프레임을 기록했습니다. PSP 기본 해상도인 480×272의 최대 네 배 해상도로 그릴 수 있고, 휴대전화에서는 화면 터치 조작을 지원합니다. 음악과 음성, 효과음도 재생하지만 영상은 아직 건너뜁니다.
《God of War: Ghost of Sparta》도 같은 스크립트와 재컴파일러로 실행했습니다. 작은 파일 하나를 처리하려고 PSP의 DRM 복호화 기능을 추가하고 시스템 호출 몇 가지와 조명 처리를 보완했습니다. 별도의 성능 최적화 없이 기본 해상도의 세 배에서 초당 55~60프레임으로 동작합니다. 두 게임은 모두 Ready at Dawn 작품이며 같은 엔진을 사용합니다. 따라서 다른 개발사의 PSP 게임에서도 같은 결과가 나올지는 알 수 없습니다.
게임 데이터는 저장소에 포함하지 않습니다. 사용자가 소유한 게임 디스크 이미지가 필요합니다. 로컬에서 게임 데이터를 추출하고 실행 파일을 복호화한 다음, 코드를 변환해 웹 페이지를 빌드합니다. 디스크의 나머지 파일은 HTTP Range 요청으로 필요한 부분만 읽습니다. 실행 파일만 먼저 내려받는 방식입니다.
재컴파일과 PSP 기능 구현
PSPRecomp가 복호화한 실행 파일에서 함수를 찾아 C++ 코드를 생성합니다. 코드 생성 단위는 게스트 코드 16KiB입니다. 변환된 코드는 PSP 레지스터 파일과 메모리 모델을 대상으로 실행됩니다. 이 저장소에는 PSPRecomp 수정 사항과 웹 실행을 위한 별도 프로필이 들어 있습니다.
PSP 운영체제 기능은 고수준 에뮬레이션으로 처리합니다. 협력형 스레드, 세마포어, 이벤트 플래그와 콜백, 메모리 파티션, 파일 시스템, 컨트롤러, 오디오 출력, 저장 데이터, 메시지 창 등을 구현했습니다. 게임 시간은 프레임 단위로 진행하므로 호스트 컴퓨터의 속도와 관계없이 게임은 초당 60회로 시간이 흐르는 것으로 봅니다.
그래픽 칩인 GE는 디스플레이 목록을 받습니다. ge.cpp가 CPU에서 정점 형식, 스키닝, 조명, 텍스처 생성, 클리핑, 후면 제거 등을 해석하고, ge_gl.cpp가 WebGL2 렌더링으로 넘깁니다. PSP의 비디오 메모리에서 같은 위치를 쓰는 프레임버퍼는 WebGL 렌더 타깃에 연결합니다. 그래서 텍스처에 그린 결과를 다시 읽는 효과도 GPU 안에서 처리합니다. PSP의 스텐실 버퍼는 프레임버퍼 알파 채널에 저장됩니다. 이 프로젝트는 스텐실과 알파 값을 GPU에서 맞추고, 안개와 픽셀 형식 변환, 블록 전송도 처리합니다.
PSP처럼 CPU와 그래픽 처리를 별도 스레드에서 진행합니다. GE와 WebGL 컨텍스트는 OffscreenCanvas를 사용하는 워커 스레드에서 실행합니다. 디스플레이 목록을 제출하면 곧바로 돌아오고, 게임이 명시적으로 동기화를 요청할 때만 기다립니다. 따라서 한 프레임 처리 시간은 두 작업을 더한 값이 아니라 둘 중 더 오래 걸리는 쪽에 가까워집니다. 브라우저가 WebGL2와 OffscreenCanvas 조합을 지원하지 않으면 단일 스레드로 전환합니다.
오디오는 PSP의 32개 음성 채널과 ADPCM, 피치, ADSR 엔벌로프를 구현해 효과음을 냅니다. 음악과 음성은 FFmpeg 디코더로 ATRAC3+ 스트림을 해독하고 AudioWorklet에서 믹싱합니다.
성능 개선 과정
초기 빌드는 초당 6프레임에 그쳤습니다. 작성자는 렌더러를 무작정 빠르게 만들기보다 프로파일링으로 실제 병목을 찾는 일이 초당 60프레임에 이르는 데 더 큰 역할을 했다고 설명합니다.
게임은 수직 동기화 신호를 기다리지 않고 프레임버퍼를 교체했습니다. 그 결과 화면에 한 프레임이 표시되는 동안 내부에서는 약 여덟 프레임을 그렸습니다. 한 번의 수직 동기화 구간에 두 번 교체하는 스레드를 다음 구간까지 기다리게 하자, 표시되는 프레임마다 필요한 작업이 약 8분의 1로 줄었습니다. PPSSPP도 사용하는 방식입니다.
WebAssembly에서 std::chrono로 시간을 읽으면 clock_gettime 호출과 JavaScript의 BigInt 변환이 따라옵니다. 프리미티브마다 시간을 읽는 프로파일링 코드가 프레임 시간의 약 3분의 1을 차지했습니다. 프로파일링을 켰을 때만 performance.now()를 읽도록 바꿨습니다.
Firefox에서는 WebGL 버퍼 업로드 때마다 GPU 프로세스로 데이터가 복사되고, 인덱스 버퍼가 바뀌면 다음 그리기 전에 버퍼 전체를 다시 검증합니다. 공유 인덱스 링 버퍼를 4MB로 만들자 성능이 초당 3프레임까지 떨어졌습니다. 각 드로에 작은 인덱스 버퍼를 따로 쓰면서 문제가 해결됐습니다. 반대로 큰 버텍스 버퍼에 데이터를 조금씩 써 넣는 방식은 어느 브라우저에서도 이득이 없었고, 모바일에서는 오히려 멈춤을 일으켰습니다. GPU가 아직 읽을 수 있는 버퍼를 수정할 때 모바일 드라이버가 대기하거나 복사하기 때문입니다.
스텐실과 알파를 전체 화면 패스로 서로 복사하는 구현은 정확했지만, 네 배 해상도에서는 프레임마다 추가로 6천만 픽셀을 처리했습니다. 실제로 바뀐 사각형과 스텐실 비트, 상수값만 추적해 추가 작업량을 약 700만 픽셀로 줄였습니다. 그래픽 처리 스레드를 분리한 뒤 Firefox에서 전투 장면의 게임 처리와 그래픽 처리 시간을 합쳐 약 17ms 걸리던 작업은 두 시간 중 긴 쪽인 약 10ms로 줄었습니다.
빌드와 제약
Linux에서 실행을 확인했으며, macOS 브라우저 빌드는 가능할 것으로 보지만 시험하지 않았습니다. Git, CMake, Ninja, C++20 컴파일러, Python 3가 필요합니다. 게임 하나마다 디스크 데이터 추출본과 생성 코드, 빌드를 합쳐 약 2GB의 디스크 공간을 사용합니다. 8코어 노트북에서는 《Chains of Olympus》 ZIP 파일을 웹 페이지로 만드는 데 약 4분이 걸렸습니다.
저장소에는 프로젝트의 원본 코드와 PSPRecomp 수정 사항, 각자 라이선스가 있는 외부 코드만 포함됩니다. 게임 코드나 데이터는 배포하지 않습니다. 생성한 C++와 WebAssembly는 게임 실행 파일을 변환한 결과이므로 프로젝트 설명은 이를 공개하지 말고 개인 컴퓨터에 보관하라고 안내합니다. 게임 디스크 이미지와 실행 파일 복호화 도구도 사용자가 준비해야 합니다.
Hacker News 반응
- @wren6991 — 약간 꼼꼼한 지적일 수 있지만, 이 설명은 에뮬레이션 스택을 묘사합니다. 많은 에뮬레이터가 이미 대상 기계어를 다른 형태로 변환하거나 JIT 컴파일합니다. 다만 WASM을 거치지 않을 뿐입니다.
- @gzalo — 대부분은 AOT 컴파일이나 정적 재컴파일을 전통적인 에뮬레이션으로 보지 않을 겁니다.
- @rowanG077 — 더는 CPU를 에뮬레이션하지 않지만, 나머지 시스템은 분명 에뮬레이션하고 있습니다. 그래서 전통적인 에뮬레이션과 상당히 가깝다고 봅니다.
- @anonymous908213 — PSP 운영체제를 고수준으로 구현했다면, 이건 단계를 더 거친 에뮬레이터일 뿐입니다. 처음부터 에뮬레이션을 제대로 하기보다 게임별 패치를 요구하는 나쁜 에뮬레이터이고, 게임과 함께 실행 파일에 에뮬레이터를 넣은 겁니다. 완전히 엉성한 작업입니다.
- @wren6991 — 이 프로젝트는 폐기 가능한 소프트웨어 시대에 들어선 느낌을 줍니다. 간단하고 일관되며 유연한 플랫폼이나 라이브러리를 만드는 수고가, 누구나 몇 분 만에 버튼을 눌러 자기 용도에 맞는 버전을 만들 수 있다면 가치가 줄어들 수 있습니다. 이 스레드 사람들은 모두 이 프로젝트를 엉성한 작업으로 봤다는 점도 마음에 듭니다.
- @doublerabbit — 이제 PSP 게임을 역컴파일해 WebGL로 실행할 수 있다니 믿기 어렵습니다. 2006년에는 PS1 에뮬레이터 ePSXe를 제대로 돌리려면 성능 좋은 Pentium이 필요했습니다.
- @0x000xca0xfe — 역컴파일할 필요도 없습니다. 저는 오래된 Pentium MMX 350MHz 게임을 비슷한 방식으로 만들면서 x86-32에서 WASM으로 가는 동적 재컴파일러를 작성했습니다. 오래된 Zen2 PC의 Firefox에서 1,000 MIPS 이상으로 실행됐습니다. 가능하다는 사실에 놀랐고, 지금도 비현실적으로 느껴집니다.
- @sigma5 — 10년 전에는 i7 노트북에서 이 게임을 에뮬레이션하려고 했는데 초당 15프레임 정도였습니다. 그래서 이번 결과가 마법처럼 느껴집니다.
- @fb03 — AI가 에뮬레이션의 거품을 완전히 깨고 있습니다. 이제는 에뮬레이션이 아니라 역공학을 거쳐 게임을 바로 포팅합니다. 오래된 게임 보존에는 멋진 시기입니다.
- @flohofwoe — 이건 보존이라기보다 리마스터링에 가깝습니다. 원본 게임 데이터와 가능하다면 원본 소스 에셋과 소스 코드도 계속 보존해야 합니다. 다만 포기된 저작물에 관한 법적 정의가 제대로 마련되면 좋겠습니다.
- @theturtletalks — 실제로 소유하고 직접 추출한 게임 ROM을 보유하는 건 합법입니다. 이 프로젝트는 ROM을 다른 콘솔인 웹에서 실행하게 해줍니다. 다만 이 경우 사용자가 ROM을 배포하면 불법입니다. 웹에서 작동하게 만드는 방법은 공정 이용입니다. 사용자가 자기 ROM을 넣고 웹에서 실행하는 사이트라면 합법적인 방식이 될 겁니다.
- @accidc — 덧붙이자면 관할 지역에 따라 다릅니다.
- @functionmouse — 구경하며 열광하는 사람에게는 좋지만, 실제 게임을 플레이하거나 보존하려는 사람에게는 끔찍합니다.
- @dfxm12 — 맥락을 덧붙이면, PPSSPP가 이미 있고 정확도도 이 프로젝트보다 떨어지지 않습니다. PSP와 게임도 합리적인 가격으로 구할 수 있습니다.
원문: Hacker News / 번역·요약: Trawling