diff options
| author | crecher <crecher@crecher.net> | 2026-10-12 22:08:09 +0200 |
|---|---|---|
| committer | crecher <crecher@crecher.net> | 2026-10-12 22:08:09 +0200 |
| commit | 96ff6d8eda2ba434682909f4ff4ce350d78b2273 (patch) | |
| tree | fc1982c54420254ef82c588b742d7ecacfbe7d48 /ppu/render.go | |
| parent | 84801d18b9885c6d70914c67a76d8ac8e3b2b20d (diff) | |
| download | gones-96ff6d8eda2ba434682909f4ff4ce350d78b2273.tar.gz gones-96ff6d8eda2ba434682909f4ff4ce350d78b2273.zip | |
ppu: add background rendering
cmd/gones: add sdl2 frontend
Diffstat (limited to 'ppu/render.go')
| -rw-r--r-- | ppu/render.go | 172 |
1 files changed, 172 insertions, 0 deletions
diff --git a/ppu/render.go b/ppu/render.go new file mode 100644 index 0000000..bfde9e4 --- /dev/null +++ b/ppu/render.go @@ -0,0 +1,172 @@ +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 +} |