Skip to content →

backgrounds, modes and tests

The SNES PPU offers a total of 4 Backgrounds that can be blended together depending on multiple settings. What can be displayed on those background also depends on the current mode that is selected.

The available modes are:

Mode   BG1         BG2         BG3         BG4
---------------------------------------------------------------------------   
0      4-color     4-color     4-color     4-color   ;Normal   
1      16-color    16-color    4-color     -         ;Normal   
2      16-color    16-color    (o.p.t)     -         ;Offset-per-tile   
3      256-color   16-color    -           -         ;Normal   
4      256-color   4-color     (o.p.t)     -         ;Offset-per-tile   
5      16-color    4-color     -           -         ;512-pix-hires   
6      16-color    -           (o.p.t)     -         ;512-pix plus Offs-p-t   
7      256-color   EXTBG       -           -         ;Rotation/Scaling 

(thanks no$)

As you can see, the modes allow for different color-depths (and more) for each individual Background. This allowed the developers to choose the most fitting scenario for the current situation, to make the most out of the available hardware.

The mode can be set with writes to register 0x2105 (BGMODE), which will also determine the Tile size for each Background.

7BG4 Tile Size (0=8×8, 1=16×16)
6BG3 Tile Size (0=8×8, 1=16×16)
5BG2 Tile Size (0=8×8, 1=16×16)
4BG1 Tile Size (0=8×8, 1=16×16)
3BG3 Priority in Mode 1 (0=Normal, 1=High)
2-0Screen Mode

We can also enable or disable these Backgrounds with writes to the Register 0x212C

7-5not used
4OBJ (0=Disable, 1=Enable)
3BG4 (0=Disable, 1=Enable)
2BG3 (0=Disable, 1=Enable)
1BG2 (0=Disable, 1=Enable)
0BG1 (0=Disable, 1=Enable)


For the moment we will focus on the first 4 modes, which are the easiest to emulate and to get running. The hardest part later will be to have a properly working mode 7, which is responsible for Fake-3D, transformations, rotations and so on.

We already discussed the color depths in previous chapters, so the 4-color, 16-color and 256-color settings for Backgrounds should already be clear.
For easy access I made some constants, to make the rendering easier to read in the end.

const enum class PPU_COLOR_DEPTH {
	CD_2BPP_4_COLORS,
	CD_4BPP_16_COLORS,
	CD_8BPP_256_COLORS,
	CD_OFFSET_PER_TILE,
	CD_EXTBG,
	CD_DISABLED
};

const PPU_COLOR_DEPTH PPU_BG_MODES[][4] = {
	{PPU_COLOR_DEPTH::CD_2BPP_4_COLORS, PPU_COLOR_DEPTH::CD_2BPP_4_COLORS, PPU_COLOR_DEPTH::CD_2BPP_4_COLORS, PPU_COLOR_DEPTH::CD_2BPP_4_COLORS },		//	mode 0	-	2bpp, 2bpp, 2bpp, 2bpp	-	Normal
	{PPU_COLOR_DEPTH::CD_4BPP_16_COLORS, PPU_COLOR_DEPTH::CD_4BPP_16_COLORS, PPU_COLOR_DEPTH::CD_2BPP_4_COLORS, PPU_COLOR_DEPTH::CD_DISABLED},			//	mode 1	-	4bpp, 4bpp, 2bpp, -		-	Normal
	{PPU_COLOR_DEPTH::CD_4BPP_16_COLORS, PPU_COLOR_DEPTH::CD_4BPP_16_COLORS, PPU_COLOR_DEPTH::CD_OFFSET_PER_TILE, PPU_COLOR_DEPTH::CD_DISABLED},		//	mode 2	-	4bpp, 4bpp, OPT, -		-	Offset-per-Tile
	{PPU_COLOR_DEPTH::CD_8BPP_256_COLORS, PPU_COLOR_DEPTH::CD_4BPP_16_COLORS, PPU_COLOR_DEPTH::CD_DISABLED, PPU_COLOR_DEPTH::CD_DISABLED},				//	mode 3	-	8bpp, 4bpp, -, -		-	Normal
	{PPU_COLOR_DEPTH::CD_8BPP_256_COLORS, PPU_COLOR_DEPTH::CD_2BPP_4_COLORS, PPU_COLOR_DEPTH::CD_OFFSET_PER_TILE, PPU_COLOR_DEPTH::CD_DISABLED},		//	mode 4	-	8bpp, 2bpp, OPT, -		-	Offset-per-Tile
	{PPU_COLOR_DEPTH::CD_4BPP_16_COLORS, PPU_COLOR_DEPTH::CD_2BPP_4_COLORS, PPU_COLOR_DEPTH::CD_DISABLED, PPU_COLOR_DEPTH::CD_DISABLED},				//	mode 5	-	4bpp, 2bpp, -, -		-	512-pix-hires
	{PPU_COLOR_DEPTH::CD_4BPP_16_COLORS, PPU_COLOR_DEPTH::CD_DISABLED, PPU_COLOR_DEPTH::CD_OFFSET_PER_TILE, PPU_COLOR_DEPTH::CD_DISABLED},				//	mode 6	-	4bpp, -, OPT, -			-	512-pix plus Offset-per-Tile
	{PPU_COLOR_DEPTH::CD_8BPP_256_COLORS, PPU_COLOR_DEPTH::CD_EXTBG, PPU_COLOR_DEPTH::CD_DISABLED, PPU_COLOR_DEPTH::CD_DISABLED}						//	mode 7	-	8bpp, EXTBG, -, -		-	Rotation/Scaling
};

running and passing krom’s tests

For this paragraph we will make use of krom’s great PPU tests, which can be found here.

Luckily krom didn’t only supply CPU tests, but also some isolated tests for the PPU. Our goals is to complete those, and we will start with the most simple ones.

BG1/2/3/4 – 32×32 Tiles – 2BPP

This is the simplest PPU test available (there are 4, it’s all the same, but once on each available BG, to test they are working), and with the information we gathered in the last chapters, shouldn’t be that difficult to pass anymore.

As a baseline here is a (current) version of my rendering code for 2BPP (which already includes future features like scrolling and flipping):

void renderBGat2BPP(u16 scrx, u16 scry, u16 *BG, u16 bg_base, u8 bg_size_w, u8 bg_size_h, u8 bg_palette_base, u16 scroll_x, u16 scroll_y, u16 texture_width) {
	const u16 orgx = scrx;												//	store original x/y position, so we can draw in the FB to it
	const u16 orgy = scry;
	scry = (scry + scroll_y) % (8 * bg_size_h);							//	scroll x and y, and adjust for line/column jumps
	scrx = (scrx + scroll_x) % (8 * bg_size_w);

	const u16 offset =
		(((bg_size_w == 64) ? (scry % 256) : (scry)) / 8) * 32 +
		((scrx % 256) / 8) +
		(scrx / 256) * 0x400 +
		(bg_size_w / 64) * ((scry / 256) * 0x800);
	const u16 tile_id = VRAM[bg_base + offset] & 0x3ff;					//	mask bits that are for index
	const u8 b_palette_nr = (VRAM[bg_base + offset] >> 10) & 0b111;
	const u8 b_priority = (VRAM[bg_base + offset] >> 13) & 1;			//	0 - lower, 1 - higher
	const u8 b_flip_x = (VRAM[bg_base + offset] >> 14) & 1;				//	0 - normal, 1 - mirror horizontally
	const u8 b_flip_y = (VRAM[bg_base + offset] >> 15) & 1;				//	0 - normal, 1 - mirror vertically
	const u8 i = scry % 8;
	const u8 j = scrx % 8;
	const u8 v_shift = i + (-i + 7 - i) * b_flip_y;
	const u8 h_shift = (7 - j) + (2 * j - 7) * b_flip_x;
	const u16 tile_address = tile_id * 8 + v_shift;					//	this doesn't have tile_base like 8bpp, fix?
	const u8 b_hi = VRAM[tile_address] >> 8;
	const u8 b_lo = VRAM[tile_address] & 0xff;
	const u8 v = ((b_lo >> h_shift) & 1) + (2 * ((b_hi >> h_shift) & 1));
	writeToFB(BG, orgx, orgy, texture_width, getRGBAFromCGRAM(v, b_palette_nr, bg_palette_base, 2));
}

So after fiddling a bit with the code we should be able to get the proper screen:

BG1 – 32×32 Tiles – 2BPP

BG1 – 32×32 Tiles – 4BPP

The “only” change here is the color depths, so we have to adapt to that, and create our bitplanes from more data.

...
const u8 b_1 = VRAM[tile_address] & 0xff;
	const u8 b_2 = VRAM[tile_address] >> 8;
	const u8 b_3 = VRAM[tile_address + 8] & 0xff;
	const u8 b_4 = VRAM[tile_address + 8] >> 8;
	const u16 v =	((b_1 >> h_shift) & 1) + 
			(2 * ((b_2 >> h_shift) & 1)) +
			(4 * ((b_3 >> h_shift) & 1)) +
			(8 * ((b_4 >> h_shift) & 1));
...

This should successfully give us the following output, which already looks a lot better than just 2BPP.

BG1 – 32×32 Tiles – 4BPP

You can see some blue-ish pattern on the bottom row, that is because right now I am rendering a 256×256 window, which is bigger than the intended SNES resolution (NTSC and PAL), and therefore the bottom of the window will always show VRAM data that is not supposed to be rendered (this will be changed later in the development process).

BG1 – 32×32 Tiles – 8BPP

Again, we must adapt to the greater color-depth here, and should be able to render this pretty soon again.

...
const u16 tile_address = tile_id * 32 + tile_base + v_shift;
	const u8 b_1 = VRAM[tile_address] & 0xff;
	const u8 b_2 = VRAM[tile_address] >> 8;
	const u8 b_3 = VRAM[tile_address + 8] & 0xff;
	const u8 b_4 = VRAM[tile_address + 8] >> 8;
	const u8 b_5 = VRAM[tile_address + 16] & 0xff;
	const u8 b_6 = VRAM[tile_address + 16] >> 8;
	const u8 b_7 = VRAM[tile_address + 24] & 0xff;
	const u8 b_8 = VRAM[tile_address + 24] >> 8;
	const u16 v =	((b_1 >> h_shift) & 1) + 
			(2 *	((b_2 >> h_shift) & 1)) + 
			(4 *	((b_3 >> h_shift) & 1)) + 
			(8 *	((b_4 >> h_shift) & 1)) + 
			(16 *	((b_5 >> h_shift) & 1)) + 
			(32 *	((b_6 >> h_shift) & 1)) +
			(64 *	((b_7 >> h_shift) & 1)) +
			(128 *	((b_8 >> h_shift) & 1));
...

This will show up, and you can see another noticable increase in quality in comparison to the 4BPP test.

BG1 – 32×32 Tiles – 8BPP

transparency & backdrop

As already mentioned, each color of each palette with the index 0 is transparency. The usual and general way the Backgrounds are sorted and rendered is illustrated in the following image:

Background sorting & Backdrop

So, if Background 1 has a transparent pixel, the pixel just below in BG2 will be rendered. If BG2 has a transparent pixel there as well, BG3 will be rendered and so on. If there happen to be pixel where none of the background have an opaque pixel, a backup color, the so called Backdrop, will be rendered in this position. The backdrop color is always the color at absolute 0x00 in CGRAM.

With this implemented (SDL for example offers blending modes for this), the BG1/2/3/4 tests with 2BPP color depths show a little change:

The arrow shows an area where the backdrop color is rendered

To actually test the transparency and blending of our BGs we can utilize another one of krom’s tests, the Rings test.

The red rings reside on BG1 while the green rings reside on BG2. Both have transparent areas, that’s why we can see parts of the green rings, and even Backdrop between the green rings themselves.

tilemap sizes

So far we have only been working with tests that have tilemap sizes of 32×32 Tiles. This is a very common size as it is enough to fill the entire screen with graphics. There are other tilemap sizes though, to, for example, already have map bigger than the screen. This is important to handle, as this will e.g. affect scrolling.

There are 4 available settings for the Tilemap size, these can be set through registers that also set the BG Base Addresses (already mentioned earlier):

0x2107 – BG1 Base Address
0x2108 – BG2 Base Address
0x2109 – BG3 Base Address
0x210A – BG4 Base Address

Bits 1-0 set the Tilemap Size, which relate to the following table:

0032×32 Tiles
0164×32 Tiles
1032×64 Tiles
1164×64 Tiles

Since our actual screen only covers the 32×32 Tiles, I made some Debug functionalities, to display the whole available Tilemap next to the actual screen.
Krom has a test for each mode, and you are only able to see all of the (VRAM) Data with these additional functionality.

64×64, 32×64, 32×32, 64×32 Tilemap

scrolling

If we are able to print Debug windows like that, our logical implementation should probably be good, but to test it properly, we will implement scrolling, to see if we see any clipping.

The part in the code above that is reliable for scrolling is this one:

...
const u16 orgx = scrx;												//	store original x/y position, so we can draw in the FB to it
const u16 orgy = scry;
scry = (scry + scroll_y) % (8 * bg_size_h);							//	scroll x and y, and adjust for line/column jumps
scrx = (scrx + scroll_x) % (8 * bg_size_w);
...

This will translate our coordinates to the actual scrolling values passed to the rendering function. We fetch the actual X- and Y-values for scrolling from the registers:

0x210DBG1 Horizontal Scroll
0x210EBG1 Vertical Scroll
0x210FBG2 Horizontal Scroll
0x2110BG2 Vertical Scroll
0x2111BG3 Horizontal Scroll
0x2112BG3 Vertical Scroll
0x2113BG4 Horizontal Scroll
0x2114BG4 Horizontal Scroll

With our correct translation and the handling of writes to the above registers we are now able to handle scrolling for all 4 backgrounds properly. We can test this now with another 256-color test from krom, the 8BPP 32×32 test.

implemented scrolling

flipping

The last simple test we can conquer for now, is tile flipping. Every Tile can be flipped on the vertical axis, on the horizontal axis, or both.
We easily achieve this by reading from the other end of the byte (horizontal), or the other end of the byte-order (vertical).

...
const u8 v_shift = i + (-i + 7 - i) * b_flip_y;
const u8 h_shift = (7 - j) + (2 * j - 7) * b_flip_x;
...

Each Tile has 2 flags in its 16-bit value in the Tilemap, if bit 15 is set the Tile will be mirrored horizontally, if bit 14 is set it will be mirrored vertically.

Krom’s tileflipping test will confirm that our implementation worked properly.

Tile flipping test

Since the white arrows point to all corners, the implementation was correct.

Comments

Leave a Reply