-
Notifications
You must be signed in to change notification settings - Fork 19
/
vcs.go
297 lines (257 loc) · 9.27 KB
/
vcs.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
// 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/tia"
"github.com/jetsetilly/gopher2600/logger"
)
// 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 components of the VCS. do not take copies of
// these pointer values because the rewind feature will change them.
CPU *cpu.CPU
Mem *memory.Memory
RIOT *riot.RIOT
TIA *tia.TIA
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 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) {
// set up environment
env, err := environment.NewEnvironment(tv, 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()
}
// Plumb the various VCS sub-systems together after a rewind.
//
// The fromDifferentEmulation indicates that the State has been created by a
// different VCS emulation than the one being plumbed into.
func (vcs *VCS) Plumb(fromDifferentEmulation bool) {
vcs.CPU.Plumb(vcs.Env, vcs.Mem)
vcs.Mem.Plumb(vcs.Env, fromDifferentEmulation)
vcs.RIOT.Plumb(vcs.Env, vcs.Mem.RIOT, vcs.Mem.TIA)
vcs.TIA.Plumb(vcs.Env, vcs.TV, vcs.Mem.TIA, vcs.RIOT.Ports, vcs.CPU)
// reset peripherals after new state has been plumbed. without this,
// controllers can feel odd if the newly plumbed state has left RIOT memory
// in a latched state
vcs.RIOT.Ports.ResetPeripherals()
vcs.Input.Plumb(vcs.TV, vcs.RIOT.Ports)
}
// 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(tvSpec string) {
switch tvSpec {
case "NTSC":
if vcs.Clock != ntscClock {
vcs.Clock = ntscClock
logger.Log("vcs", "switching to NTSC clock")
}
case "PAL":
if vcs.Clock != palClock {
vcs.Clock = palClock
logger.Log("vcs", "switching to PAL clock")
}
case "PAL-M":
if vcs.Clock != palMClock {
vcs.Clock = palMClock
logger.Log("vcs", "switching to PAL-M clock")
}
case "SECAM":
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)", tvSpec)
}
}
// 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)
}