mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-05-20 13:48:02 -04:00
d32262adff
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
482 lines
15 KiB
Go
482 lines
15 KiB
Go
// 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))
|
|
}
|
|
}
|