Skip to content →

unrom mapper, duck tales, and scrolling again

unrom mapper

To get further going, I wanted to get Duck Tales going, which was one of my all time favorite games. To get it going, we need to add another mapper type, called UnROM / UxROM.

UnROM / UxROM has the following attributes:

PRG ROM capacity256K/4096K
PRG ROM window16K + 16K fixed
PRG RAM capacityNone
CHR capacity8K

It has the iNES mapper IDs 002, 094, 180.

As you can see, the mapper is able to change its ROM banks, which is the only specialty of this very mapper. To select the ROM bank, the code writes to 0x8000 – 0xffff.

7  bit  0 
---- ---- 
xxxx pPPP      
     ||||      
     ++++- Select 16 KB PRG ROM bank for CPU $8000-$BFFF
           (UNROM uses bits 2-0; UOROM uses bits 3-0) 
struct MMC1 : Mapper {

	int PRGid = 0;
	unsigned char* m = 0;

	MMC1(int mem_size, int prg16, int chr8) : Mapper(mem_size, prg16, chr8) {

	}

	virtual void loadMem(unsigned char* c) {
		m = c;
		//	Switchable first 16k PRG
		for (int i = 0; i < 0x4000; i++) {
			memory[0x8000 + i] = c[i + 0x10];
		}
		//	Fixed last 16k PRG
		for (int i = 0; i < 0x4000; i++) {
			memory[0xc000 + i] = c[i + 0x10 + (romPRG16ks - 1) * 0x4000];
		}
		//	CHR
		for (int i = 0; i < 0x2000; i++) {
			writeCHRRAM(c, 0x10 + romPRG16ks * 0x4000);
		}
	}

	virtual void write(uint16_t adr, uint8_t val) {
		if (adr >= 0x8000 && adr <= 0xffff) {
			PRGid = val & 0b111;
		}
		else {
			memory[adr] = val;
		}
	}

	virtual unsigned char read(uint16_t adr) {
		if (adr >= 0x8000 && adr <= 0xbfff) {
			return m[(adr % 0x8000) + PRGid * 0x4000 + 0x10];
		}
		else {
			return memory[adr];
		}
	}

};

The only thing, we need to notice is, that the ROM bank number is 0-based, and not based from 0x8000 onwards as you might initially think (see highlighted line 37).

duck tales – scrolling again

With our new mapper in place, we are able to start the famous Duck Tales. From the very start everything seems normal and working very well. That is until we come across a rope / chain that gives us the possibility to changes rooms vertically.

As you can see, as soon as the scrolling animation starts, the screen kind of pops, and on the bottom end we can see 6 rows of tiles, that are clearly not supposed to be where they are. This is going to be a bit tricky to understand, so bear with me.

The exact same behavior can be seen in Mesen’s PPU viewer (but only on the NT viewport, the actual window shows the correct picture).

Mesen showing another scrolling quirk

When I checked the code, the initial Y-Scroll value I would get was 6. So, another ‘6’, already looks like a pattern. Now, if you look closely you can maybe make out that the UI is 6 tiles tall as well. A coincidence? Nope.

This took me actually quite a while to figure out, without making our other games glitch out. For starters, it’s important to know, that this scrolling behavior of Duck Tales is UB (undefined behavior). Another game, that makes use of this, is The Legend of Zelda. And the undefined scrolling has been extensively explained here.

In short, this scrolling is achieved not by writing to PPUSCROLL (0x2005), but by writing to PPUADDR (0x2006), which share a register, thus making it possible to change scrolling values, without ever touching PPUSCROLL itself. This has been discussed in the previous chapter (think back to ‘t’ and ‘v’).

So, with this method the game is able to draw the static UI from the first nametable, and then write to PPUADDR to ninja-insert new Y-Scroll values, to make the rest of the screen scroll vertically.

But why do we have this 6-tile offset? The answer is, in the end, not that hard. The actual scrolling value is supposed to be relative to the tile. If you create an emulator, with a pixel pipeline, you will probably not encounter this error, as you will increase the Y-fine, and Y-coarse values on each tile / row anyway.
For our row-rendered emulator though, we need to take certain actions to represent this behavior.

So, our actual fix is, on the second write to PPUADDR (0x2006), which is the one that makes temporary values available to the system (t ==> v), we will subtract the current tile-row (so, scanline / 8) from the Y-Scroll value, so we have a relative scrolling value to this tile.

//	PPUADDR write (2006)
void writePPUADDR(uint8_t adr) {

	if (scr_w == 0) {
		...
	}
	else {
                ...
		
		PPUSCROLL_y = tmp_PPUSCROLL_y - (ppuScanline / 8);
		PPUSCROLL_fine_y = tmp_PPUSCROLL_fine_y;

		scr_w = 0;
	}
}
Note: This also means, it is possible to have negative scrolling values, so make sure your Y-Scroll variable is not unsigned.
Duck Tales – Relative Y-Scroll, but unsigned

So, when we actually use a signed variable, and are able to account for negative relative scrolling values, we will come up with the proper scrolling of Duck Tales.

Duck Tales – Proper relative Y-Scroll, signed

Comments

Leave a Reply