User:Kmeisthax/Findings/2011/6/4/Precomposed Title Screen Sprites
ROM bank 3E + 18 ROM bank 3E + 77F
OAM sprites get copied from C000 to C09F
C000 is filled from ROM A:0198
Banks with possible sprite lists are stored at:
{Banks: HOME 094D Offsets: HOME 0956}
FOUND A FUNCTION! load_precomposed_spritelist, lives at HOME 089C
Takes a list of params in HL void* and a target BC to write the sprites to.
HL looks like this:
struct { char GlobalHiNybble, char SpriteListArrayBank, char SpriteListArrayOffset, char GlobalXOffset, char GlobalYOffset, char GlobalLoNybble };
GlobalHi and Lo nybbles are used to fill in the sprite attributes as needed. The GlobalHi nybble is stored in the upper nybble, the lower nybble is used elsewhere.
Global X and Y offsets get added to the indivdual sprite offsets. The offsets are biased by (8,16) to generate proper sprite positions.
There are multiple locations that sprite list pointers can be stored in. The SpriteListArrayBank is an offset into two lists in HOME that determine the base BANK:OFFSET address to use for the list of sprite lists. The actual void** to the sprite list is calculated as follows:
SpriteListBaseptrArray[SLAB] + SpriteListArrayOffset * 2
This is then dereferenced to get the actual sprite list.
The structure is stored in memory at WRAM HOME C0A0. The low bit
The sprites in the list get copied, in a loop, unless OAM memory is full.
One of the callers to this function loads HL from C0A0 (i.e. right after the DMA range) Another function greedily calls load_precomposed_spritelist starting from HL C0A0 and incrementing it 0x20 bytes each time. The parameter structure is only used in a call if HL->GlobalHi's 0th byte is high. This routine continues for 0xC iterations (so it reads from C0A0 to C220... that's a lotta RAM)
THEN it does a similar loop from HL=C2A0, load_precomposed_spritelist is only called if:
HL->GlobalHi & 0x81 == 0x81
i.e. if the high and low bits are active. Subsequent parameter lists are tested every 0x20 bytes for 8 iterations, so in the range [C2A0, C3A0)
And then ANOTHER loop, starting from HL=C220! This time both eligibility tests are used, so first the high and low bits are tested, then the low bit again (I think ...?)
We do this third loop 0xC times, and it kind of overlaps with the second...
Most of the code that does 'high level' title screen control lives in Rom bank 2, from function fragment ROM2 496F ("setup_titlescreen" in the IDA disassm)
This function generates the sprite list parameter lists for title screen OAM.
Here's the parameter lists it makes:
C0E0: 0x1, 0x0, 0x2, 0x28, 0xC, 0x0 C100: 0x1, 0x0, 0x3, 0x78, 0xC, 0x0 C120: 0x1, 0x0, 0x1B, 0x48, 0x40, 0x0 C140: 0x1, 0x0, 0x1D, 0x20, 0x38, 0x0 C160: 0x1, 0x0, 0x1E, 0x88, 0x38, 0x0
Also, remember that sprite list array thing? Well, the global far pointers for where the sprite list lists live are stored at HOME 094D (banks) and HOME 0956 (offsets).
I'll replicate those lists here:
Banks: 0xA, 0xE, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19 Offsets: 0x4000, 0x4120, 0x4000, 0x4000, 0x4000, 0x4000, 0x4000, 0x4000, 0x4000
Okay, so all of our titlescreen sprites live in ROM 0xA... in flat addressing, that's 0x28000. We're concerned with sprite lists (0x...) 2, 3, 1B, 1D, and 1E.
In Power Version, those pointers are...
0x2 = 0x4197 0x3 = 0x41BB 0x1B = 0x4336 0x1D = 0x4388 0x1E = 0x43A7 (0x2803C)
All of these are in the ROM bank, and they obviously live in the same bank as their pointer.
The precomposed spritelist looks like this...
First there's a byte which is just the size of the list Then the Y and X position bytes, which are summed with the global sprite positions in the parameter list, then written onto the OAM DMA area.
Then the tile pattern number, which is copied blindly into the OAM DMA area
Now there's a flag. There's three ways attributes can be copied into a sprite.
Way 0 just uses the global attributes from the parameter list. Way 1 reads an extra byte from the sprite list which is copied to the OAM DMA. Way 2 has the same format as Way 1, except that the low bits of the sprite's attribute byte are replaced with the global low nybble attribute (GlobalLoNybble).
This repeats until either the sprite list is finished or we run out of sprites.
Now let's try some ROM corruption and see what happens! By decreasing the first byte of a sprite list, it copies less sprites and we can see what sprites are missing in OAM,
0x2 appears to be the list for the phone sparks 0x3 this also appears to be the list for the phone sparks, but in the uncharged state 0x1B is the text "POWER VERSION" 0x1D This is the attribute clash removing katakana, but not the one we need to move 0x1E JACKPOT! This is the sprite list that needs to be modified to bring down the "G".
At the request of the guy who originally injected the title screen, let's instead add an extra sprite for him, instead of whatever is below this. That means moving a list out of packed storage and into some unused area of the bank. We'll select the address 0x29000 (flat-file) or 0x5000 (GB viewed) as it's easy to remember.
new bytes are
06 f0 f8 09 01 10 //these are from the old copy of the table
f0 00 0a 01 10 f8 f8 0e 01 10 f8 00 0f 01 10 00 f8 11 01 10 //This is the new sprite. 00 00 10 01 10 //This one is also unused, but I cut it off from another table
So, by altering the pointer and copying the sprite table...
So, I've modified 0x1E to pull down where the "G" is in OAM memory. Now we need to alter the tile mappings, but where the hell are they? Well, HOME 0A3D is called to load the tile mappings, so let's trace that...
--Tile map format
Tile maps are read by HOME 0A2A, a function I'm naming "DecompressTilemap2VRAM". Critically this is the function responsible for actually loading our tilemap into VRAM. It also supports decompression modes.
The critical in-param is E char, which indexes where the Program Stream should be. Additionally, there's A char, which is the BankList Offset. Like the sprite list, there's a predefined list of banks at 0x0B18 which is indexed by A. Once that bank is mapped in, we have two more params, B char and C char which are respectively, the row and cell indexes into the first map area that we want to overwrite.
That E char earlier is used to form the Program Stream address as so:
PStream = *((char**)0x4000 + E * 2);
The first byte of the Program Stream indicates the following:
PStream[0] == -1 Do nothing and return. Generally, -1 anywhere causes the decompressor to immediately return. (PStream[0] && 3) == 0 The following program stream is in "line-by-line" uncompressed mode. Any other case The following program stream is in compressed mode.
From thereon decompression depends on the mode selected.
-- Uncompressed mode Uncompressed mode is the easiest to comprehend. Bytes are copied directly from the Program Stream to the current row in the lower tilemap. The tilemap pointer will wrap around the current row.
Two special command bytes are recognized:
*PStream == -1 Do nothing and return. We're done. *PStream == -2 Go to the next row of the tilemap.
-- Compressed mode Compressed mode is more advanced and is mainly an RLE. A command byte is read from the program stream to determine the mode of the next decompression operation.
*PStream == -1 Do nothing and return. We're done. *PStream && 0xC0 == 0xC0 (I.e. both bits 7 and 6 are high) RLE mode, with automatic decrement. *PStream && 0xC0 == 0x80 (i.e. only bit 7 is high) RLE mode, with automatic increment. *PStream && 0xC0 == 0x40 (i.e. only bit 6 is high) RLE mode, with constant value. All other cases Literal mode
-- RLE modes In all RLE modes there are two extra bytes extracted from the program stream. The first byte is called Count, and is biased by +2, so that to write 5 bytes the decompressor should read a count of 3. The second is Start, and is the byte copied to the tile mapping. In Auto-increment or Auto-decrement modes, Start is increased or decreased by 1 after each write to the tile mapping.
After writing Count+2 bytes, the decompressor finishes this command and fetches another command byte.
-- Literal mode One extra byte is read from the program stream, again named Count. It is biased by +1 (i.e. to write 4 literal bytes, specify a count of 3). A total of Count+1 bytes are copied directly from the Program Stream into the tile mapping, and then decompression continues by reading another command byte.
== Now that we know how to parse a tilemap in ROM, where does it live?
Well, when looking for the OAM sprites I found the place where the tile mappings get written. HOME 04CA is called with the parameters
A = 0 E = C B = 0 C = 0
HOME 04CA is a banking-safe wrapper around DecompressTilemap2VRAM.
Evaluating the parameters, A selects the first bank from 0x0B18, which is 0x3E. The flat-address base (i.e. hex editor address) for this bank is 0xF8000. Both B and C are zero, so we're writing the top-left of the tile mapping. That leaves E, which is 0x0C. So we're 0x18 bytes into the bank, which gives us the Program Stream address 0x477F (remember, it's little-endian!). For those following along in Flat-File Land, that's 0xF877F.
We can decode along in our Hex Editor. the bytes are...
01 This means it's a compressed mode tilemap! 52 Bit 6 is active, so it's constant value RLE. 01 4A Count = 1 (so write 3 bytes) Start = 4A 00 No bits are on, so literal mode 45
...
After looking at the tilemap some more, I don't think I need to actually change this if I want to move that G down.
Okay we want to alter the tiles themselves now, and IRC tells me it's compressed graphics. They also tell me where the graphics are, and I know from disassembly that the graphics appear when HOME 04B1 is called. Now we need to find out how the function calculates addresses...
We read some bytes from 0x4000 + DE * 4, where DE is 0xA on the first load. I'm assuming this is the current ROM BANK 2, which stores the title screen code. So 0x8028 in flat-file.
Anyway we read out three bytes from that address into spill bytes. The first byte is used to select a proper ROM bank. This is ROM BANK 4, so our flat-file addr has a base of 0x10000. The next thing we do is calculate another offset from HOME 1DE1, as 0x1DE1 + DE * 2