Gopher2600/rewind/search.go
JetSetIlly 96ad0797b4 NewVCS() expects environment.Label on instantiation
this helps force the environment.Label to be set to something meaningful
2024-04-30 18:34:35 +01:00

179 lines
5.8 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("NTSC")
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("NTSC")
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
}