aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/gones/main.go128
1 files changed, 128 insertions, 0 deletions
diff --git a/cmd/gones/main.go b/cmd/gones/main.go
new file mode 100644
index 0000000..57b1b6e
--- /dev/null
+++ b/cmd/gones/main.go
@@ -0,0 +1,128 @@
+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.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
+ }
+ }
+}