badline 0.5.1 → 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 (129) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +58 -0
  4. data/exe/badline-ruby +2 -1
  5. data/lib/badline/address_bus/saved_state.rb +72 -0
  6. data/lib/badline/address_bus.rb +1 -0
  7. data/lib/badline/audio/stream.rb +6 -0
  8. data/lib/badline/cartridge/action_replay.rb +15 -0
  9. data/lib/badline/cartridge/comal80.rb +8 -0
  10. data/lib/badline/cartridge/dinamic.rb +8 -0
  11. data/lib/badline/cartridge/easy_flash.rb +20 -1
  12. data/lib/badline/cartridge/epyx_fastload.rb +8 -0
  13. data/lib/badline/cartridge/final_cartridge3.rb +12 -0
  14. data/lib/badline/cartridge/flash.rb +41 -0
  15. data/lib/badline/cartridge/fun_play.rb +8 -0
  16. data/lib/badline/cartridge/g_mod2.rb +12 -0
  17. data/lib/badline/cartridge/game_system.rb +8 -0
  18. data/lib/badline/cartridge/geo_ram.rb +16 -0
  19. data/lib/badline/cartridge/magic_desk.rb +8 -0
  20. data/lib/badline/cartridge/ocean.rb +8 -0
  21. data/lib/badline/cartridge/pagefox.rb +12 -0
  22. data/lib/badline/cartridge/retro_replay.rb +44 -1
  23. data/lib/badline/cartridge/rgcd.rb +10 -0
  24. data/lib/badline/cartridge/super_games.rb +10 -0
  25. data/lib/badline/cartridge/zaxxon.rb +16 -0
  26. data/lib/badline/cartridge.rb +72 -0
  27. data/lib/badline/cia/interrupt_register.rb +17 -0
  28. data/lib/badline/cia/serial.rb +44 -0
  29. data/lib/badline/cia/state.rb +47 -0
  30. data/lib/badline/cia/timer.rb +33 -0
  31. data/lib/badline/cia.rb +8 -0
  32. data/lib/badline/computer/attachments.rb +132 -0
  33. data/lib/badline/computer/saved_state.rb +116 -0
  34. data/lib/badline/computer.rb +7 -30
  35. data/lib/badline/cpu/microcode.rb +8 -0
  36. data/lib/badline/cpu/saved_state.rb +115 -0
  37. data/lib/badline/cpu.rb +2 -0
  38. data/lib/badline/datasette.rb +27 -0
  39. data/lib/badline/drive1541/bus.rb +13 -0
  40. data/lib/badline/drive1541/disk.rb +7 -2
  41. data/lib/badline/drive1541/disk_state.rb +80 -0
  42. data/lib/badline/drive1541/mechanism.rb +2 -0
  43. data/lib/badline/drive1541/mechanism_state.rb +67 -0
  44. data/lib/badline/drive1541/rotation.rb +10 -0
  45. data/lib/badline/drive1541/serial_port.rb +10 -0
  46. data/lib/badline/drive1541.rb +34 -0
  47. data/lib/badline/gui/application.rb +41 -14
  48. data/lib/badline/gui/gamepads.rb +7 -0
  49. data/lib/badline/gui/snapshots.rb +67 -0
  50. data/lib/badline/gui.rb +1 -0
  51. data/lib/badline/kernal_trap/channel.rb +27 -0
  52. data/lib/badline/kernal_trap/drive/channels.rb +25 -0
  53. data/lib/badline/kernal_trap/drive/memory.rb +9 -0
  54. data/lib/badline/kernal_trap/drive/status.rb +8 -0
  55. data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
  56. data/lib/badline/kernal_trap/drive.rb +19 -0
  57. data/lib/badline/kernal_trap/save.rb +8 -0
  58. data/lib/badline/kernal_trap/serial.rb +16 -0
  59. data/lib/badline/memory.rb +8 -0
  60. data/lib/badline/options.rb +3 -2
  61. data/lib/badline/ram_expansion/plus256k.rb +16 -0
  62. data/lib/badline/ram_expansion/plus60k.rb +12 -0
  63. data/lib/badline/ram_expansion/unexpanded.rb +6 -0
  64. data/lib/badline/reu/dma.rb +36 -0
  65. data/lib/badline/reu/ram.rb +15 -0
  66. data/lib/badline/reu/saved_state.rb +52 -0
  67. data/lib/badline/reu.rb +4 -0
  68. data/lib/badline/sdl/events.rb +2 -0
  69. data/lib/badline/sid/envelope/state.rb +68 -0
  70. data/lib/badline/sid/envelope.rb +7 -0
  71. data/lib/badline/sid/filter.rb +38 -0
  72. data/lib/badline/sid/voice.rb +10 -0
  73. data/lib/badline/sid/waveform/state.rb +66 -0
  74. data/lib/badline/sid/waveform.rb +2 -0
  75. data/lib/badline/sid.rb +34 -0
  76. data/lib/badline/snapshot/container.rb +117 -0
  77. data/lib/badline/snapshot/fields.rb +54 -0
  78. data/lib/badline/snapshot/image.rb +70 -0
  79. data/lib/badline/snapshot/machine_state.rb +77 -0
  80. data/lib/badline/snapshot/setup.rb +58 -0
  81. data/lib/badline/snapshot/state.rb +178 -0
  82. data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
  83. data/lib/badline/snapshot/vice/cias.rb +89 -0
  84. data/lib/badline/snapshot/vice/cias_import.rb +70 -0
  85. data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
  86. data/lib/badline/snapshot/vice/peripherals.rb +47 -0
  87. data/lib/badline/snapshot/vice/restore.rb +125 -0
  88. data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
  89. data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
  90. data/lib/badline/snapshot/vice/vicii.rb +119 -0
  91. data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
  92. data/lib/badline/snapshot/vice.rb +74 -0
  93. data/lib/badline/snapshot.rb +52 -0
  94. data/lib/badline/storage/crt_file.rb +9 -3
  95. data/lib/badline/storage/d64_image.rb +2 -0
  96. data/lib/badline/storage/d71_image.rb +2 -0
  97. data/lib/badline/storage/d81_image.rb +2 -0
  98. data/lib/badline/storage/disk_image.rb +11 -2
  99. data/lib/badline/storage/host_directory.rb +5 -0
  100. data/lib/badline/storage/t64.rb +6 -0
  101. data/lib/badline/storage/tap.rb +10 -1
  102. data/lib/badline/storage/unavailable.rb +20 -0
  103. data/lib/badline/storage.rb +34 -0
  104. data/lib/badline/time_of_day.rb +56 -0
  105. data/lib/badline/version.rb +1 -1
  106. data/lib/badline/via/control_lines.rb +15 -0
  107. data/lib/badline/via/interrupt_register.rb +9 -0
  108. data/lib/badline/via/shift_register.rb +13 -0
  109. data/lib/badline/via/state.rb +42 -0
  110. data/lib/badline/via/timer1.rb +13 -0
  111. data/lib/badline/via/timer2.rb +18 -0
  112. data/lib/badline/via.rb +2 -0
  113. data/lib/badline/vic/border_mask.rb +10 -0
  114. data/lib/badline/vic/collisions.rb +11 -0
  115. data/lib/badline/vic/color_patches.rb +11 -0
  116. data/lib/badline/vic/display_state.rb +29 -1
  117. data/lib/badline/vic/graphics_shifter.rb +26 -0
  118. data/lib/badline/vic/register_log.rb +18 -0
  119. data/lib/badline/vic/registers.rb +13 -2
  120. data/lib/badline/vic/sequencer.rb +12 -0
  121. data/lib/badline/vic/sequencer_state.rb +89 -0
  122. data/lib/badline/vic/sprite/internal_bus.rb +11 -0
  123. data/lib/badline/vic/sprite/saved_state.rb +64 -0
  124. data/lib/badline/vic/sprite.rb +2 -0
  125. data/lib/badline/vic/sprites.rb +3 -0
  126. data/lib/badline/vic/sprites_state.rb +57 -0
  127. data/lib/badline/vic.rb +94 -10
  128. data/lib/badline.rb +1 -0
  129. metadata +35 -1
@@ -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
@@ -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
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Computer
5
+ # The keyword arguments Computer.new takes to build the machine a
6
+ # State from #snapshot was taken of.
7
+ def self.setup(state)
8
+ input = Snapshot::StateReader.new(state)
9
+ input.marker("COMPUTER")
10
+ input.check_stamp
11
+ Snapshot::Setup.read(input)
12
+ end
13
+
14
+ # A new machine, built as the one a State from #snapshot was taken of,
15
+ # at that state. `detached` builds it without the host's files
16
+ # (Snapshot::StateReader).
17
+ def self.restored(state, detached: false) = setup(state).build.apply_state(state, detached:)
18
+
19
+ # The whole machine's state for a snapshot: the machine's own latches,
20
+ # the bus and its chips, the CPU, and what is attached to it
21
+ # (Attachments).
22
+ module SavedState
23
+ # How many more on_init handlers the machine a State was taken of had
24
+ # yet to run than this one has: they are the host's blocks, which a
25
+ # State can't hold. Nil until the machine is restored.
26
+ attr_reader :init_handlers_lost
27
+
28
+ # The machine's whole state, in memory. #restore takes the machine, or
29
+ # another built with the same chip models, region and RAM expansion,
30
+ # back to it. The cartridge, the disk in device 8, a true 1541 and its
31
+ # disk and the tape go in the state too. What the host holds stays
32
+ # out: the keyboard, the joysticks and the pot devices, the traps and
33
+ # callbacks, whether the display renders and whether the SID records.
34
+ def snapshot
35
+ out = Snapshot::StateWriter.new
36
+ save_state(out)
37
+ out.state
38
+ end
39
+
40
+ # Takes the machine back to a State from #snapshot. The machine keeps
41
+ # its chips, its traps and its callbacks, and puts in the cartridge,
42
+ # drives and tape the state has, taking out any it hasn't. Raises
43
+ # Snapshot::FormatError for a state of a machine built another way.
44
+ # The state goes into a new machine first, so one that fails leaves
45
+ # this machine as it was.
46
+ def restore(state)
47
+ check_setup(Computer.setup(state))
48
+ Computer.restored(state)
49
+ apply_state(state)
50
+ end
51
+
52
+ # Puts a State into the machine without trying it on a new one
53
+ # first, for a machine built for it (Computer.restored): a state that
54
+ # fails leaves the machine part restored. `detached` restores it
55
+ # without the host's files (Snapshot::StateReader).
56
+ def apply_state(state, detached: false)
57
+ input = Snapshot::StateReader.new(state, detached:)
58
+ load_state(input)
59
+ raise Snapshot::FormatError, "the state goes on past the machine" unless input.finished?
60
+
61
+ self
62
+ end
63
+
64
+ # Sets the machine's cycle count, for a snapshot from VICE, which
65
+ # counts from its own clock.
66
+ def resume_at(cycles)
67
+ @cycles = cycles
68
+ end
69
+
70
+ def save_state(out)
71
+ out.marker("COMPUTER").stamp
72
+ Snapshot::Setup.of(address_bus).write(out)
73
+ out.int(@cycles).boolean(@nmi_asserted).boolean(@cartridge_nmi).boolean(@restore_pulse).boolean(@freezing)
74
+ out.int(@freeze_writes).boolean(!@pending_keys.nil?)
75
+ out.ints(@pending_keys) if @pending_keys
76
+ out.int(booting? ? @init_handlers.length : 0)
77
+ save_cartridge(out)
78
+ address_bus.save_state(out)
79
+ @cpu.save_state(out)
80
+ save_trap_drive(out)
81
+ save_serial_bus(out)
82
+ save_reu(out)
83
+ end
84
+
85
+ def load_state(input)
86
+ input.marker("COMPUTER")
87
+ input.check_stamp
88
+ check_setup(Snapshot::Setup.read(input))
89
+ @cycles = input.int
90
+ @nmi_asserted = input.boolean?
91
+ @cartridge_nmi = input.boolean?
92
+ @restore_pulse = input.boolean?
93
+ @freezing = input.boolean?
94
+ @freeze_writes = input.int
95
+ @pending_keys = nil
96
+ @pending_keys = input.ints if input.boolean?
97
+ @init_handlers_lost = [input.int - @init_handlers.length, 0].max
98
+ load_cartridge(input)
99
+ address_bus.load_state(input)
100
+ @cpu.load_state(input)
101
+ load_trap_drive(input)
102
+ load_serial_bus(input)
103
+ load_reu(input)
104
+ end
105
+
106
+ private
107
+
108
+ def check_setup(setup)
109
+ ours = Snapshot::Setup.of(address_bus)
110
+ return if setup == ours
111
+
112
+ raise Snapshot::FormatError, "the state is of a machine with #{setup}, not #{ours}"
113
+ end
114
+ end
115
+ end
116
+ end
@@ -1,9 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/computer/attachments"
4
+ require "badline/computer/saved_state"
5
+
3
6
  module Badline
4
7
  class Computer
5
8
  include IntegerHelper
6
9
  include KeyboardBuffer
10
+ include Attachments
11
+ include SavedState
7
12
 
8
13
  attr_reader :address_bus, :cpu, :cycles, :drive1541
9
14
 
@@ -59,6 +64,7 @@ module Badline
59
64
  @pending_keys = nil
60
65
  @drive = nil
61
66
  @serial_trap = nil
67
+ @save_trap = nil
62
68
  @drive1541 = nil
63
69
  @iec_bus = nil
64
70
  @reu = reu ? plug_reu(reu) : nil
@@ -95,9 +101,7 @@ module Badline
95
101
  end
96
102
 
97
103
  def attach_cartridge(cartridge)
98
- cartridge.clock = -> { @cycles }
99
- cartridge.on_nmi_change { |level| @cartridge_nmi = level }
100
- address_bus.attach_cartridge(cartridge)
104
+ connect_cartridge(cartridge)
101
105
  power_cycle!
102
106
  end
103
107
 
@@ -151,33 +155,6 @@ module Badline
151
155
  @restore_pulse = true
152
156
  end
153
157
 
154
- # Puts the storage in device 8. Mounting again swaps the disk at any
155
- # point while the machine runs: the drive keeps its RAM, which only a
156
- # drive reset clears, and its status.
157
- def mount(storage)
158
- return @drive.insert(storage) if @drive
159
-
160
- @drive = KernalTrap::Drive.new(storage)
161
- load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
162
- cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
163
- @serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive, device: serial_trap_device).install
164
- save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
165
- cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
166
- end
167
-
168
- # Takes device 8's mounted storage out, and with it the LOAD, SAVE and
169
- # serial traps, so the KERNAL's routines go out over the serial bus.
170
- # Mounting again starts a new drive, with its RAM cleared.
171
- def unmount
172
- return unless @drive
173
-
174
- cpu.remove_trap(KernalTrap::Load::ADDRESS)
175
- cpu.remove_trap(KernalTrap::Save::ADDRESS)
176
- @serial_trap.device = nil
177
- @serial_trap = nil
178
- @drive = nil
179
- end
180
-
181
158
  # Plugs in a Drive1541, which then runs alongside the C64 on its own
182
159
  # clock and talks to it over the serial bus. The serial traps stop
183
160
  # answering the drive's device number, so the KERNAL's TALK, LISTEN and
@@ -165,5 +165,13 @@ module Badline
165
165
  INTERRUPT_WRITES = Microcode.write_mask(INTERRUPT_PLAN)
166
166
  JAMMED_WRITES = Microcode.write_mask(JAMMED_PLAN)
167
167
  MICROCODE = Microcode.table
168
+
169
+ # The instruction methods, in a fixed order a snapshot names them by.
170
+ OPERATIONS = MICROCODE.map(&:operation).uniq.freeze
171
+
172
+ # The plans a snapshot names that aren't an opcode's.
173
+ FETCH = -1
174
+ INTERRUPT = -2
175
+ JAMMED = -3
168
176
  end
169
177
  end