Today you bring together the tile map, tile-data addressing, decoding, and palette lookup to render one full 160-pixel background scanline (with no scroll yet). Get one line right and a full frame is just this same routine run 144 times.
Render one background scanline of 160 pixels at scroll 0, combining the tile-map lookup, tile-data addressing, decoding, and palette.
Now the pieces converge into an actual picture. The background is a 32×32 grid of
tile numbers - the tile map at 0x9800 - describing which tile sits where. A
tile number then has to become tile data: in the standard unsigned mode, tile
n’s bytes live at 0x8000 + n*16, with 2 bytes per row (so row r starts at
+ r*2). That tile-data addressing is the new idea today; the map lookup, the
decode from lesson 42, and the palette from lesson 43 you already have.
To render scanline LY, walk x from 0 to 159: find the tile-map cell, read the
tile number, address its data, decode the pixel, and run the index through BGP -
160 finished shades. Keep SCX/SCY at 0 for now; those scroll offsets
(which slide a world larger than the screen past the window) are an easy extension
once the un-scrolled line is right. Get one line right and the full frame is just
this same routine run 144 times.
func (p *PPU) renderLine(ly uint8) [160]uint8 {var line [160]uint8for x := 0; x < 160; x++ { // no scroll yet - SCX = SCY = 0// tile map: a 32x32 grid of tile numbers at 0x9800tile := p.mem.Read(0x9800 + uint16(ly/8)*32 + uint16(x/8))// tile DATA (unsigned mode): 16 bytes per tile, 2 bytes per rowbase := 0x8000 + uint16(tile)*16 + uint16(ly%8)*2row := decodeRow(p.mem.Read(base), p.mem.Read(base+1))line[x] = shade(p.bgp, row[x%8])}return line}
Background rendering - the 32x32 tile map at 0x9800 and tile data at 0x8000 (16 bytes/tile). Scroll (SCX/SCY) is left at 0 today.