// This file is part of Gopher2600. // // Gopher2600 is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Gopher2600 is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Gopher2600. If not, see . package hardware import ( "errors" "github.com/jetsetilly/gopher2600/cartridgeloader" "github.com/jetsetilly/gopher2600/environment" "github.com/jetsetilly/gopher2600/hardware/cpu" "github.com/jetsetilly/gopher2600/hardware/input" "github.com/jetsetilly/gopher2600/hardware/memory" "github.com/jetsetilly/gopher2600/hardware/memory/cartridge" "github.com/jetsetilly/gopher2600/hardware/memory/cpubus" "github.com/jetsetilly/gopher2600/hardware/peripherals" "github.com/jetsetilly/gopher2600/hardware/peripherals/controllers" "github.com/jetsetilly/gopher2600/hardware/preferences" "github.com/jetsetilly/gopher2600/hardware/riot" "github.com/jetsetilly/gopher2600/hardware/riot/ports/panel" "github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging" "github.com/jetsetilly/gopher2600/hardware/television" "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. const ColorClocksPerCPUCycle = 3 // VCS struct is the main container for the emulated components of the VCS. type VCS struct { Env *environment.Environment // the television is not "part" of the VCS console but it's part of the VCS system TV *television.Television // references to the different sub-systems of the VCS. these syb-systems can be // copied with the Snapshot() function creating an instance of the State type CPU *cpu.CPU Mem *memory.Memory RIOT *riot.RIOT TIA *tia.TIA // the input sub-system. this is not part of the Snapshot() process Input *input.Input // The Clock defines the basic speed at which the the machine is runningt. This governs // the speed of the CPU, the RIOT and attached peripherals. The TIA runs at // exactly three times this speed. // // The different clock speeds are due to the nature of the different TV // specifications. Put simply, a PAL machine must run slightly slower in // order to be able to send a correct PAL signal to the television. // // Unlike the real hardware however, it is not the console that governs the // clock speed but the television. A ROM will send a signal to the // television, the timings of which will be used by the tv implementation // to decide what type of TV signal (PAL or NTSC) is being sent. When the // television detects a change in the TV signal it will notify the emulated // console, allowing it to note the new implied clock speed. Clock float32 } // 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 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, notify, prefs) if err != nil { return nil, err } // set up hardware vcs := &VCS{ Env: env, TV: tv, Clock: ntscClock, } vcs.Mem = memory.NewMemory(vcs.Env) vcs.CPU = cpu.NewCPU(vcs.Env, vcs.Mem) vcs.RIOT = riot.NewRIOT(vcs.Env, vcs.Mem.RIOT, vcs.Mem.TIA) vcs.Input = input.NewInput(vcs.TV, vcs.RIOT.Ports) vcs.TIA, err = tia.NewTIA(vcs.Env, vcs.TV, vcs.Mem.TIA, vcs.RIOT.Ports, vcs.CPU) if err != nil { return nil, err } err = vcs.RIOT.Ports.Plug(plugging.PortLeft, controllers.NewStick) if err != nil { return nil, err } err = vcs.RIOT.Ports.Plug(plugging.PortRight, controllers.NewStick) if err != nil { return nil, err } err = vcs.RIOT.Ports.Plug(plugging.PortPanel, panel.NewPanel) if err != nil { return nil, err } vcs.TV.AttachVCS(vcs) return vcs, nil } // End cleans up any resources that may be dangling. func (vcs *VCS) End() { vcs.TV.End() vcs.RIOT.Ports.End() } // AttachCartridge to this VCS. While this function can be called directly it // is advised that the setup package be used in most circumstances. // // The emulated VCS is *not* reset after AttachCartridge() unless the reset // argument is true. // // Note that the emulation should always be reset before emulation commences // but some applications might need to prepare the emulation further before // that happens. func (vcs *VCS) AttachCartridge(cartload cartridgeloader.Loader, reset bool) error { err := vcs.TV.SetSpecConditional(cartload.Spec) if err != nil { return err } if cartload.Filename == "" { vcs.Mem.Cart.Eject() } else { err := vcs.Mem.Cart.Attach(cartload) if err != nil { return err } // fingerprint new peripherals. peripherals are not changed if option is not set err = vcs.FingerprintPeripheral(plugging.PortLeft, cartload) if err != nil { return err } err = vcs.FingerprintPeripheral(plugging.PortRight, cartload) if err != nil { return err } } if reset { err = vcs.Reset() if err != nil { return err } } return nil } // FingerprintPeripheral inserts the peripheral that is thought to be best // suited for the current inserted cartridge. func (vcs *VCS) FingerprintPeripheral(id plugging.PortID, cartload cartridgeloader.Loader) error { return vcs.RIOT.Ports.Plug(id, peripherals.Fingerprint(id, cartload.Data)) } // Reset emulates the reset switch on the console panel. func (vcs *VCS) Reset() error { err := vcs.TV.Reset(false) if err != nil { return err } // easiest way of resetting the TIA is to just create new one // // 27/10/21 - we do want to save the audio though in order to keep any // attached trackers // // TODO: proper Reset() function for the TIA audio := vcs.TIA.Audio vcs.TIA, err = tia.NewTIA(vcs.Env, vcs.TV, vcs.Mem.TIA, vcs.RIOT.Ports, vcs.CPU) if err != nil { return err } vcs.TIA.Audio = audio // other areas of the VCS are simply reset because the emulation may have // altered the part of the state that we do *not* want to reset. notably, // memory may have a cartridge attached - we wouldn't want to lose that. vcs.Mem.Reset() vcs.CPU.Reset() vcs.RIOT.Timer.Reset() // reset of ports must happen after reset of memory because ports will // update memory to the current state of the peripherals vcs.RIOT.Ports.ResetPeripherals() // reset PC using reset address in cartridge memory err = vcs.CPU.LoadPCIndirect(cpubus.Reset) if err != nil { if !errors.Is(err, cartridge.Ejected) { return err } } // reset cart after loaded PC value. this seems unnecessary but some // cartridge types may switch banks on LoadPCIndirect() - those that switch // on Listen() - this is an artefact of the emulation method so we need to make // sure it's initialised correctly. vcs.Mem.Cart.Reset() return nil } // clock speeds taken from // http://www.taswegian.com/WoodgrainWizard/tiki-index.php?page=Clock-Speeds const ( ntscClock = 1.193182 palClock = 1.182298 palMClock = 1.191870 secamClock = 1.187500 ) // SetClockSpeed is an implemtation of the television.VCSReturnChannel interface. func (vcs *VCS) SetClockSpeed(spec specification.Spec) { switch spec.ID { case specification.SpecNTSC.ID: if vcs.Clock != ntscClock { vcs.Clock = ntscClock logger.Log("vcs", "switching to NTSC clock") } case specification.SpecPAL.ID: if vcs.Clock != palClock { vcs.Clock = palClock logger.Log("vcs", "switching to PAL clock") } case specification.SpecPALM.ID: if vcs.Clock != palMClock { vcs.Clock = palMClock logger.Log("vcs", "switching to PAL-M clock") } case specification.SpecSECAM.ID: if vcs.Clock != secamClock { vcs.Clock = secamClock logger.Log("vcs", "switching to SECAM clock") } default: logger.Logf("vcs", "cannot set clock for unknown TV specification (%s)", spec.ID) } } // DetatchEmulationExtras removes all possible monitors, recorders, etc. from // the emulation. Currently this mean: the TIA audio tracker, the RIOT event // recorders and playback, and RIOT plug monitor. func (vcs *VCS) DetatchEmulationExtras() { vcs.TIA.Audio.SetTracker(nil) vcs.Input.ClearRecorders() vcs.Input.AttachPlayback(nil) vcs.RIOT.Ports.AttachPlugMonitor(nil) }