package ppu const ( ScreenWidth = 256 ScreenHeight = 240 preRenderLine = 261 ) // pixels of the last (or in progress) frame as ARGB8888 func (p *PPU) Framebuffer() []uint32 { return p.framebuffer[:] } // runs after Tick has moved to the new dot func (p *PPU) renderTick() { line, dot := p.Scanline, p.Dot visibleLine := line < ScreenHeight if p.renderingEnabled() && (visibleLine || line == preRenderLine) { p.backgroundFetch(dot) if dot == 256 { p.incrementY() } if dot == 257 { p.loadBackgroundShifters() p.copyHorizontal() } if line == preRenderLine && dot >= 280 && dot <= 304 { p.copyVertical() } } if visibleLine && dot >= 1 && dot <= ScreenWidth { p.drawPixel(dot-1, line) } } // the fetch cycle, every tile takes 8 dots to fetch (nametable byte, attribute // byte, pattern low, pattern high), 2 dots each. dots 321-336 fetch the first two // tiles of the next line ahead of time func (p *PPU) backgroundFetch(dot int) { if !((dot >= 2 && dot <= 257) || (dot >= 321 && dot <= 337)) { return } if p.Mask&maskShowBackground != 0 { p.shiftBackground() } switch (dot - 1) % 8 { case 0: p.loadBackgroundShifters() p.nextTileID = p.readVRAM(0x2000 | (p.v & 0x0FFF)) case 2: // attribute byte holds 4 palettes (2 bits each) for a 4x4 tile area, // coarse y/x bit 1 pick which quadrant is ours addr := 0x23C0 | (p.v & 0x0C00) | ((p.v >> 4) & 0x38) | ((p.v >> 2) & 0x07) attr := p.readVRAM(addr) if p.v&0x40 != 0 { attr >>= 4 } if p.v&0x02 != 0 { attr >>= 2 } p.nextAttr = attr & 0x03 case 4: p.nextPatternLo = p.readVRAM(p.backgroundPatternAddr()) case 6: p.nextPatternHi = p.readVRAM(p.backgroundPatternAddr() + 8) case 7: p.incrementCoarseX() } } // address of the current row of the next tiles pattern func (p *PPU) backgroundPatternAddr() uint16 { var base uint16 if p.Ctrl&ctrlBGPatternHigh != 0 { base = 0x1000 } fineY := (p.v >> 12) & 0x07 return base + uint16(p.nextTileID)*16 + fineY } func (p *PPU) shiftBackground() { p.patternLo <<= 1 p.patternHi <<= 1 p.attrLo <<= 1 p.attrHi <<= 1 } // puts fetched tile into the low byte of the shifters. the attribute bits // are the same for all 8 pixels of the tile, so they get repeated func (p *PPU) loadBackgroundShifters() { p.patternLo = (p.patternLo & 0xFF00) | uint16(p.nextPatternLo) p.patternHi = (p.patternHi & 0xFF00) | uint16(p.nextPatternHi) p.attrLo = (p.attrLo & 0xFF00) | repeatBit(p.nextAttr&0x01) p.attrHi = (p.attrHi & 0xFF00) | repeatBit(p.nextAttr&0x02) } func repeatBit(b uint8) uint16 { if b != 0 { return 0x00FF } return 0 } // scroll helpers, v layout is yyy NN YYYYY XXXXX // https://www.nesdev.org/wiki/PPU_scrolling func (p *PPU) incrementCoarseX() { if p.v&0x001F == 31 { p.v &^= 0x001F p.v ^= 0x0400 // wrap into the horizontally adjacent nametable } else { p.v++ } } func (p *PPU) incrementY() { if p.v&0x7000 != 0x7000 { p.v += 0x1000 // fine y return } p.v &^= 0x7000 y := (p.v & 0x03E0) >> 5 switch y { case 29: y = 0 p.v ^= 0x0800 // wrap into the vertically adjacent nametable case 31: y = 0 default: y++ } p.v = (p.v &^ 0x03E0) | (y << 5) } func (p *PPU) copyHorizontal() { p.v = (p.v &^ 0x041F) | (p.t & 0x041F) } func (p *PPU) copyVertical() { p.v = (p.v &^ 0x7BE0) | (p.t & 0x7BE0) } func (p *PPU) drawPixel(x, y int) { var pixel, palette uint8 if p.Mask&maskShowBackground != 0 && (x >= 8 || p.Mask&maskShowBackgroundLeft != 0) { bit := uint16(0x8000) >> p.x pixel = bitAt(p.patternHi, bit)<<1 | bitAt(p.patternLo, bit) palette = bitAt(p.attrHi, bit)<<1 | bitAt(p.attrLo, bit) } // pixel 0 of every palette is the shared backdrop color at $3F00 addr := uint16(0x3F00) if pixel != 0 { addr += uint16(palette)<<2 | uint16(pixel) } color := p.readPalette(addr) if p.Mask&maskGreyscale != 0 { color &= 0x30 } p.framebuffer[y*ScreenWidth+x] = 0xFF000000 | systemPalette[color&0x3F] } func bitAt(v, bit uint16) uint8 { if v&bit != 0 { return 1 } return 0 }