문제점

이 게임은 PS1용 사이킥 포스1이다. BIOS의 폰트 영역을 사용하지 않고, 메인 메모리에 한글 폰트만 추가하여 BIOS ROM에 의존하지 않는 한글화 방식을 연구하던 중 우연히 다음과 같은 문제를 발견했다.

  1. no$psx에서는 오프닝 동영상에 자막이 표시되지만, 소리가 출력 되지 않으며 동영상 재생이 끝난 뒤 게임이 크래시된다.

  2. DuckStation을 비롯한 다른 에뮬레이터와 실제 PS1 환경에서는 오프닝 자막이 표시되지 않는다.

DuckStation에서 자막 미출력
no$psx에서 오프닝 영상 후 크래시

나는 자막이 표시되지 않는 것이 본래의 정상 동작이며, 최종 빌드에서 제작자가 자막 표시 경로를 비활성화했음을 가정하고 작업을 시작했다. 먼저 no$psx에서만 자막이 표시되는 이유를 이해하려면 SYSTEM.CNF에 설정된 초기 스택 주소가 어떻게 되는지 알아야 한다.

SYSTEM.CNF

BOOT = cdrom:SLPS_005.20;1
TCB = 4
EVENT = 10
STACK = 801FCE00

그 다음 실제 원인을 찾기 위해 no$psx에서 프로그램 진입점인 start를 확인해 보니, $sp와 $fp가 0x801FFFF0으로 SYSTEM.CNF에 기재된 값과는 다르게 잘못 초기화되어 있었다. 이 과정에서 스택에 저장되는 복귀 주소 $ra의 값인 0x8011F5D0가 자막 활성화 여부를 결정짓는 메모리 영역에 기록된다. 자막 활성화 코드는 이 값을 유효한 값으로 판단하고 자막 표시 플래그를 켠다. 그 결과 sub_80111970의 자막 처리 경로가 실행되면서 오프닝 동영상에 자막이 나타난다. 하지만 이것은 자막이 정상적인 절차에 따라 활성화된 것이 아니다. 잘못된 스택 초기화로 발생한 메모리 오염이 우연히 자막 표시 조건을 충족했을 뿐이다. 따라서 no$psx에서는 자막이 표시되지만, 잘못 설정된 스택은 이후에 매우 큰 문제를 일으켜 게임이 정지되는 것이다.

자막 처리 구조

sub_80111AC4는 Krom2RawAdd 를 래핑한 함수이다.

sub_80111970에는 다음 함수들이 포함되어 있다.

  • sub_80123B3C(ClearImage) : 자막을 표시할 VRAM 영역을 초기화한다.

  • sub_80123C98(MoveImage): 생성된 자막 이미지를 표시용 VRAM 영역으로 복사한다.

  • sub_801239A8(DrawSync): GPU의 VRAM 처리 작업이 완료될 때까지 기다린다.

원래 코드는 자막 플래그가 0이어서 0x80111980의 분기를 통해 이 처리 경로 전체를 건너뛴다.

80111980  beqz  $v0, loc_80111AB0

따라서 글리프 데이터가 생성되어 VRAM에 올라가더라도 ClearImage, MoveImage, DrawSync가 실행되지 않는다. 자막 이미지가 메인 버퍼 영역으로 옮겨지지 않기 때문에 실제 화면에도 표시되지 않는다. 이를 확실하게 검증하려면 VRAM Viewer로 동작을 직접 확인해야 한다. 다만 다른 에뮬레이터는 VRAM 관련 디버깅 기능이 부족해 같은 방식으로 확인하기 어려웠다. 그래서 no$psx에서 실행 중인 명령어를 아래와 같이 실시간으로 수정해 자막 처리 경로를 강제로 건너뛰도록 한 뒤 VRAM 상태를 살펴보았다.

80111980  beq  v0, v0, $80111AB0

0

역시나 내 예상이 맞았다. vram 영역에 올라가기는 하지만 메인 버퍼에 rect를 설정하지도 이동조차 하지도 않는다. 이로서 이건 확인 사살이 끝난 셈이다. 이제 패치 방향은 모든 에뮬레이터에서 자막이 정상적으로 표시되도록 하고, no$psx에서도 잘못된 스택 설정으로 인한 크래시가 발생하지 않도록 하는 것이다.

실행 흐름

  1. start
    • 80111078: jal sub_80111098
    • sub_80111098
      • 801111C4: jal sub_80111970
      • sub_80111970
        • beqz $v0, loc_80111AB0
  2. start
    • 8011106C: jal sub_801118DC
    • sub_801118DC
      • 80111948: jal sub_80111AC4
      • sub_80111AC4(Krom2RawAdd)

Patch code(armips)

이 게임은 “오프닝 -> 메뉴 -> 데모 플레이 및 캐릭터 소개” 방식으로 루프를 돌기 때문에 오버레이 실행파일 모두 패치해야 한다. SLPS_005.20는 확인 결과 스택 주소가 이상이 없으며, MOVVIEW.EXE, MONOVL.EXE만 정상적인 스택 주소로 패치하면 된다. 호출이 없을거라 확신하는 DumpClut 함수를 빈공간으로 활용하여 패치를 진행하였다. 혹여 이 영역에 다른 곳에 쓰인다 하더라도 현재 코드는 엔트리 부근에서 딱 한번만 “먼저” 스택 주소를 설정하기 때문에 아무 문제가 없다.

MOVVIEW.EXE

.psx

.open "MOVVIEW.EXE", 0x80110000 - 0x800

; hook
.org 0x80110F80
  j	0x801262DC         	
  nop
;--------------------------------------------------
.org 0x801262DC
; Clear DumpClut function
.fill 0x40, 0x00         

; code cave
.org 0x801262DC
  li	v1, 0x801A7578
  li	sp, 0x801FCE00
  li	fp, 0x801FCE00	
  ; return
  j	0x80110F88  
  nop

; og code: beqz $v0, loc_80111AB0	
.org 0x80111980 ; sub_80111970
  nop 
								
.close	

MONOVL.EXE

.psx

.open "MONOVL.EXE", 0x80010000 - 0x800

; hook	
.org 0x80028340
  j	0x800C2EB4  
  nop
;--------------------------------------------------
.org 0x800C2EB4
; Clear DumpClut function
.fill 0x40, 0x00 

; code cave
.org 0x800C2EB4
  li  v1, 0x801D9A40
  li	sp, 0x801FCE00
  li	fp, 0x801FCE00	
  j	0x80028348 
  nop
.close	

패치 결과

패치 적용 후 no$psx에서도 사운드가 정상적으로 출력되었고, 오프닝 재생 이후 게임 메뉴까지 문제없이 진입할 수 있었다. 첨부한 GIF는 오프닝 대사를 두 번 진행하며 최종 동작을 확인한 결과다. 이번 작업을 통해 자막 기능을 유지하면서도 no$psx의 비정상 동작을 바로잡을 수 있었다.

1