슈퍼 패미컴
11장 롬 확장
롬을 늘리면 헤더의 크기 바이트, 미러, 체크섬이 함께 바뀝니다. 매핑마다 늘릴 수 있는 한계와, 늘리지 않고 자리를 만드는 방법을 봅니다.
왜 롬을 늘리나 쉽게
번역하면 대사가 길어지고, 한글 글꼴이 들어가고, 줄바꿈 같은 새 코드가 붙습니다. 원래 롬에 남는 자리로 모자라면 롬 파일 자체를 키울 수 있습니다. 이때 늘린 바이트가 CPU에서 어느 뱅크로 보이는지, 롬 안의 헤더를 어떻게 고치는지를 함께 정해야 합니다.
파일을 늘리는 것 자체는 쉽습니다. 뒤에 바이트를 붙이면 됩니다. 어려운 것은 세 가지입니다. 늘린 자리가 CPU에서 보이는가, 헤더의 크기와 체크섬이 맞는가, 그리고 에뮬레이터와 실제 카트리지가 늘린 롬을 원래 게임과 같은 방식으로 놓는가입니다.
헤더의 크기 바이트 쉽게
4장에서 본 내부 헤더($00:FFC0)의 $FFD7에는 롬 크기가 "1KB를 왼쪽으로 n번 민 값"으로 적혀 있습니다(1장). 2의 거듭제곱이 아닌 크기는 올려 적습니다. 파일 위치는 LoROM과 SA-1이 0x7FD7, HiROM이 0xFFD7, ExHiROM이 0x40FFD7입니다.
| 값 | 뜻 | 이 값을 쓰는 크기 | 예 |
|---|---|---|---|
0A | 2¹⁰KB = 1MB | 1MB | 제절초 일본판 |
0B | 2¹¹KB = 2MB | 1MB 초과–2MB | 돌격편·각성편, 제절초 한글판 v1.1부터 |
0C | 2¹²KB = 4MB | 2MB 초과–4MB | 슈퍼 마리오 RPG |
0D | 2¹³KB = 8MB | 4MB 초과–8MB | ExHiROM 게임 등 |
제절초 한글판 v1.0은 롬을 1MB에서 2MB로 늘리면서 이 바이트를 0A(1MB) 그대로 두었고, 체크섬도 일본판 값으로 남아 있었습니다. 게임은 에뮬레이터에서 돌았지만 헤더가 파일과 맞지 않는 롬이었습니다. v1.1이 크기 바이트를 0B로 고치고 체크섬을 다시 계산했습니다. 롬을 늘린 뒤에는 이 두 가지를 꼭 함께 고칩니다.
매핑마다 늘릴 수 있는 한계 쉽게
CPU 주소는 24비트라 16MB까지 셀 수 있지만, 그 안에 WRAM과 레지스터와 미러가 섞여 있어 롬이 보일 수 있는 자리는 그보다 훨씬 좁습니다. 어디까지 늘릴 수 있는지는 카트리지의 매핑이 정합니다.
LoROM
보통 2MB, 최대 4MB
앞 2MB는 $00–$3F(와 $80–$BF)의 위쪽 32KB에 놓입니다. 2MB 뒤는 $40–$7D·$C0–$FF에 놓이는데, 이 뱅크에서는 32KB 하나가 아래 반과 위 반에 두 번 보입니다. 그렇게 배선한 보드에서 4MB까지 씁니다.
HiROM
4MB
$C0–$FF(와 $40–$7D)에 64KB씩 놓습니다. 4MB가 끝입니다.
ExHiROM
약 8MB
앞 4MB는 $C0–$FF, 뒤쪽은 $40–$7D에 놓습니다. WRAM 뱅크 $7E–$7F만큼 8MB에 못 미칩니다. 헤더는 파일 0x40FFC0입니다.
SA-1
8MB(한 번에 보이는 것은 4MB)
Super MMC가 1MB 블록 0–7 가운데 넷을 창 4개에 보여 줍니다. 4MB 너머는 실행 중에 창을 바꿔야 읽습니다.
한계 안에서 늘릴 때도 "롬 칩을 더 큰 것으로 바꾼 같은 보드"라고 생각하면 됩니다. 원래 보이던 주소는 그대로이고, 늘린 자리는 지금까지 미러가 보이던 뱅크에 나타납니다. 아래 도구로 매핑과 크기를 바꿔 보세요.
더 깊이한계를 넘을 때: 매핑을 바꾸거나 창을 바꾸기
LoROM: 2MB를 넘으면
보통의 LoROM 보드는 롬을 $00–$3F와 그 미러 $80–$BF에만 보여 줘서 2MB가 끝입니다. 2MB 뒤를 $40–$7D·$C0–$FF에 보여 주는 보드(fullsnes의 BigLoROM)라면 4MB까지 갑니다. 이 뱅크들은 64KB 전체가 롬이라, 32KB 뱅크 하나가 아래 반과 위 반에 두 번 보입니다. 이때 2MB 뒤의 자료는 WRAM·레지스터가 없는 뱅크에 있으므로, 그쪽에 둔 코드에서 레지스터를 쓰려면 긴 주소(STA $002100 꼴)를 쓰거나 DB를 바꿔야 합니다.
HiROM에서 ExHiROM으로
HiROM은 4MB가 끝이고, 그 너머는 ExHiROM이라는 다른 매핑입니다. ExHiROM에서는 $80–$FF가 앞 4MB를, $00–$7D가 뒤쪽을 가리킵니다. 문제는 전원을 켤 때 CPU가 읽는 리셋 벡터 $00:FFFC와 헤더 $00:FFC0가 뒤쪽(파일 0x40FFC0 근처)으로 간다는 점입니다. 원래 HiROM 게임을 ExHiROM으로 바꾸려면 헤더와 벡터, 그리고 $00–$3F 뱅크 주소로 실행되던 코드를 뒤쪽에도 두어야 하고, 맵 모드 바이트($FFD5)도 $21에서 $25로 바꿉니다(FastROM이면 $31 → $35). 롬 해킹에서 흔히 하는 일은 아닙니다.
SA-1: 4MB 너머는 창을 바꿔서
SA-1 카트리지는 롬을 1MB 블록 단위로 다루고, 레지스터 $2220–$2223에 블록 번호(0–7)를 써서 $C0·$D0·$E0·$F0에서 시작하는 창 4개에 보일 블록을 고릅니다. 켤 때는 0, 1, 2, 3이라 앞 4MB가 보입니다. 블록 4–7을 읽으려면 창 하나를 바꿨다가 되돌려야 합니다. 이 레지스터는 본체 CPU(S-CPU)와 SA-1 양쪽의 창을 함께 바꾸기 때문에, 두 CPU가 동시에 도는 게임에서는 한쪽이 창을 바꾼 사이 다른 쪽이 원래 블록을 읽으면 엉뚱한 값을 받습니다.
어중간한 크기와 미러, 체크섬 쉽게
롬이 2의 거듭제곱 크기가 아니면, 예를 들어 3MB라면 CPU의 4MB 자리 가운데 1MB가 남습니다. 실제 카트리지는 이 자리를 비워 두지 않고 뒤쪽 조각을 한 번 더 보여 줍니다. fullsnes는 "큰 쪽을 0번지에, 그 뒤에 작은 쪽, 그다음 작은 쪽의 미러"라고 적습니다. 3MB라면 2MB + 1MB + (1MB 한 번 더)입니다.
체크섬도 이 모양을 따라 "헤더가 말하는 2의 거듭제곱 크기만큼" 더합니다. 그래서 롬을 늘리면 크기 바이트만 바뀌어도 체크섬이 바뀌고, 어떤 크기에서 어떤 크기로 늘리느냐에 따라 원래 데이터를 더하는 횟수가 달라지기도 합니다. 위 도구의 「체크섬에 더하는 방식」이 그 모양입니다.
더 깊이체크섬을 계산하는 정확한 순서와 예외
툴킷과 이 장의 도구는 다음 순서로 계산합니다.
- 헤더의 보수·체크섬 4바이트를
FF FF 00 00으로 놓습니다. 두 값의 바이트 합은 늘 $1FE이기 때문입니다. - 파일 크기를 넘지 않는 가장 큰 2의 거듭제곱(3MB라면 2MB)을 한 번 더합니다.
- 나머지를 같은 규칙으로 더한 값에, 나머지를 그 2의 거듭제곱 크기까지 채우는 횟수를 곱해 더합니다.
- 합의 아래 16비트가 체크섬, 그 비트를 모두 뒤집은 값이 보수입니다.
새로 붙인 자리를 한 가지 값으로만 채우면 체크섬은 그 자리 때문에는 바뀌지 않습니다. 512KB는 $10000(65,536)의 8배라서 같은 값을 512KB만큼 더한 합의 아래 16비트가 0이기 때문입니다. 실제로 제절초 일본판(체크섬 6757)을 0이나 FF로 2MB까지 채우고 크기 바이트만 0B로 바꾸면 체크섬은 6758로, 크기 바이트가 늘어난 1만큼만 바뀝니다. 반대로 1.5MB 롬을 2MB로 늘리면 원래 뒤쪽 512KB를 두 번 더하던 것이 한 번으로 줄어서, 롬 내용에 따라 체크섬이 달라집니다.
fullsnes에는 이 규칙대로 계산되는 게임(Dai Kaiju Monogatari 2는 4MB + 뒤 1MB × 4, Star Ocean은 4MB + 뒤 2MB × 2)과 예외가 함께 실려 있습니다. SPC7110을 쓰는 Momotaro Dentetsu Happy는 3MB 전체를 두 번 더하고, 5MB인 Far East of Eden Zero는 5MB를 한 번만 더합니다. 한글패치가 다루는 일반 카트리지는 위 규칙을 따릅니다.
에뮬레이터와 플래시 카트리지 쉽게
실제 카트리지는 보드의 배선이 미러를 정하지만, 에뮬레이터와 플래시 카트리지(롬 파일을 SD 카드에서 읽어 실제 기계에 꽂는 카트리지)는 파일과 헤더를 보고 흉내 냅니다. 그래서 늘린 롬은 몇 가지를 지켜야 어디서나 같게 돕니다.
- 파일 크기를 맞춥니다. 파일 크기는 2의 거듭제곱이거나, 큰 2의 거듭제곱 + 작은 2의 거듭제곱(1.5MB, 3MB, 6MB처럼)으로 맞춥니다. 그보다 어중간하면 에뮬레이터마다 채우는 방식이 달라서, SNESdev 위키는 뒤쪽 조각을 2의 거듭제곱까지 채우라고 권합니다.
- 헤더의 맵 모드·크기·체크섬을 맞춥니다. 많은 에뮬레이터가 헤더를 보고 LoROM인지 HiROM인지를 짐작하고, 체크섬과 보수가 맞는지를 그 단서로 씁니다. 툴킷의 매핑 짐작도 같은 단서를 봅니다.
- 매핑을 바꾸는 확장은 지원을 확인합니다. ExHiROM이나 SA-1의 4MB 너머처럼 드문 구성은 에뮬레이터·플래시 카트리지마다 지원이 다를 수 있습니다.
- 여러 에뮬레이터에서 돌려 봅니다. 제절초 한글판 v1.2는 libretro 코어 다섯 가지(bsnes, bsnes-hd, snes9x, snes9x2010, Beetle Supafaust)와 Mesen 2에서 새 게임과 세이브 이어하기를 확인했습니다. 함께 돌린 Mesen-S 코어는 일본판 원본도 실행되지 않아 판정하지 못했고, 실제 기계와 플래시 카트리지에서는 시험하지 않았다고 분석 문서에 적어 두었습니다. 확인한 범위를 이렇게 적어 두는 것이 다음 사람에게 도움이 됩니다.
늘리지 않고 자리를 만드는 법 쉽게
롬을 늘리는 것은 마지막 수단에 가깝습니다. 늘린 자리는 대개 원래 코드가 쓰는 뱅크와 멀어서, 거기 둔 자료를 읽으려면 읽는 코드도 바꿔야 하기 때문입니다. 네 패치 가운데 롬을 늘린 것은 제절초 하나이고, 나머지는 이렇게 자리를 만들었습니다.
원래 있던 빈 곳 쓰기
슈퍼 마리오 RPG, 돌격편, 각성편
원본에서도 0이나 FF로 비어 있는 곳을 찾아 씁니다. 앞뒤 자료가 그 자리를 쓰지 않는지 꼭 확인합니다.
안 쓰게 된 자리 다시 쓰기
제절초 v1.1·v1.2
옮기거나 바꾸면서 비워진 원래 자리(일본판 대본 자리)에 새 코드와 표를 넣습니다.
같은 뱅크 안에서 다시 채우기
돌격편, 각성편 대사
문자열을 처음부터 다시 배치하고 같은 문자열은 하나만 두어 자리를 만듭니다(8장).
압축하기
각성편·돌격편 타이틀 그림
새로 그린 그림을 원래 형식으로 다시 압축해 제자리에 넣습니다(9장).
빈 곳을 찾는 방법과 주의점은 6장의 「새 코드를 둘 빈 공간」에, 대사를 같은 뱅크 안에서 다시 채우는 방법은 8장에, 압축은 9장과 10장에 있습니다.
실제 패치에서 실제 패치에서
제절초: 1MB LoROM을 2MB로
제절초는 1MB LoROM입니다. 한글판 v1.0은 압축을 버린 69만 바이트짜리 대본을 넣으려고 롬을 2MB로 늘렸습니다(10장). 1MB LoROM은 뱅크 $00–$1F를 쓰고, 늘린 1MB는 바로 다음 $20–$3F에 나타납니다. 그전까지 앞 1MB의 미러가 보이던 자리입니다. 대본은 $20:8000부터 놓였고, v1.1 대본은 $35:AF66에서 끝나 $36–$3F는 아직 0으로 비어 있습니다.
앞에서 본 대로 v1.0은 크기 바이트와 체크섬을 고치지 않았고, v1.1이 크기 바이트를 0A에서 0B로 고쳤습니다. 지금의 패치 소스에서는 tools/build.sh가 tools/fixes.py로 크기 바이트를 고치고, 마지막에 krpatch snes checksum --fix --mapper lorom으로 체크섬을 다시 씁니다.
슈퍼 마리오 RPG: 4MB에서 멈춘 이유
슈퍼 마리오 RPG는 SA-1 카트리지라 매핑상으로는 8MB까지 늘릴 수 있습니다. 하지만 4MB 너머를 읽으려면 실행 중에 Super MMC 창을 바꿔야 하고, 이 게임은 S-CPU와 SA-1이 동시에 롬을 읽습니다. 한쪽이 창을 바꾸는 사이 다른 쪽이 원래 블록을 읽으면 오동작하므로, 한글판은 롬을 원래 크기 4MB로 두고 원본에서도 0이나 FF로 비어 있던 곳($C3:F000, $E2:B400, $E4:9D00 등)만 썼습니다.
돌격편·각성편: 2MB 안에서
돌격편과 각성편은 2MB LoROM을 그대로 두었습니다. 돌격편 v1.1은 늘린 글자 블록과 새 코드를 원본에서도 FF였던 곳($81:EF00, $84:EE00, $86:E000, $B7:FA00)에 두고, 대사는 뱅크마다 원래 문자열 영역 안에서 다시 채웠습니다. 각성편 v1.1은 FF가 이어진 곳을 빈 곳으로 보고 썼다가, 바로 앞 자료가 FFFF를 끝 표시로 쓰고 있어 우주선이 맵 밖을 헤매는 버그를 냈습니다. 늘리지 않을 때 가장 조심할 점입니다.
직접 해 보기 직접 해 보기
krpatch snes checksum은 헤더의 체크섬을 확인하고, 틀리면 1로 끝납니다. 제절초 일본판, 한글판 v1.0, v1.2를 차례로 확인해 봅니다.
$ krpatch snes checksum otogirisou-j.sfc
lorom title='OTOGIRISOU' stored=6757/98A8 computed=6757 ok
$ krpatch snes checksum otogirisou-k10.sfc
lorom title='OTOGIRISOU' stored=6757/98A8 computed=E0F7 BAD
$ krpatch snes checksum otogirisou-k12.sfc
lorom title='OTOGIRISOU' stored=6E62/919D computed=6E62 ok
$ xxd -s 0x7FD7 -l 1 otogirisou-k10.sfc
00007fd7: 0a .v1.0은 저장된 체크섬이 일본판 값 6757 그대로라 BAD가 나오고, 2MB 파일인데 크기 바이트는 0A(1MB)입니다. v1.1이 한 일을 따라 해 봅니다. 사본의 크기 바이트를 0B로 고치면 계산한 체크섬이 1 늘고(E0F7 → E0F8), --fix가 그 값과 보수를 헤더에 씁니다.
$ cp otogirisou-k10.sfc fixed.sfc
$ printf '\x0b' | dd of=fixed.sfc bs=1 seek=$((0x7FD7)) conv=notrunc
$ krpatch snes checksum fixed.sfc
lorom title='OTOGIRISOU' stored=6757/98A8 computed=E0F8 BAD
$ krpatch snes checksum fixed.sfc --fix
checksum E0F8
$ krpatch snes checksum fixed.sfc
lorom title='OTOGIRISOU' stored=E0F8/1F07 computed=E0F8 okSA-1 게임도 같습니다. krpatch snes checksum smrpg-j.sfc는 sa1 title='SUPER MARIO RPG' stored=4575/BA8A computed=4575 ok를 냅니다. 롬을 늘리는 패치를 만들 때는 빌드의 맨 마지막에 크기 바이트를 맞추고 --fix를 돌리도록 해 두면 잊지 않습니다.
실제 패치에서 이 개념이 쓰인 롬 분석 노트
노트의 자세한 절은 「자세히 보기」를 펼친 안쪽에 있습니다.
직접 해 보기 kr-patch-toolkit 명령 설명
참고 자료
- fullsnes, SNES Cartridge ROM Header — 크기 바이트 [FFD7h](1 SHL n KB, 올려 적음), 체크섬 정의, "ROM Size / Checksum Notes"의 어중간한 크기 게임 목록
- fullsnes, SNES Memory Map — 보통의 LoROM 2MB·HiROM 4MB 한계와 BigLoROM·ExHiROM 같은 넓히는 방법
- fullsnes, SNES Cart LoROM Mapping — 2MB보다 큰 LoROM 게임은 뱅크 $40부터 64KB 뱅크마다 32KB를 두 번 보여 준다는 설명
- fullsnes, SNES Cart SA-1 Memory Control — Super MMC 레지스터 $2220–$2223(1MB 블록 0–7 선택)과 두 CPU에 함께 걸리는 점
- SNESdev Wiki, ROM header — 체크섬은 헤더가 말하는 2의 거듭제곱 크기로 계산한다는 점과, 어중간한 크기면 뒤쪽 조각을 2의 거듭제곱까지 채우라는 권고
- SNESdev Wiki, Memory map — LoROM 4MB, ExHiROM의 뱅크 배치와 헤더 위치 $40FFC0
- kr-patch-toolkit, snes/header.py — 「롬 크기를 늘리면 바뀌는 것」이 옮겨 온 미러 체크섬 계산(같은 테스트 사례로 확인)
- kr-patches, 제절초 docs/ANALYSIS.ko.md 1장·7장 — 1MB → 2MB 롬 지도, 크기 바이트 0A → 0B, 쓴 공간과 남은 공간
- kr-patches, 슈퍼 마리오 RPG docs/ANALYSIS.ko.md 3장·9장 — 롬 크기를 4MB로 유지한 이유와 대신 쓴 빈 공간
댓글