Gopher2600/rewind/rewind.go
JetSetIlly 249e3c4b60 timeline window now runs the rewind.GetState()/thumbnailer in the main emulation goroutine
the thumbnailer does the actual emulation in a new goroutine so there's
no lag here but it's necessary to PushRawEvent() so that
rewind.GetState() doesn't race
2021-11-18 21:51:44 +00:00

600 lines
17 KiB
Go

// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package rewind
import (
"fmt"
"strings"
"github.com/jetsetilly/gopher2600/curated"
"github.com/jetsetilly/gopher2600/emulation"
"github.com/jetsetilly/gopher2600/hardware"
"github.com/jetsetilly/gopher2600/hardware/cpu"
"github.com/jetsetilly/gopher2600/hardware/memory"
"github.com/jetsetilly/gopher2600/hardware/riot"
"github.com/jetsetilly/gopher2600/hardware/television"
"github.com/jetsetilly/gopher2600/hardware/television/coords"
"github.com/jetsetilly/gopher2600/hardware/television/specification"
"github.com/jetsetilly/gopher2600/hardware/tia"
"github.com/jetsetilly/gopher2600/logger"
)
// Runner provides the rewind package the opportunity to run the emulation.
type Runner interface {
// CatchupLoop should loop until the frame/scanline/clock coordinates are
// met. f should be called periodically, ideally every video cycle.
//
// When implementating the CatchupLoop(), care should be takan about what
// to do for example, if the scaline/clock coordinates do no exist on the
// specified frame. Either stop when the frame becomes too large or don't
// request the rewind in the first place. Such details are outside the
// scope of the rewind package however.
CatchUpLoop(coords.TelevisionCoords) error
}
// State contains pointers to areas of the VCS emulation. They can be read for
// reference.
type State struct {
level snapshotLevel
CPU *cpu.CPU
Mem *memory.Memory
RIOT *riot.RIOT
TIA *tia.TIA
TV *television.State
}
// snapshotLevel indicates the level of snapshot.
type snapshotLevel int
// List of valid SnapshotLevel values.
const (
// reset and boundary entries should only even appear once at the start of
// the history, it at all.
levelReset snapshotLevel = iota
levelBoundary
// a frame entry is a recording on a frame boundary (as soon as possible
// after a new frame). when they are made is based on the current snapshot
// frequency.
//
// there can be many frame entries the rewind history.
levelFrame
// execution entries only ever appear once at the end of the history.
// moreover they only ever appear when the snapshot frequency is greater
// than one.
levelExecution
// temporary entries should never appear in the history.
levelTemporary
)
func (s State) String() string {
switch s.level {
case levelReset:
return fmt.Sprintf("r(%d)", s.TV.GetCoords().Frame)
case levelBoundary:
return fmt.Sprintf("b(%d)", s.TV.GetCoords().Frame)
case levelExecution:
return fmt.Sprintf("e(%d)", s.TV.GetCoords().Frame)
case levelTemporary:
return fmt.Sprintf("t(%d)", s.TV.GetCoords().Frame)
}
return fmt.Sprintf("%d", 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.Emulation
vcs *hardware.VCS
runner Runner
// optional timeline counter implementation
ctr TimelineCounter
// prefs for the rewind system
Prefs *Preferences
// timeline information. note that this is kept for convenience and sent as
// a response to GetTimeline(). for internal package purposes the Start and
// End fields are not useful and only updated when GetTimeline() is called.
timeline Timeline
// circular array of snapshotted entries. start and next indicate the
// beginning and the end of the circular array. the next index can be
// smaller than the start index
entries []*State
start int
next int
// the point at which new entries will be added
splice int
// pointer to the comparison point
comparison *State
// coordinates of execution state
executionCoords coords.TelevisionCoords
// a new frame has been triggered. resolve as soon as possible.
newFrame bool
// a rewind boundary has been detected. call reset() on next frame.
boundaryNextFrame bool
}
// NewRewind is the preferred method of initialisation for the Rewind type.
func NewRewind(emulation emulation.Emulation, runner Runner) (*Rewind, error) {
r := &Rewind{
emulation: emulation,
vcs: emulation.VCS().(*hardware.VCS),
runner: runner,
}
var err error
r.Prefs, err = newPreferences(r)
if err != nil {
return nil, curated.Errorf("rewind: %v", err)
}
r.timeline = newTimeline()
r.allocate()
return r, nil
}
// AddTimelineCounter to the rewind system. Augments Timeline information that
// would otherwisde be awkward to gather.
//
// Only one timeline counter can be used at any one time (ie. subsequent calls
// to AddTimelineCounter() will override previous calls.)
func (r *Rewind) AddTimelineCounter(ctr TimelineCounter) {
r.ctr = ctr
}
// initialise space for entries and reset rewind system.
func (r *Rewind) allocate() {
r.entries = make([]*State, r.Prefs.MaxEntries.Get().(int)+overhead)
r.reset(levelReset)
}
// Reset rewind system removes all entries and takes a snapshot of the
// execution state. Resets timeline too.
//
// This should be called whenever a new cartridge is attached to the emulation.
func (r *Rewind) Reset() {
r.reset(levelReset)
r.timeline.reset()
}
// reset rewind system and use the specified snapshotLevel for the first entry.
// this will usually be levelReset but levelBoundary is also a sensible value.
//
// levelReset should really only be used when the vcs has actually been reset.
func (r *Rewind) reset(level snapshotLevel) {
// nillify all entries
for i := range r.entries {
r.entries[i] = nil
}
r.comparison = nil
r.newFrame = false
r.boundaryNextFrame = false
// start and next equal to begin with. the first call to append() below
// will add the new State at the current next index and then advance the
// next index ready for the next append()
r.start = 1
r.next = 1
// the splice point is checked to see if it is an execution entry and is
// chopped off if it is. the insertion of a sparse boundary entry means we
// don't have to check for nil
//
// the append function will move the splice index to start
//
// this arrangement of the stand, next and splice indexes means that there
// are no special conditions in the append() function.
r.splice = 0
// the first entry is a boundary entry by definition
r.entries[r.splice] = &State{level: levelBoundary}
// add current state as first entry
r.append(r.snapshot(level))
// first comparison is to the snapshot of the reset machine
r.comparison = r.entries[r.start]
}
func (r *Rewind) String() string {
s := strings.Builder{}
if r.start < r.next {
for i := r.start; i < r.next; i++ {
e := r.entries[i]
if e != nil {
s.WriteString(fmt.Sprintf("%s ", e.String()))
}
}
} else {
for i := r.start; i < len(r.entries); i++ {
e := r.entries[i]
if e != nil {
s.WriteString(fmt.Sprintf("%s ", e.String()))
}
}
for i := 0; i < r.next; i++ {
e := r.entries[i]
if e != nil {
s.WriteString(fmt.Sprintf("%s ", e.String()))
}
}
}
return s.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 {
e := r.next - 1
if e < 0 {
e += len(r.entries)
}
return e
}
// snapshot the supplied VCS instance.
func snapshot(vcs *hardware.VCS, level snapshotLevel) *State {
return &State{
level: level,
CPU: vcs.CPU.Snapshot(),
Mem: vcs.Mem.Snapshot(),
RIOT: vcs.RIOT.Snapshot(),
TIA: vcs.TIA.Snapshot(),
TV: vcs.TV.Snapshot(),
}
}
// snapshot the 'current' VCS instance.
func (r *Rewind) snapshot(level snapshotLevel) *State {
return snapshot(r.vcs, level)
}
// RecordState should be called after every CPU instruction. A new state will
// be recorded if the current rewind policy agrees.
func (r *Rewind) RecordState() {
if !r.vcs.CPU.LastResult.Final && !r.vcs.CPU.HasReset() {
logger.Logf("rewind", "RecordFrameState() attempted mid CPU instruction")
return
}
r.boundaryNextFrame = r.boundaryNextFrame || r.vcs.Mem.Cart.RewindBoundary()
if !r.newFrame {
return
}
r.newFrame = false
if r.boundaryNextFrame {
r.boundaryNextFrame = false
r.reset(levelBoundary)
logger.Logf("rewind", "boundary added at frame %d", r.vcs.TV.GetCoords().Frame)
return
}
fn := r.vcs.TV.GetCoords().Frame
if fn%r.Prefs.Freq.Get().(int) != 0 {
r.append(r.snapshot(levelExecution))
return
}
r.append(r.snapshot(levelFrame))
}
// RecordExecutionCoords records the coordinates of the current execution state.
func (r *Rewind) RecordExecutionCoords() {
r.executionCoords = r.vcs.TV.GetCoords()
}
// append the state to the end of the list of entries. handles the splice
// point correctly and any forgetting of old states that have expired.
func (r *Rewind) append(s *State) {
// chop off the end entry if it is in execution entry. we must do this
// before any further appending. this is enough to ensure that there is
// never more than one execution entry in the history.
if r.entries[r.splice] != nil && r.entries[r.splice].level == levelExecution {
r.next = r.splice
if r.splice == 0 {
r.splice = len(r.entries) - 1
} else {
r.splice--
}
}
// append at current position
e := r.splice + 1
if e >= len(r.entries) {
e = 0
}
// update entry
r.entries[e] = s
// new position is the update point
r.splice = e
// next update point is recent update point plus one
r.next = r.splice + 1
if r.next >= len(r.entries) {
r.next = 0
}
// push start index along
if r.next == r.start {
r.start++
if r.start >= len(r.entries) {
r.start = 0
}
}
}
// Plumb state into VCS.
func Plumb(vcs *hardware.VCS, state *State, fromDifferentEmulation bool) {
// tv plumbing works a bit different to other areas because we're only
// recording the state of the TV not the entire TV itself.
vcs.TV.PlumbState(vcs, state.TV.Snapshot())
// take another snapshot of the state before plumbing. we don't want the
// machine to change what we have stored in our state array (we learned
// that lesson the hard way :-)
vcs.CPU = state.CPU.Snapshot()
vcs.Mem = state.Mem.Snapshot()
vcs.RIOT = state.RIOT.Snapshot()
vcs.TIA = state.TIA.Snapshot()
vcs.CPU.Plumb(vcs.Mem)
vcs.Mem.Plumb(fromDifferentEmulation)
vcs.RIOT.Plumb(vcs.Mem.RIOT, vcs.Mem.TIA)
vcs.TIA.Plumb(vcs.TV, vcs.Mem.TIA, vcs.RIOT.Ports, vcs.CPU)
// reset peripherals after new state has been plumbed. without this,
// controllers can feel odd if the newly plumbed state has left RIOT memory
// in a latched state
vcs.RIOT.Ports.ResetPeripherals()
}
// run from the supplied state until the cooridinates are reached.
//
// note that this will not change the splice point. use setSplicePoint() for that
func (r *Rewind) runFromStateToCoords(fromState *State, toCoords coords.TelevisionCoords) error {
Plumb(r.vcs, fromState, false)
// if this is a reset entry then TV must be reset
if fromState.level == levelReset {
err := r.vcs.TV.Reset(false)
if err != nil {
return curated.Errorf("rewind: %v", err)
}
}
err := r.runner.CatchUpLoop(toCoords)
if err != nil {
return curated.Errorf("rewind: %v", err)
}
return nil
}
// setSplicePoint sets the splice point to the supplied index. the emulation
// will be run to the supplied frame, scanline, clock point.
func (r *Rewind) setSplicePoint(fromIdx int, toCoords coords.TelevisionCoords) error {
r.splice = fromIdx + 1
if r.splice >= len(r.entries) {
r.splice -= len(r.entries)
}
fromState := r.entries[fromIdx]
// plumb in selected entry
err := r.runFromStateToCoords(fromState, toCoords)
if err != nil {
return err
}
return nil
}
// findFrameIndex returns a lot of information and so is wrapped in a
// findResults type
type findResults struct {
nearestIdx int
nearestFrame int
future bool
}
// find index nearest to the requested frame. returns the index and the frame
// number that is actually possible with the rewind system.
//
// the future value indicates that the requested frame is past the end of the
// history. if future is true then idx and frame will point to the most recent
// entry that we do have.
//
// note that findFrameIndex() searches for the frame that is two frames before
// the one that is requested.
func (r *Rewind) findFrameIndex(frame int) findResults {
searchFrame := frame - 1
if r.emulation.Mode() == emulation.ModeDebugger {
searchFrame--
}
// initialise binary search
s := r.start
e := r.lastEntryIdx()
// check whether request is out of bounds of the rewind history. if it is
// then plumb in the nearest entry
// is requested frame too old (ie. before the start of the array)
fn := r.entries[s].TV.GetCoords().Frame
if searchFrame < fn {
return findResults{nearestIdx: s, nearestFrame: fn}
}
// is requested frame too new (ie. past the end of the array)
fn = r.entries[e].TV.GetCoords().Frame
if searchFrame > fn {
return findResults{nearestIdx: e, nearestFrame: fn, future: true}
}
// the range which we must consider to be a match
freqAdj := r.Prefs.Freq.Get().(int) - 1
// because r.entries is a cirular array, there's an additional step to the
// binary search. if start (lower) is greater then end (upper) then check
// which half of the circular array to concentrate on.
if s > e {
fn := r.entries[len(r.entries)-1].TV.GetCoords().Frame
if searchFrame <= fn+freqAdj {
e = len(r.entries) - 1
} else {
e = r.start - 1
s = 0
}
}
// normal binary search
for s <= e {
idx := (s + e) / 2
fn := r.entries[idx].TV.GetCoords().Frame
// check for match, taking into consideration the gaps introduced by
// the frequency value
if searchFrame >= fn && searchFrame <= fn+freqAdj {
return findResults{nearestIdx: idx, nearestFrame: fn}
}
if searchFrame < fn {
e = idx - 1
}
if searchFrame > fn {
s = idx + 1
}
}
panic(fmt.Sprintf("rewind: cannot find frame %d in the rewind history", frame))
}
// RerunLastNFrames runs the emulation from the a point N frames in the past to
// the current state.
func (r *Rewind) RerunLastNFrames(frames int) error {
to := r.GetCurrentState()
ff := to.TV.GetCoords().Frame
if ff < 0 {
ff = 0
}
idx := r.findFrameIndex(ff).nearestIdx
err := r.setSplicePoint(idx, to.TV.GetCoords())
if err != nil {
return curated.Errorf("rewind: %v", err)
}
return nil
}
// GotoCoords moves emulation to specified frame/scanline/clock "coordinates".
func (r *Rewind) GotoCoords(toCoords coords.TelevisionCoords) error {
// get nearest index of entry from which we can being to (re)generate the
// current frame
res := r.findFrameIndex(toCoords.Frame)
if res.future {
toCoords = r.entries[res.nearestIdx].TV.GetCoords()
}
idx := res.nearestIdx
err := r.setSplicePoint(idx, toCoords)
if err != nil {
return err
}
return nil
}
// GotoLast goes to the last entry in the rewind history. It handles situations
// when the last entry is an execution state.
func (r *Rewind) GotoLast() error {
return r.GotoCoords(r.executionCoords)
}
// GotoFrame is a special case of GotoCoords that requires the frame number only.
func (r *Rewind) GotoFrame(frame int) error {
return r.GotoCoords(coords.TelevisionCoords{Frame: frame, Clock: -specification.ClksHBlank})
}
// SetComparison points comparison to the most recent rewound entry.
func (r *Rewind) SetComparison() {
r.comparison = r.GetCurrentState()
}
// NewFrame is in an implementation of television.FrameTrigger.
func (r *Rewind) NewFrame(frameInfo television.FrameInfo) error {
r.addTimelineEntry(frameInfo)
r.newFrame = true
return nil
}
// GetState returns a copy for the nearest state for the indicated frame.
func (r *Rewind) GetState(frame int) *State {
// get nearest index of entry from which we can being to (re)generate the
// current frame
res := r.findFrameIndex(frame)
s := r.entries[res.nearestIdx]
// return copy of state
return &State{
TV: s.TV.Snapshot(),
CPU: s.CPU.Snapshot(),
Mem: s.Mem.Snapshot(),
RIOT: s.RIOT.Snapshot(),
TIA: s.TIA.Snapshot(),
}
}
// GetCurrentState returns a temporary snapshot of the current state.
func (r *Rewind) GetCurrentState() *State {
return r.snapshot(levelTemporary)
}
// GetComparisonState gets a reference to current comparison point. This is not
// a copy of the state but the actual state.
func (r *Rewind) GetComparisonState() *State {
return r.comparison
}