This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.
The MadHacker once said that "NES rom expansion is so difficult and time consuming that you might as well think of it as impossible."
That was in a time when information was scarce and emulators weren't really any good. The fact is that NES Expansion isn't that complicated despite the myriad of different mappers that do things their own way. The goal of this document is to shed light on the technical details of ROM expansion and to share some common techniques that can be used to utilize the new space.
NES Hardware Basics
PRG-ROM and CHR-ROM
Most NES games have two ROMs inside a cartridge. The first ROM is PRG-ROM and the second is CHR-ROM. They stand for Program and Character respectively with the first usually containing all the code and data and the second containing the graphics. When we say ROM expansion, we are usually referring to the PRG-ROM. The PRG-ROM is split up into different sections of equal size in what are known as ROM banks. The bank size varies depending on the game and mapper. For example, the bank size is usually either 8kb or 16kb. You can find this information out by using the emulator FCEUX. Just go to Help -> Message Log.
The 6502 CPU Memory Map
The NES accesses the PRG-ROM by loading ROM banks into a specific address area of the CPU memory map. The banks are loaded into the address range of $8000-FFFF. This makes for a total size of 32kb the NES can access of the PRG-ROM at a time.
It's story time. Once upon a time when the NES first came out, PRG-ROM sizes were only 32kb, the maximum size the NES could access at a time. This worked fine until developers said, "32kb is way too small for me to fit my game into. I want to make bigger and more complex games. I need a bigger ROM." So Nintendo developed mappers, which is what you could call fancy boards, that usually handled ROM sizes of 128kb, 256kb and 512kb. Now the NES can only handle 32kb at a time, so these mappers could "swap" banks in and out as needed, thus being able to access the entire ROM, just not at once. Take this diagram to better picture it.
Game that uses 8kb banks
| 1 | 2 | 3 | <4> |
Game that uses 16kb banks
| 1 | <2> |
Picture that the NES CPU has slots and pretend that banks from the ROM go in there. Then mix and match however you want and that's how the NES accesses the PRG-ROM in a nutshell. But there's one very important piece of information you need to know. The very last bank in the PRG-ROM is what you would call the "fixed bank" or "hard-wired bank". It's always loaded into the the last ROM bank slot in the CPU and can never be changed or moved. The <> is there just to denote the difference.
How to determine NES ROM expansion limits
Technically speaking, the NES could supports ROMs that were 10MB in size, but there isn't a board or mapper that hasn't been designed to support such a size yet. When we talk about ROM size limits, we are referring to the maximum size a game can be that was designed for a particular board or mapper. Maximum ROM sizes are determined from the number of PRG bits a board/mapper that sets the number of ROM banks available. This is all technical mumbo jumbo that's not really necessary to explain because NESDev has an excellent wiki that just lists the max ROM size for all the NES mappers.
Question: Let's say I have a ROM that I expanded to 256kb, but the mapper/board can only support 128kb. Is that even possible?
Answer: There are two sides to the coin for this. If you wanted to play your translation on a real NES, the original board used wouldn't be able to support the ROM. However, it becomes a moot point when you factor in that flashcarts could play the expanded ROM fine. You can consider a flashcart a sort of all-in-one mapper that can handle any game size which usually maxes out at 512kb. The NES doesn't care about ROM sizes and that's the important thing to remember. A good example is the Hebreke translation that expanded the ROM which the original mapper/board couldn't support, but that a flashcart could play fine.
How does the NES "swap" banks
First off, ROM banks are numbered starting from #$00 and going up the hex scale. You take the bank number and you write them to the PRG register which is something that basically tells the NES to do the bank swap. This is what the code looks like to swap banks.
Translating this code into English would be something like load the bank #$01 and write it to the PRG register. Think of it like if I write #$01 o $8000 then bank 1 will go into the first PRG slot in the CPU.
That is generally how the NES swaps banks, but the register value is just a made up example and will vary by game and mapper. The mapper MMC1 does its bank swaps a little different and is not the same as the above example.
How to expand an NES ROM
Well, that's actually quite simple. KingMike made a simple program called nflate that can expand your ROM for you in a jiff. Every once in a while there will be special cases where a ROM won't work properly after expansion and you'd need to troubleshoot it and expand the ROM manually. But the vast majority of games work fine after expansion. Expanding the ROM is the easy part, but utilizing the expanded space is where things get tricky.
How to use the expanded space
In the scope of translation hacking, the goal of ROM expansion is to increase the amount of space you have available so you can fit all the translated text back in without cutting it down. Let's say you have an entire bank of text. There's no way in hell you can fit that all back in the original space. Maybe if you compressed it, but that's a different subject. So what you do is split the text between two different ROM banks effectively doubling the space you have available.
Question: How does one split the text between banks?
Answer: I use two different methods to accomplish this. To keep things simple, we'll only talk about one.
First off, you need to code a new routine that contains our bank swap code. The new code requires space in the ROM and most games usually have free space at the end of the fixed bank. That's where we put our code.
Question: But what if there's no free space in the fixed bank?
So what you do is JSR from the game's text pointer load routine. Your common game's text pointer load routine will look something like this:
Y is the index that selects which pointer from the table to load. Here's a simple breakdown of the pointer table addresses:
$8000 = 0
$8002 = 1
$8004 = 2
$8006 = 3
$8008 = 4
$800A = 5
The numbers on the right are what Y needs to be in order to load a particular text pointer.
Let's say we want to we want the text to be in another bank for string 5. Let's also assume that the PRG-ROM was 128kb and we expanded it to 256kb. The game uses 16kb banks, which with 128kb total, that makes for 8 banks. So the banks for the expanded space are going to start at #$09 and up. So here's what we do
;Load first pointer byte
;Write it to RAM
;Compare if Y is less than or equal to 4
;If so, skip this code and go here
;load bank 9
;swap bank 9 into PRG slot
23d6: 19 ADD HL,DE A:04 F:c0 BC:0000 DE:4000 HL:0204 SP:cefe
23d7: 11 00 d9 LD DE,d900 A:04 F:c0 BC:0000 DE:4000 HL:4204 SP:cefe
23da: 2a LD A,(HL+)=d1 A:04 F:c0 BC:0000 DE:d900 HL:4204 SP:cefe
23db: fe 04 CP 04 A:d1 F:c0 BC:0000 DE:d900 HL:4205 SP:cefe
23dd: 38 03 JR C, +03 [23e0] A:d1 F:c0 BC:0000 DE:d900 HL:4205 SP:cefe
23df: c3 f1 23 JP 23f1 A:d1 F:c0 BC:0000 DE:d900 HL:4205 SP:cefe
23f2: 47 LD B,A A:d1 F:c0 BC:0000 DE:d900 HL:4205 SP:cefe
23f3: 2a LD A,(HL+)=01 A:d1 F:c0 BC:d100 DE:d900 HL:4205 SP:cefe
23f4: 4f LD C,A A:01 F:c0 BC:d100 DE:d900 HL:4206 SP:cefe
23f5: 2a LD A,(HL+)=a4 A:01 F:c0 BC:d101 DE:d900 HL:4206 SP:cefe
23f6: e5 PUSH HL A:a4 F:c0 BC:d101 DE:d900 HL:4207 SP:cefe
23f7: 67 LD H,A A:a4 F:c0 BC:d101 DE:d900 HL:4207 SP:cefc
23f8: 78 LD A,B A:a4 F:c0 BC:d101 DE:d900 HL:a407 SP:cefc
23f9: cd 1e 24 CALL 241e A:d1 F:c0 BC:d101 DE:d900 HL:a407 SP:cefc
241f: cb 3f SRL A A:d1 F:c0 BC:d101 DE:d900 HL:a407 SP:cefa
2421: cb 3f SRL A A:68 F:c0 BC:d101 DE:d900 HL:a407 SP:cefa
2423: 12 LD (DE),A A:34 F:c0 BC:d101 DE:d900 HL:a407 SP:cefa
2424: 13 INC DE A:34 F:c0 BC:d101 DE:d900 HL:a407 SP:cefa
2425: c9 RET A:34 F:c0 BC:d101 DE:d901 HL:a407 SP:cefa
23fc: 79 LD A,C A:34 F:c0 BC:d101 DE:d901 HL:a407 SP:cefc
23fd: cb 38 SRL B A:01 F:c0 BC:d101 DE:d901 HL:a407 SP:cefc
23ff: 1f RRA A:01 F:c0 BC:6801 DE:d901 HL:a407 SP:cefc
2400: cb 38 SRL B A:80 F:c0 BC:6801 DE:d901 HL:a407 SP:cefc
2402: 1f RRA A:80 F:c0 BC:3401 DE:d901 HL:a407 SP:cefc
2403: cd 1e 24 CALL 241e A:40 F:c0 BC:3401 DE:d901 HL:a407 SP:cefc
241f: cb 3f SRL A A:40 F:c0 BC:3401 DE:d901 HL:a407 SP:cefa
2421: cb 3f SRL A A:20 F:c0 BC:3401 DE:d901 HL:a407 SP:cefa
2423: 12 LD (DE),A A:10 F:c0 BC:3401 DE:d901 HL:a407 SP:cefa
2424: 13 INC DE A:10 F:c0 BC:3401 DE:d901 HL:a407 SP:cefa
2425: c9 RET A:10 F:c0 BC:3401 DE:d902 HL:a407 SP:cefa
2406: 7c LD A,H A:10 F:c0 BC:3401 DE:d902 HL:a407 SP:cefc
2407: cb 39 SRL C A:a4 F:c0 BC:3401 DE:d902 HL:a407 SP:cefc
2409: 1f RRA A:a4 F:c0 BC:3400 DE:d902 HL:a407 SP:cefc
240a: cb 39 SRL C A:d2 F:c0 BC:3400 DE:d902 HL:a407 SP:cefc
240c: 1f RRA A:d2 F:c0 BC:3400 DE:d902 HL:a407 SP:cefc
240d: cb 39 SRL C A:69 F:c0 BC:3400 DE:d902 HL:a407 SP:cefc
240f: 1f RRA A:69 F:c0 BC:3400 DE:d902 HL:a407 SP:cefc
2410: cb 39 SRL C A:34 F:c0 BC:3400 DE:d902 HL:a407 SP:cefc
2412: 1f RRA A:34 F:c0 BC:3400 DE:d902 HL:a407 SP:cefc
2413: cd 1e 24 CALL 241e A:1a F:c0 BC:3400 DE:d902 HL:a407 SP:cefc
241f: cb 3f SRL A A:1a F:c0 BC:3400 DE:d902 HL:a407 SP:cefa
2421: cb 3f SRL A A:0d F:c0 BC:3400 DE:d902 HL:a407 SP:cefa
2423: 12 LD (DE),A A:06 F:c0 BC:3400 DE:d902 HL:a407 SP:cefa
2424: 13 INC DE A:06 F:c0 BC:3400 DE:d902 HL:a407 SP:cefa
2425: c9 RET A:06 F:c0 BC:3400 DE:d903 HL:a407 SP:cefa
2416: 3e 3f LD A,3f A:06 F:c0 BC:3400 DE:d903 HL:a407 SP:cefc
2418: a4 AND H A:3f F:c0 BC:3400 DE:d903 HL:a407 SP:cefc
2419: 12 LD (DE),A A:24 F:c0 BC:3400 DE:d903 HL:a407 SP:cefc
241a: 13 INC DE A:24 F:c0 BC:3400 DE:d903 HL:a407 SP:cefc
241b: e1 POP HL A:24 F:c0 BC:3400 DE:d904 HL:a407 SP:cefc
241c: c3 d9 23 JP 23d9 A:24 F:c0 BC:3400 DE:d904 HL:4207 SP:cefe
Of all the topics to read up on in Romhacking, debugging doesn't seem to be a topic covered all that much. Sure, there are plenty of documents for the basics and source code for various hacks and reverse-engineering, but not much for debugging. For those looking to take the next step, learning how to use a debugger is a paramount in attaining that goal. Not only just learning the concepts of debugging, but knowing how to apply them to help hack a game.
Before I even begin with explaining the debugging aspects, there are certain hardware details that must be known before anyone can even begin to learn debugging. Otherwise it's kinda like wanderering through the dark with no clue where you're going. Since the NES is my area of expertise, we'll be using that as an example. Keep in mind that Romhacking and debugging concepts are more or less universal concepts that can be applied to other systems.
What you need to know about the NES:
-Be able to differentiate between RAM and ROM addresses.
-ROM is pretty simple, that's the physical ROM on your computer and the addresses you are viewing in a hex .
-ROM is usually split into two parts: PRG-ROM and CHR-ROM. PRG stands for Program(Code) and CHR stands for Character(Graphics). Some games/mappers only have one ROM with the graphics lumped into the PRG-ROM.
-Now RAM refers to the memory of the NES and there are three types of memory for the NES: CPU, PPU and Sprite. Each memory mode has it owns memory map which basically tells the NES what to put where in memory
-Here are the general memory maps for the CPU and PPU
Registers galore etc (not important for our purposes)
Cartridge RAM (Not all games have/use this)
Pattern Table #0
Pattern Table #1
Name Table #0
Attribute Table #0
Now let me breakdown what some of these terms are.
First there's RAM and Cartridge RAM which are basically the same thing. The only difference between the two is that Internal RAM is located within the NES and Cartridge RAM is located in the cartidge itself for extra RAM. The game stores various variables in this area. Stuff like HP, Strength, enemy counter rates, text positions. RAM is dynamic.
Then there's PRG-RAM. You must know the difference between PRG-ROM and -RAM. The NES cannot access the entire ROM banks at one time. It is limited to accessing 32kb's of ROM banks at a time and that 32kb goes straight into PRG-RAM. Bank sizes are dependent on mapper and setup and vary like so:
32kb(note some PRG-RAM/ROM sizes are 32kb therefore the entire PRG-ROM can be accessed in one shot)
The last bank in the PRG-ROM is always mapped to $C000 or $E000 in the PRG-RAM depending on bank size. This is what is referred to as the fixed bank. It cannot be moved or changed with another bank.
Name Table is basically the screen coordinates graphics appear on and the attribute table details colors for the graphics etc.
Keep in mind that a game does not use/access ROM addresses. They do not exist. A game will only use RAM addresses. That is why something like text pointers don't always match up with their ROM addresses. It's not pointing to the ROM, it's pointing to wherever the text is stored in RAM.
That should cover most of it, let's move onto actual debugging concepts now.
A debugger is mainly used to have the a break/stop/whatever just before certain ASM instructions are executed by the game. You set the conditions in which that happens with a debugger. We call them breakpoints, if you want to keep it simple think of it as a point in which to take a break. There are three main types of breakpoints they are:
Read - This is when we want to know when the game is accessing variables in RAM, text being accessed from PRG-RAM etc. Breaking when reading data and variables.
Write - This is mainly for when we want to know when variables are being written to RAM by the game like player stats. We don't usually use it for something like PRG-RAM because that's more like static data, but for something like RAM as it is dynamic aka ever changing.
Execute- This is when we want to know when the game is accessing specific ASM instructions from the PRG-RAM.
Breakpoints can be used for the three types of memory the NES has. Want to know when/how a graphic tile appears on the screen, set a write breakpoint for the PPU address etc.
Now onto the actual debugger.
What you want to do is load up the FCEUX emulator with a game of your choice, which by the way is one of the best emulators/debuggers I have ever used, and at that top bar you'll see a bunch of options. Click Debug -> Debugger. Don't let it overwhelm you, but the main thing you should is that on the left is the entire scrollable memory map of the CPU. Then to the right, you'll see a bunch of buttons. The important ones are Run and Step Into which are used for when you want the game to "Run" normally etc for the former and to go through each ASM instruction step by step for the latter. Then there's the breakpoint screen to the right of the button. There's the Add, Delete and Edit which are all self explanatory (add breakpoint, delete...). Click Add and it'll bring up the breakpoint menu where you input the address you want to break on and the type of breakpoint etc.
Now let's try applying this to game. Let's do Mad City the Japanese version of Bayou Billy.
We are going to start a hypothetical translation for the game and the first step is to build a table and find the text in the ROM. Building a table is quick and easy by using the PPU Viewer in FCEUX. So let's fire up Mad City and get past the title screen. Pause the game when some text appears as Gordon holds Annabelle at knife point. You'll see the katakana characters on the right side of the PPU viewer starting at $70. So let's make a basic Japanese table from those values.
Once you've finished the basic table, let's try searching for that first bit of intro text by loading the table into the hex editor along with the game. Search for the hex values or use your hex editor's kana search feature, if it has one.
We're gonna focus on モラッユク because those dakuten that precede can make things more complicated for us.
What the hell! I'm not finding the text! It's compressed!
Ok, we gotta figure out what's going on here. Let's do some debugging. Make a savestate before the text appears in the intro.
What we're gonna do is see how the text gets written to the screen. That is the end of the process of the text being read from PRG-RAM and finally appearing on the screen. When you don't know where to start, start at the end and work your way.
So let's get that text up again and open up the Name Table Viewer. You're gonna see a recreation of the screen albeit with messed up graphics in parts. Pause/unpause so that you can see the text ok. You'll also see multiple screens and only the top halfs are usually used by the game, the bottom half is mostly for mirroring. Move your cursor over to the text were interested in and there will be some information for it. The tile is $9A, the X/Y coorinates and that the PPU address 20B0. We are going to set a write breakpoint for that address. So let's pause the game and load our savestate to reset the screen.
When you've done that load up the debugger and add a PPU write breakpoint to 20B0. Before we unpause the game we're also going to use the Trace Logger. A Trace Logger is a tool that logs the code as it's executed and it's complimentary to debugging by keeping track of past code in case we need to look at it. So we're gonna log the last 10,000 instructions to window and click start logging. Now unpause the game.
The game should stop when we get just before that text and the debugger snaps. Keep in the mind the goal of all this is find out how $9A gets to $20B0 in the PPU. If you look at the trace logger, you will see this:
LDA $0700,Y @ $0704 = #$9A
This is where $9A came from. If you're savestate is before the screen appears, you'll have to click run on the debugger as there is usually a blank tile loaded to clear the screen etc.
Now we want to know how $9A got to $0704. So we're gonna delete our breakpoint, stop the trace logger, pause the game and reload our state. We're gonna add a new breakpoint now. Set a write breakpoint for $0704 to the CPU. Unpause the game.
The game is going to break a lot. We're going to continue to keep clicking run until we see
LDA $B0BE,Y @ $B0E0 = #$9A
in the trace logger.
So $9A comes from that, but how does that get $9A?
Well data is being read from a table with a Y index. Y($22) gets added onto the base of $B0BE and becomes $B0EO. Where does $22 come from?
Well if we look up a bit we see this:
LDA ($00),Y @ $B1E4 = #$22
This appears appear to be the code that reads the text from the PRG-RAM, the text read routine. Maybe the text data is not the same as what appears in the PPU and hence requires an additional table to adjust for the difference. Let's try building a table from this table.
Fire up the hex editor in FCEUX and go to $B0BE. You should see 70, 71, 72... Those look like the actual tile values that are in the PPU. Let's make a table based on those values. We're gonna start the table values at 00 because that is the beginning of the table.
Now let's try searching for that text. You should wind up at 171F4 in the ROM.
Hey we found the text! It's not compressed after all! Crazy Konami, just what the hell were they thinking!