aboutsummaryrefslogtreecommitdiff
path: root/ppu/render.go
diff options
context:
space:
mode:
Diffstat (limited to 'ppu/render.go')
-rw-r--r--ppu/render.go172
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
+}