본문으로 건너뛰기

슈퍼 패미컴

11장 롬 확장

롬을 늘리면 헤더의 크기 바이트, 미러, 체크섬이 함께 바뀝니다. 매핑마다 늘릴 수 있는 한계와, 늘리지 않고 자리를 만드는 방법을 봅니다.

왜 롬을 늘리나 쉽게

번역하면 대사가 길어지고, 한글 글꼴이 들어가고, 줄바꿈 같은 새 코드가 붙습니다. 원래 롬에 남는 자리로 모자라면 롬 파일 자체를 키울 수 있습니다. 이때 늘린 바이트가 CPU에서 어느 뱅크로 보이는지, 롬 안의 헤더를 어떻게 고치는지를 함께 정해야 합니다.

파일을 늘리는 것 자체는 쉽습니다. 뒤에 바이트를 붙이면 됩니다. 어려운 것은 세 가지입니다. 늘린 자리가 CPU에서 보이는가, 헤더의 크기와 체크섬이 맞는가, 그리고 에뮬레이터와 실제 카트리지가 늘린 롬을 원래 게임과 같은 방식으로 놓는가입니다.

4장에서 본 내부 헤더($00:FFC0)의 $FFD7에는 롬 크기가 "1KB를 왼쪽으로 n번 민 값"으로 적혀 있습니다(1장). 2의 거듭제곱이 아닌 크기는 올려 적습니다. 파일 위치는 LoROM과 SA-1이 0x7FD7, HiROM이 0xFFD7, ExHiROM이 0x40FFD7입니다.

크기 바이트와 실제 크기
값뜻이 값을 쓰는 크기예
0A2¹⁰KB = 1MB1MB제절초 일본판
0B2¹¹KB = 2MB1MB 초과–2MB돌격편·각성편, 제절초 한글판 v1.1부터
0C2¹²KB = 4MB2MB 초과–4MB슈퍼 마리오 RPG
0D2¹³KB = 8MB4MB 초과–8MBExHiROM 게임 등

제절초 한글판 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 너머는 실행 중에 창을 바꿔야 읽습니다.

한계 안에서 늘릴 때도 "롬 칩을 더 큰 것으로 바꾼 같은 보드"라고 생각하면 됩니다. 원래 보이던 주소는 그대로이고, 늘린 자리는 지금까지 미러가 보이던 뱅크에 나타납니다. 아래 도구로 매핑과 크기를 바꿔 보세요.

롬 크기를 늘리면 바뀌는 것

한 칸은 롬 512KB입니다. 칸마다 위에 CPU에서 보이는 뱅크, 아래에 파일 오프셋을 적었습니다. 매핑과 크기를 고르면 늘리기 전과 뒤의 롬 공간, 헤더의 크기 바이트, 체크섬이 바뀝니다.

매핑

지금 그림: LoROM, 1MB를 2MB로 늘린 경우(제절초 한글판).

늘리기 전 (1MB) · LoROM

  1. $80–$8F0x000000원래 롬
  2. $90–$9F0x080000원래 롬
  3. $A0–$AF0x000000미러
  4. $B0–$BF0x080000미러
  5. $C0–$CF0x000000미러
  6. $D0–$DF0x080000미러
  7. $E0–$EF0x000000미러
  8. $F0–$FF0x080000미러

늘린 뒤 (2MB) · LoROM

  1. $80–$8F0x000000원래 롬
  2. $90–$9F0x080000원래 롬
  3. $A0–$AF0x100000새로 붙인 곳
  4. $B0–$BF0x180000새로 붙인 곳
  5. $C0–$CF0x000000미러
  6. $D0–$DF0x080000미러
  7. $E0–$EF0x100000미러
  8. $F0–$FF0x180000미러

원래 롬 새로 붙인 곳 미러(아래 오프셋의 데이터가 한 번 더 보임)

크기 바이트(파일 0x007FD7)
0A → 0B
1MB → 2MB
체크섬에 더하는 방식(늘린 뒤)
0x000000–0x1FFFFF × 1

원래 데이터를 더하는 횟수가 그대로입니다. 새 자리를 한 가지 값(00이나 FF)으로만 채우면 체크섬은 크기 바이트가 커진 만큼(+1)만 바뀝니다. 새 자리에 데이터를 넣으면 다시 계산합니다.

    더 깊이한계를 넘을 때: 매핑을 바꾸거나 창을 바꾸기

    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 한 번 더)입니다.

    3MB 롬의 미러4MB 자리를 1MB씩 넷으로 나누면 앞 둘은 파일 0x000000-0x1FFFFF, 셋째는 파일 0x200000-0x2FFFFF, 넷째는 셋째와 같은 0x200000-0x2FFFFF의 미러다. 체크섬은 앞 2MB를 한 번, 뒤 1MB를 두 번 더한다.0x000000–앞 2MB (× 1)0x200000–뒤 1MB0x200000–미러CPU에서 보이는 4MB체크섬 = (앞 2MB의 합) + (뒤 1MB의 합) × 2헤더의 크기 바이트는 0C(4MB)로 올려 적습니다.
    3MB 롬이 4MB 자리를 채우는 방식. 체크섬도 이 모양대로 더합니다.

    체크섬도 이 모양을 따라 "헤더가 말하는 2의 거듭제곱 크기만큼" 더합니다. 그래서 롬을 늘리면 크기 바이트만 바뀌어도 체크섬이 바뀌고, 어떤 크기에서 어떤 크기로 늘리느냐에 따라 원래 데이터를 더하는 횟수가 달라지기도 합니다. 위 도구의 「체크섬에 더하는 방식」이 그 모양입니다.

    더 깊이체크섬을 계산하는 정확한 순서와 예외

    툴킷과 이 장의 도구는 다음 순서로 계산합니다.

    1. 헤더의 보수·체크섬 4바이트를 FF FF 00 00으로 놓습니다. 두 값의 바이트 합은 늘 $1FE이기 때문입니다.
    2. 파일 크기를 넘지 않는 가장 큰 2의 거듭제곱(3MB라면 2MB)을 한 번 더합니다.
    3. 나머지를 같은 규칙으로 더한 값에, 나머지를 그 2의 거듭제곱 크기까지 채우는 횟수를 곱해 더합니다.
    4. 합의 아래 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 ok

    SA-1 게임도 같습니다. krpatch snes checksum smrpg-j.sfc는 sa1 title='SUPER MARIO RPG' stored=4575/BA8A computed=4575 ok를 냅니다. 롬을 늘리는 패치를 만들 때는 빌드의 맨 마지막에 크기 바이트를 맞추고 --fix를 돌리도록 해 두면 잊지 않습니다.