Gopher2600/rewind/search.go
JetSetIlly 9a99ff7a3e fixed searching for PAL60 in filename and PAL60 sizing
added PAL60 specification. even though it isn't a real specification it
is more convenient and provides better user feedback

moved FrameInfo into frameinfo package, now called Current as in
frameinfo.Current

clarified storage of requested specification by the television:

how the television is probed for the specification has changed, in
particular the current spec is retreived via the GetFrameInfo()
function. in fact, this was how most other packages did it but there
also existed a GetSpecID() which was uncessary

GetReqSpecID() and GetCreationSpecID() removed, replaced with
GetResetSpecID() and IsAutoSpec()

simplified SetSpec(). removed the force argument

removed reset option for vcs.AttachCartridge()
2025-02-23 17:39:08 +00:00

178 lines
5.9 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"
"github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware"
"github.com/jetsetilly/gopher2600/hardware/memory/memorymap"
"github.com/jetsetilly/gopher2600/hardware/television"
"github.com/jetsetilly/gopher2600/hardware/television/coords"
)
const searchLabel = environment.Label("search")
// SearchMemoryWrite runs an emulation between two states looking for the
// instance when the address is written to with the value (valueMask is applied
// to mask specific bits)
//
// The supplied target state is the upper limit of the search. The lower limit
// of the search is one frame before the target State.
//
// The supplied address will be normalised.
//
// Returns the most recent State at which the memory write was found. If a more
// recent address write is found but not the correct value, then no state is
// returned.
func (r *Rewind) SearchMemoryWrite(tgt *State, addr uint16, value uint8, valueMask uint8) (*State, error) {
// matchingState is a snapshot of the the most recent search match
var matchingState *State
var mostRecentTVstate string
// trace normalised address
addr, _ = memorymap.MapAddress(addr, false)
// create a new TV and VCS to search with
searchTV, err := television.NewTelevision(r.vcs.TV.GetFrameInfo().Spec.ID)
if err != nil {
return nil, fmt.Errorf("rewind: search: %w", err)
}
_ = searchTV.SetFPSCap(false)
searchVCS, err := hardware.NewVCS(searchLabel, searchTV, nil, nil)
if err != nil {
return nil, fmt.Errorf("rewind: search: %w", err)
}
// get current screen coordinates. the emulation will run until these
// values are met, if not sooner.
endCoords := tgt.TV.GetCoords()
// find a recent state from the rewind history and plumb it our searchVCS
idx := r.findFrameIndex(endCoords.Frame).nearestIdx
Plumb(searchVCS, r.entries[idx], false)
// loop until we reach (or just surpass) the target State
done := false
for !done && searchVCS.CPU.LastResult.Final {
err = searchVCS.Step(nil)
if err != nil {
return nil, fmt.Errorf("rewind: search: %w", err)
}
if searchVCS.Mem.LastCPUWrite && searchVCS.Mem.LastCPUAddressMapped == addr {
if searchVCS.Mem.LastCPUData&valueMask == value&valueMask {
matchingState = snapshot(searchVCS, levelTemporary)
}
mostRecentTVstate = searchTV.String()
}
// check to see if TV state exceeds the requested state
searchCoords := searchVCS.TV.GetCoords()
done = coords.GreaterThanOrEqual(searchCoords, endCoords)
}
// make sure the matching state is the last address match we found.
if matchingState != nil && mostRecentTVstate != matchingState.TV.String() {
matchingState = nil
}
return matchingState, nil
}
// SearchMemoryWrite runs an emulation between two states looking for the
// instance when the register is written to with the value (valueMask is
// applied to mask specific bits)
//
// The supplied target state is the upper limit of the search. The lower limit
// of the search is one frame before the target State.
//
// Returns the most recent State at which the register write was found. If a
// more recent register write is found but not the correct value, then no state
// is returned.
func (r *Rewind) SearchRegisterWrite(tgt *State, reg rune, value uint8, valueMask uint8) (*State, error) {
// see commentary in SearchMemoryWrite(). although note that when
// mostRecentTVSstate is noted is different in the case of
// SearchRegisterWrite()
var matchingState *State
var mostRecentTVstate string
searchTV, err := television.NewTelevision(r.vcs.TV.GetFrameInfo().Spec.ID)
if err != nil {
return nil, fmt.Errorf("rewind: search: %w", err)
}
_ = searchTV.SetFPSCap(false)
searchVCS, err := hardware.NewVCS(searchLabel, searchTV, nil, nil)
if err != nil {
return nil, fmt.Errorf("rewind: search: %w", err)
}
// get current screen coordinates. the emulation will run until these
// values are met, if not sooner.
endCoords := tgt.TV.GetCoords()
// find a recent state and plumb it into searchVCS
idx := r.findFrameIndex(endCoords.Frame).nearestIdx
Plumb(searchVCS, r.entries[idx], false)
// onLoad() is called whenever a CPU register is loaded with a new value
match := false
onLoad := func(v uint8) {
match = v&valueMask == value&valueMask
// note TV state whenever register is loaded
mostRecentTVstate = searchTV.String()
}
switch reg {
case 'A':
searchVCS.CPU.A.SetOnLoad(onLoad)
case 'X':
searchVCS.CPU.X.SetOnLoad(onLoad)
case 'Y':
searchVCS.CPU.Y.SetOnLoad(onLoad)
default:
panic(fmt.Sprintf("rewind: search: unrecognised CPU register (%c)", reg))
}
done := false
for !done && searchVCS.CPU.LastResult.Final {
err = searchVCS.Step(nil)
if err != nil {
return nil, fmt.Errorf("rewind: search: %w", err)
}
// make snapshot of current state at CPU instruction boundary
if match {
match = false
matchingState = snapshot(searchVCS, levelTemporary)
}
// check to see if TV state exceeds the requested state
searchCoords := searchVCS.TV.GetCoords()
done = coords.GreaterThanOrEqual(searchCoords, endCoords)
}
// make sure the matching state is the last address match we found.
if matchingState != nil && mostRecentTVstate != matchingState.TV.String() {
matchingState = nil
}
return matchingState, nil
}