Skip to content

Commit

Permalink
Implement correct sprite priority resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
Humpheh committed Jun 1, 2019
1 parent fa333d4 commit 98ad423
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 36 deletions.
8 changes: 4 additions & 4 deletions pkg/gb/gameboy.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@ type Gameboy struct {

// Matrix of pixel data which is used while the screen is rendering. When a
// frame has been completed, this data is copied into the PreparedData matrix.
screenData [160][144][3]uint8
bgPriority [160][144]bool
screenData [ScreenWidth][ScreenHeight][3]uint8
bgPriority [ScreenWidth][ScreenHeight]bool

// Track colour of tiles in scanline for priority management.
tileScanline [160]uint8
tileScanline [ScreenWidth]uint8
scanlineCounter int
screenCleared bool

// PreparedData is a matrix of screen pixel data for a single frame which has
// been fully rendered.
PreparedData [160][144][3]uint8
PreparedData [ScreenWidth][ScreenHeight][3]uint8

interruptsEnabling bool
interruptsOn bool
Expand Down
78 changes: 54 additions & 24 deletions pkg/gb/ppu.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import (
)

const (
// ScreenWidth is the number of pixels width on the GameBoy LCD panel.
ScreenWidth = 160

// ScreenHeight is the number of pixels height on the GameBoy LCD panel.
ScreenHeight = 144

// LCDC is the main LCD Control register.
LCDC = 0xFF40
)
Expand All @@ -22,15 +28,15 @@ func (gb *Gameboy) updateGraphics(cycles int) {
gb.Memory.HighRAM[0x44]++
if gb.Memory.HighRAM[0x44] > 153 {
gb.PreparedData = gb.screenData
gb.screenData = [160][144][3]uint8{}
gb.bgPriority = [160][144]bool{}
gb.screenData = [ScreenWidth][ScreenHeight][3]uint8{}
gb.bgPriority = [ScreenWidth][ScreenHeight]bool{}
gb.Memory.HighRAM[0x44] = 0
}

currentLine := gb.Memory.ReadHighRam(0xFF44)
gb.scanlineCounter += 456 * gb.getSpeed()

if currentLine == 144 {
if currentLine == ScreenHeight {
gb.requestInterrupt(0)
}
}
Expand Down Expand Up @@ -280,6 +286,8 @@ func (gb *Gameboy) getColour(colourNum byte, palette byte) (uint8, uint8, uint8)
return GetPaletteColour(col)
}

const spritePriorityOffset = 100

// Render the sprites to the screen on the current scanline using the lcdControl register.
func (gb *Gameboy) renderSprites(lcdControl byte, scanline int32) {
var ySize int32 = 8
Expand All @@ -291,10 +299,24 @@ func (gb *Gameboy) renderSprites(lcdControl byte, scanline int32) {
var palette1 = gb.Memory.ReadHighRam(0xFF48)
var palette2 = gb.Memory.ReadHighRam(0xFF49)

for sprite := 0; sprite < 40; sprite++ {
var minx [ScreenWidth]int32
var lineSprites = 0
for sprite := uint16(0); sprite < 40; sprite++ {
// Load sprite data from memory.
index := sprite * 4

// If this is true the scanline is out of the area we care about
yPos := int32(gb.Memory.Read(uint16(0xFE00+index))) - 16
if scanline < yPos || scanline >= (yPos+ySize) {
continue
}

// Only 10 sprites are allowed to be displayed on each line
if lineSprites >= 10 {
break
}
lineSprites++

xPos := int32(gb.Memory.Read(uint16(0xFE00+index+1))) - 8
tileLocation := gb.Memory.Read(uint16(0xFE00 + index + 2))
attributes := gb.Memory.Read(uint16(0xFE00 + index + 3))
Expand All @@ -309,11 +331,6 @@ func (gb *Gameboy) renderSprites(lcdControl byte, scanline int32) {
bank = 1
}

// If this is true the scanline is out of the area we care about
if scanline < yPos || scanline >= (yPos+ySize) {
continue
}

// Set the line to draw based on if the sprite is flipped on the y
line := scanline - yPos
if yFlip {
Expand All @@ -327,6 +344,20 @@ func (gb *Gameboy) renderSprites(lcdControl byte, scanline int32) {

// Draw the line of the sprite
for tilePixel := byte(0); tilePixel < 8; tilePixel++ {
pixel := int16(xPos) + int16(7-tilePixel)
if pixel < 0 || pixel >= ScreenWidth {
continue
}

// Check if the pixel has priority.
// - In DMG this is determined by the sprite with the smallest X coordinate,
// then the first sprite in the OAM.
// - In CGB this is determined by the first sprite appearing in the OAM.
// We add a fixed 100 to the xPos so we can use the 0 value as the absence of a sprite.
if minx[pixel] != 0 && (gb.IsCGB() || minx[pixel] <= xPos+spritePriorityOffset) {
continue
}

colourBit := tilePixel
if xFlip {
colourBit = byte(int8(colourBit-7) * -1)
Expand All @@ -339,24 +370,23 @@ func (gb *Gameboy) renderSprites(lcdControl byte, scanline int32) {
if colourNum == 0 {
continue
}
pixel := int16(xPos) + int16(7-tilePixel)

// Set the pixel if it is in bounds
if pixel >= 0 && pixel < 160 {
if gb.IsCGB() {
cgbPalette := attributes & 0x7
red, green, blue := gb.SpritePalette.get(cgbPalette, colourNum)
gb.setPixel(byte(pixel), byte(scanline), red, green, blue, priority)
} else {
// Determine the colour palette to use
var palette = palette1
if bits.Test(attributes, 4) {
palette = palette2
}
red, green, blue := gb.getColour(colourNum, palette)
gb.setPixel(byte(pixel), byte(scanline), red, green, blue, priority)
if gb.IsCGB() {
cgbPalette := attributes & 0x7
red, green, blue := gb.SpritePalette.get(cgbPalette, colourNum)
gb.setPixel(byte(pixel), byte(scanline), red, green, blue, priority)
} else {
// Determine the colour palette to use
var palette = palette1
if bits.Test(attributes, 4) {
palette = palette2
}
red, green, blue := gb.getColour(colourNum, palette)
gb.setPixel(byte(pixel), byte(scanline), red, green, blue, priority)
}

// Store the xpos of the sprite for this pixel for priority resolution
minx[pixel] = xPos + spritePriorityOffset
}
}
}
Expand Down
72 changes: 72 additions & 0 deletions pkg/gb/ppu_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package gb

import (
"fmt"
"image"
"image/color"
"image/png"
"os"
"testing"

"github.com/stretchr/testify/require"
)

// TestSpritePriority runs the mooneye sprite_priority.gb test rom and asserts that the
// output frame matches the expected image.
func TestSpritePriority(t *testing.T) {
// Takes about 10 frames to render the sprite priority image
const maxPPUIterations = 10

// Override the palette with the colours in the expected image
Palettes[CurrentPalette] = [][]byte{
{3, 3, 3},
{2, 2, 3},
{1, 1, 1}, // not used in expected image
{0, 0, 0},
}

// Map of colours in the image to color in the palette
var imageMap = map[color.Color]byte{
color.Gray{Y: 255}: 3,
color.Gray{Y: 111}: 2,
color.Gray{Y: 0}: 0,
}

// Load the test ROM and iterate a few frames to load the image
gb, err := NewGameboy("./../../roms/mooneye/runnable/sprite_priority.gb")
require.NoError(t, err, "error in init gb %v", err)
for i := 0; i < maxPPUIterations; i++ {
gb.Update()
}

// Load the expected output image
img, err := loadImage("../../roms/mooneye/runnable/sprite_priority-expected.png")
if err != nil {
t.Fatalf("Could not open expected image: %v", err)
}

// Iterate over the image and assert each pixel matches the expected image
for x := 0; x < ScreenWidth; x++ {
for y := 0; y < ScreenHeight; y++ {
actual := gb.PreparedData[x][y]
expected, ok := imageMap[img.At(x, y)]
require.True(t, ok, "unexpected colour in expected image: %v", img.At(x, y))
require.Equal(t, expected, actual[0], "incorrect pixel at X:%v Y:%x", x, y)
}
}
}

// Load a PNG image
func loadImage(filename string) (image.Image, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("opening image: %v", err)
}
defer file.Close()

img, err := png.Decode(file)
if err != nil {
return nil, fmt.Errorf("decoding image: %v", err)
}
return img, nil
}
16 changes: 8 additions & 8 deletions pkg/gbio/iopixel/pixels.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (mon *PixelsIOBinding) Init(disableVsync bool) {
Title: "GoBoy",
Bounds: pixel.R(
0, 0,
float64(160*PixelScale), float64(144*PixelScale),
float64(gb.ScreenWidth*PixelScale), float64(gb.ScreenHeight*PixelScale),
),
VSync: !disableVsync,
Resizable: true,
Expand All @@ -52,9 +52,9 @@ func (mon *PixelsIOBinding) Init(disableVsync bool) {
mon.UpdateCamera()

mon.picture = &pixel.PictureData{
Pix: make([]color.RGBA, 144*160),
Stride: 160,
Rect: pixel.R(0, 0, 160, 144),
Pix: make([]color.RGBA, gb.ScreenWidth*gb.ScreenHeight),
Stride: gb.ScreenWidth,
Rect: pixel.R(0, 0, gb.ScreenWidth, gb.ScreenHeight),
}
}

Expand All @@ -77,19 +77,19 @@ func (mon *PixelsIOBinding) IsRunning() bool {

// RenderScreen renders the pixels on the screen.
func (mon *PixelsIOBinding) RenderScreen() {
for y := 0; y < 144; y++ {
for x := 0; x < 160; x++ {
for y := 0; y < gb.ScreenHeight; y++ {
for x := 0; x < gb.ScreenWidth; x++ {
col := mon.Gameboy.PreparedData[x][y]
rgb := color.RGBA{R: col[0], G: col[1], B: col[2], A: 0xFF}
mon.picture.Pix[(143-y)*160+x] = rgb
mon.picture.Pix[(gb.ScreenHeight-1-y)*gb.ScreenWidth+x] = rgb
}
}

r, g, b := gb.GetPaletteColour(3)
bg := color.RGBA{R: r, G: g, B: b, A: 0xFF}
mon.Window.Clear(bg)

spr := pixel.NewSprite(pixel.Picture(mon.picture), pixel.R(0, 0, 160, 144))
spr := pixel.NewSprite(pixel.Picture(mon.picture), pixel.R(0, 0, gb.ScreenWidth, gb.ScreenHeight))
spr.Draw(mon.Window, pixel.IM)

mon.UpdateCamera()
Expand Down
Binary file added roms/mooneye/runnable/sprite_priority-expected.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added roms/mooneye/runnable/sprite_priority.gb
Binary file not shown.

0 comments on commit 98ad423

Please sign in to comment.