badline 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/CLAUDE.md +23 -23
  4. data/CONTRIBUTING.md +5 -0
  5. data/README.md +59 -1
  6. data/doc/pinned-behaviour.md +4 -2
  7. data/exe/badline-ruby +2 -1
  8. data/lib/badline/address_bus/saved_state.rb +72 -0
  9. data/lib/badline/address_bus.rb +1 -0
  10. data/lib/badline/audio/stream.rb +6 -0
  11. data/lib/badline/cartridge/action_replay.rb +15 -0
  12. data/lib/badline/cartridge/comal80.rb +8 -0
  13. data/lib/badline/cartridge/dinamic.rb +8 -0
  14. data/lib/badline/cartridge/easy_flash.rb +20 -1
  15. data/lib/badline/cartridge/epyx_fastload.rb +8 -0
  16. data/lib/badline/cartridge/final_cartridge3.rb +12 -0
  17. data/lib/badline/cartridge/flash.rb +41 -0
  18. data/lib/badline/cartridge/fun_play.rb +8 -0
  19. data/lib/badline/cartridge/g_mod2.rb +12 -0
  20. data/lib/badline/cartridge/game_system.rb +8 -0
  21. data/lib/badline/cartridge/geo_ram.rb +16 -0
  22. data/lib/badline/cartridge/magic_desk.rb +8 -0
  23. data/lib/badline/cartridge/ocean.rb +8 -0
  24. data/lib/badline/cartridge/pagefox.rb +12 -0
  25. data/lib/badline/cartridge/retro_replay.rb +44 -1
  26. data/lib/badline/cartridge/rgcd.rb +10 -0
  27. data/lib/badline/cartridge/super_games.rb +10 -0
  28. data/lib/badline/cartridge/zaxxon.rb +16 -0
  29. data/lib/badline/cartridge.rb +72 -0
  30. data/lib/badline/cia/interrupt_register.rb +17 -0
  31. data/lib/badline/cia/serial.rb +44 -0
  32. data/lib/badline/cia/state.rb +47 -0
  33. data/lib/badline/cia/timer.rb +33 -0
  34. data/lib/badline/cia.rb +24 -2
  35. data/lib/badline/computer/attachments.rb +132 -0
  36. data/lib/badline/computer/saved_state.rb +116 -0
  37. data/lib/badline/computer.rb +8 -31
  38. data/lib/badline/cpu/microcode.rb +8 -0
  39. data/lib/badline/cpu/saved_state.rb +115 -0
  40. data/lib/badline/cpu.rb +2 -0
  41. data/lib/badline/datasette.rb +27 -0
  42. data/lib/badline/drive1541/bus.rb +89 -13
  43. data/lib/badline/drive1541/disk.rb +7 -2
  44. data/lib/badline/drive1541/disk_state.rb +80 -0
  45. data/lib/badline/drive1541/idle.rb +9 -57
  46. data/lib/badline/drive1541/mechanism.rb +2 -0
  47. data/lib/badline/drive1541/mechanism_state.rb +67 -0
  48. data/lib/badline/drive1541/orbit.rb +81 -0
  49. data/lib/badline/drive1541/rotation.rb +10 -0
  50. data/lib/badline/drive1541/serial_port.rb +10 -0
  51. data/lib/badline/drive1541/sleep.rb +114 -0
  52. data/lib/badline/drive1541.rb +52 -2
  53. data/lib/badline/gui/application.rb +41 -14
  54. data/lib/badline/gui/gamepads.rb +7 -0
  55. data/lib/badline/gui/snapshots.rb +67 -0
  56. data/lib/badline/gui.rb +1 -0
  57. data/lib/badline/iec_bus.rb +16 -0
  58. data/lib/badline/kernal_trap/channel.rb +27 -0
  59. data/lib/badline/kernal_trap/drive/channels.rb +25 -0
  60. data/lib/badline/kernal_trap/drive/memory.rb +9 -0
  61. data/lib/badline/kernal_trap/drive/status.rb +8 -0
  62. data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
  63. data/lib/badline/kernal_trap/drive.rb +19 -0
  64. data/lib/badline/kernal_trap/save.rb +8 -0
  65. data/lib/badline/kernal_trap/serial.rb +16 -0
  66. data/lib/badline/memory.rb +11 -0
  67. data/lib/badline/options.rb +3 -2
  68. data/lib/badline/ram_expansion/plus256k.rb +16 -0
  69. data/lib/badline/ram_expansion/plus60k.rb +12 -0
  70. data/lib/badline/ram_expansion/unexpanded.rb +6 -0
  71. data/lib/badline/reu/dma.rb +36 -0
  72. data/lib/badline/reu/ram.rb +15 -0
  73. data/lib/badline/reu/saved_state.rb +52 -0
  74. data/lib/badline/reu.rb +4 -0
  75. data/lib/badline/sdl/events.rb +2 -0
  76. data/lib/badline/sid/envelope/state.rb +68 -0
  77. data/lib/badline/sid/envelope.rb +7 -0
  78. data/lib/badline/sid/filter.rb +38 -0
  79. data/lib/badline/sid/voice.rb +10 -0
  80. data/lib/badline/sid/waveform/state.rb +66 -0
  81. data/lib/badline/sid/waveform.rb +2 -0
  82. data/lib/badline/sid.rb +34 -0
  83. data/lib/badline/snapshot/container.rb +117 -0
  84. data/lib/badline/snapshot/fields.rb +54 -0
  85. data/lib/badline/snapshot/image.rb +70 -0
  86. data/lib/badline/snapshot/machine_state.rb +77 -0
  87. data/lib/badline/snapshot/setup.rb +58 -0
  88. data/lib/badline/snapshot/state.rb +178 -0
  89. data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
  90. data/lib/badline/snapshot/vice/cias.rb +89 -0
  91. data/lib/badline/snapshot/vice/cias_import.rb +70 -0
  92. data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
  93. data/lib/badline/snapshot/vice/peripherals.rb +47 -0
  94. data/lib/badline/snapshot/vice/restore.rb +125 -0
  95. data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
  96. data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
  97. data/lib/badline/snapshot/vice/vicii.rb +119 -0
  98. data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
  99. data/lib/badline/snapshot/vice.rb +74 -0
  100. data/lib/badline/snapshot.rb +52 -0
  101. data/lib/badline/storage/crt_file.rb +9 -3
  102. data/lib/badline/storage/d64_image.rb +2 -0
  103. data/lib/badline/storage/d71_image.rb +2 -0
  104. data/lib/badline/storage/d81_image.rb +2 -0
  105. data/lib/badline/storage/disk_image.rb +11 -2
  106. data/lib/badline/storage/host_directory.rb +5 -0
  107. data/lib/badline/storage/t64.rb +6 -0
  108. data/lib/badline/storage/tap.rb +10 -1
  109. data/lib/badline/storage/unavailable.rb +20 -0
  110. data/lib/badline/storage.rb +34 -0
  111. data/lib/badline/time_of_day.rb +56 -0
  112. data/lib/badline/version.rb +1 -1
  113. data/lib/badline/via/control_lines.rb +15 -0
  114. data/lib/badline/via/fast_forward.rb +15 -0
  115. data/lib/badline/via/interrupt_register.rb +9 -0
  116. data/lib/badline/via/shift_register.rb +13 -0
  117. data/lib/badline/via/state.rb +42 -0
  118. data/lib/badline/via/timer1.rb +19 -0
  119. data/lib/badline/via/timer2.rb +24 -0
  120. data/lib/badline/via.rb +2 -0
  121. data/lib/badline/vic/border_mask.rb +10 -0
  122. data/lib/badline/vic/collisions.rb +11 -0
  123. data/lib/badline/vic/color_patches.rb +11 -0
  124. data/lib/badline/vic/display_state.rb +29 -1
  125. data/lib/badline/vic/graphics_shifter.rb +26 -0
  126. data/lib/badline/vic/register_log.rb +18 -0
  127. data/lib/badline/vic/registers.rb +13 -2
  128. data/lib/badline/vic/sequencer.rb +12 -0
  129. data/lib/badline/vic/sequencer_state.rb +89 -0
  130. data/lib/badline/vic/sprite/internal_bus.rb +11 -0
  131. data/lib/badline/vic/sprite/saved_state.rb +64 -0
  132. data/lib/badline/vic/sprite.rb +2 -0
  133. data/lib/badline/vic/sprites.rb +3 -0
  134. data/lib/badline/vic/sprites_state.rb +57 -0
  135. data/lib/badline/vic.rb +94 -10
  136. data/lib/badline.rb +1 -0
  137. metadata +37 -1
@@ -2,6 +2,44 @@
2
2
 
3
3
  module Badline
4
4
  class Cartridge
5
+ # A Retro Replay's registers, RAM and flash, for a snapshot.
6
+ module RetroReplayState
7
+ private
8
+
9
+ def save_jumpers(out)
10
+ out.boolean(true).boolean(@flash_jumper).boolean(@bank_jumper)
11
+ end
12
+
13
+ def save_mapper(out)
14
+ [@active, @frozen, @phi1_ultimax, @ram_enabled, @ram_at_a000, @allow_bank, @no_freeze, @reu_mapping,
15
+ @write_once].each { |flag| out.boolean(flag) }
16
+ out.int(@bank).int(RetroReplay::CONTROL_MODES.index(@config))
17
+ @ram_data.each { |data| out.blob(data) }
18
+ @flash.save_state(out)
19
+ end
20
+
21
+ def load_mapper(input)
22
+ load_flags(input)
23
+ @bank = input.int
24
+ @config = RetroReplay::CONTROL_MODES.fetch(input.int)
25
+ @ram_data.each { |data| input.blob_into(data) }
26
+ @flash.load_state(input)
27
+ map_windows
28
+ end
29
+
30
+ def load_flags(input)
31
+ @active = input.boolean?
32
+ @frozen = input.boolean?
33
+ @phi1_ultimax = input.boolean?
34
+ @ram_enabled = input.boolean?
35
+ @ram_at_a000 = input.boolean?
36
+ @allow_bank = input.boolean?
37
+ @no_freeze = input.boolean?
38
+ @reu_mapping = input.boolean?
39
+ @write_once = input.boolean?
40
+ end
41
+ end
42
+
5
43
  # Retro Replay: an Am29F010 flash chip of two 64K halves, each eight 8K
6
44
  # ROM banks, and four 8K RAM banks, with the Action Replay's register at
7
45
  # $DE00 and an extended one at $DE01. The Nordic Replay (subtype 1) adds
@@ -44,6 +82,7 @@ module Badline
44
82
  # the half in bit 5.
45
83
  class RetroReplay < Cartridge
46
84
  include Freezer
85
+ include RetroReplayState
47
86
 
48
87
  CONTROL_MODES = %i[rom8k rom16k off ultimax].freeze
49
88
  IO1_PAGE = 0x1e00
@@ -212,11 +251,15 @@ module Badline
212
251
  end
213
252
 
214
253
  def configure
254
+ map_windows
255
+ changed!
256
+ end
257
+
258
+ def map_windows
215
259
  self.mode = @config
216
260
  @rom = flash_window(@bank)
217
261
  @roml = roml_window
218
262
  @romh = romh_window
219
- changed!
220
263
  end
221
264
 
222
265
  def roml_window
@@ -28,6 +28,16 @@ module Badline
28
28
 
29
29
  private
30
30
 
31
+ def save_mapper(out)
32
+ save_windows(out, [*@banks, EMPTY_BANK])
33
+ out.boolean(@disabled)
34
+ end
35
+
36
+ def load_mapper(input)
37
+ load_windows(input, [*@banks, EMPTY_BANK])
38
+ @disabled = input.boolean?
39
+ end
40
+
31
41
  def select(value)
32
42
  value ^= 0x07 if @hucky
33
43
  if value.anybits?(0x08)
@@ -20,6 +20,16 @@ module Badline
20
20
 
21
21
  private
22
22
 
23
+ def save_mapper(out)
24
+ save_windows(out, [*@roml_banks, *@romh_banks, EMPTY_BANK])
25
+ out.boolean(@locked)
26
+ end
27
+
28
+ def load_mapper(input)
29
+ load_windows(input, [*@roml_banks, *@romh_banks, EMPTY_BANK])
30
+ @locked = input.boolean?
31
+ end
32
+
23
33
  def select(value)
24
34
  @roml = bank(@roml_banks, value & 0x03)
25
35
  @romh = bank(@romh_banks, value & 0x03)
@@ -31,6 +31,14 @@ module Badline
31
31
  def peek(addr)
32
32
  @bank.peek(addr)
33
33
  end
34
+
35
+ def save_state(out)
36
+ out.int([*@banks, EMPTY_BANK].index { |bank| bank.equal?(@bank) })
37
+ end
38
+
39
+ def load_state(input)
40
+ @bank = [*@banks, EMPTY_BANK].fetch(input.int)
41
+ end
34
42
  end
35
43
 
36
44
  def reset
@@ -39,6 +47,14 @@ module Badline
39
47
 
40
48
  private
41
49
 
50
+ def save_mapper(out)
51
+ @romh.save_state(out)
52
+ end
53
+
54
+ def load_mapper(input)
55
+ @romh.load_state(input)
56
+ end
57
+
42
58
  def install_chips(chips)
43
59
  roml, romh = banks_from(chips)
44
60
  @romh = High.new(romh)
@@ -69,9 +69,28 @@ module Badline
69
69
  end
70
70
  mapper.new(crt, **)
71
71
  end
72
+
73
+ # A cartridge built afresh from what save_setup wrote, before
74
+ # load_state puts its state in.
75
+ def from_setup(input)
76
+ return GeoRAM.new(size: input.int) if input.int == GEO_RAM_SETUP
77
+
78
+ crt = Storage::CRTFile.new(bytes: input.string)
79
+ return RetroReplay.new(crt, flash_jumper: input.boolean?, bank_jumper: input.boolean?) if input.boolean?
80
+
81
+ from_crt(crt)
82
+ end
72
83
  end
73
84
 
85
+ # What save_setup writes first: a cartridge from a CRT image, or a
86
+ # GEO-RAM.
87
+ CRT_SETUP = 0
88
+ GEO_RAM_SETUP = 1
89
+
90
+ # Keeps the image it was built from, so a snapshot's machine can build
91
+ # the same cartridge before the snapshot's state goes into it.
74
92
  def initialize(crt)
93
+ @crt = crt
75
94
  @name = crt.name
76
95
  @exrom = crt.exrom
77
96
  @game = crt.game
@@ -88,6 +107,31 @@ module Badline
88
107
  @on_change = block
89
108
  end
90
109
 
110
+ # What builds the same cartridge afresh (Cartridge.from_setup): the CRT
111
+ # image it was built from, and the jumpers of a mapper that has them.
112
+ def save_setup(out)
113
+ out.int(CRT_SETUP).string(Storage::CRTFile.encode(@crt))
114
+ save_jumpers(out)
115
+ end
116
+
117
+ # The lines, the pull on NMI, the button and the mapper's own state.
118
+ # Loading sets them without calling back into the machine, which maps
119
+ # the cartridge again afterwards.
120
+ def save_state(out)
121
+ out.marker("CARTRIDGE")
122
+ out.int(@exrom).int(@game).boolean(@nmi).boolean(@button == true)
123
+ save_mapper(out)
124
+ end
125
+
126
+ def load_state(input)
127
+ input.marker("CARTRIDGE")
128
+ @exrom = input.int
129
+ @game = input.int
130
+ @nmi = input.boolean?
131
+ @button = input.boolean?
132
+ load_mapper(input)
133
+ end
134
+
91
135
  # Called with the level of the cartridge's pull on the NMI line when it
92
136
  # changes. The line is wired-OR with CIA 2's.
93
137
  def on_nmi_change(&block)
@@ -142,6 +186,34 @@ module Badline
142
186
 
143
187
  private
144
188
 
189
+ def save_jumpers(out)
190
+ out.boolean(false)
191
+ end
192
+
193
+ def save_mapper(_out); end
194
+
195
+ def load_mapper(_input); end
196
+
197
+ # Where the ROML and ROMH windows point, as their places in `windows`,
198
+ # the banks and views the mapper picks them from, or -1 for none.
199
+ def save_windows(out, windows)
200
+ out.int(window_index(@roml, windows)).int(window_index(@romh, windows))
201
+ end
202
+
203
+ def load_windows(input, windows)
204
+ @roml = window_at(input.int, windows)
205
+ @romh = window_at(input.int, windows)
206
+ end
207
+
208
+ def window_index(window, windows)
209
+ return -1 if window.nil?
210
+
211
+ windows.index { |candidate| candidate.equal?(window) } ||
212
+ raise(ArgumentError, "#{self.class.name}: a window that isn't among its banks")
213
+ end
214
+
215
+ def window_at(index, windows) = index.negative? ? nil : windows.fetch(index)
216
+
145
217
  def nmi=(level)
146
218
  return if @nmi == level
147
219
 
@@ -34,6 +34,23 @@ module Badline
34
34
 
35
35
  def interrupted? = status.value >= 0x80
36
36
 
37
+ def save_state(out)
38
+ out.int(@mask.value).int(@status.value).int(@pending)
39
+ [@read, @read_last_cycle, @read_two_cycles_ago].each { |read| out.optional_int(read) }
40
+ out.boolean(@timer_b_bug).boolean(@quiet)
41
+ end
42
+
43
+ def load_state(input)
44
+ @mask.value = input.int
45
+ @status.value = input.int
46
+ @pending = input.int
47
+ @read = input.optional_int
48
+ @read_last_cycle = input.optional_int
49
+ @read_two_cycles_ago = input.optional_int
50
+ @timer_b_bug = input.boolean?
51
+ @quiet = input.boolean?
52
+ end
53
+
37
54
  # Raises IR on this cycle.
38
55
  def assert_now
39
56
  status.interrupt = true
@@ -62,6 +62,19 @@ module Badline
62
62
  @sp_out = true
63
63
  end
64
64
 
65
+ # The bits shifted so far, and the byte waiting to go out after the
66
+ # one shifting.
67
+ attr_reader :steps, :pending
68
+
69
+ def shift_register = @shift
70
+
71
+ # Sets the data register and the shift register, as a VICE snapshot
72
+ # gives them.
73
+ def restore_shift(data, shift)
74
+ @data = data
75
+ @shift = shift
76
+ end
77
+
65
78
  def output?
66
79
  @control.serial_mode?
67
80
  end
@@ -71,6 +84,25 @@ module Badline
71
84
  @cnt = level unless output?
72
85
  end
73
86
 
87
+ def save_state(out)
88
+ out.int(@data).int(@shift).int(@steps).optional_int(@empty_in).optional_int(@busy_in)
89
+ out.boolean(@busy).boolean(@abandoned).optional_int(@pending).boolean(@underflow_high).boolean(@in_flight)
90
+ out.int(@flight_up).int(@flight_down).boolean(@idle)
91
+ [@cnt, @cnt_in, @sp_in, @sp_out].each { |level| out.boolean(level) }
92
+ end
93
+
94
+ def load_state(input)
95
+ @data = input.int
96
+ @shift = input.int
97
+ @steps = input.int
98
+ @empty_in = input.optional_int
99
+ @busy_in = input.optional_int
100
+ @busy = input.boolean?
101
+ @abandoned = input.boolean?
102
+ @pending = input.optional_int
103
+ load_lines(input)
104
+ end
105
+
74
106
  def write(value)
75
107
  @data = value
76
108
  @pending = LOAD_DELAY
@@ -132,6 +164,18 @@ module Badline
132
164
 
133
165
  private
134
166
 
167
+ def load_lines(input)
168
+ @underflow_high = input.boolean?
169
+ @in_flight = input.boolean?
170
+ @flight_up = input.int
171
+ @flight_down = input.int
172
+ @idle = input.boolean?
173
+ @cnt = input.boolean?
174
+ @cnt_in = input.boolean?
175
+ @sp_in = input.boolean?
176
+ @sp_out = input.boolean?
177
+ end
178
+
135
179
  # Without an underflow, a register with nothing counting down and
136
180
  # nothing in the delay lines has nothing to do on a cycle.
137
181
  def idle?
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CIA
5
+ # A snapshot of the chip: everything it holds but its wiring, the ports,
6
+ # the PB4 and CNT levels, the timers, the interrupt register, the serial
7
+ # port and the TOD clock.
8
+ module State
9
+ def save_state(out)
10
+ out.marker("CIA")
11
+ out.int(@data_port_a).int(@data_port_b).int(@data_dir_a).int(@data_dir_b)
12
+ out.boolean(@port_b4_high).boolean(@port_b4_driven_high).boolean(@cnt_high).boolean(@cnt_rise)
13
+ out.int(@control_a.value).int(@control_b.value)
14
+ @ta.save_state(out)
15
+ @tb.save_state(out)
16
+ @icr.save_state(out)
17
+ @serial.save_state(out)
18
+ @tod.save_state(out)
19
+ end
20
+
21
+ def load_state(input)
22
+ input.marker("CIA")
23
+ @data_port_a = input.int
24
+ @data_port_b = input.int
25
+ @data_dir_a = input.int
26
+ @data_dir_b = input.int
27
+ @port_b4_high = input.boolean?
28
+ @port_b4_driven_high = input.boolean?
29
+ @cnt_high = input.boolean?
30
+ @cnt_rise = input.boolean?
31
+ @control_a.value = input.int
32
+ @control_b.value = input.int
33
+ load_parts(input)
34
+ end
35
+
36
+ private
37
+
38
+ def load_parts(input)
39
+ @ta.load_state(input)
40
+ @tb.load_state(input)
41
+ @icr.load_state(input)
42
+ @serial.load_state(input)
43
+ @tod.load_state(input)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -42,6 +42,26 @@ module Badline
42
42
  settle
43
43
  end
44
44
 
45
+ # The counter, the latch and the pipeline between them. The control
46
+ # register is the CIA's to save.
47
+ def save_state(out)
48
+ out.int(@counter).int(@latch).int(@pipe).int(@load_delay).boolean(@reload).boolean(@underflowed)
49
+ out.int(@oneshot_linger).boolean(@toggle).boolean(@settled).boolean(@empty)
50
+ end
51
+
52
+ def load_state(input)
53
+ @counter = input.int
54
+ @latch = input.int
55
+ @pipe = input.int
56
+ @load_delay = input.int
57
+ @reload = input.boolean?
58
+ @underflowed = input.boolean?
59
+ @oneshot_linger = input.int
60
+ @toggle = input.boolean?
61
+ @settled = input.boolean?
62
+ @empty = input.boolean?
63
+ end
64
+
45
65
  def write_control(value)
46
66
  @toggle = true if value.anybits?(0x01) && !started?
47
67
  @oneshot_linger = 2 if control.run_mode? && value.nobits?(0x08)
@@ -59,6 +79,19 @@ module Badline
59
79
  @counter = @latch unless started?
60
80
  end
61
81
 
82
+ # Whether the timer's PB output toggle is high, and its count pipeline
83
+ # as two bits, the newer stage high.
84
+ attr_reader :toggle, :pipe
85
+
86
+ # Sets the control register without its load strobe, the toggle and
87
+ # the count pipeline, as a VICE snapshot gives them.
88
+ def restore_pipeline(control, toggle, pipe)
89
+ @control.value = control & ~0x10
90
+ @toggle = toggle
91
+ @pipe = pipe
92
+ settle
93
+ end
94
+
62
95
  private
63
96
 
64
97
  def started?
data/lib/badline/cia.rb CHANGED
@@ -3,11 +3,13 @@
3
3
  require "badline/cia/interrupt_register"
4
4
  require "badline/cia/serial"
5
5
  require "badline/cia/timer"
6
+ require "badline/cia/state"
6
7
 
7
8
  module Badline
8
9
  # CIA (Complex Interface Adapter) chip
9
10
  class CIA
10
11
  include Addressable
12
+ include State
11
13
 
12
14
  # The original NMOS 6526 of the breadbin C64, and the HMOS 6526A of the
13
15
  # C64C, which differs from it only in its interrupt control register.
@@ -19,6 +21,12 @@ module Badline
19
21
  # read_a and read_b, and on CIA 1 drives PB4 through port_b4_high?.
20
22
  attr_accessor :peripheral
21
23
 
24
+ # Timer A and timer B.
25
+ def timers = [@ta, @tb]
26
+
27
+ # Port A and B's data and direction registers.
28
+ def port_registers = [@data_port_a, @data_port_b, @data_dir_a, @data_dir_b]
29
+
22
30
  def interrupt_status = @icr.status
23
31
 
24
32
  def interrupt_control = @icr.mask
@@ -62,6 +70,7 @@ module Badline
62
70
 
63
71
  @peripheral = peripheral
64
72
  @port_b4_handler = nil
73
+ @port_a_handler = nil
65
74
  @port_b4_high = true
66
75
  reset!
67
76
  end
@@ -87,6 +96,7 @@ module Badline
87
96
  @ta = Timer.new(@control_a)
88
97
  @tb = Timer.new(@control_b)
89
98
  @serial = Serial.new(@control_a)
99
+ @port_a_handler&.call
90
100
  end
91
101
 
92
102
  # Register a change handler on the PB4 line. On CIA 1, this feeds the
@@ -95,6 +105,13 @@ module Badline
95
105
  @port_b4_handler = handler
96
106
  end
97
107
 
108
+ # Register a handler for writes that can move port A's lines: to the
109
+ # data register or DDR, and the reset. On CIA 2, this tells the drives
110
+ # on the serial bus that the C64 may have moved a line.
111
+ def on_port_a_write(&handler)
112
+ @port_a_handler = handler
113
+ end
114
+
98
115
  # A falling edge on the FLAG pin. On CIA 1 the datasette's tape read
99
116
  # line drives it, on CIA 2 the serial bus SRQ.
100
117
  def flag! = raise_interrupt(:flag)
@@ -153,9 +170,9 @@ module Badline
153
170
 
154
171
  def poke(addr, value)
155
172
  case offset_of(addr) & 0x0f
156
- when 0x00 then @data_port_a = value
173
+ when 0x00 then update_port_a { @data_port_a = value }
157
174
  when 0x01 then update_port_b { @data_port_b = value }
158
- when 0x02 then @data_dir_a = value
175
+ when 0x02 then update_port_a { @data_dir_a = value }
159
176
  when 0x03 then update_port_b { @data_dir_b = value }
160
177
  when 0x04 then @ta.write_latch_low(value)
161
178
  when 0x05 then @ta.write_latch_high(value)
@@ -174,6 +191,11 @@ module Badline
174
191
 
175
192
  private
176
193
 
194
+ def update_port_a
195
+ yield
196
+ @port_a_handler&.call
197
+ end
198
+
177
199
  def update_port_b
178
200
  yield
179
201
  @port_b4_driven_high = driven_lines(@data_port_b, @data_dir_b).anybits?(0x10)
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Computer
5
+ # What plugs into the machine besides its chips, the cartridge, the
6
+ # disk device 8 serves through the traps, a true 1541 and an REU, and
7
+ # their state for a snapshot.
8
+ module Attachments
9
+ # Puts the cartridge in the expansion port and wires its clock and NMI
10
+ # line to the machine, leaving the machine's state as it is. A
11
+ # restored snapshot's cartridge comes back through here.
12
+ def connect_cartridge(cartridge)
13
+ cartridge.clock = -> { @cycles }
14
+ cartridge.on_nmi_change { |level| @cartridge_nmi = level }
15
+ address_bus.attach_cartridge(cartridge)
16
+ end
17
+
18
+ # Puts the storage in device 8. Mounting again swaps the disk at any
19
+ # point while the machine runs: the drive keeps its RAM, which only a
20
+ # drive reset clears, and its status.
21
+ def mount(storage)
22
+ return @drive.insert(storage) if @drive
23
+
24
+ @drive = KernalTrap::Drive.new(storage)
25
+ load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
26
+ cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
27
+ @serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive, device: serial_trap_device).install
28
+ save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
29
+ cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
30
+ end
31
+
32
+ # Takes device 8's mounted storage out, and with it the LOAD, SAVE and
33
+ # serial traps, so the KERNAL's routines go out over the serial bus.
34
+ # Mounting again starts a new drive, with its RAM cleared.
35
+ def unmount
36
+ return unless @drive
37
+
38
+ cpu.remove_trap(KernalTrap::Load::ADDRESS)
39
+ cpu.remove_trap(KernalTrap::Save::ADDRESS)
40
+ @serial_trap.device = nil
41
+ @serial_trap = nil
42
+ @save_trap = nil
43
+ @drive = nil
44
+ end
45
+
46
+ # Whether device 8 serves a disk or directory through the traps.
47
+ def mounted? = !@drive.nil?
48
+
49
+ # Unplugs the Drive1541, leaving the serial bus with nothing on it.
50
+ def detach_drive1541
51
+ return unless @drive1541
52
+
53
+ @iec_bus&.detach(@drive1541)
54
+ @drive1541 = nil
55
+ @serial_trap&.device = serial_trap_device
56
+ end
57
+
58
+ private
59
+
60
+ def save_cartridge(out)
61
+ cartridge = address_bus.cartridge
62
+ out.boolean(!cartridge.nil?)
63
+ cartridge&.save_setup(out)
64
+ end
65
+
66
+ # A cartridge comes back built afresh from the setup it was built
67
+ # with, before the address bus puts its state into it.
68
+ def load_cartridge(input)
69
+ if input.boolean?
70
+ connect_cartridge(Cartridge.from_setup(input))
71
+ elsif address_bus.cartridge
72
+ address_bus.detach_cartridge
73
+ end
74
+ end
75
+
76
+ # The disk device 8 serves through the traps, with the traps' own
77
+ # state: open channels, the drive's status and RAM, a SAVE under way.
78
+ def save_trap_drive(out)
79
+ out.boolean(!@drive.nil?)
80
+ return unless @drive
81
+
82
+ @drive.save_state(out)
83
+ @serial_trap.save_state(out)
84
+ @save_trap.save_state(out)
85
+ end
86
+
87
+ def load_trap_drive(input)
88
+ return unmount unless input.boolean?
89
+
90
+ storage = Storage.reopen(input)
91
+ unmount
92
+ mount(storage)
93
+ @drive.load_state(input)
94
+ @serial_trap.load_state(input)
95
+ @save_trap.load_state(input)
96
+ end
97
+
98
+ # The serial bus, whose presence decides what CIA 2's port A reads,
99
+ # and a true 1541 on it.
100
+ def save_serial_bus(out)
101
+ out.boolean(!@iec_bus.nil?).boolean(!@drive1541.nil?)
102
+ return unless @drive1541
103
+
104
+ out.int(@drive1541.device)
105
+ @drive1541.save_state(out)
106
+ end
107
+
108
+ def load_serial_bus(input)
109
+ iec_bus if input.boolean?
110
+ return detach_drive1541 unless input.boolean?
111
+
112
+ device = input.int
113
+ detach_drive1541 if @drive1541 && @drive1541.device != device
114
+ attach_drive1541(Drive1541.new(device:)) unless @drive1541
115
+ @drive1541.load_state(input)
116
+ end
117
+
118
+ # The REU, which the setup says the machine has, and whether it has
119
+ # asked for the bus. Its IRQ line reaches the CPU as it was.
120
+ def save_reu(out)
121
+ @reu&.save_state(out)
122
+ out.boolean(@dma)
123
+ end
124
+
125
+ def load_reu(input)
126
+ @reu&.load_state(input)
127
+ @reu_irq = @reu ? @reu.irq? : false
128
+ @dma = input.boolean?
129
+ end
130
+ end
131
+ end
132
+ end