badline 0.5.1 → 0.6.1

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 (173) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/CLAUDE.md +10 -9
  4. data/README.md +82 -10
  5. data/doc/pinned-behaviour.md +131 -30
  6. data/exe/badline-ruby +20 -23
  7. data/lib/badline/address_bus/saved_state.rb +72 -0
  8. data/lib/badline/address_bus.rb +11 -1
  9. data/lib/badline/audio/cli.rb +18 -5
  10. data/lib/badline/audio/console.rb +2 -1
  11. data/lib/badline/audio.rb +0 -3
  12. data/lib/badline/cartridge/action_replay.rb +15 -0
  13. data/lib/badline/cartridge/comal80.rb +8 -0
  14. data/lib/badline/cartridge/dinamic.rb +8 -0
  15. data/lib/badline/cartridge/easy_flash.rb +20 -1
  16. data/lib/badline/cartridge/epyx_fastload.rb +8 -0
  17. data/lib/badline/cartridge/final_cartridge3.rb +12 -0
  18. data/lib/badline/cartridge/flash.rb +41 -0
  19. data/lib/badline/cartridge/fun_play.rb +8 -0
  20. data/lib/badline/cartridge/g_mod2.rb +12 -0
  21. data/lib/badline/cartridge/game_system.rb +8 -0
  22. data/lib/badline/cartridge/geo_ram.rb +16 -0
  23. data/lib/badline/cartridge/magic_desk.rb +8 -0
  24. data/lib/badline/cartridge/ocean.rb +8 -0
  25. data/lib/badline/cartridge/pagefox.rb +12 -0
  26. data/lib/badline/cartridge/retro_replay.rb +44 -1
  27. data/lib/badline/cartridge/rgcd.rb +10 -0
  28. data/lib/badline/cartridge/super_games.rb +10 -0
  29. data/lib/badline/cartridge/zaxxon.rb +16 -0
  30. data/lib/badline/cartridge.rb +72 -0
  31. data/lib/badline/cia/interrupt_register.rb +17 -0
  32. data/lib/badline/cia/serial.rb +44 -0
  33. data/lib/badline/cia/state.rb +47 -0
  34. data/lib/badline/cia/timer.rb +33 -0
  35. data/lib/badline/cia.rb +8 -0
  36. data/lib/badline/computer/attachments.rb +132 -0
  37. data/lib/badline/computer/saved_state.rb +116 -0
  38. data/lib/badline/computer.rb +13 -33
  39. data/lib/badline/cpu/boundary.rb +54 -0
  40. data/lib/badline/cpu/core.rb +165 -0
  41. data/lib/badline/cpu/microcode.rb +8 -0
  42. data/lib/badline/cpu/saved_state.rb +115 -0
  43. data/lib/badline/cpu.rb +3 -212
  44. data/lib/badline/datasette.rb +27 -0
  45. data/lib/badline/drive1541/bus.rb +13 -0
  46. data/lib/badline/drive1541/cpu.rb +29 -0
  47. data/lib/badline/drive1541/disk.rb +7 -2
  48. data/lib/badline/drive1541/disk_state.rb +80 -0
  49. data/lib/badline/drive1541/mechanism.rb +2 -0
  50. data/lib/badline/drive1541/mechanism_state.rb +67 -0
  51. data/lib/badline/drive1541/rotation.rb +10 -0
  52. data/lib/badline/drive1541/serial_port.rb +10 -0
  53. data/lib/badline/drive1541.rb +35 -0
  54. data/lib/badline/ffi.rb +157 -0
  55. data/lib/badline/frontend/app.rb +225 -0
  56. data/lib/badline/frontend/audio_sink.rb +72 -0
  57. data/lib/badline/frontend/boot.rb +52 -0
  58. data/lib/badline/frontend/controls.rb +163 -0
  59. data/lib/badline/frontend/drive_led.rb +51 -0
  60. data/lib/badline/frontend/frame_rate.rb +83 -0
  61. data/lib/badline/frontend/gamepads.rb +76 -0
  62. data/lib/badline/frontend/keys.rb +48 -0
  63. data/lib/badline/frontend/pacer.rb +100 -0
  64. data/lib/badline/frontend/pad_port.rb +61 -0
  65. data/lib/badline/frontend/screen.rb +65 -0
  66. data/lib/badline/frontend/screenshot.rb +22 -0
  67. data/lib/badline/frontend/snapshots.rb +60 -0
  68. data/lib/badline/frontend/sound.rb +153 -0
  69. data/lib/badline/frontend/timeline.rb +171 -0
  70. data/lib/badline/frontend.rb +18 -0
  71. data/lib/badline/kernal_trap/channel.rb +27 -0
  72. data/lib/badline/kernal_trap/drive/channels.rb +25 -0
  73. data/lib/badline/kernal_trap/drive/memory.rb +9 -0
  74. data/lib/badline/kernal_trap/drive/status.rb +8 -0
  75. data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
  76. data/lib/badline/kernal_trap/drive.rb +19 -0
  77. data/lib/badline/kernal_trap/save.rb +8 -0
  78. data/lib/badline/kernal_trap/serial.rb +16 -0
  79. data/lib/badline/memory.rb +8 -0
  80. data/lib/badline/options/event.rb +94 -0
  81. data/lib/badline/options/help.rb +78 -0
  82. data/lib/badline/options/option.rb +29 -0
  83. data/lib/badline/options/table.rb +62 -0
  84. data/lib/badline/options.rb +181 -144
  85. data/lib/badline/ram_expansion/plus256k.rb +16 -0
  86. data/lib/badline/ram_expansion/plus60k.rb +12 -0
  87. data/lib/badline/ram_expansion/unexpanded.rb +6 -0
  88. data/lib/badline/reu/dma/saved_state.rb +50 -0
  89. data/lib/badline/reu/dma.rb +56 -11
  90. data/lib/badline/reu/ram.rb +15 -0
  91. data/lib/badline/reu/saved_state.rb +53 -0
  92. data/lib/badline/reu.rb +23 -10
  93. data/lib/badline/sdl.rb +123 -51
  94. data/lib/badline/sid/envelope/state.rb +68 -0
  95. data/lib/badline/sid/envelope.rb +7 -0
  96. data/lib/badline/sid/filter.rb +38 -0
  97. data/lib/badline/sid/voice.rb +10 -0
  98. data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
  99. data/lib/badline/sid/waveform/state.rb +66 -0
  100. data/lib/badline/sid/waveform.rb +4 -3
  101. data/lib/badline/sid.rb +34 -0
  102. data/lib/badline/snapshot/container.rb +117 -0
  103. data/lib/badline/snapshot/fields.rb +54 -0
  104. data/lib/badline/snapshot/image.rb +70 -0
  105. data/lib/badline/snapshot/machine_state.rb +77 -0
  106. data/lib/badline/snapshot/setup.rb +58 -0
  107. data/lib/badline/snapshot/state.rb +178 -0
  108. data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
  109. data/lib/badline/snapshot/vice/cias.rb +89 -0
  110. data/lib/badline/snapshot/vice/cias_import.rb +70 -0
  111. data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
  112. data/lib/badline/snapshot/vice/peripherals.rb +47 -0
  113. data/lib/badline/snapshot/vice/restore.rb +125 -0
  114. data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
  115. data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
  116. data/lib/badline/snapshot/vice/vicii.rb +119 -0
  117. data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
  118. data/lib/badline/snapshot/vice.rb +74 -0
  119. data/lib/badline/snapshot.rb +52 -0
  120. data/lib/badline/storage/crt_file.rb +9 -3
  121. data/lib/badline/storage/d64_image.rb +2 -0
  122. data/lib/badline/storage/d71_image.rb +2 -0
  123. data/lib/badline/storage/d81_image.rb +2 -0
  124. data/lib/badline/storage/disk_image.rb +11 -2
  125. data/lib/badline/storage/host_directory.rb +5 -0
  126. data/lib/badline/storage/t64.rb +6 -0
  127. data/lib/badline/storage/tap.rb +10 -1
  128. data/lib/badline/storage/unavailable.rb +20 -0
  129. data/lib/badline/storage.rb +34 -0
  130. data/lib/badline/time_of_day.rb +56 -0
  131. data/lib/badline/version.rb +1 -1
  132. data/lib/badline/via/control_lines.rb +15 -0
  133. data/lib/badline/via/interrupt_register.rb +9 -0
  134. data/lib/badline/via/shift_register.rb +13 -0
  135. data/lib/badline/via/state.rb +42 -0
  136. data/lib/badline/via/timer1.rb +13 -0
  137. data/lib/badline/via/timer2.rb +18 -0
  138. data/lib/badline/via.rb +2 -0
  139. data/lib/badline/vic/bank.rb +21 -0
  140. data/lib/badline/vic/border_mask.rb +10 -0
  141. data/lib/badline/vic/collisions.rb +11 -0
  142. data/lib/badline/vic/color_patches.rb +11 -0
  143. data/lib/badline/vic/display_state.rb +29 -1
  144. data/lib/badline/vic/graphics_shifter.rb +26 -0
  145. data/lib/badline/vic/register_log.rb +18 -0
  146. data/lib/badline/vic/registers.rb +13 -2
  147. data/lib/badline/vic/sequencer.rb +12 -0
  148. data/lib/badline/vic/sequencer_state.rb +89 -0
  149. data/lib/badline/vic/sprite/internal_bus.rb +11 -0
  150. data/lib/badline/vic/sprite/saved_state.rb +64 -0
  151. data/lib/badline/vic/sprite.rb +2 -0
  152. data/lib/badline/vic/sprites.rb +3 -0
  153. data/lib/badline/vic/sprites_state.rb +57 -0
  154. data/lib/badline/vic.rb +109 -16
  155. data/lib/badline.rb +1 -0
  156. metadata +59 -18
  157. data/.claude/hooks/session-start.sh +0 -57
  158. data/.claude/settings.json +0 -14
  159. data/lib/badline/audio/sdl_sink.rb +0 -70
  160. data/lib/badline/audio/stream.rb +0 -117
  161. data/lib/badline/gui/application.rb +0 -216
  162. data/lib/badline/gui/drive_led_pane.rb +0 -29
  163. data/lib/badline/gui/gamepads.rb +0 -97
  164. data/lib/badline/gui/joy_map.rb +0 -27
  165. data/lib/badline/gui/key_map.rb +0 -39
  166. data/lib/badline/gui/palette.rb +0 -35
  167. data/lib/badline/gui/pane.rb +0 -36
  168. data/lib/badline/gui/screen_pane.rb +0 -46
  169. data/lib/badline/gui/window.rb +0 -61
  170. data/lib/badline/gui.rb +0 -13
  171. data/lib/badline/sdl/events.rb +0 -47
  172. data/lib/badline/sdl/functions.rb +0 -74
  173. data/vendor/.gitkeep +0 -0
@@ -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
@@ -249,12 +226,15 @@ module Badline
249
226
  @cpu.pending_write? || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
250
227
  end
251
228
 
229
+ # The REU's /DMA line takes the bus from the CPU from the cycle after
230
+ # it asks for it, even while it waits for BA, but RDY only halts the
231
+ # CPU on a read, so a write it makes then goes nowhere.
252
232
  def dma_cycle!
253
233
  writing = @cpu.pending_write?
254
- @reu.dma_cycle!(@vic.reu_ba_low?, writing)
255
- return @cpu.stall! if @reu.holds_bus?
234
+ @reu.dma_cycle!(@vic.ba_low?)
235
+ return writing ? @address_bus.cycle_cpu_off_bus(@cpu) : @cpu.stall! if @reu.dma?
256
236
 
257
- @dma = @reu.dma?
237
+ @dma = false
258
238
  writing || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
259
239
  end
260
240
 
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CPU
5
+ # The steps at an instruction boundary: the opcode fetch that starts
6
+ # an instruction, and the ends of one.
7
+ module Boundary
8
+ private
9
+
10
+ # The first cycle of every instruction. Starts an interrupt if one was
11
+ # pending when the previous instruction ended. Otherwise reads the
12
+ # opcode and switches to its plan.
13
+ def op_fetch
14
+ return start_interrupt if @boundary_nmi || @boundary_irq
15
+
16
+ run_traps
17
+ opcode = @memory.peek(@program_counter)
18
+ log(opcode) if @debug
19
+ micro = MICROCODE[opcode]
20
+ @operation = micro.operation
21
+ @plan = micro.plan
22
+ @writes = micro.writes
23
+ @optional_dummy = micro.optional_dummy
24
+ @index = 1
25
+ @boundary_crossed = false
26
+ @program_counter = (@program_counter + 1) & 0xffff
27
+ end
28
+
29
+ def end_instruction
30
+ @instructions += 1
31
+ end_sequence
32
+ end
33
+
34
+ # Makes the next cycle an opcode fetch, and saves the pending interrupt
35
+ # flags for op_fetch. The next cycle polls before op_fetch runs, which
36
+ # updates the live flags.
37
+ def end_sequence
38
+ @plan = FETCH_PLAN
39
+ @writes = FETCH_WRITES
40
+ @index = 0
41
+ @boundary_irq = @irq_pending
42
+ @boundary_nmi = @nmi_pending
43
+ end
44
+
45
+ def log(opcode)
46
+ instruction = Instruction.find(opcode)
47
+ puts(
48
+ "#{@cycles}: PC: #{format16(@program_counter)} - " \
49
+ "#{instruction.name.upcase} #{instruction.addressing_mode}"
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end