diff --git a/cartridgeloader/loader.go b/cartridgeloader/loader.go index 5b2555e0..027d33eb 100644 --- a/cartridgeloader/loader.go +++ b/cartridgeloader/loader.go @@ -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 } diff --git a/comparison/comparison.go b/comparison/comparison.go index b07ac491..3e9bd261 100644 --- a/comparison/comparison.go +++ b/comparison/comparison.go @@ -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 diff --git a/debugger/debugger.go b/debugger/debugger.go index 9c62b568..6084d852 100644 --- a/debugger/debugger.go +++ b/debugger/debugger.go @@ -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) { diff --git a/disassembly/disassembly.go b/disassembly/disassembly.go index 0dfe838f..4a4c1807 100644 --- a/disassembly/disassembly.go +++ b/disassembly/disassembly.go @@ -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) } diff --git a/environment/environment.go b/environment/environment.go index 176db6a3..299855c1 100644 --- a/environment/environment.go +++ b/environment/environment.go @@ -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 +} diff --git a/gui/sdlimgui/playscr_notifications.go b/gui/sdlimgui/playscr_notifications.go index 402b1cdb..4a4a1850 100644 --- a/gui/sdlimgui/playscr_notifications.go +++ b/gui/sdlimgui/playscr_notifications.go @@ -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 diff --git a/gui/sdlimgui/requests.go b/gui/sdlimgui/requests.go index 56d71b48..32ad9e6c 100644 --- a/gui/sdlimgui/requests.go +++ b/gui/sdlimgui/requests.go @@ -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: diff --git a/hardware/memory/cartridge/cartridge.go b/hardware/memory/cartridge/cartridge.go index 7c21464f..f2a963c7 100644 --- a/hardware/memory/cartridge/cartridge.go +++ b/hardware/memory/cartridge/cartridge.go @@ -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 diff --git a/hardware/memory/cartridge/mapper/mapper.go b/hardware/memory/cartridge/mapper/mapper.go index e5fc33a2..559a9f90 100644 --- a/hardware/memory/cartridge/mapper/mapper.go +++ b/hardware/memory/cartridge/mapper/mapper.go @@ -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 { diff --git a/hardware/memory/cartridge/moviecart/moviecart.go b/hardware/memory/cartridge/moviecart/moviecart.go index 3749e75c..97c259eb 100644 --- a/hardware/memory/cartridge/moviecart/moviecart.go +++ b/hardware/memory/cartridge/moviecart/moviecart.go @@ -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 { diff --git a/hardware/memory/cartridge/plusrom/plusrom.go b/hardware/memory/cartridge/plusrom/plusrom.go index a3a88996..e72cb310 100644 --- a/hardware/memory/cartridge/plusrom/plusrom.go +++ b/hardware/memory/cartridge/plusrom/plusrom.go @@ -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) } diff --git a/hardware/memory/cartridge/supercharger/doc.go b/hardware/memory/cartridge/supercharger/doc.go index 8aa25e57..0efbbaa8 100644 --- a/hardware/memory/cartridge/supercharger/doc.go +++ b/hardware/memory/cartridge/supercharger/doc.go @@ -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 diff --git a/hardware/memory/cartridge/supercharger/fastload.go b/hardware/memory/cartridge/supercharger/fastload.go index 1197b9e4..e9a89eec 100644 --- a/hardware/memory/cartridge/supercharger/fastload.go +++ b/hardware/memory/cartridge/supercharger/fastload.go @@ -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 - _ = 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 + _ = 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 +} diff --git a/hardware/memory/cartridge/supercharger/soundload.go b/hardware/memory/cartridge/supercharger/soundload.go index 03f82267..79a903f2 100644 --- a/hardware/memory/cartridge/supercharger/soundload.go +++ b/hardware/memory/cartridge/supercharger/soundload.go @@ -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 diff --git a/hardware/memory/cartridge/supercharger/supercharger.go b/hardware/memory/cartridge/supercharger/supercharger.go index 74e1cf24..369fe0a4 100644 --- a/hardware/memory/cartridge/supercharger/supercharger.go +++ b/hardware/memory/cartridge/supercharger/supercharger.go @@ -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 diff --git a/hardware/vcs.go b/hardware/vcs.go index 05fa07e5..d88dd71e 100644 --- a/hardware/vcs.go +++ b/hardware/vcs.go @@ -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 } diff --git a/notifications/doc.go b/notifications/doc.go index 2e9cd135..eca7ac8d 100644 --- a/notifications/doc.go +++ b/notifications/doc.go @@ -13,34 +13,12 @@ // You should have received a copy of the GNU General Public License // along with Gopher2600. If not, see . -// 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 diff --git a/notifications/notifications.go b/notifications/notifications.go index a1fea143..fd0a5f1c 100644 --- a/notifications/notifications.go +++ b/notifications/notifications.go @@ -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 +} diff --git a/performance/performance.go b/performance/performance.go index ddeb73a0..777bd5c7 100644 --- a/performance/performance.go +++ b/performance/performance.go @@ -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) } diff --git a/preview/preview.go b/preview/preview.go index 8e86120d..d71db6bc 100644 --- a/preview/preview.go +++ b/preview/preview.go @@ -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) } diff --git a/regression/log.go b/regression/log.go index b3acded8..f78c662c 100644 --- a/regression/log.go +++ b/regression/log.go @@ -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) } diff --git a/regression/playback.go b/regression/playback.go index 9eb49c56..f71d2422 100644 --- a/regression/playback.go +++ b/regression/playback.go @@ -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) } diff --git a/regression/video.go b/regression/video.go index 1347ea71..ef5f2b1e 100644 --- a/regression/video.go +++ b/regression/video.go @@ -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) } diff --git a/rewind/search.go b/rewind/search.go index 8e65e439..e8a2a15b 100644 --- a/rewind/search.go +++ b/rewind/search.go @@ -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) } diff --git a/thumbnailer/anim.go b/thumbnailer/anim.go index 65078ec6..147b96d7 100644 --- a/thumbnailer/anim.go +++ b/thumbnailer/anim.go @@ -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 diff --git a/thumbnailer/image.go b/thumbnailer/image.go index eab4a2e6..51e36312 100644 --- a/thumbnailer/image.go +++ b/thumbnailer/image.go @@ -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) } diff --git a/tracker/replay.go b/tracker/replay.go index 8366b6de..1ea55889 100644 --- a/tracker/replay.go +++ b/tracker/replay.go @@ -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) }