simplified how gui implements and handles notifications

debugger no longer sends play, pause notifications to the gui. the gui
polls for that information as required

govern package now has SubState type to complement the State type.
StateIntegrity() function enforces combinations of State and SubState,
called from debugger.setState() function

playmode notifications reworked and contained in a single playmode_overlay.go
file. this includes the FPS and screen detail

preference value sdlimgui.playmode.fpsOverlay replaced with
sdlimgui.playmode.fpsDetail. still toggled with F7 key

coproc icon moved to top-left corner of playmode overlay and only
visible when FPS detail is showing

when FPS detail is showing multiple (small) icons care shown. when it is
not showing, a single (large) icon is shown according to the priority of
the icon. eg. pause indicator has higher priority than the mute
indicator
This commit is contained in:
JetSetIlly 2024-04-07 09:22:29 +01:00
parent 877f282b8b
commit d32262adff
23 changed files with 709 additions and 729 deletions

View file

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

View file

@ -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)
})

View file

@ -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)
})

View file

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

View file

@ -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

View file

@ -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)
}
}

View file

@ -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:

View file

@ -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)
})

View file

@ -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'

View file

@ -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

View file

@ -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.

View file

@ -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 <https://www.gnu.org/licenses/>.
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()
}

View file

@ -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 <https://www.gnu.org/licenses/>.
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))
}
}

View file

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

View file

@ -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))
}
}

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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.