Gopher2600/rewind/search.go
JetSetIlly 24f3f32342 simplified notifications package
notifications interface instance moved to environment from
cartridgeloader. the cartridgeloader package predates the environment
package and had started to be used inappropriately

simplified how notifications.Notify() is called. in particular the
supercharger fastload starter no longer bundles a function hook. nor is
the cartridge instance sent with the notification
2024-04-06 10:12:55 +01:00

176 lines
5.7 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/hardware"
"github.com/jetsetilly/gopher2600/hardware/memory/memorymap"
"github.com/jetsetilly/gopher2600/hardware/television"
"github.com/jetsetilly/gopher2600/hardware/television/coords"
)
// 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("NTSC")
if err != nil {
return nil, fmt.Errorf("rewind: search: %w", err)
}
_ = searchTV.SetFPSCap(false)
searchVCS, err := hardware.NewVCS(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("NTSC")
if err != nil {
return nil, fmt.Errorf("rewind: search: %w", err)
}
_ = searchTV.SetFPSCap(false)
searchVCS, err := hardware.NewVCS(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
}