mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
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
600 lines
17 KiB
Go
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
|
|
}
|