-
Notifications
You must be signed in to change notification settings - Fork 0
Finding Graphics
Every foreground, background, and object tile used in the game should be referenced somewhere in the ROM via a pointer, so the game knows where to load them from. Finding these pointers is key to replacing the graphics, as they can be changed to re-point to the new, translated graphics once I've inserted them in. There may be additional steps (pointer chains, tables, compression), but pointers are the common part of all the graphical data.
The GBA has no internal file system, so these pointers have to be found manually. Opening the ROM in a hex editor for now won't help much, as that just shows a homogeneous blob of binary data. I use a variety of programs to narrow down pointer locations.
GBA Background (i.e. non-object) data is often stored as separate chunks of binary data: A tileset, a tilemap, and a palette. The tileset has graphical data in chunks of 32 (or 64) bytes, while the tilemap tells the screen how to arrange these chunks. This allows for more compact storage, as the tilemap is able to re-use and flip certain tiles that are displayed in the final image as such.
Generally, there are 2 modes background layers can be in: 4-bit or 8-bit. The number of bits describes how many available colors there are to use in the tileset. The palette is what maps this data to actual colors. With a 4-bit tileset, a total of 16 colors are possible, while an 8-bit tileset allows for 256.
Usually, tileset, tilemap, and palette data are located together. Sometimes, the palette can be re-used for different images, but the tileset and tilemap are almost always unique to each image.
In the GBA, the ROM data is located at offset 0x08000000. That is, all GBA pointers start with "0x08".
More GBA technical information (such as memory maps) can be found at GBAtek.
As an example, I will find the location of the splash screen for the first world in the game, "Kururin Village".

First of all, this screen is shown only once per save file, upon entering the world for the first time, so making a save state right before reaching that screen would be advisable.
It doesn't matter too much which GBA emulator you use, as long as it has a way of viewing the different background/object layers, as well as a memory viewer. I use No$GBA debugger as it's most intuitive to me.
Once I reach the splash screen, I pull up the VRAM viewer to inspect the tiles:
(The tileset often has more unique data then the tilemap or palette, so that would be my first search target in the ROM)
Note the bit depth. There are clearly more than 16 colors in the splash screen above, so I can infer that an 8-bit tileset is used. The VRAM viewer in No$GBA makes these inferences based on how they're used in the background layers, and sets the bit depth accordingly ("Auto"). If I were to view these tiles in 4-bit mode, I would get a bunch of garbage images for the tiles.
The VRAM viewer also shows the tiles' location in memory. In this case, the tileset data starts at offset 0x06000000. Opening the memory viewer to this offset shows all the binary data for this tileset.

As a naive approach, you can search the entire ROM for the hex sequence I highlighted above. Note the uniqueness of the hex sequence to narrow my search; I didn't search for a line of 00's or FF's since they would probably be more common in the ROM. If you're lucky, you can find the data in the ROM directly. But alas, 0 search results were returned. That must mean that there is some compression involved. A more thorough inspection of the data transfer to VRAM is needed, then.
So, the naive approach didn't work, so now I go through more complicated steps. This requires a more complex GBA emulator, one that supports debugging, setting breakpoints and the like. Once again, No$GBA is my emulator of choice, but other options include VBA SDL-H or mGBA for command line functionality.
I want the emulator to stop as soon as the tileset data is transferred to VRAM. I load a state before reaching the splash screen, and set a write breakpoint:

This pauses the emulator as soon as offset 0x6000083 is equal to 0x52, as seen in the memory viewer above.
Reaching the breakpoint shows the current (paused) code execution:

It looks intimidating, but all you need to know is that the emulator is paused at line 0x08020CBE (that is to say, just after the instruction at 0x08020CBC was executed), and the "registers" (r0 through r15) are containers for 32-bit integers.
If the various instructions (the column after the "(T)'s") look unfamiliar to you, you can look them up online under the ARMv9 THUMB instruction set. Sometimes, the GBA will run in "ARM" mode (4 bytes per instruction), but usually, THUMB (2 bytes per instruction) would be your reference.
With my knowledge, the tileset transfer (marked by the write breakpoint) happened just as the value in r0 was stored in the location in memory referenced by r2+0x08. This memory location is in the 0x04000000 region, which deals with various controls, and input/output. Fortunately, No$GBA has a built-in visual interface that differentiates these controls:

(If your GBA emulator doesn't have this I/O Map, you can always reference GBAtek to see which memory location refers to which control.)
The data transfer to the 0x06000000 block happened as a result of a DMA (direct memory access) operation. There are 4 different "buses" for DMA to happen, but, as I've highlighted, I am concerned only with DMA3 at the moment. The "source" and "destination" addresses are the first and second lines I highlighted, respectively. The above screencap tells me that the data in 0x06000000 came from 0x0200ABB0 (Work RAM region). Moving to that location in the memory editor (while still keeping the emulator paused!) will sure enough show familiar tileset binary data:

Well, now I repeat the breakpoint process. Load state, and set a write breakpoint at this new memory location. In this case, my next breakpoint would be [0200AC73]=0x52. (I also delete my previous breakpoint)
In doing so, I found that the emulator stopped at 0x080921B6, in some sort of loop. I inferred that I was looking at a decompression routine. Generally, subroutines start with a few "pushes", as a way to save certain registers on the "stack", and they end with a few "pops" to retrieve them. These subroutines can get very long, but fortunately, this one is rather short. I isolated a decompression routine that starts at offset 0x08092190 and ends at 0x080921E4.
Generally, subroutines have useful register values at the start, as they would likely contain information about the "source" and "destination" for the compressed and decompressed data, respectively.
So, I load state again, and set an execution breakpoint at 0x08092190. The emulator will pause at any time it reaches the instruction at that memory location. I load the splash screen once more.
Note that the emulator can go through this decompression routine multiple times when loading the splash screen. I use my best judgement to determine which breakpoint has useful register values I need. Fortunately, for this load, the emulator paused just once, with the following trace:
Note: I've already annotated address 0x08092190 as "LZDecomp2", which is why it shows up as such.
r1 contains the value 0x0200ABB0, the same as the address I found in the DMA map. I can infer that this is the "destination" address. r0, then must contain the "source" address of the compressed tileset data (recognize the GBA pointers starting with 08).
I've now found the tileset pointer. 0x0819C3A4 contains the compressed tileset data. You can do similar write breakpoints to locate the tilemap and palette. In terms of data size, the tilemap and palette shouldn't change, but the compressed tileset of the translated splash screen may not fit in the original space (and sure enough, it didn't). So, more work must be done to see how the value "0819C3A4" ends up in r0, so the new compressed tileset can be re-pointed.
I want to find a new spot in execution where the value 0819C3A4 is loaded into r0, so I load state again, and set a new breakpoint: r0=0x0819C3A4.
It turns out that this value is the result of addition between 2 values: 0x08138C54 in r0 and 0x63750 in r8. The former happens to be a "master" pointer to various graphical data in the ROM, and the latter is the offset. Now I break when r8=0x63750.
It turns out that r0 was originally 0x63750. Break again, and you'll find that 0x63750 was loaded from 0x080AD254 in the ROM (note that the endianness of this value in the ROM; the smallest happen to be first, as this value is little-endian):
It turns out that this data is in the middle of some script code, where this sequence of bytes in the ROM near 0x080AD254 are read in order to do different operations. The 32-bit value 00063750 is preceded by the byte sequence "09 00 00 00 00 00 00 00", which has something to do with loading tilesets. The 32-bit value 00004311 located after the highlighted data also seems suspicious. In my trace of 0x08092190, that was the value of r2. It turns out this is the size of the compressed tileset data (in bytes). To use this subroutine, it's important to also change this value when replacing the tileset!
In the screenshot above, you'll also probably notice the 32-bit values at 0x080AD264 and 0x080AD274: 0x79DF0 and 0x7B560. Re-constructing the pointer using the "master" offset 0x08138C54 shows that these point to the tilemap and palette data, respectively, both uncompressed. The tilemap is 0x4B0 bytes big while the palette is 0x100 bytes big, which sounds correct with their locations in RAM.
So, once the pointers (and the methodology for computing pointers) is found, the graphics replacement process is as follows:
- Make a custom .png of the splash screen with translated text (this is the longest step for me!).
- Re-quantize the .png to the correct palette used by the game.
- Format the .png into binary tileset and tilemap data.
- Compress the tileset data (may be optional, varies with different tilesets).
- .incbin the tileset and tilemap data using armips into free space in the ROM.
- Over-write the values at 0x080AD254 and 0x080AD264 using the locations to the new tileset and tilemap, offsetting them by "master" offset 0x08138C54.
- Also overwrite the value at 0x080AD258 with the uncompressed size of the new tileset.
Note that the palette isn't replaced at all. This is a stylistic choice by me, to keep the graphics as close to the original game as possible.