aboutsummaryrefslogtreecommitdiff
path: root/main.go
blob: 2ce3745e1cb12913a92c244972f0dbbaea7df9d4 (plain)
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
153
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
// TODO: add remapping
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
}