diff --git a/debugger/commands.go b/debugger/commands.go index 114291c2..45937dca 100644 --- a/debugger/commands.go +++ b/debugger/commands.go @@ -38,7 +38,7 @@ const ( cmdLast = "LAST" cmdList = "LIST" cmdMemMap = "MEMMAP" - cmdMetaVideo = "METAVIDEO" + cmdReflect = "REFLECT" cmdMissile = "MISSILE" cmdOnHalt = "ONHALT" cmdOnStep = "ONSTEP" @@ -78,7 +78,7 @@ var commandTemplate = []string{ cmdDebuggerState, cmdDigest + " (RESET)", cmdDisassembly, - cmdDisplay + " (OFF|DEBUG|SCALE [%P]|DEBUGCOLORS)", // see notes + cmdDisplay + " (ON|OFF|DEBUG (ON|OFF)|SCALE [%P]|ALT (ON|OFF)|OVERLAY (ON|OFF))", // see notes cmdDrop + " [BREAK|TRAP|WATCH] %N", cmdGrep + " %S", cmdHexLoad + " %N %N {%N}", @@ -86,7 +86,7 @@ var commandTemplate = []string{ cmdLast + " (DEFN)", cmdList + " [BREAKS|TRAPS|WATCHES|ALL]", cmdMemMap, - cmdMetaVideo + " (ON|OFF)", + cmdReflect + " (ON|OFF)", cmdMissile + " (0|1)", cmdOnHalt + " (OFF|ON|%S {%S})", cmdOnStep + " (OFF|ON|%S {%S})", @@ -189,6 +189,17 @@ func (dbg *Debugger) parseCommand(userInput *string, interactive bool) (parseCom return doNothing, err } + // make sure all tokens have been handled. this should only happen if + // input has been allowed by ValidateTokens() but has not been + // explicitely consumed by entactCommand() + if interactive { + defer func() { + if !tokens.IsEnd() { + dbg.print(console.StyleError, fmt.Sprintf("unhandled arguments in user input (%s)", tokens.Remainder())) + } + }() + } + // the absolute best thing about the ValidateTokens() function is that we // don't need to worrying too much about the success of tokens.Get() in the // enactCommand() function below: @@ -597,18 +608,22 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool) case cmdMemMap: dbg.print(console.StyleInstrument, "%v", dbg.vcs.Mem.MemoryMap()) - case cmdMetaVideo: + case cmdReflect: option, _ := tokens.Get() switch strings.ToUpper(option) { case "OFF": - dbg.metaVideoProcess = false + dbg.reflectProcess = false + err := dbg.gui.SetFeature(gui.ReqSetOverlay, false) + if err != nil { + dbg.print(console.StyleError, err.Error()) + } case "ON": - dbg.metaVideoProcess = true + dbg.reflectProcess = true } - if dbg.metaVideoProcess { - dbg.print(console.StyleEmulatorInfo, "metavideo processing: ON") + if dbg.reflectProcess { + dbg.print(console.StyleEmulatorInfo, "reflection: ON") } else { - dbg.print(console.StyleEmulatorInfo, "metavideo processing: OFF") + dbg.print(console.StyleEmulatorInfo, "reflection: OFF") } case cmdExit: @@ -993,48 +1008,98 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool) case cmdDisplay: var err error - action, present := tokens.Get() - if present { + action, _ := tokens.Get() + action = strings.ToUpper(action) + switch action { + case "ON": + err = dbg.gui.SetFeature(gui.ReqSetVisibility, true) + if err != nil { + return doNothing, err + } + case "OFF": + err = dbg.gui.SetFeature(gui.ReqSetVisibility, false) + if err != nil { + return doNothing, err + } + case "DEBUG": + action, _ := tokens.Get() action = strings.ToUpper(action) switch action { case "OFF": - err = dbg.gui.SetFeature(gui.ReqSetVisibility, false) + err = dbg.gui.SetFeature(gui.ReqSetMasking, false) if err != nil { return doNothing, err } - case "DEBUG": - err = dbg.gui.SetFeature(gui.ReqToggleMasking) - if err != nil { - return doNothing, err - } - case "SCALE": - scl, present := tokens.Get() - if !present { - return doNothing, errors.New(errors.CommandError, fmt.Sprintf("value required for %s %s", command, action)) - } - - scale, err := strconv.ParseFloat(scl, 32) - if err != nil { - return doNothing, errors.New(errors.CommandError, fmt.Sprintf("%s %s value not valid (%s)", command, action, scl)) - } - - err = dbg.gui.SetFeature(gui.ReqSetScale, float32(scale)) - return doNothing, err - case "DEBUGCOLORS": - err = dbg.gui.SetFeature(gui.ReqToggleAltColors) - if err != nil { - return doNothing, err - } - case "METASIGNALS": - err = dbg.gui.SetFeature(gui.ReqToggleShowMetaVideo) + case "ON": + err = dbg.gui.SetFeature(gui.ReqSetMasking, true) if err != nil { return doNothing, err } default: - // already caught by command line ValidateTokens() + err = dbg.gui.SetFeature(gui.ReqToggleMasking) + if err != nil { + return doNothing, err + } } - } else { - err = dbg.gui.SetFeature(gui.ReqSetVisibility, true) + case "SCALE": + scl, present := tokens.Get() + if !present { + return doNothing, errors.New(errors.CommandError, fmt.Sprintf("value required for %s %s", command, action)) + } + + scale, err := strconv.ParseFloat(scl, 32) + if err != nil { + return doNothing, errors.New(errors.CommandError, fmt.Sprintf("%s %s value not valid (%s)", command, action, scl)) + } + + err = dbg.gui.SetFeature(gui.ReqSetScale, float32(scale)) + return doNothing, err + case "ALT": + action, _ := tokens.Get() + action = strings.ToUpper(action) + switch action { + case "OFF": + err = dbg.gui.SetFeature(gui.ReqSetAltColors, false) + if err != nil { + return doNothing, err + } + case "ON": + err = dbg.gui.SetFeature(gui.ReqSetAltColors, true) + if err != nil { + return doNothing, err + } + default: + err = dbg.gui.SetFeature(gui.ReqToggleAltColors) + if err != nil { + return doNothing, err + } + } + case "OVERLAY": + if !dbg.reflectProcess { + return doNothing, errors.New(errors.ReflectionNotRunning) + } + + action, _ := tokens.Get() + action = strings.ToUpper(action) + switch action { + case "OFF": + err = dbg.gui.SetFeature(gui.ReqSetOverlay, false) + if err != nil { + return doNothing, err + } + case "ON": + err = dbg.gui.SetFeature(gui.ReqSetOverlay, true) + if err != nil { + return doNothing, err + } + default: + err = dbg.gui.SetFeature(gui.ReqToggleOverlay) + if err != nil { + return doNothing, err + } + } + default: + err = dbg.gui.SetFeature(gui.ReqToggleVisibility) if err != nil { return doNothing, err } diff --git a/debugger/debugger.go b/debugger/debugger.go index b9198a58..f33cc97a 100644 --- a/debugger/debugger.go +++ b/debugger/debugger.go @@ -3,6 +3,7 @@ package debugger import ( "gopher2600/debugger/commandline" "gopher2600/debugger/console" + "gopher2600/debugger/reflection" "gopher2600/debugger/script" "gopher2600/disassembly" "gopher2600/errors" @@ -45,13 +46,11 @@ type Debugger struct { // most fruitfully performed through this structure dbgmem *memoryDebug - // metavideo is additional information about the emulation state (ie. - // if a sprite was reset or if WSYNC is active, etc.) - // - // metavideo.Check() is called every video cycle to inform the gui of - // the metainformation of the last television signal - metaVideoProcess bool - metavideo *metavideoMonitor + // reflection is used to provideo additional information about the + // emulation. it is inherently slow so can be turned on/off with the + // reflectProcess switch + reflectProcess bool + relfectMonitor *reflection.Monitor // halt conditions breakpoints *breakpoints @@ -159,9 +158,9 @@ func NewDebugger(tvType string) (*Debugger, error) { // set up debugging interface to memory dbg.dbgmem = &memoryDebug{mem: dbg.vcs.Mem, symtable: &dbg.disasm.Symtable} - // set up metavideo monitor - dbg.metaVideoProcess = true - dbg.metavideo = newMetavideoMonitor(dbg.vcs, dbg.gui) + // set up reflection monitor + dbg.reflectProcess = true + dbg.relfectMonitor = reflection.NewMonitor(dbg.vcs, dbg.gui) // set up breakpoints/traps dbg.breakpoints = newBreakpoints(dbg) @@ -299,8 +298,8 @@ func (dbg *Debugger) videoCycle() error { dbg.trapMessages = dbg.traps.check(dbg.trapMessages) dbg.watchMessages = dbg.watches.check(dbg.watchMessages) - if dbg.metaVideoProcess { - return dbg.metavideo.Check() + if dbg.reflectProcess { + return dbg.relfectMonitor.Check() } return nil @@ -329,7 +328,11 @@ func (dbg *Debugger) inputLoop(inputter console.UserInput, videoCycle bool) erro } for { - dbg.checkInterruptsAndEvents() + err = dbg.checkInterruptsAndEvents() + if err != nil { + return err + } + if !dbg.running { break // for loop } @@ -459,7 +462,10 @@ func (dbg *Debugger) inputLoop(inputter console.UserInput, videoCycle bool) erro } } - dbg.checkInterruptsAndEvents() + err = dbg.checkInterruptsAndEvents() + if err != nil { + dbg.print(console.StyleError, err.Error()) + } if !dbg.running { break // for loop } diff --git a/debugger/events.go b/debugger/events.go index 2a65bd29..d923360c 100644 --- a/debugger/events.go +++ b/debugger/events.go @@ -31,8 +31,14 @@ func (dbg *Debugger) guiEventHandler(event gui.Event) error { // toggle debugging colours err = dbg.gui.SetFeature(gui.ReqToggleAltColors) case "2": - // toggle metasignals overlay - err = dbg.gui.SetFeature(gui.ReqToggleShowMetaVideo) + // toggle overlay + + // !!TODO: handle error if reflection is not being processed + // if !dbg.reflectProcess { + // return errors.New(errors.ReflectionNotRunning) + // } + + err = dbg.gui.SetFeature(gui.ReqToggleOverlay) case "=": fallthrough // equal sign is the same as plus, for convenience @@ -56,7 +62,9 @@ func (dbg *Debugger) guiEventHandler(event gui.Event) error { return err } -func (dbg *Debugger) checkInterruptsAndEvents() { +func (dbg *Debugger) checkInterruptsAndEvents() error { + var err error + // check interrupt channel and run any functions we find in there select { case <-dbg.intChan: @@ -86,9 +94,11 @@ func (dbg *Debugger) checkInterruptsAndEvents() { } } case ev := <-dbg.guiChan: - dbg.guiEventHandler(ev) + err = dbg.guiEventHandler(ev) default: // pro-tip: default case required otherwise the select will block // indefinately. } + + return err } diff --git a/debugger/help.go b/debugger/help.go index 705d89ea..8b769a84 100644 --- a/debugger/help.go +++ b/debugger/help.go @@ -19,7 +19,7 @@ var Help = map[string]string{ cmdLast: "Prints the result of the last cpu/video cycle", cmdList: "List current entries for BREAKS and TRAPS", cmdMemMap: "Display high-level VCS memory map", - cmdMetaVideo: "Turn metavideo processing on/off. Metavideo processing slows the debugger down.", + cmdReflect: "Turn reflection on/off. this will slow down the debugger.", cmdMissile: "Display the current state of the missile 0/1 sprite", cmdOnHalt: "Commands to run whenever emulation is halted (separate commands with comma)", cmdOnStep: "Commands to run whenever emulation steps forward an cpu/video cycle (separate commands with comma)", diff --git a/debugger/metavideo.go b/debugger/metavideo.go deleted file mode 100644 index 298fbed3..00000000 --- a/debugger/metavideo.go +++ /dev/null @@ -1,191 +0,0 @@ -package debugger - -// the whole metavideo system is slow. probably to do with indexing maps every -// video cycle. but I'm not too worried about it at the moment because it only -// ever runs in the debugger and the debugger is slow anyway (when compared to -// the playmode loop) -// -// it's also a bit of a hack. I didn't want to invade the emulation code too -// much but if we want to get fancier with this metavideo idea then we may have -// to. but in that case emulation performance should remain the priority. - -import ( - "gopher2600/gui/metavideo" - "gopher2600/hardware" - "gopher2600/hardware/tia/future" - "time" -) - -// metavideoMonitor watches for writes to specific video related memory locations. when -// these locations are written to, a MetaSignal is sent to the Renderer -// implementation. moreover, if the monitor detects that the effect of the -// memory write is delayed or sustained, then the signal is repeated as -// appropriate. -type metavideoMonitor struct { - VCS *hardware.VCS - Renderer metavideo.Renderer - - groupTIA metavideoGroup - groupPlayer0 metavideoGroup - groupPlayer1 metavideoGroup - groupMissile0 metavideoGroup - groupMissile1 metavideoGroup - groupBall metavideoGroup -} - -func newMetavideoMonitor(vcs *hardware.VCS, renderer metavideo.Renderer) *metavideoMonitor { - mon := &metavideoMonitor{VCS: vcs, Renderer: renderer} - - mon.groupTIA.addresses = metaSignals{ - 0x03: metavideo.MetaSignalAttributes{Label: "RSYNC", Red: 255, Green: 10, Blue: 0, Alpha: 255, Scheduled: true}, - 0x2a: metavideo.MetaSignalAttributes{Label: "HMOVE", Red: 255, Green: 20, Blue: 0, Alpha: 255, Scheduled: true}, - 0x2b: metavideo.MetaSignalAttributes{Label: "HMCLR", Red: 255, Green: 30, Blue: 0, Alpha: 255, Scheduled: false}, - } - - mon.groupPlayer0.addresses = metaSignals{ - 0x04: metavideo.MetaSignalAttributes{Label: "NUSIZx", Red: 0, Green: 10, Blue: 255, Alpha: 255, Scheduled: true}, - 0x10: metavideo.MetaSignalAttributes{Label: "RESPx", Red: 0, Green: 30, Blue: 255, Alpha: 255, Scheduled: true}, - } - - mon.groupPlayer1.addresses = metaSignals{ - 0x05: metavideo.MetaSignalAttributes{Label: "NUSIZx", Red: 0, Green: 50, Blue: 255, Alpha: 255, Scheduled: true}, - 0x11: metavideo.MetaSignalAttributes{Label: "RESPx", Red: 0, Green: 70, Blue: 255, Alpha: 255, Scheduled: true}, - } - - mon.groupMissile0.addresses = metaSignals{ - 0x04: metavideo.MetaSignalAttributes{Label: "NUSIZx", Red: 0, Green: 50, Blue: 255, Alpha: 255, Scheduled: false}, - 0x11: metavideo.MetaSignalAttributes{Label: "RESMx", Red: 0, Green: 70, Blue: 0, Alpha: 255, Scheduled: true}, - } - - mon.groupMissile1.addresses = metaSignals{ - 0x05: metavideo.MetaSignalAttributes{Label: "NUSIZx", Red: 0, Green: 50, Blue: 0, Alpha: 255, Scheduled: false}, - 0x12: metavideo.MetaSignalAttributes{Label: "RESMx", Red: 0, Green: 70, Blue: 0, Alpha: 255, Scheduled: true}, - } - - mon.groupBall.addresses = metaSignals{ - 0x14: metavideo.MetaSignalAttributes{Label: "RESBL", Red: 0, Green: 255, Blue: 10, Alpha: 255, Scheduled: true}, - } - - return mon -} - -type metaSignals map[uint16]metavideo.MetaSignalAttributes - -type metavideoGroup struct { - // the map of memory addresses to monitor - addresses metaSignals - - // ----------------- - - // when memory has been written to we note the address and timestamp. then, - // a few cycles later, we check to see if lastAddress is one the group is - // interested in seeing - lastAddress uint16 - lastAddressTimestamp time.Time - lastAddressFound int - - // if the memory write resulted in an effect that won't occur until - // sometime in the future then the Delay attribute for the part of the - // system monitored by the group will yield a pointer to the future Event - lastEvent *future.Event - - // a copy of the last metasignal sent to the metavideo renderer. we use - // this to repeat a signal when lastEvent is not nil and has not yet - // completed - signal metavideo.MetaSignalAttributes -} - -// Check should be called every video cycle to record the current state of the -// emulation/system -func (mon *metavideoMonitor) Check() error { - if err := mon.groupWSYNC(); err != nil { - return err - } - - if err := mon.checkGroup(&mon.groupTIA, mon.VCS.TIA.Delay); err != nil { - return err - } - - if err := mon.checkGroup(&mon.groupPlayer0, mon.VCS.TIA.Video.Player0.Delay); err != nil { - return err - } - - if err := mon.checkGroup(&mon.groupPlayer1, mon.VCS.TIA.Video.Player1.Delay); err != nil { - return err - } - - if err := mon.checkGroup(&mon.groupMissile0, mon.VCS.TIA.Video.Missile0.Delay); err != nil { - return err - } - - if err := mon.checkGroup(&mon.groupMissile1, mon.VCS.TIA.Video.Missile1.Delay); err != nil { - return err - } - - if err := mon.checkGroup(&mon.groupBall, mon.VCS.TIA.Video.Ball.Delay); err != nil { - return err - } - - return nil -} - -func (mon *metavideoMonitor) groupWSYNC() error { - if mon.VCS.CPU.RdyFlg { - return nil - } - - // special handling of WSYNC signal - we want every pixel to be coloured - // while the RdyFlag is false, not just when WSYNC is first triggered. - sig := metavideo.MetaSignalAttributes{Label: "WSYNC", Red: 0, Green: 0, Blue: 0, Alpha: 200} - return mon.Renderer.MetaSignal(sig) -} - -func (mon *metavideoMonitor) checkGroup(group *metavideoGroup, delay future.Observer) error { - // if a new memory location (any memory location) has been written, then - // note the new address and begin the delayed metasignal process - // - // we filter on LastAccessTimeStamp rather than LastAccessAddress. - // filtering by address will probably work in most instances but it won't - // capture repeated writes to the same memory location. - if mon.VCS.Mem.LastAccessWrite && mon.VCS.Mem.LastAccessTimeStamp != group.lastAddressTimestamp { - group.lastAddress = mon.VCS.Mem.LastAccessAddress - group.lastAddressTimestamp = mon.VCS.Mem.LastAccessTimeStamp - - // 4 cycles seems plenty of time for an address to be serviced - group.lastAddressFound = 4 - } - - var signalStart bool - var sig metavideo.MetaSignalAttributes - - if group.lastAddressFound > 0 { - if sig, signalStart = group.addresses[group.lastAddress]; signalStart { - if sig.Scheduled { - // associate memory write with delay observation - if ev, ok := delay.Observe(sig.Label); ok { - group.lastEvent = ev - group.signal = sig - group.lastAddressFound = 1 // reduced to 0 almost immediately - } - } else { - group.lastEvent = nil - group.signal = sig - group.lastAddressFound = 1 // reduced to 0 almost immediately - } - } - group.lastAddressFound-- - } - - // send metasignal if an event is still running or if this is the end of a - // writeDelay period. the second condition catches memory writes that do - // not have an associated future.Event - if group.lastEvent != nil || signalStart { - group.lastEvent = nil - err := mon.Renderer.MetaSignal(group.signal) - if err != nil { - return err - } - } - - return nil -} diff --git a/debugger/reflection/reflection.go b/debugger/reflection/reflection.go new file mode 100644 index 00000000..ebb9ca11 --- /dev/null +++ b/debugger/reflection/reflection.go @@ -0,0 +1,193 @@ +package reflection + +// the whole reflection system is slow. probably to do with indexing maps every +// video cycle. but I'm not too worried about it at the moment because it only +// ever runs in the debugger and the debugger is slow anyway (when compared to +// the playmode loop) +// +// it's also a bit of a hack. I didn't want to invade the emulation code too +// much but if we want to get fancier with this idea then we may have to. but +// in that case emulation performance should remain the priority. + +import ( + "gopher2600/gui/overlay" + "gopher2600/hardware" + "gopher2600/hardware/memory" + "gopher2600/hardware/tia/future" + "time" +) + +// Monitor watches for writes to specific video related memory locations. when +// these locations are written to, a signal is sent to the overlay.Renderer +// implementation. moreover, if the monitor detects that the effect of the +// memory write is delayed or sustained, then the signal is repeated as +// appropriate. +type Monitor struct { + vcs *hardware.VCS + renderer overlay.Renderer + + groupTIA addressMonitor + groupPlayer0 addressMonitor + groupPlayer1 addressMonitor + groupMissile0 addressMonitor + groupMissile1 addressMonitor + groupBall addressMonitor +} + +// NewMonitor is the preferred method of initialisation for the Monitor type +func NewMonitor(vcs *hardware.VCS, renderer overlay.Renderer) *Monitor { + mon := &Monitor{vcs: vcs, renderer: renderer} + + mon.groupTIA.addresses = overlaySignals{ + 0x03: overlay.Signal{Label: "RSYNC", Red: 255, Green: 10, Blue: 0, Alpha: 255, Scheduled: true}, + 0x2a: overlay.Signal{Label: "HMOVE", Red: 255, Green: 20, Blue: 0, Alpha: 255, Scheduled: true}, + 0x2b: overlay.Signal{Label: "HMCLR", Red: 255, Green: 30, Blue: 0, Alpha: 255, Scheduled: false}, + } + + mon.groupPlayer0.addresses = overlaySignals{ + 0x04: overlay.Signal{Label: "NUSIZx", Red: 0, Green: 10, Blue: 255, Alpha: 255, Scheduled: true}, + 0x10: overlay.Signal{Label: "RESPx", Red: 0, Green: 30, Blue: 255, Alpha: 255, Scheduled: true}, + } + + mon.groupPlayer1.addresses = overlaySignals{ + 0x05: overlay.Signal{Label: "NUSIZx", Red: 0, Green: 50, Blue: 255, Alpha: 255, Scheduled: true}, + 0x11: overlay.Signal{Label: "RESPx", Red: 0, Green: 70, Blue: 255, Alpha: 255, Scheduled: true}, + } + + mon.groupMissile0.addresses = overlaySignals{ + 0x04: overlay.Signal{Label: "NUSIZx", Red: 0, Green: 50, Blue: 255, Alpha: 255, Scheduled: false}, + 0x11: overlay.Signal{Label: "RESMx", Red: 0, Green: 70, Blue: 0, Alpha: 255, Scheduled: true}, + } + + mon.groupMissile1.addresses = overlaySignals{ + 0x05: overlay.Signal{Label: "NUSIZx", Red: 0, Green: 50, Blue: 0, Alpha: 255, Scheduled: false}, + 0x12: overlay.Signal{Label: "RESMx", Red: 0, Green: 70, Blue: 0, Alpha: 255, Scheduled: true}, + } + + mon.groupBall.addresses = overlaySignals{ + 0x14: overlay.Signal{Label: "RESBL", Red: 0, Green: 255, Blue: 10, Alpha: 255, Scheduled: true}, + } + + return mon +} + +// Check should be called every video cycle to record the current state of the +// emulation/system +func (mon *Monitor) Check() error { + if err := mon.checkWSYNC(); err != nil { + return err + } + + if err := mon.groupTIA.check(mon.renderer, mon.vcs.Mem, mon.vcs.TIA.Delay); err != nil { + return err + } + + if err := mon.groupPlayer0.check(mon.renderer, mon.vcs.Mem, mon.vcs.TIA.Video.Player0.Delay); err != nil { + return err + } + + if err := mon.groupPlayer1.check(mon.renderer, mon.vcs.Mem, mon.vcs.TIA.Video.Player1.Delay); err != nil { + return err + } + + if err := mon.groupMissile0.check(mon.renderer, mon.vcs.Mem, mon.vcs.TIA.Video.Missile0.Delay); err != nil { + return err + } + + if err := mon.groupMissile1.check(mon.renderer, mon.vcs.Mem, mon.vcs.TIA.Video.Missile1.Delay); err != nil { + return err + } + + if err := mon.groupBall.check(mon.renderer, mon.vcs.Mem, mon.vcs.TIA.Video.Ball.Delay); err != nil { + return err + } + + return nil +} + +func (mon *Monitor) checkWSYNC() error { + if mon.vcs.CPU.RdyFlg { + return nil + } + + // special handling of WSYNC signal - we want every pixel to be coloured + // while the RdyFlag is false, not just when WSYNC is first triggered. + sig := overlay.Signal{Label: "WSYNC", Red: 0, Green: 0, Blue: 0, Alpha: 200} + return mon.renderer.OverlaySignal(sig) +} + +type overlaySignals map[uint16]overlay.Signal + +type addressMonitor struct { + // the map of memory addresses to monitor + addresses overlaySignals + + // ----------------- + + // when memory has been written to we note the address and timestamp. then, + // a few cycles later, we check to see if lastAddress is one the group is + // interested in seeing + lastAddress uint16 + lastAddressTimestamp time.Time + lastAddressFound int + + // if the memory write resulted in an effect that won't occur until + // sometime in the future then the Delay attribute for the part of the + // system monitored by the group will yield a pointer to the future Event + lastEvent *future.Event + + // a copy of the last signal sent to the overlay renderer. we use + // this to repeat a signal when lastEvent is not nil and has not yet + // completed + signal overlay.Signal +} + +func (adm *addressMonitor) check(rend overlay.Renderer, mem *memory.VCSMemory, delay future.Observer) error { + // if a new memory location (any memory location) has been written, then + // note the new address and begin the delayed signalling process + // + // we filter on LastAccessTimeStamp rather than LastAccessAddress. + // filtering by address will probably work in most instances but it won't + // capture repeated writes to the same memory location. + if mem.LastAccessWrite && mem.LastAccessTimeStamp != adm.lastAddressTimestamp { + adm.lastAddress = mem.LastAccessAddress + adm.lastAddressTimestamp = mem.LastAccessTimeStamp + + // 4 cycles seems plenty of time for an address to be serviced + adm.lastAddressFound = 4 + } + + var signalStart bool + var sig overlay.Signal + + if adm.lastAddressFound > 0 { + if sig, signalStart = adm.addresses[adm.lastAddress]; signalStart { + if sig.Scheduled { + // associate memory write with delay observation + if ev, ok := delay.Observe(sig.Label); ok { + adm.lastEvent = ev + adm.signal = sig + adm.lastAddressFound = 1 // reduced to 0 almost immediately + } + } else { + adm.lastEvent = nil + adm.signal = sig + adm.lastAddressFound = 1 // reduced to 0 almost immediately + } + } + adm.lastAddressFound-- + } + + // send signal if an event is still running or if this is the end of a + // writeDelay period. the second condition catches memory writes that do + // not have an associated future.Event + if adm.lastEvent != nil || signalStart { + adm.lastEvent = nil + err := rend.OverlaySignal(adm.signal) + if err != nil { + return err + } + } + + return nil +} diff --git a/errors/categories.go b/errors/categories.go index 43ca6cf6..7ea1bcdd 100644 --- a/errors/categories.go +++ b/errors/categories.go @@ -8,6 +8,10 @@ const ( // panic()s and cause the program (or the sub-system) to cease as soon as // possible. // + // if is not practical to cause the program to cease then at the very + // least, the PanicError should result in the display of the error message + // in big, friendly letters. + // // actual panic()s should only be used when the mistake is so heinous that // it suggests a fundamental misunderstanding has taken place and so, as it // were, all bets are off. @@ -33,6 +37,7 @@ const ( InvalidTarget CommandError TerminalError + ReflectionNotRunning // script ScriptScribeError diff --git a/errors/messages.go b/errors/messages.go index e69232e3..deebc346 100644 --- a/errors/messages.go +++ b/errors/messages.go @@ -2,7 +2,7 @@ package errors var messages = map[Errno]string{ // panics - PanicError: "fatality: %s: %s", + PanicError: "FATALITY: %s: %s", // sentinals UserInterrupt: "user interrupt", @@ -19,11 +19,12 @@ var messages = map[Errno]string{ DisasmError: "error during disassembly: %s", // debugger - ParserError: "parser error: %s: %s (char %d)", // first placeholder is the command definition - ValidationError: "%s for %s", - InvalidTarget: "invalid target (%s)", - CommandError: "%s", - TerminalError: "%s", + ParserError: "parser error: %s: %s (char %d)", // first placeholder is the command definition + ValidationError: "%s for %s", + InvalidTarget: "invalid target (%s)", + CommandError: "%s", + TerminalError: "%s", + ReflectionNotRunning: "reflection process is not running", // script ScriptFileError: "script error: %s", diff --git a/gui/gui.go b/gui/gui.go index 452d8ae2..032a31b8 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -1,17 +1,20 @@ package gui import ( - "gopher2600/gui/metavideo" + "gopher2600/gui/overlay" "gopher2600/television" ) // FeatureReq is used to request the setting of a gui attribute -// eg. toggling the metavideo layer +// eg. toggling the overlay type FeatureReq int -// list of valid feature requests +// list of valid feature requests. argument must be of the type specified or +// else the interface{} type conversion will fail and the application will +// probably crash const ( - ReqSetVisibility FeatureReq = iota // bool, optional bool (update on show) + ReqSetVisibility FeatureReq = iota // bool, optional bool (update on show) default true + ReqToggleVisibility // optional bool (update on show) default true ReqSetVisibilityStable // none ReqSetAllowDebugging // bool ReqSetPause // bool @@ -19,8 +22,8 @@ const ( ReqToggleMasking // none ReqSetAltColors // bool ReqToggleAltColors // none - ReqSetShowMetaVideo // bool - ReqToggleShowMetaVideo // none + ReqSetOverlay // bool + ReqToggleOverlay // none ReqSetScale // float ReqIncScale // none ReqDecScale // none @@ -31,7 +34,7 @@ type GUI interface { television.Television television.Renderer television.AudioMixer - metavideo.Renderer + overlay.Renderer // returns true if GUI is currently visible. false if not IsVisible() bool diff --git a/gui/metavideo/metavideo.go b/gui/overlay/overlay.go similarity index 52% rename from gui/metavideo/metavideo.go rename to gui/overlay/overlay.go index 81d347dc..5f87b74d 100644 --- a/gui/metavideo/metavideo.go +++ b/gui/overlay/overlay.go @@ -1,21 +1,21 @@ -package metavideo +package overlay // Renderer implementations will add signal information to a presentation layer // somehow. type Renderer interface { - MetaSignal(MetaSignalAttributes) error + OverlaySignal(Signal) error } -// MetaSignalAttributes contains information about the last television signal. it is up to -// the Renderer to match this up with the last television signal -type MetaSignalAttributes struct { +// Signal contains additional debugging information from the last video cycle. +// it is up to the Renderer to match this up with the last television signal +type Signal struct { Label string // Renderer implementations are free to use the color information // as they wish (adding alpha information seems a probable scenario). Red, Green, Blue, Alpha byte - // whether the meta-signal is one that is "instant" or resolves after a + // whether the attribute is one that is "instant" or resolves after a // short scheduled delay Scheduled bool } diff --git a/gui/sdl/metavideo.go b/gui/sdl/overlay.go similarity index 81% rename from gui/sdl/metavideo.go rename to gui/sdl/overlay.go index 49ebe5a0..f4f3adeb 100644 --- a/gui/sdl/metavideo.go +++ b/gui/sdl/overlay.go @@ -1,12 +1,12 @@ package sdl import ( - "gopher2600/gui/metavideo" + "gopher2600/gui/overlay" "github.com/veandco/go-sdl2/sdl" ) -type metaVideoOverlay struct { +type sdlOverlay struct { scr *screen texture *sdl.Texture @@ -18,8 +18,8 @@ type metaVideoOverlay struct { labels [][]string } -func newMetaVideoOverlay(scr *screen) (*metaVideoOverlay, error) { - mv := new(metaVideoOverlay) +func newSdlOverlay(scr *screen) (*sdlOverlay, error) { + mv := new(sdlOverlay) mv.scr = scr // our acutal screen data @@ -51,7 +51,7 @@ func newMetaVideoOverlay(scr *screen) (*metaVideoOverlay, error) { return mv, nil } -func (mv *metaVideoOverlay) setPixel(sig metavideo.MetaSignalAttributes) error { +func (mv *sdlOverlay) setPixel(sig overlay.Signal) error { i := (mv.scr.lastY*mv.scr.maxWidth + mv.scr.lastX) * scrDepth if i >= int32(len(mv.pixels)) { @@ -69,7 +69,7 @@ func (mv *metaVideoOverlay) setPixel(sig metavideo.MetaSignalAttributes) error { return nil } -func (mv *metaVideoOverlay) newFrame() { +func (mv *sdlOverlay) newFrame() { // swap pixel array with pixelsFade array // -- see comment in sdl.screen.newFrame() function for why we do this swp := mv.pixels @@ -82,7 +82,7 @@ func (mv *metaVideoOverlay) newFrame() { } } -func (mv *metaVideoOverlay) update(paused bool) error { +func (mv *sdlOverlay) update(paused bool) error { if paused { err := mv.textureFade.Update(nil, mv.pixelsFade, int(mv.scr.maxWidth*scrDepth)) if err != nil { @@ -108,12 +108,12 @@ func (mv *metaVideoOverlay) update(paused bool) error { return nil } -// MetaSignal recieves (and processes) additional emulator information from the emulator -func (gtv *GUI) MetaSignal(sig metavideo.MetaSignalAttributes) error { +// OverlaySignal recieves (and processes) additional emulator information from the emulator +func (gtv *GUI) OverlaySignal(sig overlay.Signal) error { // don't do anything if debugging is not enabled if !gtv.allowDebugging { return nil } - return gtv.scr.metaVideo.setPixel(sig) + return gtv.scr.overlay.setPixel(sig) } diff --git a/gui/sdl/requests.go b/gui/sdl/requests.go index 9291baf2..b4d06c05 100644 --- a/gui/sdl/requests.go +++ b/gui/sdl/requests.go @@ -3,10 +3,19 @@ package sdl import ( "gopher2600/errors" "gopher2600/gui" + + "github.com/veandco/go-sdl2/sdl" ) // SetFeature is used to set a television attribute -func (gtv *GUI) SetFeature(request gui.FeatureReq, args ...interface{}) error { +func (gtv *GUI) SetFeature(request gui.FeatureReq, args ...interface{}) (returnedErr error) { + // lazy (but clear) handling of type assertion errors + defer func() { + if r := recover(); r != nil { + returnedErr = errors.New(errors.PanicError, "sdl.SetFeature()", r) + } + }() + switch request { case gui.ReqSetVisibilityStable: err := gtv.scr.stb.resolveSetVisibility() @@ -27,6 +36,19 @@ func (gtv *GUI) SetFeature(request gui.FeatureReq, args ...interface{}) error { gtv.scr.window.Hide() } + case gui.ReqToggleVisibility: + if gtv.scr.window.GetFlags()&sdl.WINDOW_HIDDEN == sdl.WINDOW_HIDDEN { + gtv.scr.window.Show() + + // update screen + // -- default args[1] of true if not present + if len(args) < 2 || args[1].(bool) { + gtv.update() + } + } else { + gtv.scr.window.Hide() + } + case gui.ReqSetAllowDebugging: gtv.setDebugging(args[0].(bool)) gtv.update() @@ -51,12 +73,12 @@ func (gtv *GUI) SetFeature(request gui.FeatureReq, args ...interface{}) error { gtv.scr.useAltPixels = !gtv.scr.useAltPixels gtv.update() - case gui.ReqSetShowMetaVideo: - gtv.scr.showMetaVideo = args[0].(bool) + case gui.ReqSetOverlay: + gtv.scr.overlayActive = args[0].(bool) gtv.update() - case gui.ReqToggleShowMetaVideo: - gtv.scr.showMetaVideo = !gtv.scr.showMetaVideo + case gui.ReqToggleOverlay: + gtv.scr.overlayActive = !gtv.scr.overlayActive gtv.update() case gui.ReqSetScale: diff --git a/gui/sdl/screen.go b/gui/sdl/screen.go index 857cc458..6229e87c 100644 --- a/gui/sdl/screen.go +++ b/gui/sdl/screen.go @@ -74,11 +74,11 @@ type screen struct { altPixelsFade []byte useAltPixels bool - // overlay for screen showing metasignal information + // overlay for screen showing additional debugging information // -- always allocated but only used when tv.allowDebugging and - // showMetaVideo are true - metaVideo *metaVideoOverlay - showMetaVideo bool + // overlayActive are true + overlay *sdlOverlay + overlayActive bool } func newScreen(gtv *GUI) (*screen, error) { @@ -160,7 +160,7 @@ func (scr *screen) changeTVSpec() error { } // new overlay - scr.metaVideo, err = newMetaVideoOverlay(scr) + scr.overlay, err = newSdlOverlay(scr) if err != nil { return err } @@ -323,9 +323,9 @@ func (scr *screen) update(paused bool) error { scr.renderer.FillRect(&sdl.Rect{X: 0, Y: 0, W: int32(television.ClocksPerHblank), H: int32(scr.spec.ScanlinesTotal)}) } - // show metasignal overlay - if scr.gtv.allowDebugging && scr.showMetaVideo { - err = scr.metaVideo.update(paused) + // show overlay + if scr.gtv.allowDebugging && scr.overlayActive { + err = scr.overlay.update(paused) if err != nil { return err } @@ -390,8 +390,8 @@ func (scr *screen) newFrame() { scr.pixels = scr.pixelsFade scr.pixelsFade = swp - // clear pixels in metavideo overlay - scr.metaVideo.newFrame() + // clear pixels in overlay + scr.overlay.newFrame() // swap pixel array with pixelsFade array // -- see comment above diff --git a/regression/regression.go b/regression/regression.go index e08ff29f..424ae840 100644 --- a/regression/regression.go +++ b/regression/regression.go @@ -56,7 +56,7 @@ func initDBSession(db *database.Session) error { // RegressList displays all entries in the database func RegressList(output io.Writer) error { if output == nil { - return errors.New(errors.PanicError, "RegressList", "io.Writer should not be nil (use nopWriter)") + return errors.New(errors.PanicError, "RegressList()", "io.Writer should not be nil (use nopWriter)") } db, err := database.StartSession(regressionDBFile, database.ActivityReading, initDBSession) diff --git a/television/television.go b/television/television.go index 0aecbf37..3d361f18 100644 --- a/television/television.go +++ b/television/television.go @@ -25,7 +25,7 @@ type SignalAttributes struct { // AltPixel allows the emulator to set an alternative color for each pixel // - used to signal the debug color in addition to the regular color - // - arguable that this be sent as a metasignal + // - arguable that this be sent as some sort of meta-signal AltPixel ColorSignal // the HSyncSimple attribute is not part of the real TV spec. The signal