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
This commit is contained in:
JetSetIlly 2024-04-06 09:40:10 +01:00
parent 4ab23ab63e
commit 24f3f32342
27 changed files with 461 additions and 484 deletions

View file

@ -29,10 +29,8 @@ import (
"slices"
"strings"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
"github.com/jetsetilly/gopher2600/hardware/television/specification"
"github.com/jetsetilly/gopher2600/logger"
"github.com/jetsetilly/gopher2600/notifications"
"github.com/jetsetilly/gopher2600/resources/fs"
)
@ -95,14 +93,6 @@ type Loader struct {
// then the stream is not open. although use the IsStreamed() function for
// this information.
stream **os.File
// callback function from the cartridge to the VCS. used for example. when
// cartridge has been successfully inserted. not all cartridge formats
// support/require this
//
// if the cartridge mapper needs to communicate more information then the
// action string should be used
NotificationHook notifications.NotificationHook
}
// sentinal error for when it is attempted to create a loader with no filename
@ -145,53 +135,50 @@ func NewLoader(filename string, mapping string) (Loader, error) {
mapping = "AUTO"
}
cl := Loader{
ld := Loader{
Filename: filename,
Mapping: mapping,
RequestedMapping: mapping,
NotificationHook: func(cart mapper.CartMapper, notice notifications.Notify, args ...interface{}) error {
return nil
},
}
// create an empty slice for the Data field to refer to
data := make([]byte, 0)
cl.Data = &data
ld.Data = &data
// decide what mapping to use if the requested mapping is AUTO
if mapping == "AUTO" {
extension := strings.ToUpper(filepath.Ext(filename))
if slices.Contains(autoFileExtensions, extension) {
cl.Mapping = "AUTO"
ld.Mapping = "AUTO"
} else if slices.Contains(explicitFileExtensions, extension) {
cl.Mapping = extension[1:]
ld.Mapping = extension[1:]
} else if slices.Contains(audioFileExtensions, extension) {
cl.Mapping = "AR"
cl.IsSoundData = true
ld.Mapping = "AR"
ld.IsSoundData = true
}
}
// if mapping value is still AUTO, make a special check for moviecart data.
// we want to do this now so we can initialise the stream
if cl.Mapping == "AUTO" {
if ld.Mapping == "AUTO" {
ok, err := fingerprintMovieCart(filename)
if err != nil {
return Loader{}, fmt.Errorf("catridgeloader: %w", err)
}
if ok {
cl.Mapping = "MVC"
ld.Mapping = "MVC"
}
}
// create stream pointer only for streaming sources. these file formats are
// likely to be very large by comparison to regular cartridge files.
if cl.Mapping == "MVC" || (cl.Mapping == "AR" && cl.IsSoundData) {
cl.stream = new(*os.File)
if ld.Mapping == "MVC" || (ld.Mapping == "AR" && ld.IsSoundData) {
ld.stream = new(*os.File)
}
cl.Spec = specification.SearchSpec(filename)
ld.Spec = specification.SearchSpec(filename)
return cl, nil
return ld, nil
}
// special handling for MVC files without the MVC file extension
@ -240,91 +227,88 @@ func NewLoaderFromEmbed(name string, data []byte, mapping string) (Loader, error
embedded: true,
Hash: fmt.Sprintf("%x", sha1.Sum(data)),
HashMD5: fmt.Sprintf("%x", md5.Sum(data)),
NotificationHook: func(cart mapper.CartMapper, notice notifications.Notify, args ...interface{}) error {
return nil
},
}, nil
}
// Close should be called before disposing of a Loader instance.
func (cl Loader) Close() error {
if cl.stream == nil || *cl.stream == nil {
func (ld Loader) Close() error {
if ld.stream == nil || *ld.stream == nil {
return nil
}
err := (**cl.stream).Close()
*cl.stream = nil
err := (**ld.stream).Close()
*ld.stream = nil
if err != nil {
return fmt.Errorf("cartridgeloader: %w", err)
}
logger.Logf("cartridgeloader", "stream closed (%s)", cl.Filename)
logger.Logf("cartridgeloader", "stream closed (%s)", ld.Filename)
return nil
}
// ShortName returns a shortened version of the CartridgeLoader filename field.
// In the case of embedded data, the filename field will be returned unaltered.
func (cl Loader) ShortName() string {
if cl.embedded {
return cl.Filename
func (ld Loader) ShortName() string {
if ld.embedded {
return ld.Filename
}
// return the empty string if filename is undefined
if len(strings.TrimSpace(cl.Filename)) == 0 {
if len(strings.TrimSpace(ld.Filename)) == 0 {
return ""
}
sn := filepath.Base(cl.Filename)
sn = strings.TrimSuffix(sn, filepath.Ext(cl.Filename))
sn := filepath.Base(ld.Filename)
sn = strings.TrimSuffix(sn, filepath.Ext(ld.Filename))
return sn
}
// IsStreaming returns two booleans. The first will be true if Loader was
// created for what appears to be a streaming source, and the second will be
// true if the stream has been open.
func (cl Loader) IsStreaming() (bool, bool) {
return cl.stream != nil, cl.stream != nil && *cl.stream != nil
func (ld Loader) IsStreaming() (bool, bool) {
return ld.stream != nil, ld.stream != nil && *ld.stream != nil
}
// IsEmbedded returns true if Loader was created from embedded data. If data
// has a length of zero then this function will return false.
func (cl Loader) IsEmbedded() bool {
return cl.embedded && len(*cl.Data) > 0
func (ld Loader) IsEmbedded() bool {
return ld.embedded && len(*ld.Data) > 0
}
// Load the cartridge data and return as a byte array. Loader filenames with a
// valid schema will use that method to load the data. Currently supported
// schemes are HTTP and local files.
func (cl *Loader) Load() error {
func (ld *Loader) Load() error {
// data is already "loaded" when using embedded data
if cl.embedded {
if ld.embedded {
return nil
}
if cl.stream != nil {
err := cl.Close()
if ld.stream != nil {
err := ld.Close()
if err != nil {
return fmt.Errorf("cartridgeloader: %w", err)
}
cl.StreamedData, err = os.Open(cl.Filename)
ld.StreamedData, err = os.Open(ld.Filename)
if err != nil {
return fmt.Errorf("cartridgeloader: %w", err)
}
logger.Logf("cartridgeloader", "stream open (%s)", cl.Filename)
logger.Logf("cartridgeloader", "stream open (%s)", ld.Filename)
*cl.stream = cl.StreamedData
*ld.stream = ld.StreamedData
return nil
}
if cl.Data != nil && len(*cl.Data) > 0 {
if ld.Data != nil && len(*ld.Data) > 0 {
return nil
}
scheme := "file"
url, err := url.Parse(cl.Filename)
url, err := url.Parse(ld.Filename)
if err == nil {
scheme = url.Scheme
}
@ -333,13 +317,13 @@ func (cl *Loader) Load() error {
case "http":
fallthrough
case "https":
resp, err := http.Get(cl.Filename)
resp, err := http.Get(ld.Filename)
if err != nil {
return fmt.Errorf("cartridgeloader: %w", err)
}
defer resp.Body.Close()
*cl.Data, err = io.ReadAll(resp.Body)
*ld.Data, err = io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("cartridgeloader: %w", err)
}
@ -351,31 +335,31 @@ func (cl *Loader) Load() error {
fallthrough
default:
f, err := os.Open(cl.Filename)
f, err := os.Open(ld.Filename)
if err != nil {
return fmt.Errorf("cartridgeloader: %w", err)
}
defer f.Close()
*cl.Data, err = io.ReadAll(f)
*ld.Data, err = io.ReadAll(f)
if err != nil {
return fmt.Errorf("cartridgeloader: %w", err)
}
}
// generate hash and check for consistency
hash := fmt.Sprintf("%x", sha1.Sum(*cl.Data))
if cl.Hash != "" && cl.Hash != hash {
hash := fmt.Sprintf("%x", sha1.Sum(*ld.Data))
if ld.Hash != "" && ld.Hash != hash {
return fmt.Errorf("cartridgeloader: unexpected hash value")
}
cl.Hash = hash
ld.Hash = hash
// generate md5 hash and check for consistency
hashmd5 := fmt.Sprintf("%x", md5.Sum(*cl.Data))
if cl.HashMD5 != "" && cl.HashMD5 != hashmd5 {
hashmd5 := fmt.Sprintf("%x", md5.Sum(*ld.Data))
if ld.HashMD5 != "" && ld.HashMD5 != hashmd5 {
return fmt.Errorf("cartridgeloader: unexpected hash value")
}
cl.HashMD5 = hashmd5
ld.HashMD5 = hashmd5
return nil
}

View file

@ -26,8 +26,6 @@ import (
"github.com/jetsetilly/gopher2600/debugger/govern"
"github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/supercharger"
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
"github.com/jetsetilly/gopher2600/hardware/television"
"github.com/jetsetilly/gopher2600/hardware/television/signal"
@ -81,7 +79,7 @@ func NewComparison(driverVCS *hardware.VCS) (*Comparison, error) {
tv.SetFPSCap(true)
// create a new VCS emulation
cmp.VCS, err = hardware.NewVCS(tv, driverVCS.Env.Prefs)
cmp.VCS, err = hardware.NewVCS(tv, cmp, driverVCS.Env.Prefs)
if err != nil {
return nil, fmt.Errorf("comparison: %w", err)
}
@ -140,6 +138,34 @@ func (cmp *Comparison) Quit() {
cmp.emulationQuit <- true
}
// Notify implements the notifications.Notify interface
func (cmp *Comparison) Notify(notice notifications.Notice) error {
switch notice {
case notifications.NotifySuperchargerFastloadEnded:
// the supercharger ROM will eventually start execution from the PC
// address given in the supercharger file
// CPU execution has been interrupted. update state of CPU
cmp.VCS.CPU.Interrupted = true
// the interrupted CPU means it never got a chance to
// finalise the result. we force that here by simply
// setting the Final flag to true.
cmp.VCS.CPU.LastResult.Final = true
// call function to complete tape loading procedure
fastload := cmp.VCS.Mem.Cart.GetSuperchargerFastLoad()
err := fastload.CommitFastload(cmp.VCS.CPU, cmp.VCS.Mem.RAM, cmp.VCS.RIOT.Timer)
if err != nil {
return err
}
case notifications.NotifySuperchargerSoundloadEnded:
return cmp.VCS.TV.Reset(true)
}
return nil
}
// CreateFromLoader will cause images to be generated from a running emulation
// initialised with the specified cartridge loader.
func (cmp *Comparison) CreateFromLoader(cartload cartridgeloader.Loader) error {
@ -147,35 +173,6 @@ func (cmp *Comparison) CreateFromLoader(cartload cartridgeloader.Loader) error {
return fmt.Errorf("comparison: emulation already running")
}
// loading hook support required for supercharger
cartload.NotificationHook = func(cart mapper.CartMapper, event notifications.Notify, args ...interface{}) error {
if _, ok := cart.(*supercharger.Supercharger); ok {
switch event {
case notifications.NotifySuperchargerFastloadEnded:
// the supercharger ROM will eventually start execution from the PC
// address given in the supercharger file
// CPU execution has been interrupted. update state of CPU
cmp.VCS.CPU.Interrupted = true
// the interrupted CPU means it never got a chance to
// finalise the result. we force that here by simply
// setting the Final flag to true.
cmp.VCS.CPU.LastResult.Final = true
// call function to complete tape loading procedure
callback := args[0].(supercharger.FastLoaded)
err := callback(cmp.VCS.CPU, cmp.VCS.Mem.RAM, cmp.VCS.RIOT.Timer)
if err != nil {
return err
}
case notifications.NotifySuperchargerSoundloadEnded:
return cmp.VCS.TV.Reset(true)
}
}
return nil
}
go func() {
defer func() {
cmp.driver.quit <- nil

View file

@ -44,8 +44,6 @@ import (
"github.com/jetsetilly/gopher2600/hardware/cpu/execution"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/moviecart"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/plusrom"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/supercharger"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
"github.com/jetsetilly/gopher2600/hardware/television"
@ -358,7 +356,7 @@ func NewDebugger(opts CommandLineOptions, create CreateUserInterface) (*Debugger
}
// create a new VCS instance
dbg.vcs, err = hardware.NewVCS(tv, nil)
dbg.vcs, err = hardware.NewVCS(tv, dbg, nil)
if err != nil {
return nil, fmt.Errorf("debugger: %w", err)
}
@ -1054,6 +1052,93 @@ func (dbg *Debugger) reset(newCartridge bool) error {
return nil
}
// Notify implements the notifications.Notify interface
func (dbg *Debugger) Notify(notice notifications.Notice) error {
switch notice {
case notifications.NotifySuperchargerLoadStarted:
if dbg.opts.Multiload >= 0 {
logger.Logf("debugger", "forcing supercharger multiload (%#02x)", uint8(dbg.opts.Multiload))
dbg.vcs.Mem.Poke(supercharger.MutliloadByteAddress, uint8(dbg.opts.Multiload))
}
case notifications.NotifySuperchargerFastloadEnded:
// the supercharger ROM will eventually start execution from the PC
// address given in the supercharger file
// CPU execution has been interrupted. update state of CPU
dbg.vcs.CPU.Interrupted = true
// the interrupted CPU means it never got a chance to
// finalise the result. we force that here by simply
// setting the Final flag to true.
dbg.vcs.CPU.LastResult.Final = true
// we've already obtained the disassembled lastResult so we
// need to change the final flag there too
dbg.liveDisasmEntry.Result.Final = true
// call commit function to complete tape loading procedure
fastload := dbg.vcs.Mem.Cart.GetSuperchargerFastLoad()
if fastload == nil {
return fmt.Errorf("NotifySuperchargerFastloadEnded sent from a non Supercharger fastload cartridge")
}
err := fastload.CommitFastload(dbg.vcs.CPU, dbg.vcs.Mem.RAM, dbg.vcs.RIOT.Timer)
if err != nil {
return err
}
// (re)disassemble memory on TapeLoaded error signal
err = dbg.Disasm.FromMemory()
if err != nil {
return err
}
case notifications.NotifySuperchargerSoundloadStarted:
err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifySuperchargerSoundloadStarted)
if err != nil {
return err
}
case notifications.NotifySuperchargerSoundloadEnded:
err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifySuperchargerSoundloadEnded)
if err != nil {
return err
}
// !!TODO: it would be nice to see partial disassemblies of supercharger tapes
// during loading. not completely necessary I don't think, but it would be
// nice to have.
err = dbg.Disasm.FromMemory()
if err != nil {
return err
}
return dbg.vcs.TV.Reset(true)
case notifications.NotifySuperchargerSoundloadRewind:
err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifySuperchargerSoundloadRewind)
if err != nil {
return err
}
case notifications.NotifyPlusROMInserted:
if dbg.vcs.Env.Prefs.PlusROM.NewInstallation {
err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifyPlusROMNewInstallation)
if err != nil {
return fmt.Errorf(err.Error())
}
}
case notifications.NotifyPlusROMNetwork:
err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifyPlusROMNetwork)
if err != nil {
return err
}
case notifications.NotifyMovieCartStarted:
return dbg.vcs.TV.Reset(true)
default:
logger.Logf("debugger", "unhandled notification for plusrom (%v)", notice)
}
return nil
}
// attachCartridge makes sure that the cartridge loaded into vcs memory and the
// available disassembly/symbols are in sync.
//
@ -1101,100 +1186,6 @@ func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error)
}
dbg.loader = &cartload
// set NotificationHook for specific cartridge formats
cartload.NotificationHook = func(cart mapper.CartMapper, event notifications.Notify, args ...interface{}) error {
if _, ok := cart.(*supercharger.Supercharger); ok {
switch event {
case notifications.NotifySuperchargerLoadStarted:
if dbg.opts.Multiload >= 0 {
logger.Logf("debugger", "forcing supercharger multiload (%#02x)", uint8(dbg.opts.Multiload))
dbg.vcs.Mem.Poke(supercharger.MutliloadByteAddress, uint8(dbg.opts.Multiload))
}
case notifications.NotifySuperchargerFastloadEnded:
// the supercharger ROM will eventually start execution from the PC
// address given in the supercharger file
// CPU execution has been interrupted. update state of CPU
dbg.vcs.CPU.Interrupted = true
// the interrupted CPU means it never got a chance to
// finalise the result. we force that here by simply
// setting the Final flag to true.
dbg.vcs.CPU.LastResult.Final = true
// we've already obtained the disassembled lastResult so we
// need to change the final flag there too
dbg.liveDisasmEntry.Result.Final = true
// call function to complete tape loading procedure
callback := args[0].(supercharger.FastLoaded)
err := callback(dbg.vcs.CPU, dbg.vcs.Mem.RAM, dbg.vcs.RIOT.Timer)
if err != nil {
return err
}
// (re)disassemble memory on TapeLoaded error signal
err = dbg.Disasm.FromMemory()
if err != nil {
return err
}
case notifications.NotifySuperchargerSoundloadStarted:
err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifySuperchargerSoundloadStarted)
if err != nil {
return err
}
case notifications.NotifySuperchargerSoundloadEnded:
err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifySuperchargerSoundloadEnded)
if err != nil {
return err
}
// !!TODO: it would be nice to see partial disassemblies of supercharger tapes
// during loading. not completely necessary I don't think, but it would be
// nice to have.
err = dbg.Disasm.FromMemory()
if err != nil {
return err
}
return dbg.vcs.TV.Reset(true)
case notifications.NotifySuperchargerSoundloadRewind:
err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifySuperchargerSoundloadRewind)
if err != nil {
return err
}
default:
logger.Logf("debugger", "unhandled hook event for supercharger (%v)", event)
}
} else if _, ok := cart.(*plusrom.PlusROM); ok {
switch event {
case notifications.NotifyPlusROMInserted:
if dbg.vcs.Env.Prefs.PlusROM.NewInstallation {
err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifyPlusROMNewInstallation)
if err != nil {
return fmt.Errorf(err.Error())
}
}
case notifications.NotifyPlusROMNetwork:
err := dbg.gui.SetFeature(gui.ReqCartridgeNotify, notifications.NotifyPlusROMNetwork)
if err != nil {
return err
}
default:
logger.Logf("debugger", "unhandled hook event for plusrom (%v)", event)
}
} else if _, ok := cart.(*moviecart.Moviecart); ok {
switch event {
case notifications.NotifyMovieCartStarted:
return dbg.vcs.TV.Reset(true)
default:
logger.Logf("debugger", "unhandled hook event for moviecart (%v)", event)
}
}
return nil
}
// reset of vcs is implied with attach cartridge
err := setup.AttachCartridge(dbg.vcs, cartload, false)
if err != nil && !errors.Is(err, cartridge.Ejected) {

View file

@ -88,7 +88,7 @@ func FromCartridge(cartload cartridgeloader.Loader) (*Disassembly, error) {
return nil, fmt.Errorf("disassembly: %w", err)
}
vcs, err := hardware.NewVCS(tv, nil)
vcs, err := hardware.NewVCS(tv, nil, nil)
if err != nil {
return nil, fmt.Errorf("disassembly: %w", err)
}

View file

@ -17,8 +17,8 @@ package environment
import (
"github.com/jetsetilly/gopher2600/hardware/preferences"
"github.com/jetsetilly/gopher2600/hardware/television"
"github.com/jetsetilly/gopher2600/hardware/television/specification"
"github.com/jetsetilly/gopher2600/notifications"
"github.com/jetsetilly/gopher2600/random"
)
@ -41,36 +41,42 @@ type Environment struct {
// the television attached to the console
TV Television
// any randomisation required by the emulation should be retreived through
// this structure
Random *random.Random
// interface to emulation. used for example, when cartridge has been
// successfully loaded. not all cartridge formats require this
Notifications notifications.Notify
// the emulation preferences
Prefs *preferences.Preferences
// any randomisation required by the emulation should be retreived through
// this structure
Random *random.Random
}
// NewEnvironment is the preferred method of initialisation for the Environment type.
//
// The two arguments must be supplied. In the case of the prefs field it can by
// nil and a new Preferences instance will be created. Providing a non-nil value
// allows the preferences of more than one VCS emulation to be synchronised.
func NewEnvironment(tv *television.Television, prefs *preferences.Preferences) (*Environment, error) {
// The Notify and Preferences can be nil. If prefs is nil then a new instance of
// the system wide preferences will be created.
func NewEnvironment(tv Television, notify notifications.Notify, prefs *preferences.Preferences) (*Environment, error) {
env := &Environment{
TV: tv,
Random: random.NewRandom(tv),
TV: tv,
Notifications: notify,
Prefs: prefs,
Random: random.NewRandom(tv.(random.TV)),
}
var err error
if notify == nil {
env.Notifications = notificationStub{}
}
if prefs == nil {
prefs, err = preferences.NewPreferences()
var err error
env.Prefs, err = preferences.NewPreferences()
if err != nil {
return nil, err
}
}
env.Prefs = prefs
return env, nil
}
@ -92,3 +98,10 @@ const MainEmulation = Label("main")
func (env *Environment) IsEmulation(label Label) bool {
return env.Label == label
}
// stub implementation of the notification interface
type notificationStub struct{}
func (_ notificationStub) Notify(_ notifications.Notice) error {
return nil
}

View file

@ -115,11 +115,11 @@ type emulationEventNotification struct {
open bool
frames int
event notifications.Notify
event notifications.Notice
mute bool
}
func (ntfy *emulationEventNotification) set(event notifications.Notify) {
func (ntfy *emulationEventNotification) set(event notifications.Notice) {
switch event {
default:
ntfy.event = event
@ -214,11 +214,11 @@ type cartridgeEventNotification struct {
open bool
frames int
event notifications.Notify
event notifications.Notice
coprocDev bool
}
func (ntfy *cartridgeEventNotification) set(event notifications.Notify) {
func (ntfy *cartridgeEventNotification) set(event notifications.Notice) {
switch event {
case notifications.NotifySuperchargerSoundloadStarted:
ntfy.event = event

View file

@ -86,14 +86,14 @@ func (img *SdlImgui) serviceSetFeature(request featureRequest) {
if img.isPlaymode() {
err = argLen(request.args, 1)
if err == nil {
img.playScr.emulationNotice.set(request.args[0].(notifications.Notify))
img.playScr.emulationNotice.set(request.args[0].(notifications.Notice))
}
}
case gui.ReqCartridgeNotify:
err = argLen(request.args, 1)
if err == nil {
notice := request.args[0].(notifications.Notify)
notice := request.args[0].(notifications.Notice)
switch notice {
case notifications.NotifyPlusROMNewInstallation:

View file

@ -221,7 +221,7 @@ func (cart *Cartridge) Attach(cartload cartridgeloader.Loader) error {
// format)
if cart.fingerprintPlusROM(cartload) {
// try creating a NewPlusROM instance
pr, err := plusrom.NewPlusROM(cart.env, cart.mapper, cartload.NotificationHook)
pr, err := plusrom.NewPlusROM(cart.env, cart.mapper)
if err != nil {
// check for known PlusROM errors
@ -501,6 +501,13 @@ func (cart *Cartridge) GetCoProc() coprocessor.CartCoProc {
return nil
}
func (cart *Cartridge) GetSuperchargerFastLoad() mapper.CartSuperChargerFastLoad {
if c, ok := cart.mapper.(mapper.CartSuperChargerFastLoad); ok {
return c
}
return nil
}
// CopyBanks returns the sequence of banks in a cartridge. To return the
// next bank in the sequence, call the function with the instance of
// mapper.BankContent returned from the previous call. The end of the sequence is

View file

@ -15,7 +15,12 @@
package mapper
import "github.com/jetsetilly/gopher2600/environment"
import (
"github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware/cpu"
"github.com/jetsetilly/gopher2600/hardware/memory/vcs"
"github.com/jetsetilly/gopher2600/hardware/riot/timer"
)
// CartContainer is a special CartMapper type that wraps another CartMapper.
// For example, the PlusROM type.
@ -266,6 +271,12 @@ type CartTapeState struct {
Data []float32
}
// CartSuperChargerFastLoad defines the commit function required when loading
// Supercharger 'Fastload' binaries
type CartSuperChargerFastLoad interface {
CommitFastload(mc *cpu.CPU, ram *vcs.RAM, tmr *timer.Timer) error
}
// CartLabelsBus will be implemented for cartridge mappers that want to report any
// special labels for the cartridge type.
type CartLabelsBus interface {

View file

@ -274,11 +274,9 @@ func (s *state) initialise() {
}
type Moviecart struct {
env *environment.Environment
notificationHook notifications.NotificationHook
specID string
env *environment.Environment
specID string
mappingID string
loader io.ReadSeekCloser
@ -289,10 +287,9 @@ type Moviecart struct {
func NewMoviecart(env *environment.Environment, loader cartridgeloader.Loader) (mapper.CartMapper, error) {
cart := &Moviecart{
env: env,
notificationHook: loader.NotificationHook,
loader: loader.StreamedData,
mappingID: "MVC",
env: env,
loader: loader.StreamedData,
mappingID: "MVC",
}
cart.state = newState()
@ -433,12 +430,9 @@ func (cart *Moviecart) processAddress(addr uint16) {
// stop title screen
cart.write8bit(addrTitleLoop, 0x18)
// call notificationHook function if one is available
if cart.notificationHook != nil {
err := cart.notificationHook(cart, notifications.NotifyMovieCartStarted)
if err != nil {
logger.Logf("moviecart", err.Error())
}
err := cart.env.Notifications.Notify(notifications.NotifyMovieCartStarted)
if err != nil {
logger.Logf("moviecart", err.Error())
}
} else if cart.state.totalCycles > titleCycles {

View file

@ -35,8 +35,6 @@ var CannotAdoptROM = errors.New("cannot adopt ROM")
type PlusROM struct {
env *environment.Environment
notificationHook notifications.NotificationHook
net *network
state *state
@ -61,9 +59,8 @@ func (s *state) Plumb(env *environment.Environment) {
s.child.Plumb(env)
}
func NewPlusROM(env *environment.Environment, child mapper.CartMapper, notificationHook notifications.NotificationHook) (mapper.CartMapper, error) {
func NewPlusROM(env *environment.Environment, child mapper.CartMapper) (mapper.CartMapper, error) {
cart := &PlusROM{env: env}
cart.notificationHook = notificationHook
cart.state = &state{}
cart.state.child = child
@ -142,12 +139,9 @@ func NewPlusROM(env *environment.Environment, child mapper.CartMapper, notificat
// log success
logger.Logf("plusrom", "will connect to %s", cart.net.ai.String())
// call notificationHook function if one is available
if cart.notificationHook != nil {
err := cart.notificationHook(cart, notifications.NotifyPlusROMInserted)
if err != nil {
return nil, fmt.Errorf("plusrom %w:", err)
}
err := cart.env.Notifications.Notify(notifications.NotifyPlusROMInserted)
if err != nil {
return nil, fmt.Errorf("plusrom %w:", err)
}
return cart, nil
@ -219,7 +213,7 @@ func (cart *PlusROM) AccessVolatile(addr uint16, data uint8, poke bool) error {
cart.rewindBoundary = true
cart.net.buffer(data)
cart.net.commit()
err := cart.notificationHook(cart, notifications.NotifyPlusROMNetwork)
err := cart.env.Notifications.Notify(notifications.NotifyPlusROMNetwork)
if err != nil {
return fmt.Errorf("plusrom %w:", err)
}

View file

@ -20,8 +20,7 @@
// The package supports both loading from a sound file (supporting most WAV and
// MP3 files) or from a "fastload" file.
//
// Tape loading "events" are handled through the cartridgeloader packages
// NotificationHook mechanism. See the notification.Notify type.
// Tape loading "events" are handled through the notifications.Notify interface.
//
// When loading from a sound file, Supercharger events can be ignored if so
// desired but for fastload files, the emulator needs to help the Supercharger

View file

@ -50,6 +50,32 @@ type FastLoad struct {
// value of loadCt on last successful load. we use this to prevent endless
// rewinding and searching
lastLoadCt int
// fastload binaries have a header which controls how the binary is read
fastloadHeader fastloadHeader
}
type fastloadHeader struct {
// PC address to jump to once loading has finished
startAddress uint16
// RAM config to be set adter tape load
configByte uint8
// number of pages to load
numPages uint8
// not using checksum in any meaningful way
checksum uint8
// we'll use this to check if the correct multiload is being read
multiload uint8
// not using progress speed in any meaningul way
progressSpeed uint16
// data is loaded according to page table
pageTable []byte
}
// FastLoaded defines the callback function that is sent to the core emulation
@ -99,122 +125,24 @@ func (tap *FastLoad) load() (uint8, error) {
// game header appears after main data
gameHeader := data[0x2000:0x2008]
tap.fastloadHeader.startAddress = (uint16(gameHeader[1]) << 8) | uint16(gameHeader[0])
tap.fastloadHeader.configByte = gameHeader[2]
tap.fastloadHeader.numPages = uint8(gameHeader[3])
tap.fastloadHeader.checksum = gameHeader[4]
tap.fastloadHeader.multiload = gameHeader[5]
tap.fastloadHeader.progressSpeed = (uint16(gameHeader[7]) << 8) | uint16(gameHeader[6])
tap.fastloadHeader.pageTable = data[0x2010:0x2028]
// PC address to jump to once loading has finished
startAddress := (uint16(gameHeader[1]) << 8) | uint16(gameHeader[0])
logger.Logf("supercharger: fastload", "header: start address: %#04x", tap.fastloadHeader.startAddress)
logger.Logf("supercharger: fastload", "header: config byte: %#08b", tap.fastloadHeader.configByte)
logger.Logf("supercharger: fastload", "header: num pages: %d", tap.fastloadHeader.numPages)
logger.Logf("supercharger: fastload", "header: checksum: %#02x", tap.fastloadHeader.checksum)
logger.Logf("supercharger: fastload", "header: multiload: %#02x", tap.fastloadHeader.multiload)
logger.Logf("supercharger: fastload", "header: progress speed: %#02x", tap.fastloadHeader.progressSpeed)
logger.Logf("supercharger: fastload", "page-table: %v", tap.fastloadHeader.pageTable)
// RAM config to be set adter tape load
configByte := gameHeader[2]
// number of pages to load
numPages := int(gameHeader[3])
// not using checksum in any meaningful way
checksum := gameHeader[4]
// we'll use this to check if the correct multiload is being read
multiload := gameHeader[5]
// not using progress speed in any meaningul way
progressSpeed := (uint16(gameHeader[7]) << 8) | uint16(gameHeader[6])
logger.Logf("supercharger: fastload", "header: start address: %#04x", startAddress)
logger.Logf("supercharger: fastload", "header: config byte: %#08b", configByte)
logger.Logf("supercharger: fastload", "header: num pages: %d", numPages)
logger.Logf("supercharger: fastload", "header: checksum: %#02x", checksum)
logger.Logf("supercharger: fastload", "header: multiload: %#02x", multiload)
logger.Logf("supercharger: fastload", "header: progress speed: %#02x", progressSpeed)
// data is loaded according to page table
pageTable := data[0x2010:0x2028]
logger.Logf("supercharger: fastload", "page-table: %v", pageTable)
// setup cartridge according to tape instructions. this requires
// cooperation from the core emulation so we use the cartridgeloader.NotificationHook mechanism.
tap.cart.notificationHook(tap.cart, notifications.NotifySuperchargerFastloadEnded, FastLoaded(func(mc *cpu.CPU, ram *vcs.RAM, tmr *timer.Timer) error {
// look up requested multiload address
m, err := ram.Peek(MutliloadByteAddress)
if err != nil {
return fmt.Errorf("fastload %w", err)
}
// this is not the mutliload we're looking for
if m != multiload {
logger.Logf("supercharger: fastload", "fastload header (%d) not matching multiload request (%d)", m, multiload)
// test for whether the tape has looped. if it has just load the
// first multiload
if tap.loadCt == tap.lastLoadCt {
logger.Logf("supercharger: fastload", "cannot find requested multiload (%d) loading mutliload 00", m)
tap.loadCt = 0
offset := tap.loadCt * fastLoadBlockSize
data = tap.data[offset : offset+fastLoadBlockSize]
tap.loadCt++
} else {
return nil
}
}
logger.Logf("supercharger: fastload", "loading multiload (%d)", multiload)
// copy data to RAM banks
for i := 0; i < numPages; i++ {
bank := pageTable[i] & 0x3
page := pageTable[i] >> 2
bankOffset := int(page) * 0x100
binOffset := i * 0x100
data := data[binOffset : binOffset+0x100]
copy(tap.cart.state.ram[bank][bankOffset:bankOffset+0x100], data)
// logger.Logf("supercharger: fastload", "copying %#04x:%#04x to bank %d page %d, offset %#04x", binOffset, binOffset+0x100, bank, page, bankOffset)
}
// initialise VCS RAM with zeros
for a := uint16(0x80); a <= 0xff; a++ {
_ = ram.Poke(a, 0x00)
}
// poke values into RAM. these values would be the by-product of the
// tape-loading process. because we are short-circuiting that process
// however, by injecting the binary data into supercharger RAM
// directly, the necessary code will not be run.
// RAM address 0x80 contains the initial configbyte
_ = ram.Poke(0x80, configByte)
// CMP $fff8
_ = ram.Poke(0xfa, 0xcd)
_ = ram.Poke(0xfb, 0xf8)
_ = ram.Poke(0xfc, 0xff)
// JMP <absolute address>
_ = ram.Poke(0xfd, 0x4c)
_ = ram.Poke(0xfe, uint8(startAddress))
_ = ram.Poke(0xff, uint8(startAddress>>8))
// reset timer. in references to real tape loading, the number of ticks
// is the value at the moment the PC reaches address 0x00fa
tmr.PokeField("divider", timer.TIM64T)
tmr.PokeField("ticksRemaining", 0x1e)
tmr.PokeField("intim", uint8(0x0a))
// jump to VCS RAM location 0x00fa. a short bootstrap program has been
// poked there already
err = mc.LoadPC(0x00fa)
if err != nil {
return fmt.Errorf("fastload: %w", err)
}
// set the value to be used in the first instruction of the bootstrap program
tap.cart.state.registers.Value = configByte
tap.cart.state.registers.Delay = 0
// note the multiload request
tap.lastLoadCt = tap.loadCt
return nil
}))
// setup cartridge according to tape instructions
tap.cart.env.Notifications.Notify(notifications.NotifySuperchargerFastloadEnded)
return 0, nil
}
@ -222,3 +150,86 @@ func (tap *FastLoad) load() (uint8, error) {
// step implements the Tape interface.
func (tap *FastLoad) step() {
}
// CommitFastLoad implements the mapper.CartSuperChargerFastLoad interface.
func (tap *FastLoad) CommitFastload(mc *cpu.CPU, ram *vcs.RAM, tmr *timer.Timer) error {
// look up requested multiload address
m, err := ram.Peek(MutliloadByteAddress)
if err != nil {
return fmt.Errorf("fastload %w", err)
}
data := tap.data
// this is not the mutliload we're looking for
if m != uint8(tap.loadCt) {
logger.Logf("supercharger: fastload", "fastload header (%d) not matching multiload request (%d)", m, tap.fastloadHeader.multiload)
// test for whether the tape has looped. if it has just load the first multiload
if tap.loadCt == tap.lastLoadCt {
logger.Logf("supercharger: fastload", "cannot find requested multiload (%d) loading mutliload 00", m)
tap.loadCt = 0
offset := tap.loadCt * fastLoadBlockSize
data = data[offset : offset+fastLoadBlockSize]
tap.loadCt++
}
}
logger.Logf("supercharger: fastload", "loading multiload (%d)", tap.fastloadHeader.multiload)
// copy data to RAM banks
for i := 0; i < int(tap.fastloadHeader.numPages); i++ {
bank := tap.fastloadHeader.pageTable[i] & 0x3
page := tap.fastloadHeader.pageTable[i] >> 2
bankOffset := int(page) * 0x100
binOffset := i * 0x100
copy(tap.cart.state.ram[bank][bankOffset:bankOffset+0x100], data[binOffset:binOffset+0x100])
// logger.Logf("supercharger: fastload", "copying %#04x:%#04x to bank %d page %d, offset %#04x", binOffset, binOffset+0x100, bank, page, bankOffset)
}
// initialise VCS RAM with zeros
for a := uint16(0x80); a <= 0xff; a++ {
_ = ram.Poke(a, 0x00)
}
// poke values into RAM. these values would be the by-product of the
// tape-loading process. because we are short-circuiting that process
// however, by injecting the binary data into supercharger RAM
// directly, the necessary code will not be run.
// RAM address 0x80 contains the initial configbyte
_ = ram.Poke(0x80, tap.fastloadHeader.configByte)
// CMP $fff8
_ = ram.Poke(0xfa, 0xcd)
_ = ram.Poke(0xfb, 0xf8)
_ = ram.Poke(0xfc, 0xff)
// JMP <absolute address>
_ = ram.Poke(0xfd, 0x4c)
_ = ram.Poke(0xfe, uint8(tap.fastloadHeader.startAddress))
_ = ram.Poke(0xff, uint8(tap.fastloadHeader.startAddress>>8))
// reset timer. in references to real tape loading, the number of ticks
// is the value at the moment the PC reaches address 0x00fa
tmr.PokeField("divider", timer.TIM64T)
tmr.PokeField("ticksRemaining", 0x1e)
tmr.PokeField("intim", uint8(0x0a))
// jump to VCS RAM location 0x00fa. a short bootstrap program has been
// poked there already
err = mc.LoadPC(0x00fa)
if err != nil {
return fmt.Errorf("fastload: %w", err)
}
// set the value to be used in the first instruction of the bootstrap program
tap.cart.state.registers.Value = tap.fastloadHeader.configByte
tap.cart.state.registers.Delay = 0
// note the multiload request
tap.lastLoadCt = tap.loadCt
return nil
}

View file

@ -150,7 +150,7 @@ func (tap *SoundLoad) load() (uint8, error) {
tap.playDelay++
return 0x00, nil
}
tap.cart.notificationHook(tap.cart, notifications.NotifySuperchargerSoundloadStarted)
tap.cart.env.Notifications.Notify(notifications.NotifySuperchargerSoundloadStarted)
tap.playing = true
tap.playDelay = 0
logger.Log(soundloadLogTag, "tape playing")
@ -195,7 +195,7 @@ func (tap *SoundLoad) step() {
// Rewind implements the mapper.CartTapeBus interface.
func (tap *SoundLoad) Rewind() {
// rewinding happens instantaneously
tap.cart.notificationHook(tap.cart, notifications.NotifySuperchargerSoundloadRewind)
tap.cart.env.Notifications.Notify(notifications.NotifySuperchargerSoundloadRewind)
tap.idx = 0
logger.Log(soundloadLogTag, "tape rewound")
tap.stepLimiter = 0

View file

@ -21,8 +21,11 @@ import (
"github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware/cpu"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
"github.com/jetsetilly/gopher2600/hardware/memory/memorymap"
"github.com/jetsetilly/gopher2600/hardware/memory/vcs"
"github.com/jetsetilly/gopher2600/hardware/riot/timer"
"github.com/jetsetilly/gopher2600/notifications"
)
@ -52,8 +55,6 @@ type Supercharger struct {
bankSize int
bios []uint8
notificationHook notifications.NotificationHook
// rewindable state
state *state
}
@ -62,11 +63,10 @@ type Supercharger struct {
// Supercharger type.
func NewSupercharger(env *environment.Environment, cartload cartridgeloader.Loader) (mapper.CartMapper, error) {
cart := &Supercharger{
env: env,
mappingID: "AR",
bankSize: 2048,
state: newState(),
notificationHook: cartload.NotificationHook,
env: env,
mappingID: "AR",
bankSize: 2048,
state: newState(),
}
var err error
@ -148,7 +148,7 @@ func (cart *Supercharger) Access(addr uint16, peek bool) (uint8, uint8, error) {
// sustained until the BIOS is "touched" as described below
if !cart.state.isLoading {
cart.state.isLoading = true
cart.notificationHook(cart, notifications.NotifySuperchargerLoadStarted)
cart.env.Notifications.Notify(notifications.NotifySuperchargerLoadStarted)
}
// call load() whenever address is touched, although do not allow
@ -188,15 +188,15 @@ func (cart *Supercharger) Access(addr uint16, peek bool) (uint8, uint8, error) {
if bios {
if cart.state.registers.ROMpower {
// trigger notificationHook() function whenever BIOS address $fa1a
// (specifically) is touched. note that this method means that the
// notificationHook() function will be called whatever the context the
// address is read and not just when the PC is at the address.
// send notification whenever BIOS address $fa1a (specifically) is
// touched. note that this method means that the notification will
// be sent whatever the context the address is read and not just
// when the PC is at the address.
if addr == 0x0a1a {
// end tape is loading state
cart.state.isLoading = false
err := cart.notificationHook(cart, notifications.NotifySuperchargerSoundloadEnded)
err := cart.env.Notifications.Notify(notifications.NotifySuperchargerSoundloadEnded)
if err != nil {
return 0, 0, fmt.Errorf("supercharger: %w", err)
}
@ -419,6 +419,14 @@ func (cart *Supercharger) SetTapeCounter(c int) {
}
}
// CommitFastLoad implements the mapper.CartSuperChargerFastLoad interface.
func (cart *Supercharger) CommitFastload(mc *cpu.CPU, ram *vcs.RAM, tmr *timer.Timer) error {
if f, ok := cart.state.tape.(mapper.CartSuperChargerFastLoad); ok {
return f.CommitFastload(mc, ram, tmr)
}
return nil
}
// GetTapeState implements the mapper.CartTapeBus interface
//
// Whether this does anything meaningful depends on the interal implementation

View file

@ -35,6 +35,7 @@ import (
"github.com/jetsetilly/gopher2600/hardware/television/specification"
"github.com/jetsetilly/gopher2600/hardware/tia"
"github.com/jetsetilly/gopher2600/logger"
"github.com/jetsetilly/gopher2600/notifications"
)
// The number of times the TIA updates every CPU cycle.
@ -77,15 +78,11 @@ type VCS struct {
// NewVCS creates a new VCS and everything associated with the hardware. It is
// used for all aspects of emulation: debugging sessions, and regular play.
//
// The two arguments must be supplied. In the case of the prefs field it can by
// nil and a new preferences instance will be created. Providing a non-nil value
// allows the preferences of more than one VCS emulation to be synchronised.
//
// The Instance.Context field should be updated except in the case of the
// "main" emulation.
func NewVCS(tv *television.Television, prefs *preferences.Preferences) (*VCS, error) {
// The Television argument should not be nil. The Notify and Preferences
// argument may be nil if required.
func NewVCS(tv *television.Television, notify notifications.Notify, prefs *preferences.Preferences) (*VCS, error) {
// set up environment
env, err := environment.NewEnvironment(tv, prefs)
env, err := environment.NewEnvironment(tv, notify, prefs)
if err != nil {
return nil, err
}

View file

@ -13,34 +13,12 @@
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
// Package notifications defines the Notify type and the possible values of
// that type. These values represent the different notifications that be sent
// to the GUI.
// Package notifications allow communication from a cartridge directly to the
// emulation instance. This is useful, for example, by the Supercharger
// emulation to indicate the start/stop of the Supercharger tape.
//
// hardware ----> emulation ----> GUI
// (eg. cartridge) (eg. debugger)
//
// Notifications flow in one direction only and can be generated and terminate
// at any of the points in the chart above.
//
// For example, a pause PlusROM network activitiy notification will be
// generated in the hardware, passed to the "emulation" package and forwarded
// to the GUI.
//
// Another example, is the rewind notification. This will be generated in the
// "emulation" package and sent to the GUI.
//
// Finally, a mute notification will be generated and consumed entirely inside
// the GUI.
//
// In some instances, the emulation may choose not to forward the notification
// to the GUI or to transform it into some other notification but these
// instances should be rare.
//
// Loosely related to the Notify type is the gui.FeatureRequest. The GUI
// FeatureRequest system is the mechanism by which notifications are forwarded
// to the GUI.
//
// Communication between hardware and the emulation is meanwhile is handled by
// the NotificationHook mechanism defined in this package.
// Notifications are sometimes passed onto the GUI to inidicate to the user the
// event that has happened (eg. tape stopped, etc.) For some notifications
// however, it is appropriate for the emulation instance to deal with the
// notification invisibly.
package notifications

View file

@ -15,71 +15,68 @@
package notifications
import "github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
// Notify describes events that somehow change the presentation of the
// Notice describes events that somehow change the presentation of the
// emulation. These notifications can be used to present additional information
// to the user
type Notify string
type Notice string
// List of defined notifications.
const (
// emulation events
NotifyInitialising Notify = "NotifyInitialising"
NotifyPause Notify = "NotifyPause"
NotifyRun Notify = "NotifyRun"
NotifyRewindBack Notify = "NotifyRewindBack"
NotifyRewindFoward Notify = "NotifyRewindFoward"
NotifyRewindAtStart Notify = "NotifyRewindAtStart"
NotifyRewindAtEnd Notify = "NotifyRewindAtEnd"
NotifyScreenshot Notify = "NotifyScreenshot"
NotifyMute Notify = "NotifyMute"
NotifyUnmute Notify = "NotifyUnmute"
// the following notifications relate to events generated by a cartridge
NotifyInitialising Notice = "NotifyInitialising"
NotifyPause Notice = "NotifyPause"
NotifyRun Notice = "NotifyRun"
NotifyRewindBack Notice = "NotifyRewindBack"
NotifyRewindFoward Notice = "NotifyRewindFoward"
NotifyRewindAtStart Notice = "NotifyRewindAtStart"
NotifyRewindAtEnd Notice = "NotifyRewindAtEnd"
NotifyScreenshot Notice = "NotifyScreenshot"
NotifyMute Notice = "NotifyMute"
NotifyUnmute Notice = "NotifyUnmute"
// LoadStarted is raised for Supercharger mapper whenever a new tape read
// sequence if started.
NotifySuperchargerLoadStarted Notify = "NotifySuperchargerLoadStarted"
NotifySuperchargerLoadStarted Notice = "NotifySuperchargerLoadStarted"
// If Supercharger is loading from a fastload binary then this event is
// raised when the loading has been completed.
NotifySuperchargerFastloadEnded Notify = "NotifySuperchargerFastloadEnded"
NotifySuperchargerFastloadEnded Notice = "NotifySuperchargerFastloadEnded"
// If Supercharger is loading from a sound file (eg. mp3 file) then these
// events area raised when the loading has started/completed.
NotifySuperchargerSoundloadStarted Notify = "NotifySuperchargerSoundloadStarted"
NotifySuperchargerSoundloadEnded Notify = "NotifySuperchargerSoundloadEnded"
NotifySuperchargerSoundloadStarted Notice = "NotifySuperchargerSoundloadStarted"
NotifySuperchargerSoundloadEnded Notice = "NotifySuperchargerSoundloadEnded"
// tape is rewinding.
NotifySuperchargerSoundloadRewind Notify = "NotifySuperchargerSoundloadRewind"
NotifySuperchargerSoundloadRewind Notice = "NotifySuperchargerSoundloadRewind"
// PlusROM cartridge has been inserted.
NotifyPlusROMInserted Notify = "NotifyPlusROMInserted"
NotifyPlusROMInserted Notice = "NotifyPlusROMInserted"
// PlusROM network activity.
NotifyPlusROMNetwork Notify = "NotifyPlusROMNetwork"
NotifyPlusROMNetwork Notice = "NotifyPlusROMNetwork"
// PlusROM new installation
NotifyPlusROMNewInstallation Notify = "NotifyPlusROMNewInstallation"
NotifyPlusROMNewInstallation Notice = "NotifyPlusROMNewInstallation"
// Moviecart started
NotifyMovieCartStarted Notify = "NotifyMoveCartStarted"
NotifyMovieCartStarted Notice = "NotifyMoveCartStarted"
// unsupported DWARF data
NotifyUnsupportedDWARF Notify = "NotifyUnsupportedDWARF"
NotifyUnsupportedDWARF Notice = "NotifyUnsupportedDWARF"
// coprocessor development information has been loaded
NotifyCoprocDevStarted Notify = "NotifyCoprocDevStarted"
NotifyCoprocDevEnded Notify = "NotifyCoprocDevEnded"
NotifyCoprocDevStarted Notice = "NotifyCoprocDevStarted"
NotifyCoprocDevEnded Notice = "NotifyCoprocDevEnded"
)
// NotificationHook is used for direct communication between a the hardware and
// the emulation package. Not often used but necessary for (currently):
// Notify is used for direct communication between a the hardware and the
// emulation package. Not often used but necessary for correct operation of:
//
// . Supercharger (eg. tape start/end)
// . PlusROM (eg. new installation)
// Supercharger 'fastload' binaries require a post-load step that initiates the
// hardware based on information in the binary file.
//
// The emulation understands how to interpret the event and forward the
// notification to the GUI using the gui.FeatureReq mechanism.
type NotificationHook func(cart mapper.CartMapper, notice Notify, args ...interface{}) error
// PlusROM cartridges need information about network connectivity from the user
// on first use of the PlusROM system.
type Notify interface {
Notify(notice Notice) error
}

View file

@ -49,7 +49,7 @@ func Check(output io.Writer, profile Profile, cartload cartridgeloader.Loader, s
tv.SetFPSCap(!uncapped)
// create vcs
vcs, err := hardware.NewVCS(tv, nil)
vcs, err := hardware.NewVCS(tv, nil, nil)
if err != nil {
return fmt.Errorf("performance: %w", err)
}

View file

@ -43,7 +43,7 @@ func NewEmulation(prefs *preferences.Preferences) (*Emulation, error) {
tv.SetFPSCap(false)
// create a new VCS emulation
em.vcs, err = hardware.NewVCS(tv, prefs)
em.vcs, err = hardware.NewVCS(tv, nil, prefs)
if err != nil {
return nil, fmt.Errorf("preview: %w", err)
}

View file

@ -137,7 +137,7 @@ func (reg *LogRegression) regress(newRegression bool, output io.Writer, msg stri
tv.SetFPSCap(false)
// create VCS and attach cartridge
vcs, err := hardware.NewVCS(tv, nil)
vcs, err := hardware.NewVCS(tv, nil, nil)
if err != nil {
return false, "", fmt.Errorf("log: %w", err)
}

View file

@ -122,7 +122,7 @@ func (reg *PlaybackRegression) regress(newRegression bool, output io.Writer, msg
return false, "", fmt.Errorf("playback: %w", err)
}
vcs, err := hardware.NewVCS(tv, nil)
vcs, err := hardware.NewVCS(tv, nil, nil)
if err != nil {
return false, "", fmt.Errorf("playback: %w", err)
}

View file

@ -195,7 +195,7 @@ func (reg *VideoRegression) regress(newRegression bool, output io.Writer, msg st
}
// create VCS and attach cartridge
vcs, err := hardware.NewVCS(tv, nil)
vcs, err := hardware.NewVCS(tv, nil, nil)
if err != nil {
return false, "", fmt.Errorf("video: %w", err)
}

View file

@ -51,7 +51,7 @@ func (r *Rewind) SearchMemoryWrite(tgt *State, addr uint16, value uint8, valueMa
}
_ = searchTV.SetFPSCap(false)
searchVCS, err := hardware.NewVCS(searchTV, nil)
searchVCS, err := hardware.NewVCS(searchTV, nil, nil)
if err != nil {
return nil, fmt.Errorf("rewind: search: %w", err)
}
@ -115,7 +115,7 @@ func (r *Rewind) SearchRegisterWrite(tgt *State, reg rune, value uint8, valueMas
}
_ = searchTV.SetFPSCap(false)
searchVCS, err := hardware.NewVCS(searchTV, nil)
searchVCS, err := hardware.NewVCS(searchTV, nil, nil)
if err != nil {
return nil, fmt.Errorf("rewind: search: %w", err)
}

View file

@ -27,8 +27,6 @@ import (
"github.com/jetsetilly/gopher2600/debugger/govern"
"github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/supercharger"
"github.com/jetsetilly/gopher2600/hardware/preferences"
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
@ -93,7 +91,7 @@ func NewAnim(prefs *preferences.Preferences) (*Anim, error) {
tv.SetFPSCap(true)
// create a new VCS emulation
thmb.vcs, err = hardware.NewVCS(tv, prefs)
thmb.vcs, err = hardware.NewVCS(tv, thmb, prefs)
if err != nil {
return nil, fmt.Errorf("thumbnailer: %w", err)
}
@ -162,6 +160,33 @@ func (thmb *Anim) wait() {
}
}
// Notify implements the notifications.Notify interface
func (thmb *Anim) Notify(notice notifications.Notice) error {
switch notice {
case notifications.NotifySuperchargerFastloadEnded:
// the supercharger ROM will eventually start execution from the PC
// address given in the supercharger file
// CPU execution has been interrupted. update state of CPU
thmb.vcs.CPU.Interrupted = true
// the interrupted CPU means it never got a chance to
// finalise the result. we force that here by simply
// setting the Final flag to true.
thmb.vcs.CPU.LastResult.Final = true
// call function to complete tape loading procedure
fastload := thmb.vcs.Mem.Cart.GetSuperchargerFastLoad()
err := fastload.CommitFastload(thmb.vcs.CPU, thmb.vcs.Mem.RAM, thmb.vcs.RIOT.Timer)
if err != nil {
return err
}
case notifications.NotifySuperchargerSoundloadEnded:
return thmb.vcs.TV.Reset(true)
}
return nil
}
// Create will cause images to be generated by a running emulation initialised
// with the specified cartridge loader. The emulation will run for a number of
// frames before ending
@ -183,35 +208,6 @@ func (thmb *Anim) Create(cartload cartridgeloader.Loader, numFrames int, monitor
// image in the render queue at the start of the animation
thmb.Reset()
// loading hook support required for supercharger
cartload.NotificationHook = func(cart mapper.CartMapper, event notifications.Notify, args ...interface{}) error {
if _, ok := cart.(*supercharger.Supercharger); ok {
switch event {
case notifications.NotifySuperchargerFastloadEnded:
// the supercharger ROM will eventually start execution from the PC
// address given in the supercharger file
// CPU execution has been interrupted. update state of CPU
thmb.vcs.CPU.Interrupted = true
// the interrupted CPU means it never got a chance to
// finalise the result. we force that here by simply
// setting the Final flag to true.
thmb.vcs.CPU.LastResult.Final = true
// call function to complete tape loading procedure
callback := args[0].(supercharger.FastLoaded)
err := callback(thmb.vcs.CPU, thmb.vcs.Mem.RAM, thmb.vcs.RIOT.Timer)
if err != nil {
return err
}
case notifications.NotifySuperchargerSoundloadEnded:
return thmb.vcs.TV.Reset(true)
}
}
return nil
}
go func() {
defer func() {
thmb.emulationCompleted <- true

View file

@ -75,7 +75,7 @@ func NewImage(prefs *preferences.Preferences) (*Image, error) {
tv.SetFPSCap(false)
// create a new VCS emulation
thmb.vcs, err = hardware.NewVCS(tv, prefs)
thmb.vcs, err = hardware.NewVCS(tv, nil, prefs)
if err != nil {
return nil, fmt.Errorf("thumbnailer: %w", err)
}

View file

@ -38,7 +38,7 @@ func (tr *Tracker) createReplayEmulation(mixer television.AudioMixer) error {
}
tv.AddAudioMixer(mixer)
tr.replayEmulation, err = hardware.NewVCS(tv, nil)
tr.replayEmulation, err = hardware.NewVCS(tv, nil, nil)
if err != nil {
return fmt.Errorf("tracker: create replay emulation: %w", err)
}