diff options
Diffstat (limited to 'cmd/gones/main.go')
| -rw-r--r-- | cmd/gones/main.go | 128 |
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 + } + } +} |