simplified notifications package

notifications interface instance moved to environment from
cartridgeloader. the cartridgeloader package predates the environment
package and had started to be used inappropriately

simplified how notifications.Notify() is called. in particular the
supercharger fastload starter no longer bundles a function hook. nor is
the cartridge instance sent with the notification
This commit is contained in:
JetSetIlly 2024-04-06 09:40:10 +01:00
parent 4ab23ab63e
commit 24f3f32342
27 changed files with 461 additions and 484 deletions

View file

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

View file

@ -26,8 +26,6 @@ import (
"github.com/jetsetilly/gopher2600/debugger/govern" "github.com/jetsetilly/gopher2600/debugger/govern"
"github.com/jetsetilly/gopher2600/environment" "github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware" "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/riot/ports"
"github.com/jetsetilly/gopher2600/hardware/television" "github.com/jetsetilly/gopher2600/hardware/television"
"github.com/jetsetilly/gopher2600/hardware/television/signal" "github.com/jetsetilly/gopher2600/hardware/television/signal"
@ -81,7 +79,7 @@ func NewComparison(driverVCS *hardware.VCS) (*Comparison, error) {
tv.SetFPSCap(true) tv.SetFPSCap(true)
// create a new VCS emulation // 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 { if err != nil {
return nil, fmt.Errorf("comparison: %w", err) return nil, fmt.Errorf("comparison: %w", err)
} }
@ -140,6 +138,34 @@ func (cmp *Comparison) Quit() {
cmp.emulationQuit <- true 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 // CreateFromLoader will cause images to be generated from a running emulation
// initialised with the specified cartridge loader. // initialised with the specified cartridge loader.
func (cmp *Comparison) CreateFromLoader(cartload cartridgeloader.Loader) error { 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") 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() { go func() {
defer func() { defer func() {
cmp.driver.quit <- nil cmp.driver.quit <- nil

View file

@ -44,8 +44,6 @@ import (
"github.com/jetsetilly/gopher2600/hardware/cpu/execution" "github.com/jetsetilly/gopher2600/hardware/cpu/execution"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge" "github.com/jetsetilly/gopher2600/hardware/memory/cartridge"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper" "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/memory/cartridge/supercharger"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging" "github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
"github.com/jetsetilly/gopher2600/hardware/television" "github.com/jetsetilly/gopher2600/hardware/television"
@ -358,7 +356,7 @@ func NewDebugger(opts CommandLineOptions, create CreateUserInterface) (*Debugger
} }
// create a new VCS instance // create a new VCS instance
dbg.vcs, err = hardware.NewVCS(tv, nil) dbg.vcs, err = hardware.NewVCS(tv, dbg, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("debugger: %w", err) return nil, fmt.Errorf("debugger: %w", err)
} }
@ -1054,6 +1052,93 @@ func (dbg *Debugger) reset(newCartridge bool) error {
return nil 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 // attachCartridge makes sure that the cartridge loaded into vcs memory and the
// available disassembly/symbols are in sync. // available disassembly/symbols are in sync.
// //
@ -1101,100 +1186,6 @@ func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error)
} }
dbg.loader = &cartload 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 // reset of vcs is implied with attach cartridge
err := setup.AttachCartridge(dbg.vcs, cartload, false) err := setup.AttachCartridge(dbg.vcs, cartload, false)
if err != nil && !errors.Is(err, cartridge.Ejected) { if err != nil && !errors.Is(err, cartridge.Ejected) {

View file

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

View file

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

View file

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

View file

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

View file

@ -221,7 +221,7 @@ func (cart *Cartridge) Attach(cartload cartridgeloader.Loader) error {
// format) // format)
if cart.fingerprintPlusROM(cartload) { if cart.fingerprintPlusROM(cartload) {
// try creating a NewPlusROM instance // 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 { if err != nil {
// check for known PlusROM errors // check for known PlusROM errors
@ -501,6 +501,13 @@ func (cart *Cartridge) GetCoProc() coprocessor.CartCoProc {
return nil 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 // CopyBanks returns the sequence of banks in a cartridge. To return the
// next bank in the sequence, call the function with the instance of // 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 // mapper.BankContent returned from the previous call. The end of the sequence is

View file

@ -15,7 +15,12 @@
package mapper 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. // CartContainer is a special CartMapper type that wraps another CartMapper.
// For example, the PlusROM type. // For example, the PlusROM type.
@ -266,6 +271,12 @@ type CartTapeState struct {
Data []float32 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 // CartLabelsBus will be implemented for cartridge mappers that want to report any
// special labels for the cartridge type. // special labels for the cartridge type.
type CartLabelsBus interface { type CartLabelsBus interface {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,8 +21,11 @@ import (
"github.com/jetsetilly/gopher2600/cartridgeloader" "github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/environment" "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/cartridge/mapper"
"github.com/jetsetilly/gopher2600/hardware/memory/memorymap" "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" "github.com/jetsetilly/gopher2600/notifications"
) )
@ -52,8 +55,6 @@ type Supercharger struct {
bankSize int bankSize int
bios []uint8 bios []uint8
notificationHook notifications.NotificationHook
// rewindable state // rewindable state
state *state state *state
} }
@ -62,11 +63,10 @@ type Supercharger struct {
// Supercharger type. // Supercharger type.
func NewSupercharger(env *environment.Environment, cartload cartridgeloader.Loader) (mapper.CartMapper, error) { func NewSupercharger(env *environment.Environment, cartload cartridgeloader.Loader) (mapper.CartMapper, error) {
cart := &Supercharger{ cart := &Supercharger{
env: env, env: env,
mappingID: "AR", mappingID: "AR",
bankSize: 2048, bankSize: 2048,
state: newState(), state: newState(),
notificationHook: cartload.NotificationHook,
} }
var err error 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 // sustained until the BIOS is "touched" as described below
if !cart.state.isLoading { if !cart.state.isLoading {
cart.state.isLoading = true 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 // 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 bios {
if cart.state.registers.ROMpower { if cart.state.registers.ROMpower {
// trigger notificationHook() function whenever BIOS address $fa1a // send notification whenever BIOS address $fa1a (specifically) is
// (specifically) is touched. note that this method means that the // touched. note that this method means that the notification will
// notificationHook() function will be called whatever the context the // be sent whatever the context the address is read and not just
// address is read and not just when the PC is at the address. // when the PC is at the address.
if addr == 0x0a1a { if addr == 0x0a1a {
// end tape is loading state // end tape is loading state
cart.state.isLoading = false cart.state.isLoading = false
err := cart.notificationHook(cart, notifications.NotifySuperchargerSoundloadEnded) err := cart.env.Notifications.Notify(notifications.NotifySuperchargerSoundloadEnded)
if err != nil { if err != nil {
return 0, 0, fmt.Errorf("supercharger: %w", err) 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 // GetTapeState implements the mapper.CartTapeBus interface
// //
// Whether this does anything meaningful depends on the interal implementation // Whether this does anything meaningful depends on the interal implementation

View file

@ -35,6 +35,7 @@ import (
"github.com/jetsetilly/gopher2600/hardware/television/specification" "github.com/jetsetilly/gopher2600/hardware/television/specification"
"github.com/jetsetilly/gopher2600/hardware/tia" "github.com/jetsetilly/gopher2600/hardware/tia"
"github.com/jetsetilly/gopher2600/logger" "github.com/jetsetilly/gopher2600/logger"
"github.com/jetsetilly/gopher2600/notifications"
) )
// The number of times the TIA updates every CPU cycle. // 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 // NewVCS creates a new VCS and everything associated with the hardware. It is
// used for all aspects of emulation: debugging sessions, and regular play. // 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 // The Television argument should not be nil. The Notify and Preferences
// nil and a new preferences instance will be created. Providing a non-nil value // argument may be nil if required.
// allows the preferences of more than one VCS emulation to be synchronised. func NewVCS(tv *television.Television, notify notifications.Notify, prefs *preferences.Preferences) (*VCS, error) {
//
// 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) {
// set up environment // set up environment
env, err := environment.NewEnvironment(tv, prefs) env, err := environment.NewEnvironment(tv, notify, prefs)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -13,34 +13,12 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>. // along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
// Package notifications defines the Notify type and the possible values of // Package notifications allow communication from a cartridge directly to the
// that type. These values represent the different notifications that be sent // emulation instance. This is useful, for example, by the Supercharger
// to the GUI. // emulation to indicate the start/stop of the Supercharger tape.
// //
// hardware ----> emulation ----> GUI // Notifications are sometimes passed onto the GUI to inidicate to the user the
// (eg. cartridge) (eg. debugger) // event that has happened (eg. tape stopped, etc.) For some notifications
// // however, it is appropriate for the emulation instance to deal with the
// Notifications flow in one direction only and can be generated and terminate // notification invisibly.
// 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.
package notifications package notifications

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,8 +27,6 @@ import (
"github.com/jetsetilly/gopher2600/debugger/govern" "github.com/jetsetilly/gopher2600/debugger/govern"
"github.com/jetsetilly/gopher2600/environment" "github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware" "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/preferences"
"github.com/jetsetilly/gopher2600/hardware/riot/ports" "github.com/jetsetilly/gopher2600/hardware/riot/ports"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging" "github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
@ -93,7 +91,7 @@ func NewAnim(prefs *preferences.Preferences) (*Anim, error) {
tv.SetFPSCap(true) tv.SetFPSCap(true)
// create a new VCS emulation // create a new VCS emulation
thmb.vcs, err = hardware.NewVCS(tv, prefs) thmb.vcs, err = hardware.NewVCS(tv, thmb, prefs)
if err != nil { if err != nil {
return nil, fmt.Errorf("thumbnailer: %w", err) 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 // 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 // with the specified cartridge loader. The emulation will run for a number of
// frames before ending // 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 // image in the render queue at the start of the animation
thmb.Reset() 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() { go func() {
defer func() { defer func() {
thmb.emulationCompleted <- true thmb.emulationCompleted <- true

View file

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

View file

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