mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-06-01 19:48:01 -04:00
28ae543e36
removed all references to hotloading. will reimplement in the future made sure then cartridgeloader.Loader instances are closed when no longer needed. added commentary where required to explain information pane in the ROM selector window is not disabled when animation emulation is not running. the load button is still visible based on the animation emulation
1490 lines
43 KiB
Go
1490 lines
43 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"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/jetsetilly/gopher2600/bots/wrangler"
|
|
"github.com/jetsetilly/gopher2600/cartridgeloader"
|
|
"github.com/jetsetilly/gopher2600/comparison"
|
|
"github.com/jetsetilly/gopher2600/coprocessor"
|
|
coproc_dev "github.com/jetsetilly/gopher2600/coprocessor/developer"
|
|
coproc_dwarf "github.com/jetsetilly/gopher2600/coprocessor/developer/dwarf"
|
|
coproc_disasm "github.com/jetsetilly/gopher2600/coprocessor/disassembly"
|
|
"github.com/jetsetilly/gopher2600/debugger/dbgmem"
|
|
"github.com/jetsetilly/gopher2600/debugger/govern"
|
|
"github.com/jetsetilly/gopher2600/debugger/script"
|
|
"github.com/jetsetilly/gopher2600/debugger/terminal"
|
|
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
|
|
"github.com/jetsetilly/gopher2600/disassembly"
|
|
"github.com/jetsetilly/gopher2600/environment"
|
|
"github.com/jetsetilly/gopher2600/gui"
|
|
"github.com/jetsetilly/gopher2600/hardware"
|
|
"github.com/jetsetilly/gopher2600/hardware/cpu/execution"
|
|
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge"
|
|
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
|
|
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/supercharger"
|
|
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
|
|
"github.com/jetsetilly/gopher2600/hardware/television"
|
|
"github.com/jetsetilly/gopher2600/hardware/television/coords"
|
|
"github.com/jetsetilly/gopher2600/logger"
|
|
"github.com/jetsetilly/gopher2600/macro"
|
|
"github.com/jetsetilly/gopher2600/notifications"
|
|
"github.com/jetsetilly/gopher2600/patch"
|
|
"github.com/jetsetilly/gopher2600/prefs"
|
|
"github.com/jetsetilly/gopher2600/preview"
|
|
"github.com/jetsetilly/gopher2600/properties"
|
|
"github.com/jetsetilly/gopher2600/recorder"
|
|
"github.com/jetsetilly/gopher2600/reflection"
|
|
"github.com/jetsetilly/gopher2600/reflection/counter"
|
|
"github.com/jetsetilly/gopher2600/resources/unique"
|
|
"github.com/jetsetilly/gopher2600/rewind"
|
|
"github.com/jetsetilly/gopher2600/setup"
|
|
"github.com/jetsetilly/gopher2600/tracker"
|
|
"github.com/jetsetilly/gopher2600/userinput"
|
|
"github.com/jetsetilly/gopher2600/wavwriter"
|
|
)
|
|
|
|
// Debugger is the basic debugging frontend for the emulation. In order to be
|
|
// kind to code that accesses the debugger from a different goroutine (ie. a
|
|
// GUI), we try not to reinitialise anything once it has been initialised. For
|
|
// example, disassembly on a cartridge change (which can happen at any time)
|
|
// updates the Disasm field, it does not reinitialise it.
|
|
type Debugger struct {
|
|
// arguments from the command line
|
|
opts CommandLineOptions
|
|
|
|
// current mode of the emulation. use setMode() to set the value
|
|
mode atomic.Value // emulation.Mode
|
|
|
|
// when playmode is entered without a ROM specified we send the GUI a
|
|
// ReqROMSelector request. we create the forcedROMselection channel and
|
|
// wait for a response from the InsertCartridge() function. sending and
|
|
// receiving on this channel occur in the same goroutine so the channel
|
|
// must be buffered
|
|
forcedROMselection chan bool
|
|
|
|
// 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
|
|
subState atomic.Value // emulation.RewindingSubState
|
|
|
|
// preferences for the emulation
|
|
Prefs *Preferences
|
|
|
|
// reference to emulated hardware. this pointer never changes through the
|
|
// life of the emulation even though the hardware may change and the
|
|
// components may change (during rewind for example)
|
|
vcs *hardware.VCS
|
|
|
|
// keep a reference to the current cartridgeloader to make sure Close() is called
|
|
cartload *cartridgeloader.Loader
|
|
|
|
// preview emulation is used to gather information about a ROM before
|
|
// running it fully
|
|
preview *preview.Emulation
|
|
|
|
// gameplay recorder/playback
|
|
recorder *recorder.Recorder
|
|
playback *recorder.Playback
|
|
|
|
// macro (only one allowed for the time being)
|
|
macro *macro.Macro
|
|
|
|
// comparison emulator
|
|
comparison *comparison.Comparison
|
|
|
|
// GUI, terminal and controllers
|
|
gui gui.GUI
|
|
term terminal.Terminal
|
|
controllers *userinput.Controllers
|
|
|
|
// stella.pro file support
|
|
pro properties.Properties
|
|
|
|
// bots coordinator
|
|
bots *wrangler.Bots
|
|
|
|
// when reading input from the terminal there are other events
|
|
// that need to be monitored
|
|
events *terminal.ReadEvents
|
|
|
|
// how often the events field should be checked
|
|
readEventsPulse *time.Ticker
|
|
|
|
// cartridge disassembly
|
|
//
|
|
// * allocated when entering debugger mode
|
|
Disasm *disassembly.Disassembly
|
|
CoProcDisasm coproc_disasm.Disassembly
|
|
CoProcDev coproc_dev.Developer
|
|
|
|
// the live disassembly entry. updated every CPU step or on halt (which may
|
|
// be mid instruction). it is also updated by the LAST command when the
|
|
// debugger is in the CLOCK quantum
|
|
//
|
|
// we use this so that we can display the instruction as it exists in the
|
|
// CPU at any given time, which means we can see the partially decoded
|
|
// instruction and easily keep track of how many cycles an instruction has
|
|
// taken so far.
|
|
//
|
|
// for CPU quantum the liveDisasmEntry will be the full entry that is in
|
|
// the disassembly
|
|
//
|
|
// for both quantums we update liveDisasmEntry with the ExecutedEntry()
|
|
// function in the disassembly package
|
|
liveDisasmEntry *disassembly.Entry
|
|
|
|
// the live disassembly entry. updated every CPU step or on halt (which may
|
|
// be mid instruction). it is also updated by the LAST command when the
|
|
// debugger is in the CLOCK quantum
|
|
liveBankInfo mapper.BankInfo
|
|
|
|
// the television coords of the last CPU instruction
|
|
cpuBoundaryLastInstruction coords.TelevisionCoords
|
|
|
|
// interface to the vcs memory with additional debugging functions
|
|
// - access to vcs memory from the debugger (eg. peeking and poking) is
|
|
// most fruitfully performed through this structure
|
|
dbgmem *dbgmem.DbgMem
|
|
|
|
// reflection is used to provideo additional information about the
|
|
// emulation. it is inherently slow so should be deactivated if not
|
|
// required
|
|
//
|
|
// * allocated when entering debugger mode
|
|
ref *reflection.Reflector
|
|
|
|
// closely related to the relection system is the counter. generally
|
|
// updated, cleared, etc. at the same time as the reflection system.
|
|
counter *counter.Counter
|
|
|
|
// halting is used to coordinate the checking of all halting conditions. it
|
|
// is updated every video cycle as appropriate (ie. not when rewinding)
|
|
halting *haltCoordination
|
|
|
|
// trace memory access
|
|
traces *traces
|
|
|
|
// commandOnHalt is the sequence of commands that runs when emulation
|
|
// halts
|
|
commandOnHalt []*commandline.Tokens
|
|
commandOnHaltStored []*commandline.Tokens
|
|
|
|
// commandOnStep is the command to run afer every cpu/video cycle
|
|
commandOnStep []*commandline.Tokens
|
|
commandOnStepStored []*commandline.Tokens
|
|
|
|
// commandOnTrace is the command run whenever a trace condition is met.
|
|
commandOnTrace []*commandline.Tokens
|
|
commandOnTraceStored []*commandline.Tokens
|
|
|
|
// Quantum to use when stepping/running
|
|
quantum atomic.Value // govern.Quantum
|
|
|
|
// record user input to a script file
|
|
scriptScribe script.Scribe
|
|
|
|
// the Rewind system stores and restores machine state
|
|
Rewind *rewind.Rewind
|
|
|
|
// the amount we rewind by is dependent on how fast the mouse wheel is
|
|
// moving or for how long the keyboard (or gamepad bumpers) have been
|
|
// depressed.
|
|
//
|
|
// for keyboard rewinding, the amount to rewind by increases for as long as
|
|
// the key-combo (gamepad bumper) is being held. the amount is reset when
|
|
// the key-combo/bumper is released
|
|
rewindKeyboardAccumulation int
|
|
|
|
// when rewinding by mouse wheel we just use the delta information from the
|
|
// input device. however, we might miss mousewheel events during the
|
|
// debugger catchup loop. the rewindMouseWheelAccumulation value allows us
|
|
// to accumulate the delta value until we have the opportunity to issue
|
|
// another rewind instruction
|
|
rewindMouseWheelAccumulation int
|
|
|
|
// audio tracker stores audio state over time
|
|
Tracker *tracker.Tracker
|
|
|
|
// \/\/\/ debugger inputLoop \/\/\/
|
|
|
|
// buffer for user input
|
|
input []byte
|
|
|
|
// any error from previous emulation step
|
|
lastStepError bool
|
|
|
|
// whether the debugger is to continue with the debugging loop
|
|
// set to false only when debugger is to finish
|
|
//
|
|
// not to be confused with Emulation.Running
|
|
running bool
|
|
|
|
// continue emulation until a halt condition is encountered
|
|
//
|
|
// we sometimes think of the halt condition as being paused as in Emulation.Paused
|
|
runUntilHalt bool
|
|
|
|
// continue the emulation. this is seemingly only used in the inputLoop()
|
|
continueEmulation bool
|
|
|
|
// halt the emulation immediately. used by HALT command.
|
|
//
|
|
// we sometimes think of the halt condition as being paused as in Emulation.Paused
|
|
haltImmediately bool
|
|
|
|
// in very specific circumstances it is necessary to step out of debugger
|
|
// loop if it's in the middle of a video step. this happens very rarely but
|
|
// is necessary in order to *feel* natural to the user - without it it can
|
|
// sometimes require an extra STEP instruction to continue, which can be
|
|
// confusing
|
|
//
|
|
// it can be thought of as a lightweight unwind loop function
|
|
//
|
|
// it is currently used only to implement stepping (in instruction quantum)
|
|
// when the emulation state is "inside" the WSYNC
|
|
stepOutOfVideoStepInputLoop bool
|
|
|
|
// some operations require that the input loop be restarted to make sure
|
|
// continued operation is not inside a video cycle loop
|
|
//
|
|
// we check this frequently inside the inputLoop() function and functions
|
|
// called by inputLoop()
|
|
unwindLoopRestart func() error
|
|
|
|
// after a rewinding event it is necessary to make sure the emulation is in
|
|
// the correct place
|
|
catchupContinue func() bool
|
|
catchupEnd func()
|
|
|
|
// the context in which the catchup loop is running
|
|
catchupContext catchupContext
|
|
|
|
// the debugger catchup loop will end on a video cycle if necessary. this
|
|
// is what we want in most situations but occasionally it is useful to stop
|
|
// on an instruction boundary. catchupEndAdj will ensure that the debugger
|
|
// halts on an instruction boundary
|
|
//
|
|
// the value will reset to false at the end of a catchup loop
|
|
catchupEndAdj bool
|
|
|
|
// when switching to debug mode from CartYield() we don't want to rewind
|
|
// and rerun the emulation because the condition which caused the yield
|
|
// might not happen again
|
|
//
|
|
// this is the case when a breakpoint is triggered as a result of user
|
|
// input. currently, user input is not reinserted into the emulation on the
|
|
// rerun. it should be possible to do given the input system's flexibility
|
|
// but for now we'll just use this switch
|
|
//
|
|
// the only feature we lose with this is incomplete reflection information
|
|
// immediately after switching to the debugger
|
|
noRewindOnSwitchToDebugger bool
|
|
}
|
|
|
|
// CreateUserInterface is used to initialise the user interface used by the
|
|
// emulation. It returns an instance of both the GUI and Terminal interfaces
|
|
// in the repsective packages.
|
|
type CreateUserInterface func(*Debugger) (gui.GUI, terminal.Terminal, error)
|
|
|
|
// NewDebugger creates and initialises everything required for a new debugging
|
|
// session.
|
|
//
|
|
// It should be followed up with a call to AddUserInterface() and call the
|
|
// Start() method to actually begin the emulation.
|
|
func NewDebugger(opts CommandLineOptions, create CreateUserInterface) (*Debugger, error) {
|
|
dbg := &Debugger{
|
|
// copy of the arguments
|
|
opts: opts,
|
|
|
|
// the ticker to indicate whether we should check for events in the
|
|
// inputLoop. this used to be set to 50ms but that is way too long and
|
|
// introduces a three frame lag to input devices. put another way, it
|
|
// results in the emulation missing crucial input information. this is
|
|
// particularly noticeable with paddle input because the paddle
|
|
// resistance value (see paddle.go file in the peripherals/controllers
|
|
// package) is only updated every three frames, causing the paddle to be
|
|
// effectively running three times slower than the screen
|
|
readEventsPulse: time.NewTicker(1 * time.Millisecond),
|
|
}
|
|
|
|
// 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)
|
|
|
|
var err error
|
|
|
|
// load preferences
|
|
dbg.Prefs, err = newPreferences()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
// creat a new television. this will be used during the initialisation of
|
|
// the VCS and not referred to directly again
|
|
tv, err := television.NewTelevision(opts.Spec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
// create a new VCS instance
|
|
dbg.vcs, err = hardware.NewVCS(tv, dbg, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("debugger: %w", err)
|
|
}
|
|
dbg.vcs.Env.Label = environment.MainEmulation
|
|
|
|
// create userinput/controllers handler
|
|
dbg.controllers = userinput.NewControllers(dbg.vcs.Input)
|
|
|
|
// create bot coordinator
|
|
dbg.bots = wrangler.NewBots(dbg.vcs.Input, dbg.vcs.TV)
|
|
|
|
// stella.pro support
|
|
dbg.pro, err = properties.Load()
|
|
if err != nil {
|
|
logger.Logf("debugger", err.Error())
|
|
}
|
|
|
|
// create preview emulation
|
|
dbg.preview, err = preview.NewEmulation(dbg.vcs.Env.Prefs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
// set up debugging interface to memory
|
|
dbg.dbgmem = &dbgmem.DbgMem{
|
|
VCS: dbg.vcs,
|
|
}
|
|
|
|
// create a new disassembly instance
|
|
dbg.Disasm, dbg.dbgmem.Sym, err = disassembly.NewDisassembly(dbg.vcs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
// create new coprocessor developer/disassembly instances
|
|
dbg.CoProcDisasm = coproc_disasm.NewDisassembly(dbg.vcs.TV)
|
|
dbg.CoProcDev = coproc_dev.NewDeveloper(dbg, dbg.vcs.TV)
|
|
dbg.vcs.TV.AddFrameTrigger(&dbg.CoProcDev)
|
|
|
|
// create a minimal lastResult for initialisation
|
|
dbg.liveDisasmEntry = &disassembly.Entry{Result: execution.Result{Final: true}}
|
|
|
|
// halting coordination
|
|
dbg.halting, err = newHaltCoordination(dbg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
// traces
|
|
dbg.traces = newTraces(dbg)
|
|
|
|
// make synchronisation channels. PushedFunctions can be pushed thick and
|
|
// fast and the channel queue should be pretty lengthy to prevent dropped
|
|
// events (see PushFunction()).
|
|
dbg.events = &terminal.ReadEvents{
|
|
UserInput: make(chan userinput.Event, 10),
|
|
UserInputHandler: dbg.userInputHandler,
|
|
Signal: make(chan os.Signal, 1),
|
|
SignalHandler: func(sig os.Signal) error {
|
|
switch sig {
|
|
case syscall.SIGHUP:
|
|
return terminal.UserReload
|
|
case syscall.SIGINT:
|
|
return terminal.UserInterrupt
|
|
case syscall.SIGQUIT:
|
|
return terminal.UserQuit
|
|
case syscall.SIGKILL:
|
|
// we're unlikely to receive the kill signal, it normally being
|
|
// intercepted by the terminal, but in case we do we treat it
|
|
// like the QUIT signal
|
|
return terminal.UserQuit
|
|
default:
|
|
}
|
|
return nil
|
|
},
|
|
PushedFunction: make(chan func(), 4096),
|
|
PushedFunctionImmediate: make(chan func(), 4096),
|
|
}
|
|
|
|
// connect signals to dbg.events.Signal channel. we include the Kill signal
|
|
// but the chances are it'll never be seen
|
|
signal.Notify(dbg.events.Signal, os.Interrupt, os.Kill, syscall.SIGHUP, syscall.SIGQUIT)
|
|
|
|
// allocate memory for user input
|
|
dbg.input = make([]byte, 255)
|
|
|
|
// create GUI
|
|
dbg.gui, dbg.term, err = create(dbg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
// add tab completion to terminal
|
|
dbg.term.RegisterTabCompletion(commandline.NewTabCompletion(debuggerCommands))
|
|
|
|
// create rewind system
|
|
dbg.Rewind, err = rewind.NewRewind(dbg, dbg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
// add reflection system to the GUI
|
|
dbg.ref = reflection.NewReflector(dbg.vcs)
|
|
if r, ok := dbg.gui.(reflection.Broker); ok {
|
|
dbg.ref.AddRenderer(r.GetReflectionRenderer())
|
|
}
|
|
|
|
// add counter to rewind system
|
|
dbg.counter = counter.NewCounter(dbg.vcs)
|
|
dbg.Rewind.AddTimelineCounter(dbg.counter)
|
|
|
|
// adding TV frame triggers in setMode(). what the TV triggers on depending
|
|
// on the mode for performance reasons (eg. no reflection required in
|
|
// playmode)
|
|
|
|
// add audio tracker
|
|
dbg.Tracker = tracker.NewTracker(dbg, dbg.Rewind)
|
|
dbg.vcs.TIA.Audio.SetTracker(dbg.Tracker)
|
|
|
|
// add plug monitor
|
|
dbg.vcs.RIOT.Ports.AttachPlugMonitor(dbg)
|
|
|
|
// set fps cap
|
|
dbg.vcs.TV.SetFPSCap(opts.FpsCap)
|
|
|
|
// initialise terminal
|
|
err = dbg.term.Initialise()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
return dbg, nil
|
|
}
|
|
|
|
// VCS implements the emulation.Emulation interface.
|
|
func (dbg *Debugger) VCS() *hardware.VCS {
|
|
return dbg.vcs
|
|
}
|
|
|
|
// TV implements the emulation.Emulation interface.
|
|
func (dbg *Debugger) TV() *television.Television {
|
|
return dbg.vcs.TV
|
|
}
|
|
|
|
// Debugger implements the emulation.Emulation interface.
|
|
func (dbg *Debugger) Debugger() *Debugger {
|
|
return dbg
|
|
}
|
|
|
|
// UserInput implements the emulation.Emulation interface.
|
|
func (dbg *Debugger) UserInput() chan userinput.Event {
|
|
return dbg.events.UserInput
|
|
}
|
|
|
|
// Mode implements the emulation.Emulation interface.
|
|
func (dbg *Debugger) Quantum() govern.Quantum {
|
|
return dbg.quantum.Load().(govern.Quantum)
|
|
}
|
|
|
|
// set the quantum state
|
|
func (dbg *Debugger) setQuantum(quantum govern.Quantum) {
|
|
dbg.quantum.Store(quantum)
|
|
}
|
|
|
|
// State implements the emulation.Emulation interface.
|
|
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, 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),
|
|
))
|
|
}
|
|
|
|
if state == govern.Rewinding {
|
|
dbg.endPlayback()
|
|
dbg.endRecording()
|
|
dbg.endComparison()
|
|
|
|
// coprocessor disassembly is an inherently slow operation particuarly
|
|
// for StrongARM type ROMs
|
|
dbg.CoProcDisasm.Inhibit(true)
|
|
} else {
|
|
// uninhibit coprocessor disassembly
|
|
dbg.CoProcDisasm.Inhibit(false)
|
|
}
|
|
|
|
err := dbg.vcs.TV.SetEmulationState(state)
|
|
if err != nil {
|
|
logger.Log("debugger", err.Error())
|
|
}
|
|
if dbg.ref != nil {
|
|
dbg.ref.SetEmulationState(state)
|
|
}
|
|
dbg.CoProcDev.SetEmulationState(state)
|
|
|
|
dbg.state.Store(state)
|
|
dbg.subState.Store(subState)
|
|
}
|
|
|
|
// set the emulation mode
|
|
func (dbg *Debugger) setMode(mode govern.Mode) error {
|
|
if dbg.Mode() == mode {
|
|
return nil
|
|
}
|
|
|
|
// don't stop the recording, playback, comparison or bot sub-systems on
|
|
// change of mode
|
|
|
|
// if there is a halting condition that is not allowed in playmode (see
|
|
// targets type) then do not change the emulation mode
|
|
//
|
|
// however, because the user has asked to switch to playmode we should
|
|
// cause the debugger mode to run until the halting condition is matched
|
|
// (which we know will occur in the almost immediate future)
|
|
if mode == govern.ModePlay && !dbg.halting.allowPlaymode() {
|
|
if dbg.Mode() == govern.ModeDebugger {
|
|
dbg.runUntilHalt = true
|
|
dbg.continueEmulation = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
prevMode := dbg.Mode()
|
|
dbg.mode.Store(mode)
|
|
|
|
// notify gui of change
|
|
err := dbg.gui.SetFeature(gui.ReqSetEmulationMode, mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// remove all triggers that we work with in the debugger. we'll add the
|
|
// one's we want depending on playmode.
|
|
//
|
|
// note that we don't remove *every* frame trigger because other sub-systems
|
|
// might have added their own.
|
|
dbg.vcs.TV.RemoveFrameTrigger(dbg.ref)
|
|
dbg.vcs.TV.RemoveFrameTrigger(dbg.Rewind)
|
|
dbg.vcs.TV.RemoveFrameTrigger(dbg.counter)
|
|
|
|
// swtich mode and make sure emulation is in correct state. we say that
|
|
// emulation is always running when entering playmode and always paused
|
|
// when entering debug mode.
|
|
//
|
|
// * the reason for this is simplicity. if we allow playmode to begin
|
|
// paused for example it complicates how we render the screen (see sdlimgui
|
|
// screen.go)
|
|
|
|
switch dbg.Mode() {
|
|
case govern.ModePlay:
|
|
dbg.vcs.TV.AddFrameTrigger(dbg.Rewind)
|
|
dbg.vcs.TV.AddFrameTrigger(dbg.counter)
|
|
|
|
// simple detection of whether cartridge is ejected when switching to
|
|
// playmode. if it is ejected then open ROM selected.
|
|
if dbg.Mode() == govern.ModePlay && dbg.vcs.Mem.Cart.IsEjected() {
|
|
err = dbg.forceROMSelector()
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
} else {
|
|
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, 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
|
|
// an instruction boundary.
|
|
//
|
|
// playmode will always break on an instruction boundary but without
|
|
// catchupEndAdj we will always enter the debugger on the last cycle of
|
|
// an instruction. although correct in terms of coordinates, is
|
|
// confusing.
|
|
if prevMode == govern.ModePlay {
|
|
if !dbg.noRewindOnSwitchToDebugger {
|
|
dbg.catchupEndAdj = true
|
|
dbg.RerunLastNFrames(2)
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("emulation mode not supported: %s", mode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// End cleans up any resources that may be dangling.
|
|
func (dbg *Debugger) end() {
|
|
dbg.endPlayback()
|
|
dbg.endRecording()
|
|
dbg.endComparison()
|
|
if dbg.macro != nil {
|
|
dbg.macro.Quit()
|
|
}
|
|
dbg.bots.Quit()
|
|
|
|
dbg.vcs.End()
|
|
|
|
defer dbg.term.CleanUp()
|
|
|
|
// set ending state
|
|
err := dbg.gui.SetFeature(gui.ReqEnd)
|
|
if err != nil {
|
|
logger.Log("debugger", err.Error())
|
|
}
|
|
|
|
// save preferences
|
|
err = dbg.Prefs.Save()
|
|
if err != nil {
|
|
logger.Log("debugger", err.Error())
|
|
}
|
|
}
|
|
|
|
// StartInDebugMode starts the emulation with the debugger activated.
|
|
func (dbg *Debugger) StartInDebugMode(filename string) error {
|
|
// set running flag as early as possible
|
|
dbg.running = true
|
|
|
|
var err error
|
|
var cartload cartridgeloader.Loader
|
|
|
|
if filename == "" {
|
|
cartload = cartridgeloader.Loader{}
|
|
} else {
|
|
cartload, err = cartridgeloader.NewLoaderFromFilename(filename, dbg.opts.Mapping)
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
}
|
|
|
|
// cartload is should be passed to attachCartridge() almost immediately. the
|
|
// closure of cartload will then be handled for us
|
|
err = dbg.attachCartridge(cartload)
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
err = dbg.setPeripheralsOnStartup()
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
err = dbg.setMode(govern.ModeDebugger)
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
// intialisation script because we're in debugger mode
|
|
if dbg.opts.InitScript != "" {
|
|
scr, err := script.RescribeScript(dbg.opts.InitScript)
|
|
if err == nil {
|
|
dbg.term.Silence(true)
|
|
err = dbg.inputLoop(scr, false)
|
|
if err != nil {
|
|
dbg.term.Silence(false)
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
dbg.term.Silence(false)
|
|
}
|
|
}
|
|
|
|
defer dbg.end()
|
|
|
|
err = dbg.run()
|
|
if err != nil {
|
|
if errors.Is(err, terminal.UserSignal) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (dbg *Debugger) setPeripheralsOnStartup() error {
|
|
dbg.term.Silence(true)
|
|
defer dbg.term.Silence(false)
|
|
|
|
err := dbg.parseCommand(fmt.Sprintf("PERIPHERAL LEFT %s", dbg.opts.Left), false, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = dbg.parseCommand(fmt.Sprintf("PERIPHERAL RIGHT %s", dbg.opts.Right), false, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if dbg.opts.Swap {
|
|
err = dbg.parseCommand(fmt.Sprintf("PERIPHERAL SWAP"), false, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// StartInPlaymode starts the emulation ready for game-play.
|
|
func (dbg *Debugger) StartInPlayMode(filename string) error {
|
|
// set running flag as early as possible
|
|
dbg.running = true
|
|
|
|
err := recorder.IsPlaybackFile(filename)
|
|
if err != nil {
|
|
if !errors.Is(err, recorder.NotAPlaybackFile) {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
var cartload cartridgeloader.Loader
|
|
|
|
if filename == "" {
|
|
cartload = cartridgeloader.Loader{}
|
|
} else {
|
|
cartload, err = cartridgeloader.NewLoaderFromFilename(filename, dbg.opts.Mapping)
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
}
|
|
|
|
// cartload is should be passed to attachCartridge() almost immediately. the
|
|
// closure of cartload will then be handled for us
|
|
err = dbg.attachCartridge(cartload)
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
err = dbg.setPeripheralsOnStartup()
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
// apply patch if requested. note that this will be in addition to any
|
|
// patches applied during setup.AttachCartridge
|
|
if dbg.opts.PatchFile != "" {
|
|
_, err := patch.CartridgeMemory(dbg.vcs.Mem.Cart, dbg.opts.PatchFile)
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
}
|
|
|
|
// record wav file
|
|
if dbg.opts.Wav {
|
|
fn := unique.Filename("audio", cartload.Name)
|
|
ww, err := wavwriter.NewWavWriter(fn)
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
dbg.vcs.TV.AddAudioMixer(ww)
|
|
}
|
|
|
|
// record gameplay
|
|
if dbg.opts.Record {
|
|
dbg.startRecording(cartload.Name)
|
|
}
|
|
} else {
|
|
if dbg.opts.Record {
|
|
return fmt.Errorf("debugger: cannot make a new recording using a playback file")
|
|
}
|
|
|
|
dbg.startPlayback(filename)
|
|
}
|
|
|
|
if dbg.opts.Macro != "" {
|
|
dbg.macro, err = macro.NewMacro(dbg.opts.Macro, dbg, dbg.vcs.Input, dbg.vcs.TV, dbg.gui)
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
}
|
|
|
|
err = dbg.startComparison(dbg.opts.ComparisonROM, dbg.opts.ComparisonPrefs)
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
err = dbg.setMode(govern.ModePlay)
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
// wait a very short time to give window time to open. this would be better
|
|
// and more consistently achieved with the help of a synchronisation channel
|
|
<-time.After(250 * time.Millisecond)
|
|
|
|
defer dbg.end()
|
|
|
|
if dbg.macro != nil {
|
|
dbg.macro.Run()
|
|
}
|
|
|
|
err = dbg.run()
|
|
if err != nil {
|
|
if errors.Is(err, terminal.UserSignal) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CartYield implements the coprocessor.CartYieldHook interface.
|
|
func (dbg *Debugger) CartYield(yield coprocessor.CoProcYieldType) coprocessor.YieldHookResponse {
|
|
// if the emulator wants to quit we need to return true to instruct the
|
|
// cartridge to return to the main loop immediately
|
|
if !dbg.running {
|
|
return coprocessor.YieldHookEnd
|
|
}
|
|
|
|
// resolve deferred yield
|
|
if dbg.halting.deferredCartridgeYield {
|
|
dbg.halting.deferredCartridgeYield = false
|
|
dbg.halting.cartridgeYield = true
|
|
return coprocessor.YieldHookEnd
|
|
}
|
|
|
|
switch yield {
|
|
case coprocessor.YieldProgramEnded:
|
|
// expected reason for CDF and DPC+ cartridges
|
|
return coprocessor.YieldHookContinue
|
|
|
|
case coprocessor.YieldSyncWithVCS:
|
|
// expected reason for ACE and ELF cartridges
|
|
return coprocessor.YieldHookContinue
|
|
}
|
|
|
|
// if emulation is in the intialisation state then we cause coprocessor
|
|
// execution to end unless it's a memory or access erorr
|
|
//
|
|
// this is an area that's likely to change. it's of particular interest to
|
|
// ACE and ELF ROMs in which the coprocessor is run very early in order to
|
|
// retrive the 6507 reset address
|
|
//
|
|
// a deferred YeildHookEnd might be a better option
|
|
if dbg.State() == govern.Initialising {
|
|
dbg.halting.deferredCartridgeYield = true
|
|
return coprocessor.YieldHookContinue
|
|
}
|
|
|
|
dbg.halting.cartridgeYield = true
|
|
dbg.continueEmulation = dbg.halting.check()
|
|
|
|
switch dbg.Mode() {
|
|
case govern.ModePlay:
|
|
dbg.noRewindOnSwitchToDebugger = true
|
|
dbg.setMode(govern.ModeDebugger)
|
|
dbg.noRewindOnSwitchToDebugger = false
|
|
case govern.ModeDebugger:
|
|
dbg.inputLoop(dbg.term, true)
|
|
}
|
|
|
|
return coprocessor.YieldHookEnd
|
|
}
|
|
|
|
func (dbg *Debugger) run() error {
|
|
// end script recording gracefully. this way we don't have to worry too
|
|
// hard about script scribes
|
|
defer func() {
|
|
err := dbg.scriptScribe.EndSession()
|
|
if err != nil {
|
|
logger.Logf("debugger", err.Error())
|
|
}
|
|
}()
|
|
|
|
// make sure any cartridge loader has been finished with
|
|
defer func() {
|
|
if dbg.cartload != nil {
|
|
err := dbg.cartload.Close()
|
|
if err != nil {
|
|
logger.Logf("debugger", err.Error())
|
|
}
|
|
}
|
|
}()
|
|
|
|
// inputloop will continue until debugger is to be terminated
|
|
for dbg.running {
|
|
switch dbg.Mode() {
|
|
case govern.ModePlay:
|
|
err := dbg.playLoop()
|
|
if err != nil {
|
|
// if we ever encounter a cartridge ejected error in playmode
|
|
// then simply open up the ROM selector and continue with the
|
|
// running loop
|
|
if errors.Is(err, cartridge.Ejected) {
|
|
err = dbg.forceROMSelector()
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
} else if errors.Is(err, terminal.UserReload) {
|
|
err = dbg.reloadCartridge()
|
|
if err != nil {
|
|
logger.Logf("debugger", err.Error())
|
|
}
|
|
} else {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
}
|
|
|
|
case govern.ModeDebugger:
|
|
switch dbg.State() {
|
|
case govern.Running:
|
|
dbg.runUntilHalt = true
|
|
dbg.continueEmulation = true
|
|
case govern.Paused:
|
|
dbg.haltImmediately = true
|
|
case govern.Rewinding:
|
|
default:
|
|
return fmt.Errorf("emulation state not supported on *start* of debugging loop: %s", dbg.State())
|
|
}
|
|
|
|
err := dbg.inputLoop(dbg.term, false)
|
|
if err != nil {
|
|
// there is no special handling for the cartridge ejected error,
|
|
// unlike in play mode
|
|
if errors.Is(err, terminal.UserReload) {
|
|
dbg.reloadCartridge()
|
|
} else {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("emulation mode not supported: %s", dbg.mode)
|
|
}
|
|
|
|
// handle inputLoopRestart and any on-restart function
|
|
if dbg.unwindLoopRestart != nil {
|
|
err := dbg.unwindLoopRestart()
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
dbg.unwindLoopRestart = nil
|
|
} else if dbg.State() == govern.Ending {
|
|
dbg.running = false
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// reset of VCS should go through this function to makes sure debugger is reset
|
|
// accordingly also. note that debugging features (breakpoints, etc.) are not
|
|
// reset.
|
|
//
|
|
// the newCartridge flag will cause breakpoints, traces, etc. to be reset
|
|
// as well. it is sometimes appropriate to reset these (eg. on new cartridge
|
|
// insert)
|
|
func (dbg *Debugger) reset(newCartridge bool) error {
|
|
err := dbg.vcs.Reset()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dbg.Rewind.Reset()
|
|
dbg.Tracker.Reset()
|
|
|
|
// reset other debugger properties that might not make sense for a new cartride
|
|
if newCartridge {
|
|
dbg.halting.breakpoints.clear()
|
|
dbg.halting.traps.clear()
|
|
dbg.halting.watches.clear()
|
|
dbg.traces.clear()
|
|
}
|
|
|
|
dbg.liveDisasmEntry = &disassembly.Entry{Result: execution.Result{Final: true}}
|
|
return nil
|
|
}
|
|
|
|
// Notify implements the notifications.Notify interface
|
|
func (dbg *Debugger) Notify(notice notifications.Notice) error {
|
|
switch notice {
|
|
|
|
case notifications.NotifySuperchargerFastload:
|
|
// the supercharger ROM will eventually start execution from the PC
|
|
// address given in the supercharger file
|
|
|
|
// CPU execution has been interrupted. update state of CPU
|
|
dbg.vcs.CPU.Interrupted = true
|
|
|
|
// the interrupted CPU means it never got a chance to
|
|
// finalise the result. we force that here by simply
|
|
// setting the Final flag to true.
|
|
dbg.vcs.CPU.LastResult.Final = true
|
|
|
|
// we've already obtained the disassembled lastResult so we
|
|
// need to change the final flag there too
|
|
dbg.liveDisasmEntry.Result.Final = true
|
|
|
|
// force multiload value for supercharger fastload
|
|
if dbg.opts.Multiload >= 0 {
|
|
dbg.vcs.Mem.Poke(supercharger.MutliloadByteAddress, uint8(dbg.opts.Multiload))
|
|
}
|
|
|
|
// call commit function to complete tape loading procedure
|
|
fastload := dbg.vcs.Mem.Cart.GetSuperchargerFastLoad()
|
|
if fastload == nil {
|
|
return fmt.Errorf("NotifySuperchargerFastloadEnded sent from a non Supercharger fastload cartridge")
|
|
}
|
|
err := fastload.Fastload(dbg.vcs.CPU, dbg.vcs.Mem.RAM, dbg.vcs.RIOT.Timer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// (re)disassemble memory on TapeLoaded error signal
|
|
err = dbg.Disasm.FromMemory()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case notifications.NotifySuperchargerSoundloadStarted:
|
|
// force multiload value for supercharger soundload
|
|
if dbg.opts.Multiload >= 0 {
|
|
dbg.vcs.Mem.Poke(supercharger.MutliloadByteAddress, uint8(dbg.opts.Multiload))
|
|
}
|
|
|
|
err := dbg.gui.SetFeature(gui.ReqNotification, notifications.NotifySuperchargerSoundloadStarted)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case notifications.NotifySuperchargerSoundloadEnded:
|
|
err := dbg.gui.SetFeature(gui.ReqNotification, notifications.NotifySuperchargerSoundloadEnded)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// !!TODO: it would be nice to see partial disassemblies of supercharger tapes
|
|
// during loading. not completely necessary I don't think, but it would be
|
|
// nice to have.
|
|
err = dbg.Disasm.FromMemory()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return dbg.vcs.TV.Reset(true)
|
|
case notifications.NotifySuperchargerSoundloadRewind:
|
|
err := dbg.gui.SetFeature(gui.ReqNotification, notifications.NotifySuperchargerSoundloadRewind)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case notifications.NotifyPlusROMNewInstall:
|
|
err := dbg.gui.SetFeature(gui.ReqNotification, notifications.NotifyPlusROMNewInstall)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case 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:
|
|
logger.Logf("debugger", "unhandled notification for plusrom (%v)", notice)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// attachCartridge makes sure that the cartridge loaded into vcs memory and the
|
|
// available disassembly/symbols are in sync.
|
|
//
|
|
// NEVER call vcs.AttachCartridge() or setup.AttachCartridge() except through
|
|
// this function
|
|
//
|
|
// this is the glue that hold the cartridge and disassembly packages together.
|
|
// especially important is the repointing of the symbols table in the instance of dbgmem.
|
|
//
|
|
// if the new cartridge loader has the same filename as the previous loader
|
|
// then reset() is called with a newCartridge argument of false.
|
|
//
|
|
// VERY IMPORTANT that the supplied cartload is assigned to dbg.cartload before
|
|
// returning from the function
|
|
func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error) {
|
|
// is this a new cartridge we're loading. value is used for dbg.reset()
|
|
newCartridge := dbg.cartload == nil || cartload.Filename != dbg.cartload.Filename
|
|
|
|
// stop optional sub-systems that shouldn't survive a new cartridge insertion
|
|
dbg.endPlayback()
|
|
dbg.endRecording()
|
|
dbg.endComparison()
|
|
dbg.bots.Quit()
|
|
|
|
// attching a cartridge implies the initialise state
|
|
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, govern.Normal)
|
|
} else {
|
|
dbg.setState(govern.Paused, govern.Normal)
|
|
}
|
|
case govern.ModePlay:
|
|
dbg.setState(govern.Running, govern.Normal)
|
|
}
|
|
}()
|
|
|
|
// close any existing loader before continuing
|
|
if dbg.cartload != nil {
|
|
err := dbg.cartload.Close()
|
|
if err != nil {
|
|
logger.Logf("debuger", err.Error())
|
|
}
|
|
}
|
|
dbg.cartload = &cartload
|
|
|
|
// reset of vcs is implied with attach cartridge
|
|
err := setup.AttachCartridge(dbg.vcs, cartload, false)
|
|
if err != nil && !errors.Is(err, cartridge.Ejected) {
|
|
logger.Logf("debugger", err.Error())
|
|
// an error has occurred so attach the ejected cartridge
|
|
//
|
|
// !TODO: a special error cartridge to make it more obvious what has happened
|
|
if err := setup.AttachCartridge(dbg.vcs, cartridgeloader.Loader{}, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// check for cartridge ejection. if the NoEject option is set then return error
|
|
if dbg.opts.NoEject && dbg.vcs.Mem.Cart.IsEjected() {
|
|
// if there is an error left over from the AttachCartridge() call
|
|
// above, return that rather than "cartridge ejected"
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fmt.Errorf("cartridge ejected")
|
|
}
|
|
|
|
// clear existing reflection and counter data
|
|
dbg.ref.Clear()
|
|
dbg.counter.Clear()
|
|
|
|
err = dbg.Disasm.FromMemory()
|
|
if err != nil {
|
|
logger.Logf("debugger", err.Error())
|
|
}
|
|
|
|
dbg.CoProcDisasm.AttachCartridge(dbg)
|
|
err = dbg.CoProcDev.AttachCartridge(dbg, cartload.Filename, dbg.opts.ELF)
|
|
if err != nil {
|
|
logger.Logf("debugger", err.Error())
|
|
if errors.Is(err, coproc_dwarf.UnsupportedDWARF) {
|
|
err = dbg.gui.SetFeature(gui.ReqNotification, notifications.NotifyUnsupportedDWARF)
|
|
if err != nil {
|
|
logger.Logf("debugger", err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
// attach current debugger as the yield hook for cartridge
|
|
dbg.vcs.Mem.Cart.SetYieldHook(dbg)
|
|
|
|
// make sure everything is reset after disassembly (including breakpoints, etc.)
|
|
dbg.reset(newCartridge)
|
|
|
|
// run preview emulation
|
|
err = dbg.preview.Run(cartload)
|
|
if err != nil {
|
|
if !errors.Is(err, cartridgeloader.NoFilename) {
|
|
return err
|
|
}
|
|
}
|
|
dbg.vcs.TV.SetVisible(dbg.preview.Results().FrameInfo)
|
|
cartload.Seek(0, io.SeekStart)
|
|
|
|
// activate bot if possible
|
|
feedback, err := dbg.bots.ActivateBot(dbg.vcs.Mem.Cart.Hash)
|
|
if err != nil {
|
|
logger.Logf("debugger", err.Error())
|
|
}
|
|
|
|
// always ReqBotFeedback. if feedback is nil then the bot features will be disbaled
|
|
err = dbg.gui.SetFeature(gui.ReqBotFeedback, feedback)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// record the most filename as the most recent ROM loaded if appropriate
|
|
if !dbg.vcs.Mem.Cart.IsEjected() {
|
|
dbg.Prefs.RecentROM.Set(cartload.Filename)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (dbg *Debugger) startRecording(cartShortName string) error {
|
|
recording := unique.Filename("recording", cartShortName)
|
|
|
|
var err error
|
|
|
|
dbg.recorder, err = recorder.NewRecorder(recording, dbg.vcs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (dbg *Debugger) endRecording() {
|
|
if dbg.recorder == nil {
|
|
return
|
|
}
|
|
defer func() {
|
|
dbg.recorder = nil
|
|
}()
|
|
|
|
err := dbg.recorder.End()
|
|
if err != nil {
|
|
logger.Logf("debugger", err.Error())
|
|
}
|
|
}
|
|
|
|
func (dbg *Debugger) startPlayback(filename string) error {
|
|
plb, err := recorder.NewPlayback(filename, dbg.opts.PlaybackCheckROM)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = dbg.attachCartridge(plb.CartLoad)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = plb.AttachToVCSInput(dbg.vcs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dbg.playback = plb
|
|
|
|
return nil
|
|
}
|
|
|
|
func (dbg *Debugger) endPlayback() {
|
|
if dbg.playback == nil {
|
|
return
|
|
}
|
|
|
|
dbg.playback = nil
|
|
dbg.vcs.Input.AttachPlayback(nil)
|
|
}
|
|
|
|
func (dbg *Debugger) startComparison(comparisonROM string, comparisonPrefs string) error {
|
|
if comparisonROM == "" {
|
|
return nil
|
|
}
|
|
|
|
// add any bespoke comparision prefs
|
|
prefs.PushCommandLineStack(comparisonPrefs)
|
|
|
|
var err error
|
|
|
|
dbg.comparison, err = comparison.NewComparison(dbg.vcs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = dbg.gui.SetFeature(gui.ReqComparison, dbg.comparison.Render, dbg.comparison.DiffRender)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// cartload is passed to comparision.CreateFromLoader(). closure will be
|
|
// handled from there
|
|
cartload, err := cartridgeloader.NewLoaderFromFilename(comparisonROM, "AUTO")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// comparision emulation handles closure of cartridgeloader
|
|
dbg.comparison.CreateFromLoader(cartload)
|
|
|
|
// check use of comparison prefs
|
|
comparisonPrefs = prefs.PopCommandLineStack()
|
|
if comparisonPrefs != "" {
|
|
logger.Logf("debugger", "%s unused for comparison emulation", comparisonPrefs)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (dbg *Debugger) endComparison() {
|
|
if dbg.comparison == nil {
|
|
return
|
|
}
|
|
|
|
dbg.comparison.Quit()
|
|
dbg.comparison = nil
|
|
err := dbg.gui.SetFeature(gui.ReqComparison, nil, nil)
|
|
if err != nil {
|
|
logger.Logf("debugger", err.Error())
|
|
}
|
|
}
|
|
|
|
// parseInput splits the input into individual commands. each command is then
|
|
// passed to parseCommand for processing
|
|
//
|
|
// interactive argument should be true if the input that has just come from
|
|
// the user (ie. via an interactive terminal). only interactive input will be
|
|
// added to a new script file.
|
|
//
|
|
// auto argument should be true if command is being run as part of ONHALT or
|
|
// ONSTEP
|
|
//
|
|
// returns a boolean stating whether the emulation should continue with the
|
|
// next step.
|
|
func (dbg *Debugger) parseInput(input string, interactive bool, auto bool) error {
|
|
var err error
|
|
|
|
// ignore comments
|
|
if strings.HasPrefix(input, "#") {
|
|
return nil
|
|
}
|
|
|
|
// divide input if necessary
|
|
commands := strings.Split(input, ";")
|
|
|
|
// loop through commands
|
|
for i := 0; i < len(commands); i++ {
|
|
// parse command
|
|
err = dbg.parseCommand(commands[i], interactive, !auto)
|
|
if err != nil {
|
|
// we don't want to record bad commands in script
|
|
dbg.scriptScribe.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Plugged implements the plugging.PlugMonitor interface.
|
|
func (dbg *Debugger) Plugged(port plugging.PortID, peripheral plugging.PeripheralID) {
|
|
if dbg.vcs.Mem.Cart.IsEjected() {
|
|
return
|
|
}
|
|
err := dbg.gui.SetFeature(gui.ReqPeripheralPlugged, port, peripheral)
|
|
if err != nil {
|
|
logger.Log("debugger", err.Error())
|
|
}
|
|
}
|
|
|
|
func (dbg *Debugger) reloadCartridge() error {
|
|
if dbg.cartload == nil {
|
|
return nil
|
|
}
|
|
|
|
// reset macro to beginning
|
|
if dbg.macro != nil {
|
|
dbg.macro.Reset()
|
|
}
|
|
|
|
return dbg.insertCartridge(dbg.cartload.Filename)
|
|
}
|
|
|
|
// ReloadCartridge inserts the current cartridge and states the emulation over.
|
|
func (dbg *Debugger) ReloadCartridge() {
|
|
dbg.events.Signal <- syscall.SIGHUP
|
|
}
|
|
|
|
func (dbg *Debugger) insertCartridge(filename string) error {
|
|
if filename == "" {
|
|
filename = dbg.cartload.Filename
|
|
}
|
|
|
|
cartload, err := cartridgeloader.NewLoaderFromFilename(filename, "AUTO")
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
// cartload is should be passed to attachCartridge() almost immediately. the
|
|
// closure of cartload will then be handled for us
|
|
err = dbg.attachCartridge(cartload)
|
|
if err != nil {
|
|
return fmt.Errorf("debugger: %w", err)
|
|
}
|
|
|
|
if dbg.forcedROMselection != nil {
|
|
dbg.forcedROMselection <- true
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// InsertCartridge into running emulation. If filename argument is empty the
|
|
// currently inserted cartridge will be reinserted.
|
|
//
|
|
// It should not be run directly from the emulation/debugger goroutine, use
|
|
// insertCartridge() for that
|
|
func (dbg *Debugger) InsertCartridge(filename string) {
|
|
dbg.PushFunctionImmediate(func() {
|
|
dbg.setState(govern.Initialising, govern.Normal)
|
|
dbg.unwindLoop(func() error {
|
|
return dbg.insertCartridge(filename)
|
|
})
|
|
})
|
|
}
|
|
|
|
// GetLiveDisasmEntry returns the formatted disasembly entry of the last CPU
|
|
// execution and the bank information
|
|
func (dbg *Debugger) GetLiveDisasmEntry() disassembly.Entry {
|
|
if dbg.liveDisasmEntry == nil {
|
|
return disassembly.Entry{}
|
|
}
|
|
|
|
return *dbg.liveDisasmEntry
|
|
}
|
|
|
|
// GetCoProcBus returns the interface to a cartridge's coprocessor
|
|
func (dbg *Debugger) GetCoProcBus() coprocessor.CartCoProcBus {
|
|
return dbg.vcs.Mem.Cart.GetCoProcBus()
|
|
}
|