diff options
| author | crecher <crecher@crecher.net> | 2026-10-13 01:13:13 +0200 |
|---|---|---|
| committer | crecher <crecher@crecher.net> | 2026-10-13 01:13:13 +0200 |
| commit | 99cae5a5a37f41f878a84857361eda47910303ba (patch) | |
| tree | 31c8665ca991f95e09e93bb2e6ec91d81d2427ff /ppu | |
| parent | 96ff6d8eda2ba434682909f4ff4ce350d78b2273 (diff) | |
| download | gones-99cae5a5a37f41f878a84857361eda47910303ba.tar.gz gones-99cae5a5a37f41f878a84857361eda47910303ba.zip | |
sprites!!!!!!
Diffstat (limited to 'ppu')
| -rw-r--r-- | ppu/memory.go | 6 | ||||
| -rw-r--r-- | ppu/ppu.go | 15 | ||||
| -rw-r--r-- | ppu/render.go | 36 | ||||
| -rw-r--r-- | ppu/sprites.go | 97 |
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 @@ -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 +} |