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 \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 } } }