badline 0.11.0 → 0.12.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/CLAUDE.md +24 -6
  4. data/CONTRIBUTING.md +15 -9
  5. data/README.md +16 -5
  6. data/doc/media.md +28 -1
  7. data/doc/pinned-behaviour.md +31 -1
  8. data/exe/badline-ruby +1 -1
  9. data/lib/badline/c128/bus.rb +4 -17
  10. data/lib/badline/c128/drives.rb +108 -0
  11. data/lib/badline/c128/fast_serial.rb +63 -0
  12. data/lib/badline/c128/io_pages.rb +35 -0
  13. data/lib/badline/c128/mmu.rb +24 -7
  14. data/lib/badline/c128/modes.rb +17 -4
  15. data/lib/badline/c128/saved_state.rb +23 -0
  16. data/lib/badline/c128/z80.rb +49 -0
  17. data/lib/badline/c128/z80_pages.rb +118 -0
  18. data/lib/badline/c128/z80_turns.rb +106 -0
  19. data/lib/badline/c128.rb +25 -26
  20. data/lib/badline/cartridge/action_replay.rb +2 -2
  21. data/lib/badline/cartridge/comal80.rb +1 -7
  22. data/lib/badline/cartridge/dinamic.rb +0 -8
  23. data/lib/badline/cartridge/final_cartridge3.rb +6 -6
  24. data/lib/badline/cartridge/fun_play.rb +0 -8
  25. data/lib/badline/cartridge/game_system.rb +0 -8
  26. data/lib/badline/cartridge/kcs_power.rb +10 -7
  27. data/lib/badline/cartridge/mach5.rb +2 -4
  28. data/lib/badline/cartridge/magic_desk.rb +0 -8
  29. data/lib/badline/cartridge/ocean.rb +0 -8
  30. data/lib/badline/cartridge/pagefox.rb +2 -2
  31. data/lib/badline/cartridge/retro_replay.rb +7 -6
  32. data/lib/badline/cartridge/rex_utility.rb +2 -6
  33. data/lib/badline/cartridge/rgcd.rb +2 -2
  34. data/lib/badline/cartridge/simons_basic.rb +0 -5
  35. data/lib/badline/cartridge/super_games.rb +4 -2
  36. data/lib/badline/cartridge/westermann.rb +2 -6
  37. data/lib/badline/cartridge.rb +37 -9
  38. data/lib/badline/cia/fast_forward.rb +60 -0
  39. data/lib/badline/cia/serial.rb +12 -0
  40. data/lib/badline/cia/timer.rb +39 -0
  41. data/lib/badline/cia.rb +10 -5
  42. data/lib/badline/computer/attachments.rb +4 -50
  43. data/lib/badline/computer/true_drives.rb +80 -0
  44. data/lib/badline/computer.rb +4 -0
  45. data/lib/badline/core.rb +2 -0
  46. data/lib/badline/drive/core.rb +147 -0
  47. data/lib/badline/drive/fast_serial.rb +73 -0
  48. data/lib/badline/{drive1541 → drive}/idle.rb +18 -29
  49. data/lib/badline/drive/idle_cpu.rb +28 -0
  50. data/lib/badline/{drive1541 → drive}/orbit.rb +3 -5
  51. data/lib/badline/drive/serial_lines.rb +73 -0
  52. data/lib/badline/{drive1541 → drive}/sleep.rb +4 -5
  53. data/lib/badline/drive/vias.rb +84 -0
  54. data/lib/badline/drive/watch.rb +150 -0
  55. data/lib/badline/drive.rb +11 -0
  56. data/lib/badline/drive1541/bus.rb +5 -129
  57. data/lib/badline/drive1541/byte_ready.rb +48 -0
  58. data/lib/badline/drive1541/cpu.rb +1 -19
  59. data/lib/badline/drive1541/disk.rb +77 -20
  60. data/lib/badline/drive1541/disk_state.rb +7 -2
  61. data/lib/badline/drive1541/disk_via.rb +3 -0
  62. data/lib/badline/drive1541/mechanism.rb +48 -14
  63. data/lib/badline/drive1541/mechanism_state.rb +5 -4
  64. data/lib/badline/drive1541/rotation.rb +5 -5
  65. data/lib/badline/drive1541/serial_port.rb +4 -60
  66. data/lib/badline/drive1541.rb +14 -172
  67. data/lib/badline/drive1571/bus.rb +111 -0
  68. data/lib/badline/drive1571/cpu.rb +11 -0
  69. data/lib/badline/drive1571/serial_port.rb +37 -0
  70. data/lib/badline/drive1571/serial_via.rb +25 -0
  71. data/lib/badline/drive1571/wd1770.rb +120 -0
  72. data/lib/badline/drive1571.rb +175 -0
  73. data/lib/badline/drive1581/bus.rb +137 -0
  74. data/lib/badline/drive1581/cpu.rb +11 -0
  75. data/lib/badline/drive1581/disk.rb +135 -0
  76. data/lib/badline/drive1581/disk_state.rb +87 -0
  77. data/lib/badline/drive1581/mechanism.rb +117 -0
  78. data/lib/badline/drive1581/serial_port.rb +52 -0
  79. data/lib/badline/drive1581/slot.rb +78 -0
  80. data/lib/badline/drive1581/track.rb +100 -0
  81. data/lib/badline/drive1581/track_bytes.rb +75 -0
  82. data/lib/badline/drive1581/wd1772/registers.rb +89 -0
  83. data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
  84. data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
  85. data/lib/badline/drive1581/wd1772/state.rb +66 -0
  86. data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
  87. data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
  88. data/lib/badline/drive1581/wd1772/turn.rb +73 -0
  89. data/lib/badline/drive1581/wd1772.rb +216 -0
  90. data/lib/badline/drive1581.rb +233 -0
  91. data/lib/badline/frontend/app.rb +2 -4
  92. data/lib/badline/frontend/audio_device.rb +67 -0
  93. data/lib/badline/frontend/audio_sink.rb +16 -50
  94. data/lib/badline/frontend/boot.rb +18 -10
  95. data/lib/badline/frontend/buttons.rb +7 -7
  96. data/lib/badline/frontend/drive_led.rb +1 -1
  97. data/lib/badline/frontend/keys.rb +33 -8
  98. data/lib/badline/frontend/media_slots.rb +42 -0
  99. data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
  100. data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
  101. data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
  102. data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
  103. data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +7 -9
  104. data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
  105. data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
  106. data/lib/badline/frontend/menu/theme.rb +18 -0
  107. data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
  108. data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
  109. data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
  110. data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
  111. data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
  112. data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
  113. data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
  114. data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
  115. data/lib/badline/frontend/player/theme.rb +18 -0
  116. data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
  117. data/lib/badline/frontend/sound.rb +19 -38
  118. data/lib/badline/frontend/timeline.rb +3 -14
  119. data/lib/badline/frontend.rb +24 -20
  120. data/lib/badline/iec_bus.rb +68 -6
  121. data/lib/badline/machine.rb +9 -6
  122. data/lib/badline/media/boot_disk.rb +50 -0
  123. data/lib/badline/media/not_disk.rb +8 -0
  124. data/lib/badline/media/true_drive.rb +48 -25
  125. data/lib/badline/media.rb +9 -3
  126. data/lib/badline/options/event.rb +2 -2
  127. data/lib/badline/options/family.rb +3 -9
  128. data/lib/badline/options/help.rb +3 -1
  129. data/lib/badline/options/table.rb +1 -1
  130. data/lib/badline/roms/README +18 -0
  131. data/lib/badline/roms/dos1571.rom +0 -0
  132. data/lib/badline/roms/dos1581.rom +0 -0
  133. data/lib/badline/sdl.rb +37 -20
  134. data/lib/badline/snapshot/state.rb +5 -4
  135. data/lib/badline/storage/d64_image.rb +3 -0
  136. data/lib/badline/storage/d71_image.rb +9 -4
  137. data/lib/badline/storage/g64_image.rb +7 -2
  138. data/lib/badline/storage/p00.rb +8 -0
  139. data/lib/badline/time_of_day.rb +9 -0
  140. data/lib/badline/version.rb +1 -1
  141. data/lib/badline/via/fast_forward.rb +3 -3
  142. data/lib/badline/vic20/attachments.rb +23 -3
  143. data/lib/badline/vic20/saved_state.rb +2 -0
  144. data/lib/badline/vic20.rb +3 -0
  145. data/lib/badline/z80/arithmetic.rb +117 -0
  146. data/lib/badline/z80/bit_page.rb +55 -0
  147. data/lib/badline/z80/block_io.rb +58 -0
  148. data/lib/badline/z80/block_transfers.rb +59 -0
  149. data/lib/badline/z80/bus_cycles.rb +111 -0
  150. data/lib/badline/z80/core.rb +113 -0
  151. data/lib/badline/z80/exchanges.rb +50 -0
  152. data/lib/badline/z80/extended_page.rb +89 -0
  153. data/lib/badline/z80/flags.rb +37 -0
  154. data/lib/badline/z80/interrupt_acceptance.rb +46 -0
  155. data/lib/badline/z80/jumps.rb +82 -0
  156. data/lib/badline/z80/loads.rb +98 -0
  157. data/lib/badline/z80/main_page.rb +134 -0
  158. data/lib/badline/z80/registers.rb +120 -0
  159. data/lib/badline/z80/saved_state.rb +69 -0
  160. data/lib/badline/z80/shifts.rb +64 -0
  161. data/lib/badline/z80.rb +11 -0
  162. metadata +89 -24
  163. /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
  164. /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
  165. /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
  166. /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
@@ -19,14 +19,6 @@ module Badline
19
19
 
20
20
  private
21
21
 
22
- def save_mapper(out)
23
- save_windows(out, [*@banks, EMPTY_BANK])
24
- end
25
-
26
- def load_mapper(input)
27
- load_windows(input, [*@banks, EMPTY_BANK])
28
- end
29
-
30
22
  # In 16K mode the selected bank shows through ROMH as well.
31
23
  def select_bank(number)
32
24
  @roml = bank(@banks, number)
@@ -30,12 +30,12 @@ module Badline
30
30
  private
31
31
 
32
32
  def save_mapper(out)
33
- save_windows(out, windows)
33
+ super
34
34
  @ram_banks.each { |ram| out.blob(ram.data) }
35
35
  end
36
36
 
37
37
  def load_mapper(input)
38
- load_windows(input, windows)
38
+ super
39
39
  @ram_banks.each { |ram| input.blob_into(ram.data) }
40
40
  end
41
41
 
@@ -90,6 +90,11 @@ module Badline
90
90
 
91
91
  attr_reader :flash
92
92
 
93
+ def self.from_jumpers(crt, input)
94
+ input.boolean?
95
+ new(crt, flash_jumper: input.boolean?, bank_jumper: input.boolean?)
96
+ end
97
+
93
98
  def initialize(crt, flash_jumper: false, bank_jumper: false)
94
99
  @nordic = crt.subtype == 1
95
100
  @flash_jumper = flash_jumper
@@ -131,9 +136,7 @@ module Badline
131
136
  end
132
137
  end
133
138
 
134
- def phi1_ultimax?
135
- @phi1_ultimax
136
- end
139
+ def phi1_ultimax? = @phi1_ultimax
137
140
 
138
141
  def ultimax_a000
139
142
  @isolated[ram_view_bank] if @nordic && @ram_at_a000 && @frozen
@@ -157,9 +160,7 @@ module Badline
157
160
 
158
161
  private
159
162
 
160
- def freeze_allowed?
161
- !@no_freeze && !@flash_jumper
162
- end
163
+ def freeze_allowed? = !@no_freeze && !@flash_jumper
163
164
 
164
165
  def status
165
166
  ((@bank & 0x03) << 3) | ((@bank & 0x04) << 5) | ((@bank & 0x08) << 2) | (@flash_jumper ? 0x01 : 0) |
@@ -10,16 +10,12 @@ module Badline
10
10
  end
11
11
 
12
12
  def peek(addr)
13
- if addr >= 0xdf00
14
- self.mode = (addr & 0xff) < 0xc0 ? :off : :rom8k
15
- changed!
16
- end
13
+ select_mode((addr & 0xff) < 0xc0 ? :off : :rom8k) if addr >= 0xdf00
17
14
  open_bus(addr)
18
15
  end
19
16
 
20
17
  def reset
21
- self.mode = :rom8k
22
- changed!
18
+ select_mode(:rom8k)
23
19
  end
24
20
 
25
21
  private
@@ -29,12 +29,12 @@ module Badline
29
29
  private
30
30
 
31
31
  def save_mapper(out)
32
- save_windows(out, [*@banks, EMPTY_BANK])
32
+ super
33
33
  out.boolean(@disabled)
34
34
  end
35
35
 
36
36
  def load_mapper(input)
37
- load_windows(input, [*@banks, EMPTY_BANK])
37
+ super
38
38
  @disabled = input.boolean?
39
39
  end
40
40
 
@@ -24,11 +24,6 @@ module Badline
24
24
 
25
25
  private
26
26
 
27
- def select_mode(mode)
28
- self.mode = mode
29
- changed!
30
- end
31
-
32
27
  def install_chips(chips)
33
28
  roml, romh = banks_from(chips)
34
29
  @roml = bank(roml, 0)
@@ -21,15 +21,17 @@ module Badline
21
21
  private
22
22
 
23
23
  def save_mapper(out)
24
- save_windows(out, [*@roml_banks, *@romh_banks, EMPTY_BANK])
24
+ super
25
25
  out.boolean(@locked)
26
26
  end
27
27
 
28
28
  def load_mapper(input)
29
- load_windows(input, [*@roml_banks, *@romh_banks, EMPTY_BANK])
29
+ super
30
30
  @locked = input.boolean?
31
31
  end
32
32
 
33
+ def windows = [*@roml_banks, *@romh_banks, EMPTY_BANK]
34
+
33
35
  def select(value)
34
36
  @roml = bank(@roml_banks, value & 0x03)
35
37
  @romh = bank(@romh_banks, value & 0x03)
@@ -9,16 +9,12 @@ module Badline
9
9
  end
10
10
 
11
11
  def peek(addr)
12
- if addr >= 0xdf00
13
- self.mode = :rom8k
14
- changed!
15
- end
12
+ select_mode(:rom8k) if addr >= 0xdf00
16
13
  open_bus(addr)
17
14
  end
18
15
 
19
16
  def reset
20
- self.mode = :rom16k
21
- changed!
17
+ select_mode(:rom16k)
22
18
  end
23
19
 
24
20
  private
@@ -63,11 +63,7 @@ module Badline
63
63
  end
64
64
 
65
65
  def from_crt(crt, **)
66
- mapper = HARDWARE_TYPES.fetch(crt.hardware_type) do
67
- raise UnsupportedTypeError,
68
- "Unsupported cartridge hardware type #{crt.hardware_type}"
69
- end
70
- mapper.new(crt, **)
66
+ mapper_for(crt).new(crt, **)
71
67
  end
72
68
 
73
69
  # A cartridge built afresh from what save_setup wrote, before
@@ -76,9 +72,21 @@ module Badline
76
72
  return GeoRAM.new(size: input.int) if input.int == GEO_RAM_SETUP
77
73
 
78
74
  crt = Storage::CRTFile.new(bytes: input.string)
79
- return RetroReplay.new(crt, flash_jumper: input.boolean?, bank_jumper: input.boolean?) if input.boolean?
75
+ mapper_for(crt).from_jumpers(crt, input)
76
+ end
80
77
 
81
- from_crt(crt)
78
+ # A cartridge from its image and what save_jumpers wrote: whether
79
+ # there are jumpers, then their settings on a mapper that has them.
80
+ def from_jumpers(crt, input)
81
+ input.boolean?
82
+ new(crt)
83
+ end
84
+
85
+ def mapper_for(crt)
86
+ HARDWARE_TYPES.fetch(crt.hardware_type) do
87
+ raise UnsupportedTypeError,
88
+ "Unsupported cartridge hardware type #{crt.hardware_type}"
89
+ end
82
90
  end
83
91
  end
84
92
 
@@ -95,6 +103,7 @@ module Badline
95
103
  @exrom = crt.exrom
96
104
  @game = crt.game
97
105
  @roml = @romh = nil
106
+ @banks = nil
98
107
  @on_change = nil
99
108
  @on_nmi_change = nil
100
109
  @nmi = false
@@ -193,9 +202,22 @@ module Badline
193
202
  out.boolean(false)
194
203
  end
195
204
 
196
- def save_mapper(_out); end
205
+ # The mapper's own state: where its windows point, for a mapper that
206
+ # moves them, and whatever a mapper adds after calling super.
207
+ def save_mapper(out)
208
+ save_windows(out, windows) if windows
209
+ end
197
210
 
198
- def load_mapper(_input); end
211
+ def load_mapper(input)
212
+ load_windows(input, windows) if windows
213
+ end
214
+
215
+ # The banks and views ROML and ROMH can point at, which a snapshot
216
+ # names them by: the mapper's banks, or nothing for a mapper whose
217
+ # windows stay where install_chips put them.
218
+ def windows
219
+ [*@banks, EMPTY_BANK] if @banks
220
+ end
199
221
 
200
222
  # Where the ROML and ROMH windows point, as their places in `windows`,
201
223
  # the banks and views the mapper picks them from, or -1 for none.
@@ -232,6 +254,12 @@ module Badline
232
254
  @exrom, @game = MODES.fetch(mode)
233
255
  end
234
256
 
257
+ # Sets the lines and tells the machine to map the cartridge again.
258
+ def select_mode(mode)
259
+ self.mode = mode
260
+ changed!
261
+ end
262
+
235
263
  def open_bus(addr)
236
264
  @open_bus ? @open_bus.peek(addr) : 0xff
237
265
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CIA
5
+ # Running a CIA a stretch of cycles at once, for Drive::Idle.
6
+ module FastForward
7
+ # quiet_cycles for a CIA whose counters will never set a flag.
8
+ QUIET = 1 << 40
9
+
10
+ # Whether fast_forward can run the CIA: timer B stopped with nothing
11
+ # in its pipeline, no interrupt or acknowledge on its way, CNT where it
12
+ # was, the TOD clock stopped, and timer A either stopped, or free
13
+ # running (Timer#free_running?) with its interrupt masked and the
14
+ # serial port an idle input. Drive::Idle sleeps the 1571's CIA through
15
+ # such cycles, which its DOS leaves timer A running in.
16
+ def quiet?
17
+ return false unless @tb.idle? && @icr.quiet && @tod.quiet? && cnt_steady?
18
+
19
+ @ta.idle? ? @serial.idle : timer_a_free_running?
20
+ end
21
+
22
+ # How many cycles the CIA can run with nothing but its counters
23
+ # moving: as quiet? allows, but with timer B free running too, up to
24
+ # the cycle before it underflows. The 1581's DOS leaves timer B
25
+ # interrupting every 10 ms.
26
+ def quiet_cycles
27
+ return 0 unless @icr.quiet && @tod.quiet? && cnt_steady? && timer_a_quiet?
28
+ return QUIET if @tb.idle?
29
+ return 0 unless @tb.free_running? && @control_b.value.nobits?(0x60)
30
+
31
+ @tb.counter - 1
32
+ end
33
+
34
+ # Runs +cycles+ cycles at once while quiet? or for quiet_cycles, as
35
+ # cycle! would: timer A's underflows flag in the ICR, and reach the
36
+ # serial port's underflow line.
37
+ def fast_forward(cycles)
38
+ @tod.fast_forward(cycles)
39
+ @tb.fast_forward(cycles) unless @tb.idle?
40
+ return if @ta.idle?
41
+
42
+ @icr.flag(:timer_a) if @ta.fast_forward(cycles).positive?
43
+ @serial.follow_underflow(@ta.underflowed)
44
+ end
45
+
46
+ private
47
+
48
+ def timer_a_quiet? = @ta.idle? ? @serial.idle : timer_a_free_running?
49
+
50
+ # Timer A free running with its interrupt masked, and the serial port
51
+ # an idle input it only clocks.
52
+ def timer_a_free_running?
53
+ @ta.free_running? && !@control_a.serial_mode? && @icr.mask.value.nobits?(0x01) && @serial.drained?
54
+ end
55
+
56
+ # Whether CNT holds the level the last cycle sampled.
57
+ def cnt_steady? = @serial.cnt == @cnt_high && !@cnt_rise
58
+ end
59
+ end
60
+ end
@@ -79,6 +79,18 @@ module Badline
79
79
  @control.serial_mode?
80
80
  end
81
81
 
82
+ # Whether nothing is counting down or in the delay lines, so only
83
+ # timer A's underflows reach the register.
84
+ def drained? = idle?
85
+
86
+ # Takes timer A's underflow line as cycle! would have left it, with
87
+ # +underflowed+ its level on the last cycle, for a CIA run without
88
+ # the register in use (CIA#fast_forward).
89
+ def follow_underflow(underflowed)
90
+ @underflow_high = underflowed
91
+ @idle = !underflowed
92
+ end
93
+
82
94
  def cnt_in=(level)
83
95
  @cnt_in = level
84
96
  @cnt = level unless output?
@@ -24,6 +24,32 @@ module Badline
24
24
  control.out_mode? ? @toggle : @underflowed
25
25
  end
26
26
 
27
+ # Whether the timer is stopped with nothing in its pipeline, so a
28
+ # cycle changes nothing.
29
+ def idle? = @empty && !started?
30
+
31
+ # Whether the timer counts ø2 in continuous mode with nothing pending
32
+ # but the reload after an underflow, so fast_forward can run it. A
33
+ # latch below 2 is left out.
34
+ def free_running?
35
+ started? && !@control.run_mode? && @control.value.nobits?(0x20) && @latch >= 2 && @pipe == 0b11 &&
36
+ (@load_delay | @oneshot_linger).zero? && (@settled || @reload)
37
+ end
38
+
39
+ # Runs a free-running timer +cycles+ cycles at once, as cycle! would:
40
+ # an underflow every latch + 1 cycles, each one followed by the
41
+ # reload. Returns the underflows.
42
+ def fast_forward(cycles)
43
+ return 0 if cycles.zero?
44
+ return after_underflow(cycles, 0) unless @settled
45
+
46
+ if cycles < @counter
47
+ @counter -= cycles
48
+ return 0
49
+ end
50
+ after_underflow(cycles - @counter, 1)
51
+ end
52
+
27
53
  def cycle!(feed)
28
54
  feed &&= @control.value & 0x01 != 0
29
55
  if @settled
@@ -98,6 +124,19 @@ module Badline
98
124
  @control.value & 0x01 != 0
99
125
  end
100
126
 
127
+ # Where fast_forward lands +cycles+ after an underflow, with
128
+ # +underflows+ underflows counted so far.
129
+ def after_underflow(cycles, underflows)
130
+ periods, rest = cycles.divmod(@latch + 1)
131
+ underflows += periods
132
+ @toggle = !@toggle if underflows.odd?
133
+ @underflowed = rest.zero?
134
+ @reload = rest.zero?
135
+ @counter = @latch - (rest.zero? ? 0 : rest - 1)
136
+ settle
137
+ underflows
138
+ end
139
+
101
140
  def enter
102
141
  @pipe = 0b10
103
142
  @empty = false
data/lib/badline/cia.rb CHANGED
@@ -4,12 +4,14 @@ require "badline/cia/interrupt_register"
4
4
  require "badline/cia/serial"
5
5
  require "badline/cia/timer"
6
6
  require "badline/cia/state"
7
+ require "badline/cia/fast_forward"
7
8
 
8
9
  module Badline
9
10
  # CIA (Complex Interface Adapter) chip
10
11
  class CIA
11
12
  include Addressable
12
13
  include State
14
+ include FastForward
13
15
 
14
16
  # The original NMOS 6526 of the breadbin C64, and the HMOS 6526A of the
15
17
  # C64C, which differs from it only in its interrupt control register.
@@ -59,14 +61,16 @@ module Badline
59
61
  @tb.latch = value
60
62
  end
61
63
 
62
- def initialize(start: 0, peripheral: nil, model: :mos6526, region: Region::PAL)
64
+ # +tod_hz+ is the rate of the pulses on the TOD pin, the region's mains
65
+ # unless a machine feeds it otherwise, and 0 for a pin held still.
66
+ def initialize(start: 0, peripheral: nil, model: :mos6526, region: Region::PAL, tod_hz: nil)
63
67
  raise ArgumentError, "unknown CIA model #{model}" unless MODELS.include?(model)
64
68
 
65
69
  addressable_at(start, length: 2**8)
66
70
 
67
71
  @model = model
68
72
  @clock_hz = region.clock_hz
69
- @mains_hz = region.mains_hz
73
+ @mains_hz = tod_hz || region.mains_hz
70
74
 
71
75
  @peripheral = peripheral
72
76
  @port_b4_handler = nil
@@ -137,9 +141,10 @@ module Badline
137
141
 
138
142
  # Port A as driven by the data/direction registers alone, without
139
143
  # peripheral pulldown. Cheap path for the VIC bank lookup.
140
- def port_a_lines
141
- driven_lines(@data_port_a, @data_dir_a)
142
- end
144
+ def port_a_lines = driven_lines(@data_port_a, @data_dir_a)
145
+
146
+ # Port B as driven by the data/direction registers alone.
147
+ def port_b_lines = driven_lines(@data_port_b, @data_dir_b)
143
148
 
144
149
  def read_port_b
145
150
  lines = driven_lines(@data_port_b, @data_dir_b)
@@ -3,9 +3,11 @@
3
3
  module Badline
4
4
  class Computer
5
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.
6
+ # disk device 8 serves through the traps, its true drives (TrueDrives)
7
+ # and an REU, and their state for a snapshot.
8
8
  module Attachments
9
+ include TrueDrives
10
+
9
11
  # Puts the cartridge in the expansion port and wires its clock and NMI
10
12
  # line to the machine, leaving the machine's state as it is. A
11
13
  # restored snapshot's cartridge comes back through here.
@@ -44,36 +46,8 @@ module Badline
44
46
  # it.
45
47
  attr_reader :iec_bus
46
48
 
47
- # Plugs in a Drive1541, which then runs alongside the C64 on its own
48
- # clock and talks to it over the serial bus. The serial traps stop
49
- # answering the drive's device number, so the KERNAL's TALK, LISTEN and
50
- # byte transfers reach the drive. The LOAD and SAVE traps stay, and
51
- # still serve a mounted image.
52
- def attach_drive1541(drive)
53
- @iec_bus.detach(@drive1541) if @drive1541
54
- drive.host_clock_hz = region.clock_hz
55
- @drive1541 = drive
56
- drive.connect(iec_bus)
57
- @serial_trap&.device = serial_trap_device
58
- end
59
-
60
- # Unplugs the Drive1541, leaving the serial bus with nothing on it.
61
- def detach_drive1541
62
- return unless @drive1541
63
-
64
- @iec_bus.detach(@drive1541)
65
- @drive1541 = nil
66
- @serial_trap&.device = serial_trap_device
67
- end
68
-
69
49
  private
70
50
 
71
- # The device number the serial traps answer: device 8, unless a true
72
- # drive is on the bus as device 8.
73
- def serial_trap_device
74
- @drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
75
- end
76
-
77
51
  # CIA 2's port A drives the serial bus, which reads CLK and DATA back
78
52
  # into it.
79
53
  def plug_serial_bus
@@ -126,26 +100,6 @@ module Badline
126
100
  @save_trap.load_state(input)
127
101
  end
128
102
 
129
- # A true 1541 on the serial bus. The leading flag is the bus itself,
130
- # which every machine now has, kept so older snapshots still load.
131
- def save_serial_bus(out)
132
- out.boolean(true).boolean(!@drive1541.nil?)
133
- return unless @drive1541
134
-
135
- out.int(@drive1541.device)
136
- @drive1541.save_state(out)
137
- end
138
-
139
- def load_serial_bus(input)
140
- input.boolean?
141
- return detach_drive1541 unless input.boolean?
142
-
143
- device = input.int
144
- detach_drive1541 if @drive1541 && @drive1541.device != device
145
- attach_drive1541(Drive1541.new(device:)) unless @drive1541
146
- @drive1541.load_state(input)
147
- end
148
-
149
103
  # The REU, which the setup says the machine has, and whether it has
150
104
  # asked for the bus. Its IRQ line reaches the CPU as it was.
151
105
  def save_reu(out)
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Computer
5
+ # The true drives on the C64's serial bus: a Drive1541, and a Drive1581
6
+ # (Drive1581::Slot), each in a field of its own, and their state for a
7
+ # snapshot.
8
+ module TrueDrives
9
+ include Drive1581::Slot
10
+
11
+ # Plugs in a Drive1541, which then runs alongside the C64 on its own
12
+ # clock and talks to it over the serial bus. The serial traps stop
13
+ # answering the drive's device number, so the KERNAL's TALK, LISTEN and
14
+ # byte transfers reach the drive. The LOAD and SAVE traps stay, and
15
+ # still serve a mounted image.
16
+ def attach_drive1541(drive)
17
+ @iec_bus.detach(@drive1541) if @drive1541
18
+ drive.host_clock_hz = region.clock_hz
19
+ @drive1541 = drive
20
+ drive.connect(iec_bus)
21
+ @serial_trap&.device = serial_trap_device
22
+ end
23
+
24
+ # The true drive on the serial bus: the Drive1581 ahead of the
25
+ # Drive1541, or nil.
26
+ def true_drive = @drive1581 || @drive1541
27
+
28
+ # Plugs a Drive1541 in as device 8, in place of a 1581 there, and
29
+ # returns it.
30
+ def plug_true_drive
31
+ detach_drive1581 if drive1581_on_device8?
32
+ Drive1541.new.tap { |drive| attach_drive1541(drive) }
33
+ end
34
+
35
+ # Unplugs the Drive1541, leaving the serial bus with nothing on it.
36
+ def detach_drive1541
37
+ return unless @drive1541
38
+
39
+ @iec_bus.detach(@drive1541)
40
+ @drive1541 = nil
41
+ @serial_trap&.device = serial_trap_device
42
+ end
43
+
44
+ private
45
+
46
+ # The device number the serial traps answer: device 8, unless a true
47
+ # drive is on the bus as device 8.
48
+ def serial_trap_device
49
+ device = KernalTrap::Routine::DEVICE
50
+ @drive1541&.device == device || drive1581_on_device8? ? nil : device
51
+ end
52
+
53
+ # A true 1541 on the serial bus, then a 1581. The leading flag is the
54
+ # bus itself, which every machine now has.
55
+ def save_serial_bus(out)
56
+ out.boolean(true).boolean(!@drive1541.nil?)
57
+ if @drive1541
58
+ out.int(@drive1541.device)
59
+ @drive1541.save_state(out)
60
+ end
61
+ save_drive1581(out)
62
+ end
63
+
64
+ def load_serial_bus(input)
65
+ input.boolean?
66
+ load_drive1541(input)
67
+ load_drive1581(input)
68
+ end
69
+
70
+ def load_drive1541(input)
71
+ return detach_drive1541 unless input.boolean?
72
+
73
+ device = input.int
74
+ detach_drive1541 if @drive1541 && @drive1541.device != device
75
+ attach_drive1541(Drive1541.new(device:)) unless @drive1541
76
+ @drive1541.load_state(input)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/computer/true_drives"
3
4
  require "badline/computer/attachments"
4
5
  require "badline/computer/kernal_traps"
5
6
  require "badline/computer/saved_state"
@@ -77,6 +78,7 @@ module Badline
77
78
  @serial_trap = nil
78
79
  @save_trap = nil
79
80
  @drive1541 = nil
81
+ @drive1581 = nil
80
82
  plug_serial_bus
81
83
  @reu = reu ? plug_reu(reu) : nil
82
84
  end
@@ -101,6 +103,7 @@ module Badline
101
103
  watch_freeze if @freezing
102
104
  clock_cpu
103
105
  @drive1541&.host_cycle!
106
+ @drive1581&.host_cycle!
104
107
 
105
108
  @cycles += 1
106
109
  end
@@ -174,6 +177,7 @@ module Badline
174
177
  @dma = false
175
178
  @drive&.reset!
176
179
  @drive1541&.reset!
180
+ @drive1581&.reset!
177
181
  @freezing = false
178
182
  @nmi_asserted = false
179
183
  cpu.reset!
data/lib/badline/core.rb CHANGED
@@ -38,6 +38,8 @@ require "badline/vic"
38
38
  require "badline/keyboard_buffer"
39
39
  require "badline/iec_bus"
40
40
  require "badline/drive1541"
41
+ require "badline/drive1581"
42
+ require "badline/drive1581/slot"
41
43
  require "badline/snapshot/state"
42
44
  require "badline/snapshot/setup"
43
45
  require "badline/computer"