1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"time"
"github.com/veandco/go-sdl2/sdl"
"crecher.net/gones/v2/cart"
"crecher.net/gones/v2/nes"
"crecher.net/gones/v2/ppu"
)
const (
windowScale = 3
// nes: 1789773 cpu cycles per sec / 29780.5 cycles per frame
framesPerSecond = 60.0988
)
func init() {
// sdl wants to be driven from the main thread
runtime.LockOSThread()
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "usage: %s <rom.nes>\n", os.Args[0])
os.Exit(2)
}
if err := run(os.Args[1]); err != nil {
log.Fatal(err)
}
}
func run(romPath string) error {
c, err := cart.Load(romPath)
if err != nil {
return err
}
console := nes.New(c)
name := filepath.Base(romPath)
if err := sdl.Init(sdl.INIT_VIDEO); err != nil {
return err
}
defer sdl.Quit()
window, err := sdl.CreateWindow("gones - "+name,
sdl.WINDOWPOS_CENTERED, sdl.WINDOWPOS_CENTERED,
ppu.ScreenWidth*windowScale, ppu.ScreenHeight*windowScale,
sdl.WINDOW_SHOWN|sdl.WINDOW_RESIZABLE)
if err != nil {
return err
}
defer window.Destroy()
renderer, err := sdl.CreateRenderer(window, -1, sdl.RENDERER_ACCELERATED|sdl.RENDERER_PRESENTVSYNC)
if err != nil {
return err
}
defer renderer.Destroy()
// the window can be resized freely, sdl scales (and letterboxes) the 256x240 picture
if err := renderer.SetLogicalSize(ppu.ScreenWidth, ppu.ScreenHeight); err != nil {
return err
}
// framebuffer is ARGB8888, streaming since we rewrite it every frame
texture, err := renderer.CreateTexture(sdl.PIXELFORMAT_ARGB8888, sdl.TEXTUREACCESS_STREAMING,
ppu.ScreenWidth, ppu.ScreenHeight)
if err != nil {
return err
}
defer texture.Destroy()
fps := framesPerSecond // a variable, a constant would have to be a whole number of nanoseconds
frameTime := time.Duration(float64(time.Second) / fps)
next := time.Now()
fpsStart, frames := next, 0
jamShown := false
for {
for e := sdl.PollEvent(); e != nil; e = sdl.PollEvent() {
switch ev := e.(type) {
case *sdl.QuitEvent:
return nil
case *sdl.KeyboardEvent:
if ev.Type == sdl.KEYDOWN && ev.Keysym.Sym == sdl.K_ESCAPE {
return nil
}
}
}
console.SetButtons(0, heldButtons())
console.StepFrame()
// UpdateRGBA multiplies the pitch by 4 itself, so this is in pixels, not bytes
if err := texture.UpdateRGBA(nil, console.PPU.Framebuffer(), ppu.ScreenWidth); err != nil {
return err
}
renderer.Clear()
renderer.Copy(texture, nil, nil)
renderer.Present()
if console.CPU.Jammed && !jamShown {
window.SetTitle("gones - " + name + " - CPU jammed (reset needed)")
jamShown = true
}
frames++
if elapsed := time.Since(fpsStart); elapsed >= time.Second && !jamShown {
window.SetTitle(fmt.Sprintf("gones - %s - %.1f fps", name, float64(frames)/elapsed.Seconds()))
fpsStart, frames = time.Now(), 0
}
// vsync alone would run too fast on a 120/144hz monitor, so pace to the nes frame rate
next = next.Add(frameTime)
if wait := time.Until(next); wait > 0 {
time.Sleep(wait)
} else if wait < -100*time.Millisecond {
next = time.Now() // fell far behind (window drag etc), don't try to catch up
}
}
}
// keyboard layout for controller 1
var keyMap = map[sdl.Scancode]uint8{
sdl.SCANCODE_X: nes.ButtonA,
sdl.SCANCODE_Y: nes.ButtonB,
sdl.SCANCODE_RSHIFT: nes.ButtonSelect,
sdl.SCANCODE_RETURN: nes.ButtonStart,
sdl.SCANCODE_UP: nes.ButtonUp,
sdl.SCANCODE_DOWN: nes.ButtonDown,
sdl.SCANCODE_LEFT: nes.ButtonLeft,
sdl.SCANCODE_RIGHT: nes.ButtonRight,
}
func heldButtons() uint8 {
keys := sdl.GetKeyboardState()
var b uint8
for scancode, button := range keyMap {
if keys[scancode] != 0 {
b |= button
}
}
return b
}
|