aboutsummaryrefslogtreecommitdiff
path: root/ppu
diff options
context:
space:
mode:
Diffstat (limited to 'ppu')
-rw-r--r--ppu/memory.go6
-rw-r--r--ppu/ppu.go15
-rw-r--r--ppu/render.go36
-rw-r--r--ppu/sprites.go97
4 files changed, 134 insertions, 20 deletions
diff --git a/ppu/memory.go b/ppu/memory.go
index 3379242..d2b0a54 100644
--- a/ppu/memory.go
+++ b/ppu/memory.go
@@ -2,12 +2,6 @@ package ppu
import "crecher.net/gones/v2/cart"
-// maps a $2000-$3EFF address onto an offset into the 2KB of nametable RAM,
-// according to the cartridge's mirroring mode. four-screen carts will be
-// expected to use their own extra 2KB of RAM via the mapper, but since no
-// implemented mapper does that yet, four-screen just falls back to the vertical
-// mirroring as a placeholder
-
func (p *PPU) nametableOffset(addr uint16) int {
addr &= 0x0FFF
table := addr / 0x400
diff --git a/ppu/ppu.go b/ppu/ppu.go
index 322cf3d..42b3b4c 100644
--- a/ppu/ppu.go
+++ b/ppu/ppu.go
@@ -3,13 +3,16 @@ package ppu
import "crecher.net/gones/v2/cart"
const (
- ctrlNametableMask = 0x03
- ctrlIncrement32 = 0x04
- ctrlBGPatternHigh = 0x10
- ctrlNMIEnable = 0x80
+ ctrlNametableMask = 0x03
+ ctrlIncrement32 = 0x04
+ ctrlSpritePatternHigh = 0x08
+ ctrlBGPatternHigh = 0x10
+ ctrlSpriteSize16 = 0x20
+ ctrlNMIEnable = 0x80
maskGreyscale = 0x01
maskShowBackgroundLeft = 0x02
+ maskShowSpritesLeft = 0x04
maskShowBackground = 0x08
maskShowSprites = 0x10
@@ -59,6 +62,10 @@ type PPU struct {
patternLo, patternHi uint16
attrLo, attrHi uint16
+ // sprites for the line being drawn, picked at the end of the previous line
+ spriteLine [8]sprite
+ spriteCount int
+
nmiOutput bool // PPUCTRL bit 7
nmiLine bool // vblank && nmiOutput, latched each tick for edge detection
nmiPending bool // set on nmiLine's rising edge, consumed by PollNMI
diff --git a/ppu/render.go b/ppu/render.go
index bfde9e4..ef3ad9a 100644
--- a/ppu/render.go
+++ b/ppu/render.go
@@ -31,14 +31,15 @@ func (p *PPU) renderTick() {
}
}
+ if dot == 257 && (visibleLine || line == preRenderLine) {
+ p.evaluateSprites()
+ }
+
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
@@ -144,18 +145,33 @@ func (p *PPU) copyVertical() {
p.v = (p.v &^ 0x7BE0) | (p.t & 0x7BE0)
}
+func (p *PPU) backgroundPixel(x int) (pixel, palette uint8) {
+ if p.Mask&maskShowBackground == 0 || (x < 8 && p.Mask&maskShowBackgroundLeft == 0) {
+ return 0, 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)
+ return pixel, palette
+}
+
+// mixes the background and sprite pixel at (x, y) into the framebuffer
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)
+ bgPixel, bgPalette := p.backgroundPixel(x)
+ spritePixel, spritePalette, behind, isSprite0 := p.spritePixel(x)
+
+ // opaque sprite 0 pixel over an opaque background pixel, never at x = 255
+ if isSprite0 && bgPixel != 0 && x != 255 {
+ p.sprite0Hit = true
}
// pixel 0 of every palette is the shared backdrop color at $3F00
addr := uint16(0x3F00)
- if pixel != 0 {
- addr += uint16(palette)<<2 | uint16(pixel)
+ switch {
+ case spritePixel != 0 && (bgPixel == 0 || !behind):
+ addr = 0x3F10 | uint16(spritePalette)<<2 | uint16(spritePixel)
+ case bgPixel != 0:
+ addr |= uint16(bgPalette)<<2 | uint16(bgPixel)
}
color := p.readPalette(addr)
if p.Mask&maskGreyscale != 0 {
diff --git a/ppu/sprites.go b/ppu/sprites.go
new file mode 100644
index 0000000..83d1703
--- /dev/null
+++ b/ppu/sprites.go
@@ -0,0 +1,97 @@
+package ppu
+
+import "math/bits"
+
+const (
+ spriteAttrPalette = 0x03
+ spriteAttrBehind = 0x20 // draw behind opaque background pixels
+ spriteAttrFlipH = 0x40
+ spriteAttrFlipV = 0x80
+)
+
+type sprite struct {
+ patternLo, patternHi uint8
+ attr uint8
+ x uint8
+ isSprite0 bool
+}
+
+// ill add comments later im tired
+// https://www.nesdev.org/wiki/PPU_sprite_evaluation
+func (p *PPU) evaluateSprites() {
+ p.spriteCount = 0
+ // nothing is drawn above line 1, so the pre-render line and the last line leave it empty
+ if !p.renderingEnabled() || p.Scanline >= ScreenHeight-1 {
+ return
+ }
+
+ height := 8
+ if p.Ctrl&ctrlSpriteSize16 != 0 {
+ height = 16
+ }
+
+ for i := 0; i < 64; i++ {
+ row := p.Scanline - int(p.OAM[i*4])
+ if row < 0 || row >= height {
+ continue
+ }
+ if p.spriteCount == 8 {
+ p.spriteOverflow = true
+ break
+ }
+ p.spriteLine[p.spriteCount] = p.fetchSprite(i, row, height)
+ p.spriteCount++
+ }
+}
+
+func (p *PPU) fetchSprite(i, row, height int) sprite {
+ tile := p.OAM[i*4+1]
+ attr := p.OAM[i*4+2]
+ if attr&spriteAttrFlipV != 0 {
+ row = height - 1 - row
+ }
+
+ var addr uint16
+ if height == 16 {
+ table := uint16(tile&1) << 12
+ tile &^= 1
+ if row >= 8 {
+ tile++
+ row -= 8
+ }
+ addr = table | uint16(tile)<<4 | uint16(row)
+ } else {
+ var base uint16
+ if p.Ctrl&ctrlSpritePatternHigh != 0 {
+ base = 0x1000
+ }
+ addr = base | uint16(tile)<<4 | uint16(row)
+ }
+
+ lo := p.readVRAM(addr)
+ hi := p.readVRAM(addr + 8)
+ if attr&spriteAttrFlipH != 0 {
+ lo, hi = bits.Reverse8(lo), bits.Reverse8(hi)
+ }
+ return sprite{patternLo: lo, patternHi: hi, attr: attr, x: p.OAM[i*4+3], isSprite0: i == 0}
+}
+
+func (p *PPU) spritePixel(x int) (pixel, palette uint8, behind, isSprite0 bool) {
+ if p.Mask&maskShowSprites == 0 || (x < 8 && p.Mask&maskShowSpritesLeft == 0) {
+ return 0, 0, false, false
+ }
+ for i := 0; i < p.spriteCount; i++ {
+ s := &p.spriteLine[i]
+ offset := x - int(s.x)
+ if offset < 0 || offset > 7 {
+ continue
+ }
+ shift := 7 - offset
+ pixel = (s.patternHi>>shift&1)<<1 | (s.patternLo >> shift & 1)
+ if pixel == 0 {
+ continue
+ }
+ return pixel, s.attr & spriteAttrPalette, s.attr&spriteAttrBehind != 0, s.isSprite0
+ }
+ return 0, 0, false, false
+}