mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
o debugger
- renamed metavideo concept to reflection - moved metavideo/reflection to its own package - added ReflectionNotRunning error - error message shown if overlay is turned on without reflection processing running - improved DISPLAY command o sdl - type assertion panics now caught in sdl.SetFeture() and returned as a PanicError
This commit is contained in:
parent
8db28019c6
commit
1814ed0544
15 changed files with 410 additions and 296 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -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
|
||||
}
|
193
debugger/reflection/reflection.go
Normal file
193
debugger/reflection/reflection.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
17
gui/gui.go
17
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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue