diff --git a/debugger/debugger.go b/debugger/debugger.go index 66338d21b..17ae5eeef 100644 --- a/debugger/debugger.go +++ b/debugger/debugger.go @@ -460,6 +460,9 @@ func NewDebugger(opts CommandLineOptions, create CreateUserInterface) (*Debugger return nil, fmt.Errorf("debugger: %w", err) } + // attach rewind system as an event recorder + dbg.vcs.Input.AddRecorder(dbg.Rewind) + // add reflection system to the GUI dbg.ref = reflection.NewReflector(dbg.vcs) if r, ok := dbg.gui.(reflection.Broker); ok { @@ -576,6 +579,7 @@ func (dbg *Debugger) setState(state govern.State, subState govern.SubState) { dbg.ref.SetEmulationState(state) } dbg.CoProcDev.SetEmulationState(state) + dbg.Rewind.SetEmulationState(state) dbg.state.Store(state) dbg.subState.Store(subState) diff --git a/hardware/input/input.go b/hardware/input/input.go index 3969771c1..4a4202833 100644 --- a/hardware/input/input.go +++ b/hardware/input/input.go @@ -81,24 +81,26 @@ func (inp *Input) PeripheralID(id plugging.PortID) plugging.PeripheralID { // If a playback is currently active the input will not be handled and false // will be returned. func (inp *Input) HandleInputEvent(ev ports.InputEvent) (bool, error) { - for _, r := range inp.recorder { - err := r.RecordEvent(ports.TimedInputEvent{Time: inp.tv.GetCoords(), InputEvent: ev}) - if err != nil { - return false, err - } - } - handled, err := inp.ports.HandleInputEvent(ev) if err != nil { return handled, err } - // forward to passenger if one is defined - if handled && inp.toPassenger != nil { - select { - case inp.toPassenger <- ports.TimedInputEvent{Time: inp.tv.GetCoords(), InputEvent: ev}: - default: - return handled, fmt.Errorf("input: passenger event queue is full: input dropped") + if handled { + for _, r := range inp.recorder { + err := r.RecordEvent(ports.TimedInputEvent{Time: inp.tv.GetCoords(), InputEvent: ev}) + if err != nil { + return false, err + } + } + + // forward to passenger if one is defined + if handled && inp.toPassenger != nil { + select { + case inp.toPassenger <- ports.TimedInputEvent{Time: inp.tv.GetCoords(), InputEvent: ev}: + default: + return handled, fmt.Errorf("input: passenger event queue is full: input dropped") + } } } diff --git a/hardware/input/recording.go b/hardware/input/recording.go index 807995165..27c3c2fbb 100644 --- a/hardware/input/recording.go +++ b/hardware/input/recording.go @@ -71,7 +71,7 @@ func (inp *Input) handlePlaybackEvents() error { return nil } - // loop with GetPlayback() until we encounter a NoPortID or NoEvent + // loop with GetPlayback() until we encounter an PortUnplugged or NoEvent // condition. there might be more than one entry for a particular // frame/scanline/horizpas state so we need to make sure we've processed // them all. @@ -88,10 +88,13 @@ func (inp *Input) handlePlaybackEvents() error { morePlayback = ev.Port != plugging.PortUnplugged && ev.Ev != ports.NoEvent if morePlayback { - _, err := inp.ports.HandleInputEvent(ev.InputEvent) + handled, err := inp.ports.HandleInputEvent(ev.InputEvent) if err != nil { return err } + if !handled { + continue + } // forward to passenger if necessary if inp.toPassenger != nil { diff --git a/hardware/riot/ports/events.go b/hardware/riot/ports/events.go index 1a8ef09c6..81ff8f49b 100644 --- a/hardware/riot/ports/events.go +++ b/hardware/riot/ports/events.go @@ -170,9 +170,17 @@ type InputEvent struct { D EventData } +func (ev InputEvent) String() string { + return fmt.Sprintf("%s=%v on %s", ev.Ev, ev.D, ev.Port) +} + // TimedInputEvent embeds the InputEvent type and adds a Time field (time // measured by TelevisionCoords). type TimedInputEvent struct { Time coords.TelevisionCoords InputEvent } + +func (ev TimedInputEvent) String() string { + return fmt.Sprintf("%s @ %s", ev.InputEvent, ev.Time) +} diff --git a/hardware/television/coords/coords.go b/hardware/television/coords/coords.go index b30af1295..c51355ba5 100644 --- a/hardware/television/coords/coords.go +++ b/hardware/television/coords/coords.go @@ -56,11 +56,11 @@ func (c TelevisionCoords) String() string { // // If the Frame field is undefined for either argument then the Frame field is // ignored for the test. -func Equal(A, B TelevisionCoords) bool { - if A.Frame == FrameIsUndefined || B.Frame == FrameIsUndefined { - return A.Scanline == B.Scanline && A.Clock == B.Clock +func Equal(a, b TelevisionCoords) bool { + if a.Frame == FrameIsUndefined || b.Frame == FrameIsUndefined { + return a.Scanline == b.Scanline && a.Clock == b.Clock } - return A.Frame == B.Frame && A.Scanline == B.Scanline && A.Clock == B.Clock + return a.Frame == b.Frame && a.Scanline == b.Scanline && a.Clock == b.Clock } // GreaterThanOrEqual compares two instances of TelevisionCoords and return @@ -68,12 +68,12 @@ func Equal(A, B TelevisionCoords) bool { // // If the Frame field is undefined for either argument then the Frame field is // ignored for the test. -func GreaterThanOrEqual(A, B TelevisionCoords) bool { - if A.Frame == FrameIsUndefined || B.Frame == FrameIsUndefined { - return A.Scanline > B.Scanline || (A.Scanline == B.Scanline && A.Clock >= B.Clock) +func GreaterThanOrEqual(a, b TelevisionCoords) bool { + if a.Frame == FrameIsUndefined || b.Frame == FrameIsUndefined { + return a.Scanline > b.Scanline || (a.Scanline == b.Scanline && a.Clock >= b.Clock) } - return A.Frame > B.Frame || (A.Frame == B.Frame && A.Scanline > B.Scanline) || (A.Frame == B.Frame && A.Scanline == B.Scanline && A.Clock >= B.Clock) + return a.Frame > b.Frame || (a.Frame == b.Frame && a.Scanline > b.Scanline) || (a.Frame == b.Frame && a.Scanline == b.Scanline && a.Clock >= b.Clock) } // GreaterThan compares two instances of TelevisionCoords and return true if A @@ -81,11 +81,11 @@ func GreaterThanOrEqual(A, B TelevisionCoords) bool { // // If the Frame field is undefined for either argument then the Frame field is // ignored for the test. -func GreaterThan(A, B TelevisionCoords) bool { - if A.Frame == FrameIsUndefined || B.Frame == FrameIsUndefined { - return A.Scanline > B.Scanline || (A.Scanline == B.Scanline && A.Clock > B.Clock) +func GreaterThan(a, b TelevisionCoords) bool { + if a.Frame == FrameIsUndefined || b.Frame == FrameIsUndefined { + return a.Scanline > b.Scanline || (a.Scanline == b.Scanline && a.Clock > b.Clock) } - return A.Frame > B.Frame || (A.Frame == B.Frame && A.Scanline > B.Scanline) || (A.Frame == B.Frame && A.Scanline == B.Scanline && A.Clock > B.Clock) + return a.Frame > b.Frame || (a.Frame == b.Frame && a.Scanline > b.Scanline) || (a.Frame == b.Frame && a.Scanline == b.Scanline && a.Clock > b.Clock) } // Diff returns the difference between the B and A instances. The @@ -95,11 +95,11 @@ func GreaterThan(A, B TelevisionCoords) bool { // // If the Frame field is undefined for either TelevisionCoords argument then the // Frame field in the result of the function is also undefined. -func Diff(A, B TelevisionCoords, scanlinesPerFrame int) TelevisionCoords { +func Diff(a, b TelevisionCoords, scanlinesPerFrame int) TelevisionCoords { D := TelevisionCoords{ - Frame: A.Frame - B.Frame, - Scanline: A.Scanline - B.Scanline, - Clock: A.Clock - B.Clock, + Frame: a.Frame - b.Frame, + Scanline: a.Scanline - b.Scanline, + Clock: a.Clock - b.Clock, } if D.Clock < specification.ClksHBlank { @@ -120,7 +120,7 @@ func Diff(A, B TelevisionCoords, scanlinesPerFrame int) TelevisionCoords { // if the Frame field in either A or B is undefined then we can set the diff // Frame field as undefined alse - if A.Frame == FrameIsUndefined || B.Frame == FrameIsUndefined { + if a.Frame == FrameIsUndefined || b.Frame == FrameIsUndefined { D.Frame = FrameIsUndefined } @@ -131,11 +131,22 @@ func Diff(A, B TelevisionCoords, scanlinesPerFrame int) TelevisionCoords { // // If the Frame field is undefined for the TelevisionCoords then the Frame field // in the result of the function is also undefined. -func Sum(A TelevisionCoords, scanlinesPerFrame int) int { - if A.Frame == FrameIsUndefined { - return (A.Scanline * specification.ClksScanline) + A.Clock +func Sum(a TelevisionCoords, scanlinesPerFrame int) int { + if a.Frame == FrameIsUndefined { + return (a.Scanline * specification.ClksScanline) + a.Clock } numPerFrame := scanlinesPerFrame * specification.ClksScanline - return (A.Frame * numPerFrame) + (A.Scanline * specification.ClksScanline) + A.Clock + return (a.Frame * numPerFrame) + (a.Scanline * specification.ClksScanline) + a.Clock +} + +// Cmp returns 0 if A and B are equal, 1 if A > B and -1 if A < B +func Cmp(a, b TelevisionCoords) int { + if Equal(a, b) { + return 0 + } + if GreaterThan(a, b) { + return 1 + } + return -1 } diff --git a/rewind/comparison.go b/rewind/comparison.go new file mode 100644 index 000000000..d00a5b744 --- /dev/null +++ b/rewind/comparison.go @@ -0,0 +1,52 @@ +// 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 . + +package rewind + +// ComparisonState is returned by GetComparisonState() +type ComparisonState struct { + State *State + Locked bool +} + +// GetComparisonState gets a reference to current comparison point +func (r *Rewind) GetComparisonState() ComparisonState { + return ComparisonState{ + State: r.comparison.snapshot(), + Locked: r.comparisonLocked, + } +} + +// UpdateComparison points comparison to the current state +func (r *Rewind) UpdateComparison() { + if r.comparisonLocked { + return + } + r.comparison = r.GetCurrentState() +} + +// SetComparison points comparison to the supplied state +func (r *Rewind) SetComparison(frame int) { + res := r.findFrameIndexExact(frame) + s := r.entries[res.nearestIdx] + if s != nil { + r.comparison = s.snapshot() + } +} + +// LockComparison stops the comparison point from being updated +func (r *Rewind) LockComparison(locked bool) { + r.comparisonLocked = locked +} diff --git a/rewind/rewind.go b/rewind/rewind.go index 4767195da..3a2ff0397 100644 --- a/rewind/rewind.go +++ b/rewind/rewind.go @@ -105,13 +105,6 @@ func (s *State) String() string { return fmt.Sprintf("f%03d", s.TV.GetCoords().Frame) } -// an overhead of two is required: -// (1) to accommodate the next index required for effective appending -// (2) we can't generate a screen for the first entry in the history, unless -// it's a reset entry, so we do not allow the rewind system to move to that -// frame. -const overhead = 2 - // Rewind contains a history of machine states for the emulation. type Rewind struct { emulation Emulation @@ -139,6 +132,29 @@ type Rewind struct { // the point at which new entries will be added splice int + // user input is recorded separately to the machine state. userinput is + // re-inserted into the emulation via the playback mechanism during catchup + // + // the oldest userinput entry is no older than the oldest state in the + // entries field + // + // a great test case for userinput is Pitfall: + // + // WATCH WRITE $e1 + // STICK LEFT RIGHT + // CPU + // PC=f913 A=00 X=02 Y=00 SP=ff SR=sv-BdIzc + // STEP BACK + // CPU + // PC=f911 A=00 X=02 Y=00 SP=ff SR=sv-BdIZc + // + // without userinput insertion STEP BACK will leave the PC register + // somewhere else. this is because the memory write to $e1 happens after the + // last frame snapshot and the write happens only in response to the STICK + // input. without userinput insertion this memorywrite is lost and so the + // desired state as a result of STEP BACK can not be recreated + userinput userinput + // pointer to the comparison point comparison *State comparisonLocked bool @@ -151,6 +167,9 @@ type Rewind struct { // a reset boundary has been detected resetBoundaryNextFrame bool + + // the current state of the emulation + emulationState govern.State } // NewRewind is the preferred method of initialisation for the Rewind type. @@ -175,7 +194,7 @@ func NewRewind(emulation Emulation, runner Runner) (*Rewind, error) { } // AddTimelineCounter to the rewind system. Augments Timeline information that -// would otherwisde be awkward to gather. +// would otherwise be awkward to gather. // // Only one timeline counter can be used at any one time (ie. subsequent calls // to AddTimelineCounter() will override previous calls.) @@ -185,6 +204,13 @@ func (r *Rewind) AddTimelineCounter(ctr TimelineCounter) { // initialise space for entries and reset rewind system. func (r *Rewind) allocate() { + // an overhead of two is required when allocating space for the entries array: + // (1) to accommodate the next index required for effective appending + // (2) we can't generate a screen for the first entry in the history, unless + // it's a reset entry, so we do not allow the rewind system to move to that + // frame. + const overhead = 2 + r.entries = make([]*State, r.Prefs.MaxEntries.Get().(int)+overhead) r.reset(levelReset) } @@ -208,8 +234,6 @@ func (r *Rewind) reset(level snapshotLevel) { r.entries[i] = nil } - r.comparison = nil - r.newFrame = false r.resetBoundaryNextFrame = false @@ -233,9 +257,11 @@ func (r *Rewind) reset(level snapshotLevel) { // and as the second entry r.append(r.snapshot(levelFrame)) + // reset userinput + r.userinput.reset() + // first comparison is to the snapshot of the reset machine r.comparison = r.entries[r.start] - } // String outputs the entry information for the entire rewind history. The @@ -268,53 +294,6 @@ func (r *Rewind) String() string { return s.String() } -// Peephole outputs a short summary of the state of the rewind system centered -// on the current splice value -func (r *Rewind) Peephole() string { - const peephole = 5 - - var split bool - peepi := r.splice - peephole - if peepi < 0 { - peepi += len(r.entries) - split = true - } - peepj := r.splice + peephole - if peepj >= len(r.entries) { - peepj -= len(r.entries) - if split { - panic("length of entries in rewind is too short") - } - split = true - } - - // build output string - b := strings.Builder{} - - f := func(i, j int) { - for k, e := range r.entries[i:j] { - if k+i == r.splice { - b.WriteString(fmt.Sprintf("(%s) ", e)) - } else { - b.WriteString(fmt.Sprintf("%s ", e)) - } - } - } - - b.WriteString(fmt.Sprintf("[%03d] ", peepi)) - if split { - f(peepi, len(r.entries)) - b.WriteString(fmt.Sprintf("| ")) - f(0, peepj) - } else { - b.WriteString(" ") - f(peepi, peepj) - } - b.WriteString(fmt.Sprintf("[%03d]\n", peepj)) - - return b.String() -} - // the index of the last entry in the circular rewind history to be written to. // the end index points to the *next* entry to be written to. func (r *Rewind) lastEntryIdx() int { @@ -339,6 +318,16 @@ func (r *Rewind) snapshot(level snapshotLevel) *State { return snapshot(r.vcs, level) } +// SetEmulationState is called by the emulation whenever state changes +func (r *Rewind) SetEmulationState(state govern.State) { + if r.emulationState != state && r.emulationState == govern.Running { + // make sure user input is not being inserted by the rewind system + r.vcs.Input.AttachPlayback(nil) + r.userinput.stopPlayback() + } + r.emulationState = state +} + // RecordState should be called after every CPU instruction. A new state will // be recorded if the current rewind policy agrees. func (r *Rewind) RecordState() { @@ -366,15 +355,17 @@ func (r *Rewind) RecordState() { return } - // add an "execution" rewind state if the frame is not coincident with the - // rewind frequency fn := r.vcs.TV.GetCoords().Frame - if fn%r.Prefs.Freq.Get().(int) != 0 { + if fn%r.Prefs.Freq.Get().(int) == 0 { + // create frame snapshot if frame number is coincident with frequency preference + r.append(r.snapshot(levelFrame)) + } else { + // a temporary execution snapshot for interim frame numbers r.append(r.snapshot(levelExecution)) - return } - r.append(r.snapshot(levelFrame)) + // crop old entries from userinput list + r.userinput.crop(r.entries[r.splice].TV.GetCoords()) } // RecordExecutionCoords records the coordinates of the current execution state. @@ -449,6 +440,14 @@ func (r *Rewind) runFromStateToCoords(fromState *State, toCoords coords.Televisi } } + // start playback and if successful attach rewind to VCS input as a playback source + if r.userinput.startPlayback(fromState.TV.GetCoords()) { + err := r.vcs.Input.AttachPlayback(r) + if err != nil { + return fmt.Errorf("rewind: %w", err) + } + } + err := r.runner.CatchUpLoop(toCoords) if err != nil { return fmt.Errorf("rewind: %w", err) @@ -506,9 +505,11 @@ type findResults struct { func (r *Rewind) findFrameIndex(frame int) findResults { // the number of frames to rerun. in the case of the debugger we like rerun // an additional frame so that onion-skinning is correct - frame-- - if r.emulation.Mode() == govern.ModeDebugger && frame > 0 { + if frame > 0 { frame-- + if r.emulation.Mode() == govern.ModeDebugger && frame > 0 { + frame-- + } } return r.findFrameIndexExact(frame) } @@ -621,28 +622,6 @@ func (r *Rewind) GotoFrame(frame int) error { return r.GotoCoords(coords.TelevisionCoords{Frame: frame, Clock: -specification.ClksHBlank}) } -// UpdateComparison points comparison to the current state -func (r *Rewind) UpdateComparison() { - if r.comparisonLocked { - return - } - r.comparison = r.GetCurrentState() -} - -// SetComparison points comparison to the supplied state -func (r *Rewind) SetComparison(frame int) { - res := r.findFrameIndexExact(frame) - s := r.entries[res.nearestIdx] - if s != nil { - r.comparison = s.snapshot() - } -} - -// LockComparison stops the comparison point from being updated -func (r *Rewind) LockComparison(locked bool) { - r.comparisonLocked = locked -} - // NewFrame is in an implementation of television.FrameTrigger. func (r *Rewind) NewFrame(frameInfo television.FrameInfo) error { r.addTimelineEntry(frameInfo) @@ -666,16 +645,49 @@ func (r *Rewind) GetCurrentState() *State { return r.snapshot(levelTemporary) } -// ComparisonState is returned by GetComparisonState() -type ComparisonState struct { - State *State - Locked bool -} +// Peephole outputs a short summary of the state of the rewind system centered +// on the current splice value +func (r *Rewind) Peephole() string { + const peephole = 5 + + var split bool + peepi := r.splice - peephole + if peepi < 0 { + peepi += len(r.entries) + split = true + } + peepj := r.splice + peephole + if peepj >= len(r.entries) { + peepj -= len(r.entries) + if split { + panic("length of entries in rewind is too short") + } + split = true + } + + // build output string + b := strings.Builder{} -// GetComparisonState gets a reference to current comparison point -func (r *Rewind) GetComparisonState() ComparisonState { - return ComparisonState{ - State: r.comparison.snapshot(), - Locked: r.comparisonLocked, + f := func(i, j int) { + for k, e := range r.entries[i:j] { + if k+i == r.splice { + b.WriteString(fmt.Sprintf("(%s) ", e)) + } else { + b.WriteString(fmt.Sprintf("%s ", e)) + } + } } + + b.WriteString(fmt.Sprintf("[%03d] ", peepi)) + if split { + f(peepi, len(r.entries)) + b.WriteString(fmt.Sprintf("| ")) + f(0, peepj) + } else { + b.WriteString(" ") + f(peepi, peepj) + } + b.WriteString(fmt.Sprintf("[%03d]\n", peepj)) + + return b.String() } diff --git a/rewind/userinput.go b/rewind/userinput.go new file mode 100644 index 000000000..1a28074b2 --- /dev/null +++ b/rewind/userinput.go @@ -0,0 +1,109 @@ +// 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 . + +package rewind + +import ( + "github.com/jetsetilly/gopher2600/hardware/riot/ports" + "github.com/jetsetilly/gopher2600/hardware/television/coords" +) + +// the number of frames we must preserve in the userinput queue +const userinputOverhead = 2 + +type userinput struct { + queue []ports.TimedInputEvent + playback bool + idx int +} + +func (u *userinput) reset() { + u.queue = u.queue[:0] + u.playback = false + u.idx = 0 +} + +func (u *userinput) crop(earliest coords.TelevisionCoords) { + if len(u.queue) == 0 { + return + } + + var i int + + for i < len(u.queue) { + if coords.GreaterThanOrEqual(u.queue[i].Time, earliest) { + break // for loop + } + i++ + } + + if i > 0 && i < len(u.queue) { + u.queue = u.queue[i:] + } +} + +func (u *userinput) stopPlayback() { + u.playback = false +} + +func (u *userinput) startPlayback(start coords.TelevisionCoords) bool { + // find first relevant playback entry. searching from the beginning. this + // could probably be improved + u.idx = 0 + for u.idx < len(u.queue) { + if coords.GreaterThanOrEqual(u.queue[u.idx].Time, start) { + u.playback = true + return true + } + u.idx++ + } + return false +} + +// RecordEvent implements input.EventRecorder interface +func (r *Rewind) RecordEvent(ev ports.TimedInputEvent) error { + if r.userinput.playback { + return nil + } + r.userinput.queue = append(r.userinput.queue, ev) + return nil +} + +// GetPlayback implements input.EventPlayback interface +func (r *Rewind) GetPlayback() (ports.TimedInputEvent, error) { + c := r.vcs.TV.GetCoords() + + if r.userinput.idx >= len(r.userinput.queue) { + return ports.TimedInputEvent{ + Time: c, + InputEvent: ports.InputEvent{ + Ev: ports.NoEvent, + }, + }, nil + } + + if coords.Equal(c, r.userinput.queue[r.userinput.idx].Time) { + s := r.userinput.queue[r.userinput.idx] + r.userinput.idx++ + return s, nil + } + + return ports.TimedInputEvent{ + Time: c, + InputEvent: ports.InputEvent{ + Ev: ports.NoEvent, + }, + }, nil +}