Skip to content →

scrolling & color fixing NTs

The next game we want to get to boot is “Super Mario Bros.”. The booting part at this point is probably easy, but you can quickly see, that we are still missing stuff.

As you can see, the scrolling part is missing. At the moment, we are only drawing the first Nametable. And you can see the NT being redrawn, bit by bit as Mario advances. This is because the 2nd NT is used as the ‘next‘ part of the screen. The viewport is moved by the scrolling values (and wraps) and thus gives the impression of ‘endless‘ levels.

NTs background loading and moving viewports

So, to realize the scrolling we will need to implement the PPUs scrolling mechanisms. For scrolling the X values, we use the PPU register 0x2005 (PPUSCROLL), which is written twice to (first the X value, secondly the Y value). By intercepting these writes, we can store and use the values for our scrolling mechanism.

void writePPUSCROLL(uint8_t val) {
	if (PPUSCROLL_pos == 0) {
		PPUSCROLL_pos = 1;
		PPUSCROLL_x = val;
	}
	else {
		PPUSCROLL_pos = 0;
		PPUSCROLL_y = val;
	}
}

Since the NTs are not right beside each other, and also have the attribute tables at the end of each table, getting the scrolling right, can be a bit of a mess. In the end I only needed to alternate my rendering function a bit, but the thinking part took some time.

tile_id = ((r / 8) * 32) + ((col + PPUSCROLL_x % 8) / 8);										
uint16_t natural_address = 0x2000 + (r / 8 * 32) + ((col + PPUSCROLL_x % 8) / 8);
natural_address += 0x40 * ((natural_address % 0x2000) / 0x3c0);				
uint16_t scrolled_address = natural_address + PPUSCROLL_x / 8;
if ((natural_address & 0xffe0) != (scrolled_address & 0xffe0))				//	check if X-boundary crossed 
{
	scrolled_address ^= 0x400;	//	XOR the bit, that changes NTs horizontally
	scrolled_address -= 0x20;	//	remove the 'line break'
}
tile_nr = VRAM[scrolled_address];								
adr = PPU_CTRL.background_pattern_table_adr_value + (tile_nr * 0x10) + (r % 8);

//	select the correct byte of the attribute table
tile_attr_nr = VRAM[(scrolled_address & 0xfc00) + 0x03c0 + ((r / 32) * 8) + ((col + PPUSCROLL_x) / 32)];

//	select the part of the byte that we need (2-bits)
attr_shift = (((tile_id % 32) / 2 % 2) + (tile_id / 64 % 2) * 2) * 2;
palette_offset = ((tile_attr_nr >> attr_shift) & 0x3) * 4;
pixel = ((VRAM[adr] >> (7 - ((col + PPUSCROLL_x) % 8))) & 1) + (((VRAM[adr + 8] >> (7 - ((col + PPUSCROLL_x) % 8))) & 1) * 2);

framebuffer[(r * 256 * 3) + (col * 3)] = (PALETTE[VRAM[fixPalette((0x3f00 + palette_offset + pixel) & 0xff0f)]] >> 16) & 0xff;
framebuffer[(r * 256 * 3) + (col * 3) + 1] = (PALETTE[VRAM[fixPalette((0x3f00 + palette_offset + pixel) & 0xff0f)]] >> 8) & 0xff;
framebuffer[(r * 256 * 3) + (col * 3) + 2] = (PALETTE[VRAM[fixPalette((0x3f00 + palette_offset + pixel) & 0xff0f)]]) & 0xff;

This somewhat did it for the scrolling, but then I noticed that all my NTs, that are not my first NT, have the wrong colors (and also, the screen jumps back to the wrong NTs, but I will address that later).

So, I wanted to fix this first, before proceeding any further. To fix this, I had to think quite a bit, as the rendering function with all the math and bit logic isn’t quite easy to understand. In the end, I hadn’t accounted for the attribute tables at the end of each NT (I needed to skip them in the loop), and also did not account for the half attribute row on the very last row of each NT – which would make the first row in the next NT the wrong value (it would think it was the second half of said attribute row).

After trying and thinking and even more trying, I was finally able to figure everything out, as you can see below in the modified function, for the NT window.

uint16_t tile_id = ((r / 8) * 32) + (col / 8);												//	sequential tile number
uint16_t natural_address = 0x2000 + tile_id;
natural_address += 0x40 * ((natural_address % 0x2000) / 0x3c0);
uint16_t tile_nr = VRAM[natural_address];													//	tile ID at the current address (skip attribute tables)
uint16_t adr = PPU_CTRL.background_pattern_table_adr_value + (tile_nr * 0x10) + (r % 8);	//	adress of the tile in CHR RAM

//	select the correct byte of the attribute table
uint16_t tile_attr_nr = VRAM[(natural_address & 0xfc00) + 0x03c0 + (((((r + (r / 240) * 16) / 32) * 8) + (col / 32)) % 0x40)];

//	select the part of the byte that we need (2-bits)
uint16_t attr_shift = (((tile_id % 32) / 2 % 2) + ( (tile_id + (r/240) * 64) / 64 % 2) * 2) * 2;
uint16_t palette_offset = ((tile_attr_nr >> attr_shift) & 0x3) * 4;
uint8_t pixel = ((VRAM[adr] >> (7 - (col % 8))) & 1) + (((VRAM[adr + 8] >> (7 - (col % 8))) & 1) * 2);

framebuffer_nt[(r * 256 * 3) + (col * 3)] = (PALETTE[VRAM[fixPalette(0x3f00 + palette_offset + pixel)]] >> 16 ) & 0xff;
framebuffer_nt[(r * 256 * 3) + (col * 3) + 1] = (PALETTE[VRAM[fixPalette(0x3f00 + palette_offset + pixel)]] >> 8) & 0xff;
framebuffer_nt[(r * 256 * 3) + (col * 3) + 2] = (PALETTE[VRAM[fixPalette(0x3f00 + palette_offset + pixel)]] ) & 0xff;

Which would finally (this really caused me headaches for an evening) output the NT window properly.

cropped to the first 2 NTs

To implement this fix for the actual scrolling viewport, we need to adapt the changes. For the sake of visibility I decided to make 2 extra functions that will get the correct byte from the attribute table, and will identify which tile part the current pixel is a part of.

uint16_t getAttribute(uint16_t adr) {
	uint16_t base = (adr & 0xfc00) + 0x03c0;
	uint16_t diff = adr - (adr & 0xfc00);
	uint16_t cell = (diff / 128) * 8 + ((diff % 128) % 32 / 4);
	return VRAM[base + cell];
}

uint8_t getAttributeTilePart(uint16_t adr) {
	uint16_t diff = adr - (adr & 0xfc00);
	uint8_t tilePar = (diff / 2) % 2 + ((diff / 64) % 2) * 2;
	return tilePar * 2;
}

With these functions, and the changes to our rendering code, we are now able to render properly for X-scrolling (despite the wrong tile on the very right side of the screen).

void renderScanline(uint16_t row) {
	int r = row;
	if (PPU_MASK.show_bg) {
		for (int col = 0; col < 256; col++) {
			tile_id = ((r / 8) * 32) + ((col + PPUSCROLL_x % 8) / 8);					//	sequential tile number
			uint16_t natural_address = PPU_CTRL.base_nametable_address_value + tile_id;
			uint16_t scrolled_address = natural_address + PPUSCROLL_x / 8;
			if ((natural_address & 0xffe0) != (scrolled_address & 0xffe0))				//	check if X-boundary crossed 
			{
				scrolled_address ^= 0x400;	//	XOR the bit, that changes NTs horizontally
				scrolled_address -= 0x20;	//	remove the 'line break'
			}
			tile_nr = VRAM[scrolled_address];								//	tile ID at the current address
			adr = PPU_CTRL.background_pattern_table_adr_value + (tile_nr * 0x10) + (r % 8);	//	adress of the tile in CHR RAM

			//	select the correct byte of the attribute table
			tile_attr_nr = getAttribute(scrolled_address);

			//	select the part of the byte that we need (2-bits)
			attr_shift = getAttributeTilePart(scrolled_address);
			palette_offset = ((tile_attr_nr >> attr_shift) & 0x3) * 4;


			pixel = ((VRAM[adr] >> (7 - ((col + PPUSCROLL_x % 8) % 8))) & 1) + (((VRAM[adr + 8] >> (7 - ((col + PPUSCROLL_x % 8) % 8))) & 1) * 2);

			framebuffer[(r * 256 * 3) + (col * 3)] = (PALETTE[VRAM[fixPalette((0x3f00 + palette_offset + pixel) & 0xff0f)]] >> 16) & 0xff;
			framebuffer[(r * 256 * 3) + (col * 3) + 1] = (PALETTE[VRAM[fixPalette((0x3f00 + palette_offset + pixel) & 0xff0f)]] >> 8) & 0xff;
			framebuffer[(r * 256 * 3) + (col * 3) + 2] = (PALETTE[VRAM[fixPalette((0x3f00 + palette_offset + pixel) & 0xff0f)]]) & 0xff;

		}
	}
...
// Sprite drawing is irrelevant for X-Scrolling, and hasn't been changed