Gopher2600/hardware/vcs.go
JetSetIlly 24f3f32342 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
2024-04-06 10:12:55 +01:00

279 lines
8.7 KiB
Go

// 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 <https://www.gnu.org/licenses/>.
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)
}