Skip to content

Commit

Permalink
feat(ppu): Add option to remove the original hardware sprite limit
Browse files Browse the repository at this point in the history
The original limit will still be enforced if a masking effect is detected.
  • Loading branch information
gabe565 committed Oct 3, 2024
1 parent f4ed210 commit f075eae
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 26 deletions.
2 changes: 2 additions & 0 deletions config_example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ scale = 3.0
pause_unfocused = true
# Palette (.pal) file to use. An embedded palette will be used when blank.
palette = ''
# Removes the original hardware's 8 horizontal sprite limitation. When enabled, sprites will no longer flicker.
remove_sprite_limit = true
# Change the number of rows/cols of overscan.
overscan = {top = 8, right = 0, bottom = 8, left = 0}

Expand Down
11 changes: 6 additions & 5 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ type Config struct {
}

type UI struct {
Fullscreen bool `toml:"fullscreen" comment:"Default fullscreen state. Fullscreen can also be toggled with a key (F11 by default)."`
Scale float64 `toml:"scale" comment:"Multiplier used to scale the UI."`
PauseUnfocused bool `toml:"pause_unfocused" comment:"Pauses when the window loses focus. Optional, but audio will be glitchy when the game is running in the background."`
Palette string `toml:"palette" comment:"Palette (.pal) file to use. An embedded palette will be used when blank."`
Overscan Overscan `toml:"overscan,inline" comment:"Change the number of rows/cols of overscan."`
Fullscreen bool `toml:"fullscreen" comment:"Default fullscreen state. Fullscreen can also be toggled with a key (F11 by default)."`
Scale float64 `toml:"scale" comment:"Multiplier used to scale the UI."`
PauseUnfocused bool `toml:"pause_unfocused" comment:"Pauses when the window loses focus. Optional, but audio will be glitchy when the game is running in the background."`
Palette string `toml:"palette" comment:"Palette (.pal) file to use. An embedded palette will be used when blank."`
RemoveSpriteLimit bool `toml:"remove_sprite_limit" comment:"Removes the original hardware's 8 horizontal sprite limitation. When enabled, sprites will no longer flicker."`
Overscan Overscan `toml:"overscan,inline" comment:"Change the number of rows/cols of overscan."`
}

type Overscan struct {
Expand Down
8 changes: 4 additions & 4 deletions internal/config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
func NewDefault() *Config {
return &Config{
UI: UI{
Fullscreen: false,
Scale: 3,
PauseUnfocused: true,
Overscan: Overscan{Top: 8, Bottom: 8},
Scale: 3,
PauseUnfocused: true,
RemoveSpriteLimit: true,
Overscan: Overscan{Top: 8, Bottom: 8},
},
State: State{
Resume: true,
Expand Down
3 changes: 3 additions & 0 deletions internal/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ const (

Width = 256
Height = 240

PPUOAMSize = 256
PPUSpriteLimit = 8
)
16 changes: 14 additions & 2 deletions internal/ppu/ppu.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/gabe565/gones/internal/cartridge"
"github.com/gabe565/gones/internal/config"
"github.com/gabe565/gones/internal/consts"
"github.com/gabe565/gones/internal/interrupt"
"github.com/gabe565/gones/internal/log"
"github.com/gabe565/gones/internal/memory"
Expand All @@ -22,12 +23,23 @@ type CPU interface {

func New(conf *config.Config, mapper cartridge.Mapper) *PPU {
rect := conf.UI.Overscan.Rect()
spriteLimit := uint8(8)
if conf.UI.RemoveSpriteLimit {
spriteLimit = consts.PPUOAMSize / 4
}
return &PPU{
offsets: rect.Min,
mapper: mapper,
image: image.NewRGBA(image.Rect(0, 0, rect.Dx(), rect.Dy())),
Cycles: 21,
systemPalette: &palette.Default,
SpriteData: SpriteData{
limit: spriteLimit,
Patterns: make([]uint32, spriteLimit),
Positions: make([]byte, spriteLimit),
Priorities: make([]byte, spriteLimit),
Indexes: make([]byte, spriteLimit),
},
}
}

Expand All @@ -45,8 +57,8 @@ type PPU struct {
FineX byte
VRAM [0x800]byte `msgpack:"alias:Vram"`

OAMAddr byte `msgpack:"alias:OamAddr"`
OAM [0x100]byte `msgpack:"alias:Oam"`
OAMAddr byte `msgpack:"alias:OamAddr"`
OAM [consts.PPUOAMSize]byte `msgpack:"alias:Oam"`
systemPalette *palette.Palette
Palette [0x20]byte

Expand Down
82 changes: 67 additions & 15 deletions internal/ppu/render_fg.go
Original file line number Diff line number Diff line change
@@ -1,46 +1,98 @@
package ppu

const MaxSprites = 8
import (
"github.com/gabe565/gones/internal/consts"
"github.com/vmihailenco/msgpack/v5"
)

type SpriteData struct {
Count uint8
Patterns [MaxSprites]uint32
Positions [MaxSprites]byte
Priorities [MaxSprites]byte
Indexes [MaxSprites]byte
limit uint8
Patterns []uint32
Positions []byte
Priorities []byte
Indexes []byte
}

var _ msgpack.CustomDecoder = &SpriteData{}

func (s *SpriteData) DecodeMsgpack(dec *msgpack.Decoder) error {
type tmpSpriteData SpriteData
if err := dec.Decode((*tmpSpriteData)(s)); err != nil {
return err
}
limit := int(s.limit)
if len(s.Patterns) > limit {
clear(s.Patterns[limit:])
s.Patterns = s.Patterns[:limit:limit]
} else if len(s.Patterns) < limit {
s.Patterns = append(s.Patterns, make([]uint32, limit-len(s.Patterns))...)
}
if len(s.Positions) > limit {
clear(s.Positions[limit:])
s.Positions = s.Positions[:limit:limit]
} else if len(s.Positions) < limit {
s.Positions = append(s.Positions, make([]byte, limit-len(s.Positions))...)
}
if len(s.Priorities) > limit {
clear(s.Priorities[limit:])
s.Priorities = s.Priorities[:limit:limit]
} else if len(s.Priorities) < limit {
s.Priorities = append(s.Priorities, make([]byte, limit-len(s.Priorities))...)
}
if len(s.Indexes) > limit {
clear(s.Indexes[limit:])
s.Indexes = s.Indexes[:limit:limit]
} else if len(s.Indexes) < limit {
s.Indexes = append(s.Indexes, make([]byte, limit-len(s.Indexes))...)
}
return nil
}

func (p *PPU) evaluateSprites() {
height := int(p.Ctrl.SpriteSize())
var count uint8

for i := range 64 {
sprite := p.OAM[i*4 : i*4+4 : i*4+4]
y := sprite[0]
a := sprite[2]
x := sprite[3]
var prevY, prevTile uint8
consecutive := uint8(1)
for i := range consts.PPUOAMSize / 4 {
i *= 4
y, tile, a, x := p.OAM[i], p.OAM[i+1], p.OAM[i+2], p.OAM[i+3]

row := p.Scanline - int(y)
if row < 0 || row >= height {
continue
}

if count < 8 {
p.SpriteData.Patterns[count] = p.fetchSpritePattern(sprite[1], a, row)
if count < p.SpriteData.limit {
p.SpriteData.Patterns[count] = p.fetchSpritePattern(tile, a, row)
p.SpriteData.Positions[count] = x
p.SpriteData.Priorities[count] = (a >> 5) & 1
p.SpriteData.Indexes[count] = byte(i)

if y == prevY && tile == prevTile {
consecutive++
}
prevY, prevTile = y, tile
}

count++
}

if count > 8 {
count = 8
if count > consts.PPUSpriteLimit {
p.Status.SpriteOverflow = true
}

p.SpriteData.Count = count
switch {
case consecutive >= consts.PPUSpriteLimit:
// Force original limit when masking effect is active
// See https://nesdev.org/wiki/Sprite_overflow_games#Detecting_masking_effects
p.SpriteData.Count = consts.PPUSpriteLimit
case count > p.SpriteData.limit:
p.SpriteData.Count = p.SpriteData.limit
default:
p.SpriteData.Count = count
}
}

func (p *PPU) fetchSpritePattern(tile, attributes byte, row int) uint32 {
Expand Down

0 comments on commit f075eae

Please sign in to comment.