aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore18
-rw-r--r--README.md54
-rw-r--r--cart/cart.go83
-rw-r--r--cart/mapper.go30
-rw-r--r--cart/nrom.go47
-rw-r--r--tests/cart_test.go115
-rw-r--r--tests/cpu_test.go (renamed from cpu/cpu_test.go)88
-rw-r--r--tests/nestest_test.go (renamed from cpu/nestest_test.go)78
-rw-r--r--tests/testdata/nestest.log (renamed from cpu/testdata/nestest.log)0
-rw-r--r--tests/testdata/nestest.nes (renamed from cpu/testdata/nestest.nes)bin24592 -> 24592 bytes
10 files changed, 419 insertions, 94 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9006382
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+*.sav
+
+*.nes
+!tests/testdata/nestest.nes
+
+/gones
+/cmd/gones/gones
+
+*.test
+*.out
+*.prof
+
+.idea/
+.vscode
+
+# fuck you emacs <3
+*~
+.#* \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9fa3119
--- /dev/null
+++ b/README.md
@@ -0,0 +1,54 @@
+nes emulator in go
+
+TODO:
+emulator base
+- [x] cpu passing nestest
+- [x] cart package (iNES parser, mapper 0)
+
+system bus (nes package)
+- [ ] Bus type: 2KB RAM mirrored through $1FFF, PPU regs $2000-$3FFF (mirrored every 8), APU/IO $4000-$4017, cart $4020+
+- [ ] OAM DMA at $4014 (stalls CPU 513/514 cycles)
+- [ ] reuse it in nestest_test.go instead of the test-only bus
+- [ ] CPU stall hook and proper IRQ/NMI line (polled between instructions)
+
+ppu, background
+- [ ] registers $2000-$2007 and v/t/x/w latch logic
+- [ ] nametables with mirroring, pattern tables, palettes, buffered $2007 reads
+- [ ] 341 dots x 262 lines timing, 3 dots per CPU cycle
+- [ ] vblank flag and NMI
+- [ ] 256x240 framebuffer and RGB palette
+
+frontend and input
+- [ ] cmd/gones with Ebitengine, one frame per 1/60s
+- [ ] controller $4016/$4017 strobe and shift register, keyboard mapping
+
+ppu, sprites
+- [ ] OAM, per-scanline evaluation, 8-sprite limit, 8x8 and 8x16, flip, priority
+- [ ] sprite-0 hit
+
+more mappers
+- [ ] MMC1 (1), UxROM (2), CNROM (3), AxROM (7)
+- [ ] MMC3 (4) with scanline IRQ
+- [ ] battery-backed save files
+
+apu
+- [ ] pulse x2, triangle, noise, DMC, frame counter, mixer
+- [ ] resampling and audio output
+
+accuracy
+- [ ] missing unofficial NOPs
+- [ ] run blargg's CPU, PPU, APU and MMC3 test ROMs
+- [ ] decide whether to go cycle-stepped
+
+polish
+- [ ] save states, pause/reset, fullscreen, gamepad
+- [ ] headless mode with framebuffer-hash regression tests
+
+
+testing:
+run "go test ./..." to run all test, "go test ./... -v" for verbose
+
+disclaimer:
+not affiliated with or endorsed by nintendo. no commercial roms are included, bring your own. (i dont want to get shot by nintendo :D)
+tests/testdata/nestest.nes and nestest.log are by kevin horton (kevtris), included for testing only.
+provided as is, without warranty.
diff --git a/cart/cart.go b/cart/cart.go
new file mode 100644
index 0000000..2438c1b
--- /dev/null
+++ b/cart/cart.go
@@ -0,0 +1,83 @@
+package cart
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "os"
+)
+
+type Cartridge struct {
+ PRG []uint8
+ CHR []uint8
+ CHRIsRAM bool
+ MapperID uint8
+ Mirroring Mirroring
+ Battery bool
+
+ Mapper Mapper
+}
+
+func Load(path string) (*Cartridge, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ return Parse(data)
+}
+
+func Parse(data []byte) (*Cartridge, error) {
+ if len(data) < 16 || !bytes.Equal(data[:4], []byte("NES\x1A")) {
+ return nil, errors.New("cart: not an iNES file")
+ }
+
+ prgSize := int(data[4]) * 16 * 1024
+ chrSize := int(data[5]) * 8 * 1024
+ flags6, flags7 := data[6], data[7]
+
+ isNES2 := flags7&0x0C == 0x08
+ mapperID := flags6 >> 4
+ if isNES2 || bytes.Equal(data[12:16], []byte{0, 0, 0, 0}) {
+ mapperID |= flags7 & 0xF0
+ }
+
+ c := &Cartridge{
+ MapperID: mapperID,
+ Battery: flags6&0x02 != 0,
+ }
+
+ switch {
+ case flags6&0x08 != 0:
+ c.Mirroring = FourScreen
+ case flags6&0x01 != 0:
+ c.Mirroring = Vertical
+ default:
+ c.Mirroring = Horizontal
+ }
+
+ offset := 16
+ if flags6&0x04 != 0 {
+ offset += 512
+ }
+ if len(data) < offset+prgSize+chrSize {
+ return nil, fmt.Errorf("cart: file truncated: need %d bytes, have %d",
+ offset+prgSize+chrSize, len(data))
+ }
+
+ c.PRG = data[offset : offset+prgSize]
+ offset += prgSize
+
+ if chrSize == 0 {
+ c.CHR = make([]uint8, 8*1024)
+ c.CHRIsRAM = true
+ } else {
+ c.CHR = data[offset : offset+chrSize]
+ }
+
+ m, err := newMapper(c)
+ if err != nil {
+ return nil, err
+ }
+ c.Mapper = m
+ return c, nil
+}
diff --git a/cart/mapper.go b/cart/mapper.go
new file mode 100644
index 0000000..7ddd8a9
--- /dev/null
+++ b/cart/mapper.go
@@ -0,0 +1,30 @@
+package cart
+
+import "fmt"
+
+type Mirroring uint8
+
+const (
+ Horizontal Mirroring = iota
+ Vertical
+ FourScreen
+ SingleScreenLower
+ SingleScreenUpper
+)
+
+type Mapper interface {
+ CPURead(addr uint16) uint8
+ CPUWrite(addr uint16, v uint8)
+ PPURead(addr uint16) uint8
+ PPUWrite(addr uint16, v uint8)
+ Mirroring() Mirroring
+}
+
+func newMapper(c *Cartridge) (Mapper, error) {
+ switch c.MapperID {
+ case 0:
+ return newNROM(c), nil
+ default:
+ return nil, fmt.Errorf("cart: unsupported mapper %d", c.MapperID)
+ }
+}
diff --git a/cart/nrom.go b/cart/nrom.go
new file mode 100644
index 0000000..3828c16
--- /dev/null
+++ b/cart/nrom.go
@@ -0,0 +1,47 @@
+package cart
+
+type nrom struct {
+ prg []uint8
+ chr []uint8
+ chrIsRAM bool
+ prgRAM [8 * 1024]uint8
+ mirroring Mirroring
+}
+
+func newNROM(c *Cartridge) *nrom {
+ return &nrom{
+ prg: c.PRG,
+ chr: c.CHR,
+ chrIsRAM: c.CHRIsRAM,
+ mirroring: c.Mirroring,
+ }
+}
+
+func (m *nrom) CPURead(addr uint16) uint8 {
+ switch {
+ case addr >= 0x8000:
+ return m.prg[int(addr-0x8000)%len(m.prg)]
+ case addr >= 0x6000:
+ return m.prgRAM[addr-0x6000]
+ default:
+ return 0
+ }
+}
+
+func (m *nrom) CPUWrite(addr uint16, v uint8) {
+ if addr >= 0x6000 && addr < 0x8000 {
+ m.prgRAM[addr-0x6000] = v
+ }
+}
+
+func (m *nrom) PPURead(addr uint16) uint8 {
+ return m.chr[int(addr)%len(m.chr)]
+}
+
+func (m *nrom) PPUWrite(addr uint16, v uint8) {
+ if m.chrIsRAM {
+ m.chr[int(addr)%len(m.chr)] = v
+ }
+}
+
+func (m *nrom) Mirroring() Mirroring { return m.mirroring }
diff --git a/tests/cart_test.go b/tests/cart_test.go
new file mode 100644
index 0000000..5b41007
--- /dev/null
+++ b/tests/cart_test.go
@@ -0,0 +1,115 @@
+package tests
+
+import (
+ "crecher.net/gones/v2/cart"
+ "os"
+ "testing"
+)
+
+const nestestPath = "testdata/nestest.nes"
+
+func header(prgBanks, chrBanks, flags6, flags7 byte) []byte {
+ h := make([]byte, 16, 16+int(prgBanks)*16*1024+int(chrBanks)*8*1024)
+ copy(h, "NES\x1A")
+ h[4], h[5], h[6], h[7] = prgBanks, chrBanks, flags6, flags7
+ h = append(h, make([]byte, int(prgBanks)*16*1024)...)
+ h = append(h, make([]byte, int(chrBanks)*8*1024)...)
+ return h
+}
+
+func TestNestestHeader(t *testing.T) {
+ raw, err := os.ReadFile(nestestPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Logf("header: % x", raw[:16])
+
+ c, err := cart.Parse(raw)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if want := (raw[6] >> 4) | (raw[7] & 0xF0); c.MapperID != want {
+ t.Errorf("MapperID = %d, want %d", c.MapperID, want)
+ }
+ if c.MapperID != 0 {
+ t.Errorf("nestest should be mapper 0, got %d", c.MapperID)
+ }
+ if want := int(raw[4]) * 16 * 1024; len(c.PRG) != want {
+ t.Errorf("len(PRG) = %d, want %d", len(c.PRG), want)
+ }
+ if want := int(raw[5]) * 8 * 1024; len(c.CHR) != want {
+ t.Errorf("len(CHR) = %d, want %d", len(c.CHR), want)
+ }
+ wantMirror := cart.Horizontal
+ if raw[6]&0x01 != 0 {
+ wantMirror = cart.Vertical
+ }
+ if c.Mirroring != wantMirror {
+ t.Errorf("Mirroring = %v, want %v", c.Mirroring, wantMirror)
+ }
+}
+
+func TestLoadNestestResetVector(t *testing.T) {
+ c, err := cart.Load(nestestPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ lo := uint16(c.Mapper.CPURead(0xFFFC))
+ hi := uint16(c.Mapper.CPURead(0xFFFD))
+ if got := hi<<8 | lo; got != 0xC004 {
+ t.Errorf("reset vector = %#04x, want 0xC004", got)
+ }
+}
+
+func TestParseErrors(t *testing.T) {
+ tests := []struct {
+ name string
+ data []byte
+ }{
+ {"empty", nil},
+ {"shorter than header", []byte("NES\x1A")},
+ {"bad magic", append([]byte("XXXX"), make([]byte, 12)...)},
+ {"truncated PRG", header(2, 1, 0, 0)[:16+1024]},
+ {"unsupported mapper 1", header(1, 1, 0x10, 0)},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if _, err := cart.Parse(tt.data); err == nil {
+ t.Error("expected error, got nil")
+ }
+ })
+ }
+}
+
+func TestParseCHRRAM(t *testing.T) {
+ c, err := cart.Parse(header(1, 0, 0, 0))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !c.CHRIsRAM || len(c.CHR) != 8*1024 {
+ t.Errorf("CHRIsRAM = %v, len(CHR) = %d; want true, 8192", c.CHRIsRAM, len(c.CHR))
+ }
+ c.Mapper.PPUWrite(0x0010, 0xAB)
+ if got := c.Mapper.PPURead(0x0010); got != 0xAB {
+ t.Errorf("CHR-RAM readback = %#02x, want 0xAB", got)
+ }
+}
+
+func TestParseFlags(t *testing.T) {
+ c, err := cart.Parse(header(1, 1, 0x03, 0))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if c.Mirroring != cart.Vertical || !c.Battery {
+ t.Errorf("Mirroring = %v, Battery = %v; want cart.Vertical, true", c.Mirroring, c.Battery)
+ }
+
+ c, err = cart.Parse(header(1, 1, 0x09, 0))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if c.Mirroring != cart.FourScreen {
+ t.Errorf("Mirroring = %v, want cart.FourScreen", c.Mirroring)
+ }
+}
diff --git a/cpu/cpu_test.go b/tests/cpu_test.go
index e4a32a6..87743b1 100644
--- a/cpu/cpu_test.go
+++ b/tests/cpu_test.go
@@ -1,6 +1,10 @@
-package cpu
+package tests
-import "testing"
+import (
+ "testing"
+
+ "crecher.net/gones/v2/cpu"
+)
type ramBus struct {
mem [65536]uint8
@@ -9,12 +13,14 @@ type ramBus struct {
func (b *ramBus) Read(addr uint16) uint8 { return b.mem[addr] }
func (b *ramBus) Write(addr uint16, v uint8) { b.mem[addr] = v }
-func newTestCPU(prog []uint8) (*CPU, *ramBus) {
+func has(c *cpu.CPU, flag uint8) bool { return c.Status&flag != 0 }
+
+func newTestCPU(prog []uint8) (*cpu.CPU, *ramBus) {
bus := &ramBus{}
copy(bus.mem[0x8000:], prog)
bus.mem[0xFFFC] = 0x00
bus.mem[0xFFFD] = 0x80
- c := NewCPU(bus)
+ c := cpu.NewCPU(bus)
c.Reset()
return c, bus
}
@@ -38,7 +44,7 @@ func TestLDAImmediate(t *testing.T) {
if cycles != 2 {
t.Fatalf("cycles = %d, want 2", cycles)
}
- if c.flag(FlagZ) || c.flag(FlagN) {
+ if has(c, cpu.FlagZ) || has(c, cpu.FlagN) {
t.Fatalf("unexpected flags: 0x%02X", c.Status)
}
}
@@ -46,19 +52,19 @@ func TestLDAImmediate(t *testing.T) {
func TestLDAZeroAndNegativeFlags(t *testing.T) {
c, _ := newTestCPU([]uint8{0xA9, 0x00})
c.Step()
- if !c.flag(FlagZ) {
+ if !has(c, cpu.FlagZ) {
t.Fatalf("Z flag should be set for A=0")
}
c, _ = newTestCPU([]uint8{0xA9, 0x80})
c.Step()
- if !c.flag(FlagN) {
+ if !has(c, cpu.FlagN) {
t.Fatalf("N flag should be set for A=0x80")
}
}
func TestSTAAbsolute(t *testing.T) {
- c, bus := newTestCPU([]uint8{0xA9, 0x55, 0x8D, 0x00, 0x02}) // lda #$55 ; sta $0200
+ c, bus := newTestCPU([]uint8{0xA9, 0x55, 0x8D, 0x00, 0x02})
c.Step()
c.Step()
if bus.mem[0x0200] != 0x55 {
@@ -67,10 +73,10 @@ func TestSTAAbsolute(t *testing.T) {
}
func TestAbsoluteXPageCross(t *testing.T) {
- // lda $80ff,X with X=1 crosses into page $8100 -> extra cycle
+ // 0x80FF + X(1): page cross, 5 cycles
c, bus := newTestCPU([]uint8{0xA2, 0x01, 0xBD, 0xFF, 0x80})
bus.mem[0x8100] = 0x99
- c.Step() // ldx #1
+ c.Step()
cycles := c.Step()
if c.A != 0x99 {
t.Fatalf("A = 0x%02X, want 0x99", c.A)
@@ -81,55 +87,55 @@ func TestAbsoluteXPageCross(t *testing.T) {
}
func TestAdcCarryAndOverflow(t *testing.T) {
- // 0x50 + 0x50 = 0xa0, signed overflow (positive+positive=negative), no carry
+ // 0x50 + 0x50 = 0xA0, V=1, C=0
c, _ := newTestCPU([]uint8{0xA9, 0x50, 0x69, 0x50})
c.Step()
c.Step()
if c.A != 0xA0 {
t.Fatalf("A = 0x%02X, want 0xA0", c.A)
}
- if !c.flag(FlagV) {
+ if !has(c, cpu.FlagV) {
t.Fatalf("V flag should be set")
}
- if c.flag(FlagC) {
+ if has(c, cpu.FlagC) {
t.Fatalf("C flag should be clear")
}
- // 0xff + 0x01 = 0x00, carry out, no overflow
+ // 0xFF + 0x01 = 0x00, C=1, V=0
c, _ = newTestCPU([]uint8{0xA9, 0xFF, 0x69, 0x01})
c.Step()
c.Step()
if c.A != 0x00 {
t.Fatalf("A = 0x%02X, want 0x00", c.A)
}
- if !c.flag(FlagC) {
+ if !has(c, cpu.FlagC) {
t.Fatalf("C flag should be set")
}
- if !c.flag(FlagZ) {
+ if !has(c, cpu.FlagZ) {
t.Fatalf("Z flag should be set")
}
- if c.flag(FlagV) {
+ if has(c, cpu.FlagV) {
t.Fatalf("V flag should be clear")
}
}
func TestSbcBorrow(t *testing.T) {
- // 0x10 - 0x01 with carry preset (no borrow): result 0x0f, carry stays set
- c, _ := newTestCPU([]uint8{0x38, 0xA9, 0x10, 0xE9, 0x01}) // sec ; lda #$10 ; sbc #$01
+ // 0x10 - 0x01 = 0x0F, C=1
+ c, _ := newTestCPU([]uint8{0x38, 0xA9, 0x10, 0xE9, 0x01})
c.Step()
c.Step()
c.Step()
if c.A != 0x0F {
t.Fatalf("A = 0x%02X, want 0x0F", c.A)
}
- if !c.flag(FlagC) {
+ if !has(c, cpu.FlagC) {
t.Fatalf("C flag should be set (no borrow)")
}
}
func TestBranchTakenAndPageCross(t *testing.T) {
- // beq with Z set, forward branch within the same page: +1 cycle
- c, _ := newTestCPU([]uint8{0xA9, 0x00, 0xF0, 0x02, 0xEA, 0xEA, 0xEA}) // lda #0 ; beq +2 ; nop ; nop ; nop
+ // beq taken, same page: 3 cycles
+ c, _ := newTestCPU([]uint8{0xA9, 0x00, 0xF0, 0x02, 0xEA, 0xEA, 0xEA})
c.Step()
cycles := c.Step()
if cycles != 3 {
@@ -141,21 +147,20 @@ func TestBranchTakenAndPageCross(t *testing.T) {
}
func TestJsrRts(t *testing.T) {
- // jsr $8004 ; (pad) ; rts at $8004, followed by nop at $8003 after return
prog := []uint8{0x20, 0x04, 0x80, 0xEA, 0x60}
c, _ := newTestCPU(prog)
- c.Step() // jsr
+ c.Step()
if c.PC != 0x8004 {
t.Fatalf("PC after JSR = 0x%04X, want 0x8004", c.PC)
}
- c.Step() // rts
+ c.Step()
if c.PC != 0x8003 {
t.Fatalf("PC after RTS = 0x%04X, want 0x8003", c.PC)
}
}
func TestStackPushPullRoundTrip(t *testing.T) {
- c, _ := newTestCPU([]uint8{0xA9, 0x7E, 0x48, 0xA9, 0x00, 0x68}) // lda #$7e ; pha ; lda #0 ; pla
+ c, _ := newTestCPU([]uint8{0xA9, 0x7E, 0x48, 0xA9, 0x00, 0x68})
c.Step()
c.Step()
c.Step()
@@ -166,25 +171,25 @@ func TestStackPushPullRoundTrip(t *testing.T) {
}
func TestPhpPlpPreservesFlags(t *testing.T) {
- c, _ := newTestCPU([]uint8{0x38, 0x08, 0x18, 0x28}) // sec ; php ; clc ; plp
- c.Step() // sec
- c.Step() // php
- c.Step() // clc
- if c.flag(FlagC) {
+ c, _ := newTestCPU([]uint8{0x38, 0x08, 0x18, 0x28})
+ c.Step()
+ c.Step()
+ c.Step()
+ if has(c, cpu.FlagC) {
t.Fatalf("C flag should be clear after CLC")
}
- c.Step() // plp
- if !c.flag(FlagC) {
+ c.Step()
+ if !has(c, cpu.FlagC) {
t.Fatalf("C flag should be restored by PLP")
}
}
func TestIndirectJmpPageBug(t *testing.T) {
- // jmp ($30ff) must read the high byte from $3000, not $3100
+ // jmp ($30FF) reads high byte from $3000
c, bus := newTestCPU([]uint8{0x6C, 0xFF, 0x30})
bus.mem[0x30FF] = 0x34
- bus.mem[0x3000] = 0x12 // wrap target for the bug
- bus.mem[0x3100] = 0x99 // must not be used
+ bus.mem[0x3000] = 0x12
+ bus.mem[0x3100] = 0x99
c.Step()
if c.PC != 0x1234 {
t.Fatalf("PC = 0x%04X, want 0x1234", c.PC)
@@ -204,18 +209,19 @@ func TestNmiPushesStateAndJumps(t *testing.T) {
if c.SP != spBefore-3 {
t.Fatalf("SP after NMI = 0x%02X, want 0x%02X", c.SP, spBefore-3)
}
- c.pop() // discard pushed status
- if pc := c.pop16(); pc != pcBefore {
+ stack := func(sp uint8) uint16 { return uint16(bus.mem[0x0100+uint16(sp)]) }
+ pc := stack(spBefore)<<8 | stack(spBefore-1)
+ if pc != pcBefore {
t.Fatalf("pushed PC = 0x%04X, want 0x%04X", pc, pcBefore)
}
}
func TestCompareFlags(t *testing.T) {
- // cmp #$10 with A=$10 -> Z=1, C=1, N=0
+ // Z=1, C=1, N=0
c, _ := newTestCPU([]uint8{0xA9, 0x10, 0xC9, 0x10})
c.Step()
c.Step()
- if !c.flag(FlagZ) || !c.flag(FlagC) || c.flag(FlagN) {
+ if !has(c, cpu.FlagZ) || !has(c, cpu.FlagC) || has(c, cpu.FlagN) {
t.Fatalf("flags = 0x%02X, want Z and C set, N clear", c.Status)
}
}
@@ -226,6 +232,6 @@ func TestUnimplementedOpcodePanics(t *testing.T) {
t.Fatalf("expected panic for illegal opcode")
}
}()
- c, _ := newTestCPU([]uint8{0x02}) // illegal/unofficial opcode
+ c, _ := newTestCPU([]uint8{0x02})
c.Step()
}
diff --git a/cpu/nestest_test.go b/tests/nestest_test.go
index 4786c5b..de98156 100644
--- a/cpu/nestest_test.go
+++ b/tests/nestest_test.go
@@ -1,4 +1,4 @@
-package cpu
+package tests
import (
"bufio"
@@ -6,45 +6,36 @@ import (
"regexp"
"strconv"
"testing"
+
+ "crecher.net/gones/v2/cart"
+ "crecher.net/gones/v2/cpu"
)
type nestestBus struct {
- ram [0x0800]uint8
- prg []uint8
+ ram [0x0800]uint8
+ cart cart.Mapper
}
func (b *nestestBus) Read(addr uint16) uint8 {
switch {
case addr < 0x2000:
return b.ram[addr%0x0800]
- case addr >= 0x8000:
- return b.prg[int(addr-0x8000)%len(b.prg)]
+ case addr >= 0x4020:
+ return b.cart.CPURead(addr)
default:
return 0
}
}
func (b *nestestBus) Write(addr uint16, v uint8) {
- if addr < 0x2000 {
+ switch {
+ case addr < 0x2000:
b.ram[addr%0x0800] = v
+ case addr >= 0x4020:
+ b.cart.CPUWrite(addr, v)
}
}
-func loadNestestPRG(path string) ([]uint8, error) {
- data, err := os.ReadFile(path)
- if err != nil {
- return nil, err
- }
- prgBanks := int(data[4])
- prgSize := prgBanks * 16 * 1024
- hasTrainer := data[6]&0x04 != 0
- offset := 16
- if hasTrainer {
- offset += 512
- }
- return data[offset : offset+prgSize], nil
-}
-
type cpuState struct {
pc uint16
a, x, y, p, sp uint8
@@ -61,6 +52,11 @@ func loadNestestLog(path string) ([]cpuState, error) {
}
defer f.Close()
+ parseHex8 := func(s string) uint8 {
+ v, _ := strconv.ParseUint(s, 16, 8)
+ return uint8(v)
+ }
+
var lines []cpuState
scanner := bufio.NewScanner(f)
for scanner.Scan() {
@@ -68,10 +64,6 @@ func loadNestestLog(path string) ([]cpuState, error) {
if m == nil {
continue
}
- parseHex8 := func(s string) uint8 {
- v, _ := strconv.ParseUint(s, 16, 8)
- return uint8(v)
- }
pc, _ := strconv.ParseUint(m[1], 16, 16)
cyc, _ := strconv.ParseUint(m[7], 10, 64)
lines = append(lines, cpuState{
@@ -87,16 +79,11 @@ func loadNestestLog(path string) ([]cpuState, error) {
return lines, scanner.Err()
}
-// TestNestest runs nestest.nes and diffs the state against a known good log before each instruction
func TestNestest(t *testing.T) {
const romPath = "testdata/nestest.nes"
const logPath = "testdata/nestest.log"
- if _, err := os.Stat(romPath); err != nil {
- t.Skip("testdata/nestest.nes not present; add it and testdata/nestest.log to run this test")
- }
-
- prg, err := loadNestestPRG(romPath)
+ c, err := cart.Load(romPath)
if err != nil {
t.Fatalf("loading ROM: %v", err)
}
@@ -108,32 +95,17 @@ func TestNestest(t *testing.T) {
t.Fatal("no log lines parsed")
}
- bus := &nestestBus{prg: prg}
- c := NewCPU(bus)
- c.PC = 0xC000
- c.SP = 0xFD
- c.Status = 0x24
- c.Cycles = want[0].cyc
+ core := cpu.NewCPU(&nestestBus{cart: c.Mapper})
+ core.PC = 0xC000
+ core.SP = 0xFD
+ core.Status = 0x24
+ core.Cycles = want[0].cyc
for i, w := range want {
- got := cpuState{c.PC, c.A, c.X, c.Y, c.Status, c.SP, c.Cycles}
+ got := cpuState{core.PC, core.A, core.X, core.Y, core.Status, core.SP, core.Cycles}
if got != w {
t.Fatalf("line %d: got %+v, want %+v", i+1, got, w)
}
-
- stopped := func() (panicked bool) {
- defer func() {
- if r := recover(); r != nil {
- t.Logf("stopped at line %d: %v", i+1, r)
- panicked = true
- }
- }()
- c.Step()
- return false
- }()
- if stopped {
- t.Logf("matched %d instructions before stopping", i+1)
- return
- }
+ core.Step()
}
}
diff --git a/cpu/testdata/nestest.log b/tests/testdata/nestest.log
index fead022..fead022 100644
--- a/cpu/testdata/nestest.log
+++ b/tests/testdata/nestest.log
diff --git a/cpu/testdata/nestest.nes b/tests/testdata/nestest.nes
index fc2a88c..fc2a88c 100644
--- a/cpu/testdata/nestest.nes
+++ b/tests/testdata/nestest.nes
Binary files differ