From 24f3f32342abe9792752cece082fb98788cac91a Mon Sep 17 00:00:00 2001 From: JetSetIlly Date: Sat, 6 Apr 2024 09:40:10 +0100 Subject: [PATCH] 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 --- cartridgeloader/loader.go | 108 ++++---- comparison/comparison.go | 61 +++-- debugger/debugger.go | 185 +++++++------- disassembly/disassembly.go | 2 +- environment/environment.go | 41 ++- gui/sdlimgui/playscr_notifications.go | 8 +- gui/sdlimgui/requests.go | 4 +- hardware/memory/cartridge/cartridge.go | 9 +- hardware/memory/cartridge/mapper/mapper.go | 13 +- .../memory/cartridge/moviecart/moviecart.go | 22 +- hardware/memory/cartridge/plusrom/plusrom.go | 16 +- hardware/memory/cartridge/supercharger/doc.go | 3 +- .../memory/cartridge/supercharger/fastload.go | 239 +++++++++--------- .../cartridge/supercharger/soundload.go | 4 +- .../cartridge/supercharger/supercharger.go | 34 ++- hardware/vcs.go | 13 +- notifications/doc.go | 36 +-- notifications/notifications.go | 69 +++-- performance/performance.go | 2 +- preview/preview.go | 2 +- regression/log.go | 2 +- regression/playback.go | 2 +- regression/video.go | 2 +- rewind/search.go | 4 +- thumbnailer/anim.go | 60 ++--- thumbnailer/image.go | 2 +- tracker/replay.go | 2 +- 27 files changed, 461 insertions(+), 484 deletions(-) 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) }