diff --git a/debugger/commands.go b/debugger/commands.go index d487dde6..6ac4713b 100644 --- a/debugger/commands.go +++ b/debugger/commands.go @@ -277,7 +277,7 @@ func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error { return fmt.Errorf("unknown STEP BACK mode (%s)", mode) } - dbg.setState(govern.Rewinding) + dbg.setState(govern.Rewinding, govern.RewindingBackwards) dbg.unwindLoop(func() error { dbg.catchupContext = catchupStepBack return dbg.Rewind.GotoCoords(coords) @@ -401,20 +401,27 @@ func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error { dbg.runUntilHalt = false if arg == "LAST" { - dbg.setState(govern.Rewinding) + dbg.setState(govern.Rewinding, govern.RewindingForwards) dbg.unwindLoop(dbg.Rewind.GotoLast) } else if arg == "SUMMARY" { dbg.printLine(terminal.StyleInstrument, dbg.Rewind.Peephole()) } else { frame, _ := strconv.Atoi(arg) - dbg.setState(govern.Rewinding) - dbg.unwindLoop(func() error { - err := dbg.Rewind.GotoFrame(frame) - if err != nil { - return err + coords := dbg.TV().GetCoords() + if frame != coords.Frame { + if frame < coords.Frame { + dbg.setState(govern.Rewinding, govern.RewindingBackwards) + } else { + dbg.setState(govern.Rewinding, govern.RewindingForwards) } - return nil - }) + dbg.unwindLoop(func() error { + err := dbg.Rewind.GotoFrame(frame) + if err != nil { + return err + } + return nil + }) + } } return nil } @@ -437,21 +444,26 @@ func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error { } case cmdGoto: - coords := dbg.vcs.TV.GetCoords() + fromCoords := dbg.vcs.TV.GetCoords() + toCoords := fromCoords if s, ok := tokens.Get(); ok { - coords.Clock, _ = strconv.Atoi(s) + toCoords.Clock, _ = strconv.Atoi(s) if s, ok := tokens.Get(); ok { - coords.Scanline, _ = strconv.Atoi(s) + toCoords.Scanline, _ = strconv.Atoi(s) if s, ok := tokens.Get(); ok { - coords.Frame, _ = strconv.Atoi(s) + toCoords.Frame, _ = strconv.Atoi(s) } } } - dbg.setState(govern.Rewinding) + if coords.GreaterThan(toCoords, fromCoords) { + dbg.setState(govern.Rewinding, govern.RewindingForwards) + } else { + dbg.setState(govern.Rewinding, govern.RewindingBackwards) + } dbg.unwindLoop(func() error { - err := dbg.Rewind.GotoCoords(coords) + err := dbg.Rewind.GotoCoords(toCoords) if err != nil { return err } diff --git a/debugger/debugger.go b/debugger/debugger.go index 5a29969e..500d691c 100644 --- a/debugger/debugger.go +++ b/debugger/debugger.go @@ -87,7 +87,8 @@ type Debugger struct { // state is an atomic value because we need to be able to read it from the // GUI thread (see State() function) - state atomic.Value // emulation.State + state atomic.Value // emulation.State + subState atomic.Value // emulation.RewindingSubState // preferences for the emulation Prefs *Preferences @@ -333,10 +334,10 @@ func NewDebugger(opts CommandLineOptions, create CreateUserInterface) (*Debugger readEventsPulse: time.NewTicker(1 * time.Millisecond), } - // emulator is starting in the "none" mode (the advangatge of this is that - // we get to set the underlying type of the atomic.Value early before - // anyone has a change to call State() or Mode() from another thread) + // set atomics to defaults values. if we don't do this we can cause panics + // due to the GUI asking for values before we've had a chance to set them dbg.state.Store(govern.EmulatorStart) + dbg.subState.Store(govern.RewindingBackwards) dbg.mode.Store(govern.ModeNone) dbg.quantum.Store(govern.QuantumInstruction) @@ -526,19 +527,26 @@ func (dbg *Debugger) State() govern.State { return dbg.state.Load().(govern.State) } +// SubState implements the emulation.Emulation interface. +func (dbg *Debugger) SubState() govern.SubState { + return dbg.subState.Load().(govern.SubState) +} + // Mode implements the emulation.Emulation interface. func (dbg *Debugger) Mode() govern.Mode { return dbg.mode.Load().(govern.Mode) } // set the emulation state -func (dbg *Debugger) setState(state govern.State) { - dbg.setStateQuiet(state, false) -} +func (dbg *Debugger) setState(state govern.State, subState govern.SubState) { + // intentionally panic if state/sub-state combination is not allowed + if !govern.StateIntegrity(state, subState) { + panic(fmt.Sprintf("illegal sub-state (%s) for %s state (prev state: %s)", + subState, state, + dbg.state.Load().(govern.State), + )) + } -// same as setState but with quiet argument, to indicate that EmulationEvent -// should not be issued to the gui. -func (dbg *Debugger) setStateQuiet(state govern.State, quiet bool) { if state == govern.Rewinding { dbg.endPlayback() dbg.endRecording() @@ -561,30 +569,8 @@ func (dbg *Debugger) setStateQuiet(state govern.State, quiet bool) { } dbg.CoProcDev.SetEmulationState(state) - prevState := dbg.State() dbg.state.Store(state) - - if !quiet && dbg.Mode() == govern.ModePlay { - switch state { - case govern.Initialising: - err := dbg.gui.SetFeature(gui.ReqEmulationNotify, notifications.NotifyInitialising) - if err != nil { - logger.Log("debugger", err.Error()) - } - case govern.Paused: - err := dbg.gui.SetFeature(gui.ReqEmulationNotify, notifications.NotifyPause) - if err != nil { - logger.Log("debugger", err.Error()) - } - case govern.Running: - if prevState > govern.Initialising { - err := dbg.gui.SetFeature(gui.ReqEmulationNotify, notifications.NotifyRun) - if err != nil { - logger.Log("debugger", err.Error()) - } - } - } - } + dbg.subState.Store(subState) } // set the emulation mode @@ -649,14 +635,14 @@ func (dbg *Debugger) setMode(mode govern.Mode) error { return fmt.Errorf("debugger: %w", err) } } else { - dbg.setState(govern.Running) + dbg.setState(govern.Running, govern.Normal) } case govern.ModeDebugger: dbg.vcs.TV.AddFrameTrigger(dbg.Rewind) dbg.vcs.TV.AddFrameTrigger(dbg.ref) dbg.vcs.TV.AddFrameTrigger(dbg.counter) - dbg.setState(govern.Paused) + dbg.setState(govern.Paused, govern.Normal) // debugger needs knowledge about previous frames (via the reflector) // if we're moving from playmode. also we want to make sure we end on @@ -1098,12 +1084,12 @@ func (dbg *Debugger) Notify(notice notifications.Notice) error { dbg.vcs.Mem.Poke(supercharger.MutliloadByteAddress, uint8(dbg.opts.Multiload)) } - err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifySuperchargerSoundloadStarted) + err := dbg.gui.SetFeature(gui.ReqNotification, notifications.NotifySuperchargerSoundloadStarted) if err != nil { return err } case notifications.NotifySuperchargerSoundloadEnded: - err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifySuperchargerSoundloadEnded) + err := dbg.gui.SetFeature(gui.ReqNotification, notifications.NotifySuperchargerSoundloadEnded) if err != nil { return err } @@ -1118,23 +1104,20 @@ func (dbg *Debugger) Notify(notice notifications.Notice) error { return dbg.vcs.TV.Reset(true) case notifications.NotifySuperchargerSoundloadRewind: - err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifySuperchargerSoundloadRewind) + err := dbg.gui.SetFeature(gui.ReqNotification, notifications.NotifySuperchargerSoundloadRewind) if err != nil { return err } - case notifications.NotifyPlusROMInserted: - if dbg.vcs.Env.Prefs.PlusROM.NewInstallation { - err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifyPlusROMNewInstallation) - if err != nil { - return fmt.Errorf(err.Error()) - } + case notifications.NotifyPlusROMNewInstall: + err := dbg.gui.SetFeature(gui.ReqNotification, notifications.NotifyPlusROMNewInstall) + if err != nil { + return err } case notifications.NotifyPlusROMNetwork: - err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifyPlusROMNetwork) + err := dbg.gui.SetFeature(gui.ReqNotification, notifications.NotifyPlusROMNetwork) if err != nil { return err } - case notifications.NotifyMovieCartStarted: return dbg.vcs.TV.Reset(true) default: @@ -1166,19 +1149,19 @@ func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error) dbg.bots.Quit() // attching a cartridge implies the initialise state - dbg.setState(govern.Initialising) + dbg.setState(govern.Initialising, govern.Normal) // set state after initialisation according to the emulation mode defer func() { switch dbg.Mode() { case govern.ModeDebugger: if dbg.runUntilHalt && e == nil { - dbg.setState(govern.Running) + dbg.setState(govern.Running, govern.Normal) } else { - dbg.setState(govern.Paused) + dbg.setState(govern.Paused, govern.Normal) } case govern.ModePlay: - dbg.setState(govern.Running) + dbg.setState(govern.Running, govern.Normal) } }() @@ -1227,23 +1210,13 @@ func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error) if err != nil { logger.Logf("debugger", err.Error()) if errors.Is(err, coproc_dwarf.UnsupportedDWARF) { - err = dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifyUnsupportedDWARF) + err = dbg.gui.SetFeature(gui.ReqNotification, notifications.NotifyUnsupportedDWARF) if err != nil { logger.Logf("debugger", err.Error()) } } } - // notify GUI of coprocessor state - if dbg.CoProcDev.HasSource() { - err = dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifyCoprocDevStarted) - } else { - err = dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifyCoprocDevEnded) - } - if err != nil { - logger.Logf("debugger", err.Error()) - } - // attach current debugger as the yield hook for cartridge dbg.vcs.Mem.Cart.SetYieldHook(dbg) @@ -1386,15 +1359,12 @@ func (dbg *Debugger) endComparison() { func (dbg *Debugger) hotload() (e error) { // tell GUI that we're in the initialistion phase - dbg.setState(govern.Initialising) + dbg.setState(govern.Initialising, govern.Normal) defer func() { if dbg.runUntilHalt && e == nil { - dbg.setState(govern.Running) + dbg.setState(govern.Running, govern.Normal) } else { - err := dbg.gui.SetFeature(gui.ReqEmulationNotify, notifications.NotifyPause) - if err != nil { - logger.Log("debugger", err.Error()) - } + dbg.setState(govern.Paused, govern.Normal) } }() @@ -1472,14 +1442,14 @@ func (dbg *Debugger) Plugged(port plugging.PortID, peripheral plugging.Periphera if dbg.vcs.Mem.Cart.IsEjected() { return } - err := dbg.gui.SetFeature(gui.ReqPeripheralNotify, port, peripheral) + err := dbg.gui.SetFeature(gui.ReqPeripheralPlugged, port, peripheral) if err != nil { logger.Log("debugger", err.Error()) } } func (dbg *Debugger) reloadCartridge() error { - dbg.setState(govern.Initialising) + dbg.setState(govern.Initialising, govern.Normal) spec := dbg.vcs.TV.GetFrameInfo().Spec.ID err := dbg.insertCartridge("") @@ -1534,7 +1504,7 @@ func (dbg *Debugger) insertCartridge(filename string) error { // insertCartridge() for that func (dbg *Debugger) InsertCartridge(filename string) { dbg.PushFunctionImmediate(func() { - dbg.setState(govern.Initialising) + dbg.setState(govern.Initialising, govern.Normal) dbg.unwindLoop(func() error { return dbg.insertCartridge(filename) }) diff --git a/debugger/deeppoke.go b/debugger/deeppoke.go index 1b324840..e992e148 100644 --- a/debugger/deeppoke.go +++ b/debugger/deeppoke.go @@ -47,7 +47,7 @@ func (dbg *Debugger) PushDeepPoke(addr uint16, value uint8, newValue uint8, valu } dbg.PushFunctionImmediate(func() { - dbg.setStateQuiet(govern.Rewinding, true) + dbg.setState(govern.Rewinding, govern.Normal) dbg.unwindLoop(doDeepPoke) }) diff --git a/debugger/govern/state.go b/debugger/govern/state.go index 0a7c4cad..dda8fead 100644 --- a/debugger/govern/state.go +++ b/debugger/govern/state.go @@ -26,13 +26,7 @@ type State int // Initialising can be used when reinitialising the emulator. for example, when // a new cartridge is being inserted. // -// Values are ordered so that order comparisons are meaningful. For example, -// Running is "greater than" Stepping, Paused, etc. -// -// Note that there is a sub-state of the rewinding state that we can potentially -// think of as the "catch-up" state. This occurs in the brief transition period -// between Rewinding and the Running or Pausing state. For simplicity, the -// catch-up loop is part of the Rewinding state +// Paused and Rewinding can have meaningful sub-states const ( EmulatorStart State = iota Initialising @@ -63,3 +57,58 @@ func (s State) String() string { return "" } + +// SubState allows more detail for some states. NoSubState indicates that there +// is not more information to impart about the state +type SubState int + +// List of possible rewinding sub states +const ( + Normal SubState = iota + RewindingBackwards + RewindingForwards + PausedAtStart + PausedAtEnd +) + +func (s SubState) String() string { + switch s { + case RewindingBackwards: + return "Backwards" + case RewindingForwards: + return "Forwards" + case PausedAtStart: + return "Paused at start" + case PausedAtEnd: + return "Paused at end" + } + return "" +} + +// StateIntegrity checks whether the combination of state, sub-state makes +// sense. The previous state is also required for a complete check. +// +// Rules: +// +// 1. NoSubState can coexist with any state +// +// 2. PausedAtStart and PausedAtEnd can only be paired with the Paused State +// +// 3. RewindingBackwards and RewindingForwards can only be paired with the +// Rewinding state +func StateIntegrity(state State, subState SubState) bool { + if subState == Normal { + return true + } + switch state { + case Rewinding: + if subState == RewindingBackwards || subState == RewindingForwards { + return true + } + case Paused: + if subState == PausedAtEnd || subState == PausedAtStart { + return true + } + } + return false +} diff --git a/debugger/loop_catchup.go b/debugger/loop_catchup.go index 26d87d23..d5aff164 100644 --- a/debugger/loop_catchup.go +++ b/debugger/loop_catchup.go @@ -79,7 +79,7 @@ func (dbg *Debugger) CatchUpLoop(tgt coords.TelevisionCoords) error { dbg.vcs.TV.SetFPSCap(fpsCap) dbg.catchupContinue = nil dbg.catchupEnd = nil - dbg.setState(govern.Paused) + dbg.setState(govern.Paused, govern.Normal) dbg.runUntilHalt = false dbg.continueEmulation = dbg.catchupEndAdj dbg.catchupEndAdj = false diff --git a/debugger/loop_debugger.go b/debugger/loop_debugger.go index 5047313d..8b2df38d 100644 --- a/debugger/loop_debugger.go +++ b/debugger/loop_debugger.go @@ -290,7 +290,7 @@ func (dbg *Debugger) inputLoop(inputter terminal.Input, nonInstructionQuantum bo } // set pause emulation state - dbg.setState(govern.Paused) + dbg.setState(govern.Paused, govern.Normal) // take note of current machine state if the emulation was in a running // state and is halting just now @@ -358,10 +358,10 @@ func (dbg *Debugger) inputLoop(inputter terminal.Input, nonInstructionQuantum bo // stepping. if dbg.halting.volatileTraps.isEmpty() { if inputter.IsInteractive() { - dbg.setState(govern.Running) + dbg.setState(govern.Running, govern.Normal) } } else { - dbg.setState(govern.Stepping) + dbg.setState(govern.Stepping, govern.Normal) } // update comparison point before execution continues @@ -369,7 +369,7 @@ func (dbg *Debugger) inputLoop(inputter terminal.Input, nonInstructionQuantum bo dbg.Rewind.UpdateComparison() } } else if inputter.IsInteractive() { - dbg.setState(govern.Stepping) + dbg.setState(govern.Stepping, govern.Normal) } } diff --git a/debugger/push.go b/debugger/push.go index 8234962a..ed560949 100644 --- a/debugger/push.go +++ b/debugger/push.go @@ -55,9 +55,9 @@ func (dbg *Debugger) PushSetPause(paused bool) { case govern.ModePlay: dbg.PushFunction(func() { if paused { - dbg.setState(govern.Paused) + dbg.setState(govern.Paused, govern.Normal) } else { - dbg.setState(govern.Running) + dbg.setState(govern.Running, govern.Normal) } }) case govern.ModeDebugger: diff --git a/debugger/rewind.go b/debugger/rewind.go index a9aee6fe..1d0e20cc 100644 --- a/debugger/rewind.go +++ b/debugger/rewind.go @@ -22,53 +22,37 @@ import ( "fmt" "github.com/jetsetilly/gopher2600/debugger/govern" - "github.com/jetsetilly/gopher2600/gui" "github.com/jetsetilly/gopher2600/hardware/television/coords" - "github.com/jetsetilly/gopher2600/logger" - "github.com/jetsetilly/gopher2600/notifications" ) // RewindByAmount moves forwards or backwards by specified frames. Negative // numbers indicate backwards -func (dbg *Debugger) RewindByAmount(amount int) bool { +func (dbg *Debugger) RewindByAmount(amount int) { switch dbg.Mode() { case govern.ModePlay: coords := dbg.vcs.TV.GetCoords() tl := dbg.Rewind.GetTimeline() - if amount < 0 && coords.Frame-1 <= tl.AvailableStart { - dbg.setStateQuiet(govern.Paused, true) - err := dbg.gui.SetFeature(gui.ReqEmulationNotify, notifications.NotifyRewindAtStart) - if err != nil { - logger.Log("debugger", err.Error()) - } - return false - } - - if amount > 0 && coords.Frame+1 >= tl.AvailableEnd { - dbg.setStateQuiet(govern.Paused, true) - err := dbg.gui.SetFeature(gui.ReqEmulationNotify, notifications.NotifyRewindAtEnd) - if err != nil { - logger.Log("debugger", err.Error()) - } - return false - } - - dbg.setStateQuiet(govern.Rewinding, true) - dbg.Rewind.GotoFrame(coords.Frame + amount) - dbg.setStateQuiet(govern.Paused, true) - - var err error if amount < 0 { - err = dbg.gui.SetFeature(gui.ReqEmulationNotify, notifications.NotifyRewindBack) - } else { - err = dbg.gui.SetFeature(gui.ReqEmulationNotify, notifications.NotifyRewindFoward) - } - if err != nil { - logger.Log("debugger", err.Error()) + if coords.Frame-1 < tl.AvailableStart { + dbg.setState(govern.Paused, govern.PausedAtStart) + return + } + dbg.setState(govern.Rewinding, govern.RewindingBackwards) } - return true + if amount > 0 { + if coords.Frame+1 > tl.AvailableEnd { + dbg.setState(govern.Paused, govern.PausedAtEnd) + return + } + dbg.setState(govern.Rewinding, govern.RewindingForwards) + } + + dbg.Rewind.GotoFrame(coords.Frame + amount) + dbg.setState(govern.Paused, govern.Normal) + + return } panic(fmt.Sprintf("Rewind: unsupported mode (%v)", dbg.Mode())) @@ -107,7 +91,11 @@ func (dbg *Debugger) RewindToFrame(fn int, last bool) bool { dbg.PushFunctionImmediate(func() { // set state to govern.Rewinding as soon as possible (but // remembering that we must do it in the debugger goroutine) - dbg.setState(govern.Rewinding) + if fn > dbg.vcs.TV.GetCoords().Frame { + dbg.setState(govern.Rewinding, govern.RewindingForwards) + } else { + dbg.setState(govern.Rewinding, govern.RewindingBackwards) + } dbg.unwindLoop(doRewind) }) @@ -118,7 +106,7 @@ func (dbg *Debugger) RewindToFrame(fn int, last bool) bool { } // GotoCoords rewinds the emulation to the specified coordinates. -func (dbg *Debugger) GotoCoords(coords coords.TelevisionCoords) bool { +func (dbg *Debugger) GotoCoords(toCoords coords.TelevisionCoords) bool { switch dbg.Mode() { case govern.ModeDebugger: if dbg.State() == govern.Rewinding { @@ -130,7 +118,7 @@ func (dbg *Debugger) GotoCoords(coords coords.TelevisionCoords) bool { // upate catchup context before starting rewind process dbg.catchupContext = catchupGotoCoords - err := dbg.Rewind.GotoCoords(coords) + err := dbg.Rewind.GotoCoords(toCoords) if err != nil { return err } @@ -143,7 +131,13 @@ func (dbg *Debugger) GotoCoords(coords coords.TelevisionCoords) bool { dbg.PushFunctionImmediate(func() { // set state to govern.Rewinding as soon as possible (but // remembering that we must do it in the debugger goroutine) - dbg.setState(govern.Rewinding) + + fromCoords := dbg.vcs.TV.GetCoords() + if coords.GreaterThan(toCoords, fromCoords) { + dbg.setState(govern.Rewinding, govern.RewindingForwards) + } else { + dbg.setState(govern.Rewinding, govern.RewindingBackwards) + } dbg.unwindLoop(doRewind) }) @@ -168,7 +162,7 @@ func (dbg *Debugger) RerunLastNFrames(frames int) bool { // if we're in between instruction boundaries therefore we need to push a // GotoCoords() request. get the current coordinates now correctCoords := !dbg.liveDisasmEntry.Result.Final - coords := dbg.vcs.TV.GetCoords() + toCoords := dbg.vcs.TV.GetCoords() // the function to push to the debugger/emulation routine doRewind := func() error { @@ -178,7 +172,7 @@ func (dbg *Debugger) RerunLastNFrames(frames int) bool { } if correctCoords { - err = dbg.Rewind.GotoCoords(coords) + err = dbg.Rewind.GotoCoords(toCoords) if err != nil { return err } @@ -195,7 +189,7 @@ func (dbg *Debugger) RerunLastNFrames(frames int) bool { // set state to govern.Rewinding as soon as possible (but // remembering that we must do it in the debugger goroutine) - dbg.setState(govern.Rewinding) + dbg.setState(govern.Rewinding, govern.Normal) dbg.unwindLoop(doRewind) }) diff --git a/gui/fonts/fonts.go b/gui/fonts/fonts.go index d0633239..bb74d316 100644 --- a/gui/fonts/fonts.go +++ b/gui/fonts/fonts.go @@ -46,8 +46,8 @@ const ( EmulationRun = '\uf04b' EmulationRewindBack = '\uf04a' EmulationRewindForward = '\uf04e' - EmulationRewindAtStart = '\uf049' - EmulationRewindAtEnd = '\uf050' + EmulationPausedAtStart = '\uf049' + EmulationPausedAtEnd = '\uf050' MusicNote = '\uf001' VolumeRising = '\uf062' VolumeFalling = '\uf063' diff --git a/gui/gui.go b/gui/gui.go index 3a311eed..9565bf25 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -47,16 +47,12 @@ const ( // the size of the monitor). ReqFullScreen FeatureReq = "ReqFullScreen" // bool - // an event generated by the emulation has occured. for example, the - // emulation has been paused. - ReqEmulationNotify FeatureReq = "ReqEmulationNotify" // notifications.Notify - // an event generated by the cartridge has occured. for example, network // activity from a PlusROM cartridge. - ReqCartridgeNotify FeatureReq = "ReqCartridgeNotify" // notifications.Notify + ReqNotification FeatureReq = "ReqNotification" // notifications.Notify - // peripheral has changed for one of the ports. - ReqPeripheralNotify FeatureReq = "ReqPeripheralNotify" // plugging.PortID, plugging.PeripheralID + // peripheral has been changed for one of the ports. + ReqPeripheralPlugged FeatureReq = "ReqPeripheralPlugging" // plugging.PortID, plugging.PeripheralID // open ROM selector. ReqROMSelector FeatureReq = "ReqROMSelector" // nil diff --git a/gui/sdlimgui/playscr.go b/gui/sdlimgui/playscr.go index cbd00829..2065b7c6 100644 --- a/gui/sdlimgui/playscr.go +++ b/gui/sdlimgui/playscr.go @@ -16,11 +16,9 @@ package sdlimgui import ( - "fmt" "time" "github.com/inkyblackness/imgui-go/v4" - "github.com/jetsetilly/gopher2600/gui/fonts" "github.com/jetsetilly/gopher2600/hardware/television/specification" ) @@ -52,32 +50,20 @@ type playScr struct { // number of scanlines in current image. taken from screen but is crit section safe visibleScanlines int - // fps overlay - fpsPulse *time.Ticker - fps string - hz string - - // controller notifications - peripheralLeft peripheralNotification - peripheralRight peripheralNotification - - // emulation notifications - emulationNotice emulationEventNotification - - // cartridge notifications - cartridgeNotice cartridgeEventNotification + // overlay for play screen + overlay playscrOverlay } func newPlayScr(img *SdlImgui) *playScr { win := &playScr{ - img: img, - scr: img.screen, - fpsPulse: time.NewTicker(time.Second), - fps: "waiting", - peripheralRight: peripheralNotification{ - rightAlign: true, + img: img, + scr: img.screen, + overlay: playscrOverlay{ + fpsPulse: time.NewTicker(time.Second), + fps: "waiting", }, } + win.overlay.playscr = win // set texture, creation of textures will be done after every call to resize() // clamp is important for LINEAR filtering. not noticeable for NEAREST filtering @@ -99,94 +85,7 @@ func (win *playScr) draw() { dl := imgui.BackgroundDrawList() dl.AddImage(imgui.TextureID(win.displayTexture.getID()), win.imagePosMin, win.imagePosMax) - win.peripheralLeft.draw(win) - win.peripheralRight.draw(win) - win.cartridgeNotice.draw(win) - - if !win.drawFPS() { - win.emulationNotice.draw(win, false) - } -} - -func (win *playScr) toggleFPS() { - fps := win.img.prefs.fpsOverlay.Get().(bool) - win.img.prefs.fpsOverlay.Set(!fps) -} - -func (win *playScr) drawFPS() bool { - if !win.img.prefs.fpsOverlay.Get().(bool) { - return false - } - - // update fps - select { - case <-win.fpsPulse.C: - fps, hz := win.img.dbg.VCS().TV.GetActualFPS() - win.fps = fmt.Sprintf("%03.2f fps", fps) - win.hz = fmt.Sprintf("%03.2fhz", hz) - default: - } - - imgui.SetNextWindowPos(imgui.Vec2{0, 0}) - - imgui.PushStyleColor(imgui.StyleColorWindowBg, win.img.cols.Transparent) - imgui.PushStyleColor(imgui.StyleColorBorder, win.img.cols.Transparent) - - fpsOpen := true - imgui.BeginV("##playscrfps", &fpsOpen, imgui.WindowFlagsAlwaysAutoResize| - imgui.WindowFlagsNoScrollbar|imgui.WindowFlagsNoTitleBar| - imgui.WindowFlagsNoDecoration|imgui.WindowFlagsNoSavedSettings| - imgui.WindowFlagsNoBringToFrontOnFocus) - - imgui.Text(fmt.Sprintf("Emulation: %s", win.fps)) - fr := imgui.CurrentIO().Framerate() - if fr == 0.0 { - imgui.Text("Rendering: waiting") - } else { - imgui.Text(fmt.Sprintf("Rendering: %03.2f fps", fr)) - } - - imguiSeparator() - - if coproc := win.img.cache.VCS.Mem.Cart.GetCoProc(); coproc != nil { - clk := float32(win.img.dbg.VCS().Env.Prefs.ARM.Clock.Get().(float64)) - imgui.Text(fmt.Sprintf("%s Clock: %.0f Mhz", coproc.ProcessorID(), clk)) - imguiSeparator() - } - - imgui.Text(fmt.Sprintf("%.1fx scaling", win.yscaling)) - imgui.Text(fmt.Sprintf("%d total scanlines", win.scr.crit.frameInfo.TotalScanlines)) - - imguiSeparator() - - imgui.Text(win.img.screen.crit.frameInfo.Spec.ID) - imgui.SameLine() - imgui.Text(win.hz) - if !win.scr.crit.frameInfo.VSync { - imgui.SameLine() - imgui.Text(string(fonts.NoVSYNC)) - } - - imguiSeparator() - imgui.Text(fmt.Sprintf("%d frame input lag", win.scr.crit.frameQueueLen)) - if win.scr.nudgeIconCt > 0 { - imgui.SameLine() - imgui.Text(string(fonts.Nudge)) - } - - // if win.img.screen.crit.frameInfo.IsAtariSafe() { - // imguiSeparator() - // imgui.Text("atari safe") - // } - - imgui.PopStyleColorV(2) - - imgui.Spacing() - win.emulationNotice.draw(win, true) - - imgui.End() - - return true + win.overlay.draw() } // resize() implements the textureRenderer interface. diff --git a/gui/sdlimgui/playscr_notifications.go b/gui/sdlimgui/playscr_notifications.go deleted file mode 100644 index 4a4a1850..00000000 --- a/gui/sdlimgui/playscr_notifications.go +++ /dev/null @@ -1,381 +0,0 @@ -// This file is part of Gopher2600. -// -// Gopher2600 is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Gopher2600 is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Gopher2600. If not, see . - -package sdlimgui - -import ( - "fmt" - - "github.com/inkyblackness/imgui-go/v4" - "github.com/jetsetilly/gopher2600/gui/fonts" - "github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging" - "github.com/jetsetilly/gopher2600/notifications" -) - -const ( - notificationDurationPeripheral = 90 - notificationDurationCartridge = 60 - notificationDurationEmulation = 60 -) - -// peripheralNotification is used to draw an indicator on the screen for controller change events. -type peripheralNotification struct { - frames int - icon string - rightAlign bool -} - -func (ntfy *peripheralNotification) set(peripheral plugging.PeripheralID) { - ntfy.frames = notificationDurationPeripheral - switch peripheral { - case plugging.PeriphStick: - ntfy.icon = fmt.Sprintf("%c", fonts.Stick) - case plugging.PeriphPaddles: - ntfy.icon = fmt.Sprintf("%c", fonts.Paddle) - case plugging.PeriphKeypad: - ntfy.icon = fmt.Sprintf("%c", fonts.Keypad) - case plugging.PeriphSavekey: - ntfy.icon = fmt.Sprintf("%c", fonts.Savekey) - case plugging.PeriphGamepad: - ntfy.icon = fmt.Sprintf("%c", fonts.Gamepad) - case plugging.PeriphAtariVox: - ntfy.icon = fmt.Sprintf("%c", fonts.AtariVox) - default: - ntfy.icon = "" - return - } -} - -// pos should be the coordinate of the *extreme* bottom left or bottom right of -// the playscr window. the values will be adjusted according to whether we're -// display an icon or text. -func (ntfy *peripheralNotification) draw(win *playScr) { - if ntfy.frames <= 0 { - return - } - ntfy.frames-- - - if !win.img.prefs.controllerNotifcations.Get().(bool) { - return - } - - // position window so that it is fully visible at the bottom of the screen. - // taking special care of the right aligned window - var id string - var pos imgui.Vec2 - dimen := win.img.plt.displaySize() - if ntfy.rightAlign { - pos = imgui.Vec2{dimen[0], dimen[1]} - id = "##rightPeriphNotification" - pos.X -= win.img.fonts.gopher2600IconsSize * 1.35 - } else { - pos = imgui.Vec2{0, dimen[1]} - id = "##leftPeriphNotification" - pos.X += win.img.fonts.gopher2600IconsSize * 0.20 - } - pos.Y -= win.img.fonts.gopher2600IconsSize * 1.35 - - imgui.SetNextWindowPos(pos) - imgui.PushStyleColor(imgui.StyleColorWindowBg, win.img.cols.Transparent) - imgui.PushStyleColor(imgui.StyleColorBorder, win.img.cols.Transparent) - defer imgui.PopStyleColorV(2) - - imgui.PushFont(win.img.fonts.gopher2600Icons) - defer imgui.PopFont() - - a := float32(win.img.prefs.notificationVisibility.Get().(float64)) - imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{a, a, a, a}) - defer imgui.PopStyleColor() - - periphOpen := true - imgui.BeginV(id, &periphOpen, imgui.WindowFlagsAlwaysAutoResize| - imgui.WindowFlagsNoScrollbar|imgui.WindowFlagsNoTitleBar| - imgui.WindowFlagsNoDecoration|imgui.WindowFlagsNoSavedSettings) - - imgui.Text(ntfy.icon) - - imgui.End() -} - -// emulationEventNotification is used to draw an indicator on the screen for -// events defined in the emulation package. -type emulationEventNotification struct { - open bool - frames int - - event notifications.Notice - mute bool -} - -func (ntfy *emulationEventNotification) set(event notifications.Notice) { - switch event { - default: - ntfy.event = event - ntfy.frames = notificationDurationEmulation - ntfy.open = true - case notifications.NotifyPause: - ntfy.event = event - ntfy.frames = 0 - ntfy.open = true - case notifications.NotifyMute: - ntfy.event = event - ntfy.frames = 0 - ntfy.open = true - ntfy.mute = true - case notifications.NotifyUnmute: - ntfy.event = event - ntfy.frames = 0 - ntfy.open = true - ntfy.mute = false - } -} - -func (ntfy *emulationEventNotification) tick() { - if ntfy.frames <= 0 { - return - } - ntfy.frames-- - - if ntfy.frames == 0 { - if ntfy.mute { - ntfy.event = notifications.NotifyMute - ntfy.open = true - } else { - ntfy.open = false - } - } -} - -func (ntfy *emulationEventNotification) draw(win *playScr, hosted bool) { - ntfy.tick() - if !ntfy.open { - return - } - - if !hosted { - imgui.SetNextWindowPos(imgui.Vec2{X: 10, Y: 10}) - imgui.PushStyleColor(imgui.StyleColorWindowBg, win.img.cols.Transparent) - imgui.PushStyleColor(imgui.StyleColorBorder, win.img.cols.Transparent) - defer imgui.PopStyleColorV(2) - - a := float32(win.img.prefs.notificationVisibility.Get().(float64)) - imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{a, a, a, a}) - defer imgui.PopStyleColor() - - imgui.BeginV("##emulationNotification", &ntfy.open, imgui.WindowFlagsAlwaysAutoResize| - imgui.WindowFlagsNoScrollbar|imgui.WindowFlagsNoTitleBar| - imgui.WindowFlagsNoDecoration|imgui.WindowFlagsNoSavedSettings| - imgui.WindowFlagsNoBringToFrontOnFocus) - defer imgui.End() - - imgui.PushFont(win.img.fonts.veryLargeFontAwesome) - defer imgui.PopFont() - } - - switch ntfy.event { - case notifications.NotifyInitialising: - imgui.Text("") - case notifications.NotifyPause: - imgui.Text(string(fonts.EmulationPause)) - case notifications.NotifyRun: - imgui.Text(string(fonts.EmulationRun)) - case notifications.NotifyRewindBack: - imgui.Text(string(fonts.EmulationRewindBack)) - case notifications.NotifyRewindFoward: - imgui.Text(string(fonts.EmulationRewindForward)) - case notifications.NotifyRewindAtStart: - imgui.Text(string(fonts.EmulationRewindAtStart)) - case notifications.NotifyRewindAtEnd: - imgui.Text(string(fonts.EmulationRewindAtEnd)) - case notifications.NotifyScreenshot: - imgui.Text(string(fonts.Camera)) - default: - if ntfy.mute && win.img.prefs.audioMuteNotification.Get().(bool) { - imgui.Text(string(fonts.AudioMute)) - } - } -} - -// cartridgeEventNotification is used to draw an indicator on the screen for cartridge -// events defined in the mapper package. -type cartridgeEventNotification struct { - open bool - frames int - - event notifications.Notice - coprocDev bool -} - -func (ntfy *cartridgeEventNotification) set(event notifications.Notice) { - switch event { - case notifications.NotifySuperchargerSoundloadStarted: - ntfy.event = event - ntfy.frames = 0 - ntfy.open = true - case notifications.NotifySuperchargerSoundloadEnded: - ntfy.event = event - ntfy.frames = notificationDurationCartridge - ntfy.open = true - case notifications.NotifySuperchargerSoundloadRewind: - ntfy.event = event - ntfy.frames = notificationDurationCartridge - ntfy.open = true - case notifications.NotifyPlusROMNetwork: - ntfy.event = event - ntfy.frames = notificationDurationCartridge - ntfy.open = true - case notifications.NotifyCoprocDevStarted: - ntfy.coprocDev = true - ntfy.frames = 0 - ntfy.open = true - case notifications.NotifyCoprocDevEnded: - ntfy.coprocDev = false - ntfy.frames = 0 - ntfy.open = false - } -} - -func (ntfy *cartridgeEventNotification) tick() { - if ntfy.frames <= 0 { - return - } - ntfy.frames-- - - if ntfy.frames == 0 { - // always remain open if coprocessor development is active - if ntfy.coprocDev { - ntfy.open = true - } else { - switch ntfy.event { - case notifications.NotifySuperchargerSoundloadRewind: - ntfy.event = notifications.NotifySuperchargerSoundloadStarted - default: - ntfy.open = false - } - } - } -} - -func (ntfy *cartridgeEventNotification) draw(win *playScr) { - ntfy.tick() - if !ntfy.open { - return - } - - // notifications are made up of an icon and a sub-icon. icons must be from - // the gopher2600Icons font and the sub-icon from the largeFontAwesome font - icon := "" - secondaryIcon := "" - - useGopherFont := false - - plusrom := false - supercharger := false - coprocDev := false - - switch win.cartridgeNotice.event { - case notifications.NotifySuperchargerSoundloadStarted: - supercharger = true - useGopherFont = true - icon = fmt.Sprintf("%c", fonts.Tape) - secondaryIcon = fmt.Sprintf("%c", fonts.TapePlay) - case notifications.NotifySuperchargerSoundloadEnded: - supercharger = true - useGopherFont = true - icon = fmt.Sprintf("%c", fonts.Tape) - secondaryIcon = fmt.Sprintf("%c", fonts.TapeStop) - case notifications.NotifySuperchargerSoundloadRewind: - supercharger = true - useGopherFont = true - icon = fmt.Sprintf("%c", fonts.Tape) - secondaryIcon = fmt.Sprintf("%c", fonts.TapeRewind) - case notifications.NotifyPlusROMNetwork: - plusrom = true - useGopherFont = true - secondaryIcon = "" - icon = fmt.Sprintf("%c", fonts.Wifi) - default: - if ntfy.coprocDev { - coprocDev = true - useGopherFont = false - icon = fmt.Sprintf("%c", fonts.Developer) - } else { - return - } - } - - // check preferences and return if the notification is not to be displayed - if plusrom && !win.img.prefs.plusromNotifications.Get().(bool) { - return - } - if supercharger && !win.img.prefs.superchargerNotifications.Get().(bool) { - return - } - if coprocDev && !win.img.prefs.coprocDevNotification.Get().(bool) { - return - } - - dimen := win.img.plt.displaySize() - pos := imgui.Vec2{dimen[0], 0} - - width := win.img.fonts.gopher2600IconsSize * 1.5 - if secondaryIcon != "" { - width += win.img.fonts.largeFontAwesomeSize * 1.5 - } - - // position is based on which font we're using - if useGopherFont { - imgui.PushFont(win.img.fonts.gopher2600Icons) - pos.X -= win.img.fonts.gopher2600IconsSize * 1.2 - if secondaryIcon != "" { - pos.X -= win.img.fonts.largeFontAwesomeSize * 2.0 - } - } else { - imgui.PushFont(win.img.fonts.veryLargeFontAwesome) - pos.X -= win.img.fonts.veryLargeFontAwesomeSize - pos.X -= 20 - pos.Y += 10 - } - defer imgui.PopFont() - - imgui.SetNextWindowPos(pos) - imgui.PushStyleColor(imgui.StyleColorWindowBg, win.img.cols.Transparent) - imgui.PushStyleColor(imgui.StyleColorBorder, win.img.cols.Transparent) - defer imgui.PopStyleColorV(2) - - a := float32(win.img.prefs.notificationVisibility.Get().(float64)) - imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{a, a, a, a}) - defer imgui.PopStyleColor() - - imgui.BeginV("##cartridgeNotification", &ntfy.open, imgui.WindowFlagsAlwaysAutoResize| - imgui.WindowFlagsNoScrollbar|imgui.WindowFlagsNoTitleBar|imgui.WindowFlagsNoDecoration) - - imgui.Text(icon) - - imgui.SameLine() - - if secondaryIcon != "" { - // position sub-icon so that it is centered vertically with the main icon - dim := imgui.CursorScreenPos() - dim.Y += (win.img.fonts.gopher2600IconsSize - win.img.fonts.largeFontAwesomeSize) * 0.5 - imgui.SetCursorScreenPos(dim) - - imgui.PushFont(win.img.fonts.largeFontAwesome) - imgui.Text(secondaryIcon) - imgui.PopFont() - } - - imgui.End() -} diff --git a/gui/sdlimgui/playscr_overlay.go b/gui/sdlimgui/playscr_overlay.go new file mode 100644 index 00000000..26e962a6 --- /dev/null +++ b/gui/sdlimgui/playscr_overlay.go @@ -0,0 +1,481 @@ +// This file is part of Gopher2600. +// +// Gopher2600 is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Gopher2600 is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Gopher2600. If not, see . + +package sdlimgui + +import ( + "fmt" + "time" + + "github.com/inkyblackness/imgui-go/v4" + "github.com/jetsetilly/gopher2600/coprocessor/developer/dwarf" + "github.com/jetsetilly/gopher2600/debugger/govern" + "github.com/jetsetilly/gopher2600/gui/fonts" + "github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging" + "github.com/jetsetilly/gopher2600/notifications" +) + +type overlayDuration int + +const ( + overlayDurationPinned = -1 + overlayDurationOff = 0 + overlayDurationBrief = 30 + overlayDurationShort = 60 + overlayDurationLong = 90 +) + +// reduces the duration value. returns false if count has expired. if the +// duration has been "pinned" then value will return true +func (ct *overlayDuration) tick() bool { + if *ct == overlayDurationOff { + return false + } + if *ct == overlayDurationPinned { + return true + } + *ct = *ct - 1 + return true +} + +// returns true if duration is not off or pinned +func (ct *overlayDuration) running() bool { + return *ct == overlayDurationPinned || *ct > overlayDurationOff +} + +type playscrOverlay struct { + playscr *playScr + + // fps information is updated on every pule of the time.Ticker. the fps and + // hz string is updated at that time and then displayed on every draw() + fpsPulse *time.Ticker + fps string + hz string + + // top-left corner of the overlay includes emulation state. if the + // "fpsOverlay" is active then these will be drawn alongside the FPS + // information + state govern.State + subState govern.SubState + stateDur overlayDuration + + // events are user-activated events and require immediate feedback + event notifications.Notice + eventDur overlayDuration + + // icons in the top-left corner of the overlay are drawn according to a + // priority. the iconQueue list the icons to be drawn in order + iconQueue []rune + + // top-right corner of the overlay + cartridge notifications.Notice + cartridgeDur overlayDuration + + // bottom-left corner of the overlay + leftPort plugging.PeripheralID + leftPortDur overlayDuration + + // bottom-right corner of the overlay + rightPort plugging.PeripheralID + rightPortDur overlayDuration + + // visibility of icons is set from the preferences once per draw() + visibility float32 +} + +const overlayPadding = 10 + +func (oly *playscrOverlay) set(v any, args ...any) { + switch n := v.(type) { + case plugging.PortID: + switch n { + case plugging.PortLeft: + oly.leftPort = args[0].(plugging.PeripheralID) + oly.leftPortDur = overlayDurationShort + case plugging.PortRight: + oly.rightPort = args[0].(plugging.PeripheralID) + oly.rightPortDur = overlayDurationShort + } + case notifications.Notice: + switch n { + case notifications.NotifySuperchargerSoundloadStarted: + oly.cartridge = n + oly.cartridgeDur = overlayDurationPinned + case notifications.NotifySuperchargerSoundloadEnded: + oly.cartridge = n + oly.cartridgeDur = overlayDurationShort + case notifications.NotifySuperchargerSoundloadRewind: + return + + case notifications.NotifyPlusROMNetwork: + oly.cartridge = n + oly.cartridgeDur = overlayDurationShort + + case notifications.NotifyScreenshot: + oly.event = n + oly.eventDur = overlayDurationShort + + default: + return + } + } +} + +func (oly *playscrOverlay) draw() { + imgui.PushStyleColor(imgui.StyleColorWindowBg, oly.playscr.img.cols.Transparent) + imgui.PushStyleColor(imgui.StyleColorBorder, oly.playscr.img.cols.Transparent) + defer imgui.PopStyleColorV(2) + + imgui.PushStyleVarVec2(imgui.StyleVarWindowPadding, imgui.Vec2{}) + defer imgui.PopStyleVarV(1) + + imgui.SetNextWindowPos(imgui.Vec2{0, 0}) + imgui.BeginV("##playscrOverlay", nil, imgui.WindowFlagsAlwaysAutoResize| + imgui.WindowFlagsNoScrollbar|imgui.WindowFlagsNoTitleBar| + imgui.WindowFlagsNoDecoration|imgui.WindowFlagsNoSavedSettings| + imgui.WindowFlagsNoBringToFrontOnFocus) + defer imgui.End() + + oly.visibility = float32(oly.playscr.img.prefs.notificationVisibility.Get().(float64)) + + oly.drawTopLeft() + oly.drawTopRight() + oly.drawBottomLeft() + oly.drawBottomRight() +} + +// information in the top left corner of the overlay are about the emulation. +// eg. whether audio is mute, or the emulation is paused, etc. it is also used +// to display the FPS counter and other TV information +func (oly *playscrOverlay) drawTopLeft() { + pos := imgui.CursorScreenPos() + pos.X += overlayPadding + pos.Y += overlayPadding + + // by default only one icon is shown in the top left corner. however, if the + // FPS overlay is being used we use the space to draw smaller icons + var useIconQueue bool + + // draw FPS information if it's enabled + if oly.playscr.img.prefs.fpsDetail.Get().(bool) { + // it's easier if we put topleft of overlay in a window because the window + // will control the width and positioning automatically. if we don't then + // the horizntal rules will stretch the width of the screen and each new line of + // text in the fps detail will need to be repositioned for horizontal + // padding + imgui.SetNextWindowPos(pos) + imgui.BeginV("##fpsDetail", nil, imgui.WindowFlagsAlwaysAutoResize| + imgui.WindowFlagsNoScrollbar|imgui.WindowFlagsNoTitleBar| + imgui.WindowFlagsNoDecoration|imgui.WindowFlagsNoSavedSettings| + imgui.WindowFlagsNoBringToFrontOnFocus) + defer imgui.End() + + select { + case <-oly.fpsPulse.C: + fps, hz := oly.playscr.img.dbg.VCS().TV.GetActualFPS() + oly.fps = fmt.Sprintf("%03.2f fps", fps) + oly.hz = fmt.Sprintf("%03.2fhz", hz) + default: + } + + imgui.Text(fmt.Sprintf("Emulation: %s", oly.fps)) + fr := imgui.CurrentIO().Framerate() + if fr == 0.0 { + imgui.Text("Rendering: waiting") + } else { + imgui.Text(fmt.Sprintf("Rendering: %03.2f fps", fr)) + } + + imguiSeparator() + + if coproc := oly.playscr.img.cache.VCS.Mem.Cart.GetCoProc(); coproc != nil { + clk := float32(oly.playscr.img.dbg.VCS().Env.Prefs.ARM.Clock.Get().(float64)) + imgui.Text(fmt.Sprintf("%s Clock: %.0f Mhz", coproc.ProcessorID(), clk)) + imguiSeparator() + } + + imgui.Text(fmt.Sprintf("%.1fx scaling", oly.playscr.yscaling)) + imgui.Text(fmt.Sprintf("%d total scanlines", oly.playscr.scr.crit.frameInfo.TotalScanlines)) + + imguiSeparator() + imgui.Text(oly.playscr.img.screen.crit.frameInfo.Spec.ID) + + imgui.SameLine() + imgui.Text(oly.hz) + if !oly.playscr.scr.crit.frameInfo.VSync { + imgui.SameLine() + imgui.Text(string(fonts.NoVSYNC)) + } + + imguiSeparator() + imgui.Text(fmt.Sprintf("%d frame input lag", oly.playscr.scr.crit.frameQueueLen)) + if oly.playscr.scr.nudgeIconCt > 0 { + imgui.SameLine() + imgui.Text(string(fonts.Nudge)) + } + + // create space in the window for any icons that we might want to draw. + // what's good about this is that it makes sure that the window is large + // enough from frame-to-frame. without this, there will be a visble + // delay when the window is resized + imgui.Spacing() + p := imgui.CursorScreenPos() + imgui.Text("") + imgui.SetCursorScreenPos(p) + + // draw developer icon if BorrowSource() returns a non-nil value + oly.playscr.img.dbg.CoProcDev.BorrowSource(func(src *dwarf.Source) { + if src != nil { + imgui.Text(string(fonts.Developer)) + imgui.SameLine() + } + }) + + // we can draw multiple icons if required + useIconQueue = true + + } else { + // we'll only be drawing one icon so we only need to set the cursor + // position once, so there's no need for a window as would be the case + // if fps detail was activated + imgui.SetCursorScreenPos(pos) + + // FPS overlay is not active so we increase the font size for any icons + // that may be drawn hereafter in this window + imgui.PushFont(oly.playscr.img.fonts.veryLargeFontAwesome) + defer imgui.PopFont() + + // add visibility adjustment if there is no FPS overlay + imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{oly.visibility, oly.visibility, oly.visibility, oly.visibility}) + defer imgui.PopStyleColor() + } + + // start a new icons queue + oly.iconQueue = oly.iconQueue[:0] + + // mute is likely to be the icon visible the longest so has the lowest priority + if oly.playscr.img.prefs.audioMutePlaymode.Get().(bool) && oly.playscr.img.prefs.audioMuteNotification.Get().(bool) { + oly.iconQueue = append(oly.iconQueue, fonts.AudioMute) + } + + // the real current state as set by the emulation is used to decide what + // state to use for the overlay icon. we need to be careful with this + // decision and to only update the state/substate when a suitable duration + // has passed + state := oly.playscr.img.dbg.State() + subState := oly.playscr.img.dbg.SubState() + switch state { + case govern.Paused: + if state != oly.state && !oly.stateDur.running() { + oly.state = state + oly.subState = subState + oly.stateDur = overlayDurationPinned + } + case govern.Running: + if state != oly.state { + oly.state = state + oly.subState = subState + oly.stateDur = overlayDurationShort + } + case govern.Rewinding: + oly.state = state + oly.subState = subState + + // refresh how the hold duration on every render frame that the + // rewinding state is seen. this is so that the duration of the rewind + // icon doesn't expire causing the pause icon to appear every so often + // + // (the way rewinding is implemented in the emulation means that the + // rewinding state is interspersed very quickly with the paused state. + // that works great for internal emulation purposes but requires careful + // handling for UI purposes) + oly.stateDur = overlayDurationBrief + } + + // the state duration is ticked and the currently decided state shown unless + // the tick has expired (returns false) + if oly.stateDur.tick() { + switch oly.state { + case govern.Paused: + switch oly.subState { + case govern.PausedAtStart: + oly.iconQueue = append(oly.iconQueue, fonts.EmulationPausedAtStart) + case govern.PausedAtEnd: + oly.iconQueue = append(oly.iconQueue, fonts.EmulationPausedAtEnd) + default: + oly.iconQueue = append(oly.iconQueue, fonts.EmulationPause) + } + case govern.Running: + oly.iconQueue = append(oly.iconQueue, fonts.EmulationRun) + case govern.Rewinding: + switch oly.subState { + case govern.RewindingBackwards: + oly.iconQueue = append(oly.iconQueue, fonts.EmulationRewindBack) + case govern.RewindingForwards: + oly.iconQueue = append(oly.iconQueue, fonts.EmulationRewindForward) + default: + } + } + } + + // events have the highest priority. we can think of these as user activated + // events, such as the triggering of a screenshot. we therefore want to give + // the user confirmation feedback immediately over other icons + if oly.eventDur.tick() { + switch oly.event { + case notifications.NotifyScreenshot: + oly.iconQueue = append(oly.iconQueue, fonts.Camera) + } + } + + // draw only the last (ie. most important) icon unless the icon queue flag + // has been set + if !useIconQueue { + if len(oly.iconQueue) > 0 { + imgui.Text(string(oly.iconQueue[len(oly.iconQueue)-1])) + } + return + } + + // draw icons in order of priority + for _, i := range oly.iconQueue { + imgui.Text(string(i)) + imgui.SameLine() + } + return +} + +// information in the top right of the overlay is about the cartridge. ie. +// information from the cartridge about what is happening. for example, +// supercharger tape activity, or PlusROM network activity, etc. +func (oly *playscrOverlay) drawTopRight() { + if !oly.cartridgeDur.tick() { + return + } + + var icon string + var secondaryIcon string + + switch oly.cartridge { + case notifications.NotifySuperchargerSoundloadStarted: + if oly.playscr.img.prefs.superchargerNotifications.Get().(bool) { + icon = fmt.Sprintf("%c", fonts.Tape) + secondaryIcon = fmt.Sprintf("%c", fonts.TapePlay) + } + case notifications.NotifySuperchargerSoundloadEnded: + if oly.playscr.img.prefs.superchargerNotifications.Get().(bool) { + icon = fmt.Sprintf("%c", fonts.Tape) + secondaryIcon = fmt.Sprintf("%c", fonts.TapeStop) + } + case notifications.NotifySuperchargerSoundloadRewind: + if oly.playscr.img.prefs.superchargerNotifications.Get().(bool) { + icon = fmt.Sprintf("%c", fonts.Tape) + secondaryIcon = fmt.Sprintf("%c", fonts.TapeRewind) + } + case notifications.NotifyPlusROMNetwork: + if oly.playscr.img.prefs.plusromNotifications.Get().(bool) { + icon = fmt.Sprintf("%c", fonts.Wifi) + } + default: + return + } + + pos := imgui.Vec2{oly.playscr.img.plt.displaySize()[0], 0} + pos.X -= oly.playscr.img.fonts.gopher2600IconsSize + overlayPadding + if secondaryIcon != "" { + pos.X -= oly.playscr.img.fonts.largeFontAwesomeSize * 2 + } + + imgui.PushFont(oly.playscr.img.fonts.gopher2600Icons) + defer imgui.PopFont() + + imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{oly.visibility, oly.visibility, oly.visibility, oly.visibility}) + defer imgui.PopStyleColor() + + imgui.SetCursorScreenPos(pos) + imgui.Text(icon) + + if secondaryIcon != "" { + imgui.PushFont(oly.playscr.img.fonts.largeFontAwesome) + defer imgui.PopFont() + + imgui.SameLine() + pos = imgui.CursorScreenPos() + pos.Y += (oly.playscr.img.fonts.gopher2600IconsSize - oly.playscr.img.fonts.largeFontAwesomeSize) * 0.5 + + imgui.SetCursorScreenPos(pos) + imgui.Text(secondaryIcon) + } +} + +func (oly *playscrOverlay) drawBottomLeft() { + if !oly.leftPortDur.tick() { + return + } + + if !oly.playscr.img.prefs.controllerNotifcations.Get().(bool) { + return + } + + pos := imgui.Vec2{0, oly.playscr.img.plt.displaySize()[1]} + pos.X += overlayPadding + pos.Y -= oly.playscr.img.fonts.gopher2600IconsSize + overlayPadding + + imgui.SetCursorScreenPos(pos) + oly.drawPeripheral(oly.leftPort) +} + +func (oly *playscrOverlay) drawBottomRight() { + if !oly.rightPortDur.tick() { + return + } + + if !oly.playscr.img.prefs.controllerNotifcations.Get().(bool) { + return + } + + d := oly.playscr.img.plt.displaySize() + pos := imgui.Vec2{d[0], d[1]} + pos.X -= oly.playscr.img.fonts.gopher2600IconsSize + overlayPadding + pos.Y -= oly.playscr.img.fonts.gopher2600IconsSize + overlayPadding + + imgui.SetCursorScreenPos(pos) + oly.drawPeripheral(oly.rightPort) +} + +// drawPeripheral is used to draw the peripheral in the bottom left and bottom +// right corners of the overlay +func (oly *playscrOverlay) drawPeripheral(peripID plugging.PeripheralID) { + imgui.PushFont(oly.playscr.img.fonts.gopher2600Icons) + defer imgui.PopFont() + + imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{oly.visibility, oly.visibility, oly.visibility, oly.visibility}) + defer imgui.PopStyleColor() + + switch peripID { + case plugging.PeriphStick: + imgui.Text(fmt.Sprintf("%c", fonts.Stick)) + case plugging.PeriphPaddles: + imgui.Text(fmt.Sprintf("%c", fonts.Paddle)) + case plugging.PeriphKeypad: + imgui.Text(fmt.Sprintf("%c", fonts.Keypad)) + case plugging.PeriphSavekey: + imgui.Text(fmt.Sprintf("%c", fonts.Savekey)) + case plugging.PeriphGamepad: + imgui.Text(fmt.Sprintf("%c", fonts.Gamepad)) + case plugging.PeriphAtariVox: + imgui.Text(fmt.Sprintf("%c", fonts.AtariVox)) + } +} diff --git a/gui/sdlimgui/preferences.go b/gui/sdlimgui/preferences.go index 1882a5bd..b5cf0eca 100644 --- a/gui/sdlimgui/preferences.go +++ b/gui/sdlimgui/preferences.go @@ -51,7 +51,7 @@ type preferences struct { // playmode preferences audioMutePlaymode prefs.Bool - fpsOverlay prefs.Bool + fpsDetail prefs.Bool activePause prefs.Bool // playmode notifications @@ -59,7 +59,6 @@ type preferences struct { plusromNotifications prefs.Bool superchargerNotifications prefs.Bool audioMuteNotification prefs.Bool - coprocDevNotification prefs.Bool notificationVisibility prefs.Float // fonts @@ -91,15 +90,13 @@ func newPreferences(img *SdlImgui) (*preferences, error) { p.showTooltips.Set(true) p.showTimelineThumbnail.Set(false) p.colorDisasm.Set(true) - p.fpsOverlay.Set(false) + p.fpsDetail.Set(false) p.activePause.Set(false) p.audioMutePlaymode.Set(false) p.controllerNotifcations.Set(true) p.plusromNotifications.Set(true) p.superchargerNotifications.Set(true) p.audioMuteNotification.Set(true) - p.coprocDevNotification.Set(true) - p.coprocDevNotification.Set(true) p.notificationVisibility.Set(0.75) p.guiFontSize.Set(13) p.terminalFontSize.Set(12) @@ -141,7 +138,7 @@ func newPreferences(img *SdlImgui) (*preferences, error) { // debugger audio mute options later // playmode options - err = p.dsk.Add("sdlimgui.playmode.fpsOverlay", &p.fpsOverlay) + err = p.dsk.Add("sdlimgui.playmode.fpsDetail", &p.fpsDetail) if err != nil { return nil, err } @@ -165,10 +162,6 @@ func newPreferences(img *SdlImgui) (*preferences, error) { if err != nil { return nil, err } - err = p.dsk.Add("sdlimgui.playmode.coprocDevNotification", &p.coprocDevNotification) - if err != nil { - return nil, err - } err = p.dsk.Add("sdlimgui.playmode.notifcationVisibility", &p.notificationVisibility) if err != nil { return nil, err @@ -263,7 +256,7 @@ func newPreferences(img *SdlImgui) (*preferences, error) { if err != nil { return nil, err } - err = p.saveOnExitDsk.Add("sdlimgui.playmode.fpsOverlay", &p.fpsOverlay) + err = p.saveOnExitDsk.Add("sdlimgui.playmode.fpsDetail", &p.fpsDetail) if err != nil { return nil, err } diff --git a/gui/sdlimgui/requests.go b/gui/sdlimgui/requests.go index 32ad9e6c..5863538f 100644 --- a/gui/sdlimgui/requests.go +++ b/gui/sdlimgui/requests.go @@ -23,7 +23,6 @@ import ( "github.com/jetsetilly/gopher2600/coprocessor/developer/dwarf" "github.com/jetsetilly/gopher2600/debugger/govern" "github.com/jetsetilly/gopher2600/gui" - "github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging" "github.com/jetsetilly/gopher2600/notifications" ) @@ -70,38 +69,22 @@ func (img *SdlImgui) serviceSetFeature(request featureRequest) { img.plt.setFullScreen(request.args[0].(bool)) } - case gui.ReqPeripheralNotify: + case gui.ReqPeripheralPlugged: err = argLen(request.args, 2) if err == nil { - port := request.args[0].(plugging.PortID) - switch port { - case plugging.PortLeft: - img.playScr.peripheralLeft.set(request.args[1].(plugging.PeripheralID)) - case plugging.PortRight: - img.playScr.peripheralRight.set(request.args[1].(plugging.PeripheralID)) - } + img.playScr.overlay.set(request.args[0], request.args[1]) } - case gui.ReqEmulationNotify: - if img.isPlaymode() { - err = argLen(request.args, 1) - if err == nil { - img.playScr.emulationNotice.set(request.args[0].(notifications.Notice)) - } - } - - case gui.ReqCartridgeNotify: + case gui.ReqNotification: err = argLen(request.args, 1) if err == nil { - notice := request.args[0].(notifications.Notice) - - switch notice { - case notifications.NotifyPlusROMNewInstallation: + switch request.args[0].(notifications.Notice) { + case notifications.NotifyPlusROMNewInstall: img.modal = modalPlusROMFirstInstallation case notifications.NotifyUnsupportedDWARF: img.modal = modalUnsupportedDWARF default: - img.playScr.cartridgeNotice.set(notice) + img.playScr.overlay.set(request.args[0].(notifications.Notice)) } } diff --git a/gui/sdlimgui/sdlimgui.go b/gui/sdlimgui/sdlimgui.go index 70ee65d9..7bb2c671 100644 --- a/gui/sdlimgui/sdlimgui.go +++ b/gui/sdlimgui/sdlimgui.go @@ -27,7 +27,6 @@ import ( "github.com/jetsetilly/gopher2600/gui/sdlaudio" "github.com/jetsetilly/gopher2600/gui/sdlimgui/caching" "github.com/jetsetilly/gopher2600/logger" - "github.com/jetsetilly/gopher2600/notifications" "github.com/jetsetilly/gopher2600/prefs" "github.com/jetsetilly/gopher2600/reflection" "github.com/jetsetilly/gopher2600/resources" @@ -311,7 +310,7 @@ func (img *SdlImgui) quit() { } // end program. this differs from quit in that this function is called when we -// receive a ReqEnd, which *may* have been sent in reponse to a EventQuit. +// receive a ReqEnd, which *may* have been sent in reponse to a userinput.EventQuit func (img *SdlImgui) end() { img.prefs.saveWindowPreferences() } @@ -411,11 +410,6 @@ func (img *SdlImgui) applyAudioMutePreference() { if img.isPlaymode() { mute = img.prefs.audioMutePlaymode.Get().(bool) - if mute { - img.playScr.emulationNotice.set(notifications.NotifyMute) - } else { - img.playScr.emulationNotice.set(notifications.NotifyUnmute) - } img.dbg.VCS().RIOT.Ports.MutePeripherals(mute) } else { mute = img.prefs.audioMuteDebugger.Get().(bool) diff --git a/gui/sdlimgui/service_keyboard.go b/gui/sdlimgui/service_keyboard.go index 497f4158..5f28fe49 100644 --- a/gui/sdlimgui/service_keyboard.go +++ b/gui/sdlimgui/service_keyboard.go @@ -140,7 +140,8 @@ func (img *SdlImgui) serviceKeyboard(ev *sdl.KeyboardEvent) { case sdl.SCANCODE_F7: if img.isPlaymode() { - img.playScr.toggleFPS() + fps := img.prefs.fpsDetail.Get().(bool) + img.prefs.fpsDetail.Set(!fps) } case sdl.SCANCODE_F8: @@ -164,8 +165,7 @@ func (img *SdlImgui) serviceKeyboard(ev *sdl.KeyboardEvent) { } else { img.screenshot(modeSingle, "") } - - img.playScr.emulationNotice.set(notifications.NotifyScreenshot) + img.playScr.overlay.set(notifications.NotifyScreenshot) case sdl.SCANCODE_F14: fallthrough diff --git a/gui/sdlimgui/win_prefs.go b/gui/sdlimgui/win_prefs.go index 6418d498..b9446029 100644 --- a/gui/sdlimgui/win_prefs.go +++ b/gui/sdlimgui/win_prefs.go @@ -211,10 +211,9 @@ a television image that is sympathetic to the display kernel of the ROM.`) imgui.Spacing() - if imgui.CollapsingHeader("Notifications") { - + if imgui.CollapsingHeader("Notification Icons") { controllerNotifications := win.img.prefs.controllerNotifcations.Get().(bool) - if imgui.Checkbox("Controller Change", &controllerNotifications) { + if imgui.Checkbox("Controller Changes", &controllerNotifications) { win.img.prefs.controllerNotifcations.Set(controllerNotifications) } @@ -233,11 +232,6 @@ of the ROM.`) win.img.prefs.audioMuteNotification.Set(audioMuteNotification) } - coprocDevNotification := win.img.prefs.coprocDevNotification.Get().(bool) - if imgui.Checkbox("Coprocessor Development", &coprocDevNotification) { - win.img.prefs.coprocDevNotification.Set(coprocDevNotification) - } - visibility := float32(win.img.prefs.notificationVisibility.Get().(float64)) * 100 if imgui.SliderFloatV("Visibility", &visibility, 0.0, 100.0, "%.0f%%", imgui.SliderFlagsNone) { win.img.prefs.notificationVisibility.Set(visibility / 100) diff --git a/hardware/memory/cartridge/plusrom/plusrom.go b/hardware/memory/cartridge/plusrom/plusrom.go index e72cb310..393375dc 100644 --- a/hardware/memory/cartridge/plusrom/plusrom.go +++ b/hardware/memory/cartridge/plusrom/plusrom.go @@ -139,9 +139,11 @@ func NewPlusROM(env *environment.Environment, child mapper.CartMapper) (mapper.C // log success logger.Logf("plusrom", "will connect to %s", cart.net.ai.String()) - err := cart.env.Notifications.Notify(notifications.NotifyPlusROMInserted) - if err != nil { - return nil, fmt.Errorf("plusrom %w:", err) + if cart.env.Prefs.PlusROM.NewInstallation { + err := cart.env.Notifications.Notify(notifications.NotifyPlusROMNewInstall) + if err != nil { + return nil, fmt.Errorf("plusrom %w:", err) + } } return cart, nil diff --git a/hardware/run.go b/hardware/run.go index 7d25fe47..87cfb67a 100644 --- a/hardware/run.go +++ b/hardware/run.go @@ -85,7 +85,7 @@ func (vcs *VCS) Run(continueCheck func() (govern.State, error)) error { } case govern.Paused: default: - return fmt.Errorf("vcs: unsupported emulation state (%d) in Run() function", state) + return fmt.Errorf("vcs: unsupported emulation state (%s) in Run() function", state) } state, err = continueCheck() diff --git a/notifications/doc.go b/notifications/doc.go index eca7ac8d..6d86cc29 100644 --- a/notifications/doc.go +++ b/notifications/doc.go @@ -21,4 +21,16 @@ // event that has happened (eg. tape stopped, etc.) For some notifications // however, it is appropriate for the emulation instance to deal with the // notification invisibly. +// +// Finally, some notifications are not generated by the cartridge but by the +// emulation instance, and sent to the GUI. +// +// We can therefore think of the flow of notifications in the following manner: +// +// (a) (b) (c) +// Cartridge -----> Emulation Instance -----> GUI ---- +// 1 2 \____| 3 +// +// The diagram also suggests that a GUI can both produce and receive a +// notification. This is possible and sometimes convenient to do. package notifications diff --git a/notifications/notifications.go b/notifications/notifications.go index 00e8ad60..c334035e 100644 --- a/notifications/notifications.go +++ b/notifications/notifications.go @@ -22,47 +22,27 @@ type Notice string // List of defined notifications. const ( - NotifyInitialising Notice = "NotifyInitialising" - NotifyPause Notice = "NotifyPause" - NotifyRun Notice = "NotifyRun" - NotifyRewindBack Notice = "NotifyRewindBack" - NotifyRewindFoward Notice = "NotifyRewindFoward" - NotifyRewindAtStart Notice = "NotifyRewindAtStart" - NotifyRewindAtEnd Notice = "NotifyRewindAtEnd" - NotifyScreenshot Notice = "NotifyScreenshot" - NotifyMute Notice = "NotifyMute" - NotifyUnmute Notice = "NotifyUnmute" + // a screen shot is taking place + NotifyScreenshot Notice = "NotifyScreenshot" - // If Supercharger is loading from a fastload binary then this event is + // notifications sent when supercharger is loading from a sound file (eg. mp3 file) + NotifySuperchargerSoundloadStarted Notice = "NotifySuperchargerSoundloadStarted" + NotifySuperchargerSoundloadEnded Notice = "NotifySuperchargerSoundloadEnded" + NotifySuperchargerSoundloadRewind Notice = "NotifySuperchargerSoundloadRewind" + + // if Supercharger is loading from a fastload binary then this event is // raised when the ROM requests the next block be loaded from the "tape NotifySuperchargerFastload Notice = "NotifySuperchargerFastload" - // If Supercharger is loading from a sound file (eg. mp3 file) then these - // events area raised when the loading has started and ended. - NotifySuperchargerSoundloadStarted Notice = "NotifySuperchargerSoundloadStarted" - NotifySuperchargerSoundloadEnded Notice = "NotifySuperchargerSoundloadEnded" + // notifications sent by plusrom + NotifyPlusROMNewInstall Notice = "NotifyPlusROMNewInstall" + NotifyPlusROMNetwork Notice = "NotifyPlusROMNetwork" - // tape is rewinding. - NotifySuperchargerSoundloadRewind Notice = "NotifySuperchargerSoundloadRewind" - - // PlusROM cartridge has been inserted. - NotifyPlusROMInserted Notice = "NotifyPlusROMInserted" - - // PlusROM network activity. - NotifyPlusROMNetwork Notice = "NotifyPlusROMNetwork" - - // PlusROM new installation - NotifyPlusROMNewInstallation Notice = "NotifyPlusROMNewInstallation" - - // Moviecart started + // moviecart has started NotifyMovieCartStarted Notice = "NotifyMoveCartStarted" // unsupported DWARF data NotifyUnsupportedDWARF Notice = "NotifyUnsupportedDWARF" - - // coprocessor development information has been loaded - NotifyCoprocDevStarted Notice = "NotifyCoprocDevStarted" - NotifyCoprocDevEnded Notice = "NotifyCoprocDevEnded" ) // Notify is used for direct communication between a the hardware and the diff --git a/prefs/defunct.go b/prefs/defunct.go index 9258798c..b7554c40 100644 --- a/prefs/defunct.go +++ b/prefs/defunct.go @@ -66,6 +66,8 @@ var defunct = []string{ "crt.syncPowerOn", "crt.syncSpeed", "crt.syncSensitivity", + "sdlimgui.playmode.coprocDevNotification", + "sdlimgui.playmode.fpsOverlay", } // returns true if string is in list of defunct values.