Gopher2600/debugger/loop_debugger.go
JetSetIlly d32262adff 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
2024-04-12 18:20:29 +01:00

637 lines
19 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 debugger
import (
"errors"
"io"
"github.com/jetsetilly/gopher2600/debugger/govern"
"github.com/jetsetilly/gopher2600/debugger/script"
"github.com/jetsetilly/gopher2600/debugger/terminal"
"github.com/jetsetilly/gopher2600/hardware/cpu"
"github.com/jetsetilly/gopher2600/logger"
)
// unwindLoop is called whenever it is required that the inputLoop/catchupLoop
// converge on the next instruction boundary. in other words, if the emulation
// is between CPU instructions, the loops must be unwound so that the emulation
// can continue safely.
//
// generally, this means that unwindLoop should be called whenever a rewind
// function is being called.
//
// note that the debugger state is not changed by this function. it is up to
// the caller of the function to set govern.State appropriately.
func (dbg *Debugger) unwindLoop(onRestart func() error) {
dbg.unwindLoopRestart = onRestart
}
// catchupLoop is a special purpose loop designed to run inside of the inputLoop. it is called only
// when catchupContinue has been set in CatchUpLoop(), which is called as a consequence of a rewind event.
func (dbg *Debugger) catchupLoop(inputter terminal.Input) error {
var ended bool
callback := func(isCycle bool) error {
if dbg.unwindLoopRestart != nil {
return nil
}
// not updating disassembly. the halt condition in the inputLoop will
// give us an opportunity to update *even if the catchupLoop is still
// executing*
// we do need to update the reflection however
err := dbg.ref.Step(dbg.liveBankInfo)
if err != nil {
return err
}
dbg.counter.Step(1, dbg.liveBankInfo)
if ended {
if !dbg.vcs.CPU.LastResult.Final {
// if we're in the rewinding state then a new rewind event has
// started and we must return immediately so that it can continue...
if dbg.State() == govern.Rewinding {
return nil
}
// ...otherwise catchup has ended but we've not reached a CPU
// instruction boundary then continue with nonInstructionQuantum loop.
// for QuantumCycle we need to pay attention to the isCycle flag
switch dbg.Quantum() {
case govern.QuantumInstruction:
return dbg.inputLoop(inputter, true)
case govern.QuantumCycle:
if isCycle {
return dbg.inputLoop(inputter, true)
}
case govern.QuantumClock:
return dbg.inputLoop(inputter, true)
}
}
} else if dbg.catchupContinue != nil && !dbg.catchupContinue() {
ended = true
dbg.catchupEnd()
// small optimisation if the catchup context is rewind to frame. we
// never want to call the input loop in this case
if dbg.catchupContext == catchupRewindToFrame {
return nil
}
// if quantum if QuantumInstruction and we're not at the end of the
// instruction then we never want to call the input loop
if dbg.Quantum() == govern.QuantumInstruction && !dbg.vcs.CPU.LastResult.Final {
return nil
}
return dbg.inputLoop(inputter, true)
}
return nil
}
// loop until the ended flag is false.
//
// there are a couple of additional conditions we need to be careful of in
// this loop. first is what happens when a new cartridge is inserted or the
// machine is otherwise reset. in those situations the CPU may be in an
// illegal state so we need to (a) check for the cpu.ResetMidInstruction
// sentinal error; and (b) whether the CPU has the HasReset() flag raised.
// in both situations the loop is ended early
for !ended {
dbg.liveBankInfo = dbg.vcs.Mem.Cart.GetBank(dbg.vcs.CPU.PC.Address())
// coords of CPU instruction before calling vcs.Step()
if dbg.vcs.CPU.RdyFlg {
dbg.cpuBoundaryLastInstruction = dbg.vcs.TV.GetCoords()
}
err := dbg.vcs.Step(callback)
if err != nil {
if errors.Is(err, cpu.ResetMidInstruction) {
return nil
}
return err
}
if dbg.vcs.CPU.HasReset() {
return nil
}
// update disassembly after every CPU instruction. even during a catch
// up we need to do this.
dbg.liveDisasmEntry = dbg.Disasm.ExecutedEntry(dbg.liveBankInfo, dbg.vcs.CPU.LastResult, true, dbg.vcs.CPU.PC.Value())
// make sure reflection has been updated at the end of the instruction
if err = dbg.ref.Step(dbg.liveBankInfo); err != nil {
return err
}
dbg.counter.Step(1, dbg.liveBankInfo)
if dbg.unwindLoopRestart != nil {
return nil
}
}
return nil
}
// inputLoop has two modes, defined by the nonInstructionQuantum argument.
func (dbg *Debugger) inputLoop(inputter terminal.Input, nonInstructionQuantum bool) error {
var err error
for dbg.running {
if dbg.Mode() != govern.ModeDebugger {
return nil
}
if dbg.catchupContinue != nil {
if nonInstructionQuantum {
panic("refusing to run catchup loop inside a nonInstructionQuantum step")
}
err = dbg.catchupLoop(inputter)
if err != nil {
return err
}
if dbg.unwindLoopRestart != nil {
return nil
}
}
// checkTerm is used to decide whether to perform a full call to TermRead() and to potentially
// halt the inputLoop - which we don't want to do unless there is something to process
//
// it will be false unless TermReadCheck() returns true
var checkTerm bool
// the select will take the eventCheckPulse channel if there is a tick
// waiting only when we reach this point in the loop. it will not delay
// the loop if the tick has not happened yet
select {
case <-dbg.readEventsPulse.C:
err = dbg.readEventsHandler()
if err != nil {
if errors.Is(err, terminal.UserInterrupt) {
dbg.handleInterrupt(inputter)
} else if errors.Is(err, terminal.UserSignal) {
return err
} else {
dbg.printLine(terminal.StyleError, "%s", err)
}
}
// emulation has been put into a different mode. exit loop immediately
if dbg.Mode() != govern.ModeDebugger {
return nil
}
// if debugger is no longer running after checking interrupts and
// events then break for loop
if !dbg.running {
break // dbg.running loop
}
// unwindLoopRestart or catchupContinue may have been set as a result
// of readEventsHandler()
if dbg.unwindLoopRestart != nil {
return nil
}
if dbg.catchupContinue != nil {
continue // dbg.running loop
}
checkTerm = inputter.TermReadCheck()
default:
}
// return immediately if this inputLoop() is a nonInstructionQuantum
// AND the prevailing quantum mode has been changed to instruction AND
// the emulation has been asked to continue (eg. with STEP)
//
// this is important in a very specific situation:
//
// a) the emulation has been in nonInstrucution quantum mode (eg. CLOCK)
// b) it is mid-way through a single CPU instruction
// c) the debugger has been changed to INSTRUCTION quantum mode
//
// if we don't do this then debugging output will be wrong and confusing.
if nonInstructionQuantum && dbg.continueEmulation && dbg.Quantum() == govern.QuantumInstruction {
return nil
}
// check trace and output in context of last CPU result
//
// unlike halt conditions, I don't believe there is any need to do
// check every color clock
trace := dbg.traces.check()
if trace != "" {
if dbg.commandOnTrace != nil {
err := dbg.processTokensList(dbg.commandOnTrace)
if err != nil {
dbg.printLine(terminal.StyleError, "%s", err)
}
}
dbg.printLine(terminal.StyleFeedback, trace)
}
// bring all the halt conditions together
halt := dbg.halting.halt || !dbg.runUntilHalt || dbg.haltImmediately || dbg.lastStepError
// reset last step error
dbg.lastStepError = false
if halt {
// check that dbg.running hasn't been unset while we've been
// waiting for the halt condition.
//
// this can sometimes happen if the QUIT event is sent whilst the
// emulation is halted mid CPU instruction
if !dbg.running {
break // dbg.running loop
}
// if this is a nonInstructionQuantum step and we've reach this stage then we need to
// update the disassembly. we do not update the nextAddr however
if nonInstructionQuantum {
dbg.liveBankInfo = dbg.vcs.Mem.Cart.GetBank(dbg.vcs.CPU.PC.Address())
dbg.liveDisasmEntry = dbg.Disasm.ExecutedEntry(dbg.liveBankInfo, dbg.vcs.CPU.LastResult, false, 0)
}
// always clear volatile breakpoints/traps. if the emulation has halted for any
// reason then any existing step trap is stale.
dbg.halting.volatileBreakpoints.clear()
dbg.halting.volatileTraps.clear()
// input has halted. print on halt command if it is defined
if dbg.commandOnHalt != nil {
err := dbg.processTokensList(dbg.commandOnHalt)
if err != nil {
dbg.printLine(terminal.StyleError, "%s", err)
}
}
// set pause emulation state
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
if dbg.continueEmulation && inputter.IsInteractive() {
dbg.Rewind.RecordExecutionCoords()
}
// reset halting flag before we resume execution.
dbg.halting.reset()
// reset run until halt flag - it will be set again if the parsed
// command requires it (eg. the RUN command)
dbg.runUntilHalt = false
// reset continueEmulation flag - it will set again by any command
// that requires it
dbg.continueEmulation = false
// reset haltImmediately flag - it will be set again with the next
// HALT command
dbg.haltImmediately = false
// we've been instructed to abandon this inputLoop().
if dbg.stepOutOfVideoStepInputLoop {
dbg.stepOutOfVideoStepInputLoop = false
// check that we really are in a nonInstructionQuantum
if nonInstructionQuantum {
return nil
} else {
logger.Log("debugger", "asked to 'step out of nonInstructionQuantum step input loop' inappropriately")
}
}
// read input from terminal inputter and parse/run commands
err = dbg.termRead(inputter)
if err != nil {
if errors.Is(err, script.ScriptEnd) {
dbg.printLine(terminal.StyleFeedback, err.Error())
return nil
}
return err
}
// emulation has been put into a different mode. exit loop immediately
if dbg.Mode() != govern.ModeDebugger {
return nil
}
if dbg.unwindLoopRestart != nil {
return nil
}
if dbg.catchupContinue != nil {
continue // dbg.running loop
}
// unpause emulation if we're continuing emulation
if dbg.runUntilHalt {
// runUntilHalt is set to true when stepping by more than a
// clock (ie. by scanline of frame) but in those cases we want
// to set gui state to StateStepping and not StateRunning.
//
// Setting to StateRunning may have different graphical
// side-effects which would look ugly when we're only in fact
// stepping.
if dbg.halting.volatileTraps.isEmpty() {
if inputter.IsInteractive() {
dbg.setState(govern.Running, govern.Normal)
}
} else {
dbg.setState(govern.Stepping, govern.Normal)
}
// update comparison point before execution continues
if !nonInstructionQuantum {
dbg.Rewind.UpdateComparison()
}
} else if inputter.IsInteractive() {
dbg.setState(govern.Stepping, govern.Normal)
}
}
if checkTerm {
err := dbg.termRead(inputter)
if err != nil {
return err
}
if dbg.unwindLoopRestart != nil {
return nil
}
if dbg.catchupContinue != nil {
continue // dbg.running loop
}
}
if dbg.continueEmulation {
// make sure we still want to continue after the call to resumAfterHalt()
if dbg.continueEmulation {
// input loops with the isVideoStep flag must never execute another
// call to vcs.Step() under any circumstances
//
// we also don't allow this call to inputLoop() to loop. if there
// is any more nonInstructionQuantum steps to handle, the function will be called
// again
if nonInstructionQuantum {
return nil
}
err = dbg.step(inputter, false)
if err != nil {
return err
}
// skip over WSYNC (CPU RDY flag is false) only if we're in instruction quantum
if dbg.Quantum() == govern.QuantumInstruction {
for !dbg.vcs.CPU.RdyFlg {
err = dbg.step(inputter, false)
if err != nil {
return err
}
}
}
// check for unwind loop
if dbg.unwindLoopRestart != nil {
return nil
}
}
}
}
return nil
}
func (dbg *Debugger) step(inputter terminal.Input, catchup bool) error {
callback := func(isCycle bool) error {
var err error
// check for unwind loop
if dbg.unwindLoopRestart != nil {
return nil
}
// we do need to update the reflection however
err = dbg.ref.Step(dbg.liveBankInfo)
if err != nil {
return err
}
dbg.counter.Step(1, dbg.liveBankInfo)
q := dbg.Quantum()
// process commandOnStep for non-instruction quantums (equivalent for
// instruction quantum is the main body of Debugger.step() below)
if dbg.commandOnStep != nil {
// we don't do this if we're in catchup mode or the "final" result from the CPU
if !catchup && !dbg.vcs.CPU.LastResult.Final {
if q == govern.QuantumClock || (q == govern.QuantumCycle && isCycle) {
err := dbg.processTokensList(dbg.commandOnStep)
if err != nil {
dbg.printLine(terminal.StyleError, "%s", err)
}
}
}
}
// check halt condition. a second check is made after vcs.Step()
// returns below
dbg.continueEmulation = dbg.halting.check()
if q == govern.QuantumClock || (q == govern.QuantumCycle && isCycle) || !dbg.continueEmulation {
// start another inputLoop() with the clockCycle boolean set to true
return dbg.inputLoop(inputter, true)
}
return nil
}
// get bank information before we execute the next instruction. we
// use this when formatting the last result from the CPU. this has
// to happen before we call the VCS.Step() function
dbg.liveBankInfo = dbg.vcs.Mem.Cart.GetBank(dbg.vcs.CPU.PC.Address())
// coords of CPU instruction before calling vcs.Step()
if dbg.vcs.CPU.RdyFlg {
dbg.cpuBoundaryLastInstruction = dbg.vcs.TV.GetCoords()
}
// not using the err variable because we'll clobber it before we
// get to check the result of VCS.Step()
stepErr := dbg.vcs.Step(callback)
// check halt condition again now that the instruction has finished (the
// Final flag is true). this does mean that some breakpoints/traps are
// matched twice but that's not currently a problem
dbg.halting.check()
dbg.continueEmulation = !dbg.halting.halt
// update disassembly after every CPU instruction. no exceptions.
dbg.liveDisasmEntry = dbg.Disasm.ExecutedEntry(dbg.liveBankInfo, dbg.vcs.CPU.LastResult, true, dbg.vcs.CPU.PC.Value())
// make sure reflection has been updated at the end of the instruction
if err := dbg.ref.Step(dbg.liveBankInfo); err != nil {
return err
}
dbg.counter.Step(1, dbg.liveBankInfo)
if dbg.unwindLoopRestart != nil {
return nil
}
if stepErr != nil {
// ...set lastStepError instead and allow emulation to halt
dbg.lastStepError = true
dbg.printLine(terminal.StyleError, "%s", stepErr)
// error has occurred before CPU has completed its instruction
if !dbg.vcs.CPU.LastResult.Final {
dbg.printLine(terminal.StyleError, "CPU halted mid-instruction. next step may be inaccurate.")
dbg.vcs.CPU.Interrupted = true
}
} else {
// update rewind state if the last CPU instruction took place during a new
// frame event. but not if we're in catchup mode
if !catchup {
dbg.Rewind.RecordState()
}
// process commandOnStep for instruction quantum (equivalent for clock
// quantum is the vcs.Step() callback above)
if dbg.vcs.CPU.RdyFlg {
q := dbg.Quantum()
if q == govern.QuantumInstruction || (q != govern.QuantumInstruction && dbg.vcs.CPU.LastResult.Final) {
if dbg.commandOnStep != nil {
err := dbg.processTokensList(dbg.commandOnStep)
if err != nil {
dbg.printLine(terminal.StyleError, "%s", err)
}
}
}
}
}
return nil
}
// termRead uses the TermRead() function of the inputter and process the output
// as required by the debugger.
func (dbg *Debugger) termRead(inputter terminal.Input) error {
// get user input from terminal.Input implementatio
inputLen, err := inputter.TermRead(dbg.input, dbg.buildPrompt(), dbg.events)
if dbg.unwindLoopRestart != nil {
return nil
}
// if there was no error from TermRead parse input (leading to execution)
// of the command
if err == nil {
if inputLen > 0 {
err = dbg.parseInput(string(dbg.input[:inputLen-1]), inputter.IsInteractive(), false)
if err != nil {
dbg.printLine(terminal.StyleError, "%s", err)
}
}
return nil
}
if errors.Is(err, terminal.UserInterrupt) {
// user interrupts are used to quit or halt an operation
dbg.handleInterrupt(inputter)
} else if errors.Is(err, io.EOF) {
// an EOF error causes the emulation to exit immediately
dbg.running = false
dbg.continueEmulation = false
return nil
} else {
// all other errors are passed upwards to the calling function
return err
}
return nil
}
// interrupt signals need some special care depending on the current state and
// what sort of terminal is being used.
func (dbg *Debugger) handleInterrupt(inputter terminal.Input) {
// end script scribe (if one is running)
err := dbg.scriptScribe.EndSession()
if err != nil {
logger.Logf("debugger", err.Error())
}
// exit immediately if inputter is not a real terminal
if !inputter.IsRealTerminal() {
dbg.running = false
dbg.continueEmulation = false
return
}
// if the emulation is currently running then stop emulation
if dbg.runUntilHalt {
dbg.runUntilHalt = false
dbg.continueEmulation = false
return
}
// terminal is not interactive so we set running to false which will
// quit the debugger as soon as possible
if !inputter.IsInteractive() {
dbg.running = false
dbg.continueEmulation = false
return
}
// terminal is interactive so we ask for quit confirmation
confirm := make([]byte, 1)
_, err = inputter.TermRead(confirm,
terminal.Prompt{
Content: "really quit (y/n) ",
Type: terminal.PromptTypeConfirm,
},
dbg.events)
if err != nil {
// another UserInterrupt has occurred. we treat a second UserInterrupt
// as thought 'y' was pressed
if errors.Is(err, terminal.UserInterrupt) {
confirm[0] = 'y'
} else {
dbg.printLine(terminal.StyleError, err.Error())
}
}
// check if confirmation has been confirmed
if confirm[0] == 'y' || confirm[0] == 'Y' {
dbg.running = false
}
}