Gopher2600/debugger/debugger.go
JetSetIlly 28ae543e36 cartridgeloader commentary and documenation
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
2024-04-16 22:15:39 +01:00

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