badline 0.11.1 → 0.12.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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/CLAUDE.md +8 -7
  4. data/CONTRIBUTING.md +7 -6
  5. data/README.md +12 -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/address_bus/saved_state.rb +4 -10
  10. data/lib/badline/address_bus.rb +8 -27
  11. data/lib/badline/audio/bare_player.rb +1 -1
  12. data/lib/badline/c128/bus.rb +17 -52
  13. data/lib/badline/c128/bus_state.rb +5 -5
  14. data/lib/badline/c128/drives.rb +35 -22
  15. data/lib/badline/c128/fast_serial.rb +1 -0
  16. data/lib/badline/c128/io_pages.rb +35 -0
  17. data/lib/badline/c128/mmu.rb +9 -7
  18. data/lib/badline/c128/modes.rb +17 -4
  19. data/lib/badline/c128/saved_state.rb +22 -0
  20. data/lib/badline/c128/z80.rb +49 -0
  21. data/lib/badline/c128/z80_pages.rb +118 -0
  22. data/lib/badline/c128/z80_turns.rb +106 -0
  23. data/lib/badline/c128.rb +21 -83
  24. data/lib/badline/cartridge/action_replay.rb +2 -2
  25. data/lib/badline/cartridge/comal80.rb +1 -7
  26. data/lib/badline/cartridge/dinamic.rb +0 -8
  27. data/lib/badline/cartridge/final_cartridge3.rb +6 -6
  28. data/lib/badline/cartridge/fun_play.rb +0 -8
  29. data/lib/badline/cartridge/game_system.rb +0 -8
  30. data/lib/badline/cartridge/kcs_power.rb +10 -7
  31. data/lib/badline/cartridge/mach5.rb +2 -4
  32. data/lib/badline/cartridge/magic_desk.rb +0 -8
  33. data/lib/badline/cartridge/ocean.rb +0 -8
  34. data/lib/badline/cartridge/pagefox.rb +2 -2
  35. data/lib/badline/cartridge/retro_replay.rb +7 -6
  36. data/lib/badline/cartridge/rex_utility.rb +2 -6
  37. data/lib/badline/cartridge/rgcd.rb +2 -2
  38. data/lib/badline/cartridge/simons_basic.rb +0 -5
  39. data/lib/badline/cartridge/super_games.rb +4 -2
  40. data/lib/badline/cartridge/westermann.rb +2 -6
  41. data/lib/badline/cartridge.rb +37 -9
  42. data/lib/badline/cia/fast_forward.rb +21 -3
  43. data/lib/badline/cia.rb +8 -5
  44. data/lib/badline/computer/attachments.rb +14 -95
  45. data/lib/badline/computer/common.rb +34 -0
  46. data/lib/badline/computer/kernal_traps.rb +65 -5
  47. data/lib/badline/computer/true_drives.rb +69 -0
  48. data/lib/badline/computer.rb +9 -68
  49. data/lib/badline/core.rb +4 -2
  50. data/lib/badline/cpu_port.rb +112 -0
  51. data/lib/badline/drive/core.rb +8 -33
  52. data/lib/badline/drive/fast_serial.rb +73 -0
  53. data/lib/badline/drive/idle.rb +1 -11
  54. data/lib/badline/drive/orbit.rb +2 -5
  55. data/lib/badline/drive/sleep.rb +0 -2
  56. data/lib/badline/drive/vias.rb +84 -0
  57. data/lib/badline/drive.rb +2 -0
  58. data/lib/badline/drive1541/disk.rb +14 -17
  59. data/lib/badline/drive1541/disk_state.rb +7 -15
  60. data/lib/badline/drive1541.rb +1 -0
  61. data/lib/badline/drive1571/wd1770.rb +94 -8
  62. data/lib/badline/drive1571.rb +8 -58
  63. data/lib/badline/drive1581/bus.rb +137 -0
  64. data/lib/badline/drive1581/cpu.rb +11 -0
  65. data/lib/badline/drive1581/disk.rb +135 -0
  66. data/lib/badline/drive1581/disk_state.rb +87 -0
  67. data/lib/badline/drive1581/mechanism.rb +117 -0
  68. data/lib/badline/drive1581/serial_port.rb +52 -0
  69. data/lib/badline/drive1581/slot.rb +78 -0
  70. data/lib/badline/drive1581/track.rb +100 -0
  71. data/lib/badline/drive1581/track_bytes.rb +75 -0
  72. data/lib/badline/drive1581/wd1772/registers.rb +89 -0
  73. data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
  74. data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
  75. data/lib/badline/drive1581/wd1772/state.rb +66 -0
  76. data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
  77. data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
  78. data/lib/badline/drive1581/wd1772/turn.rb +73 -0
  79. data/lib/badline/drive1581/wd1772.rb +216 -0
  80. data/lib/badline/drive1581.rb +233 -0
  81. data/lib/badline/frontend/app.rb +1 -3
  82. data/lib/badline/frontend/audio_device.rb +67 -0
  83. data/lib/badline/frontend/audio_sink.rb +16 -50
  84. data/lib/badline/frontend/boot.rb +24 -67
  85. data/lib/badline/frontend/buttons.rb +7 -7
  86. data/lib/badline/frontend/keys.rb +33 -8
  87. data/lib/badline/frontend/media_slots.rb +42 -0
  88. data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
  89. data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
  90. data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
  91. data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
  92. data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +5 -8
  93. data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
  94. data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
  95. data/lib/badline/frontend/menu/theme.rb +18 -0
  96. data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
  97. data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
  98. data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
  99. data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
  100. data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
  101. data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
  102. data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
  103. data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
  104. data/lib/badline/frontend/player/theme.rb +18 -0
  105. data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
  106. data/lib/badline/frontend/sound.rb +19 -38
  107. data/lib/badline/frontend/timeline.rb +3 -14
  108. data/lib/badline/frontend.rb +24 -20
  109. data/lib/badline/iec_bus.rb +14 -2
  110. data/lib/badline/kernal_trap/{file.rb → file_routine.rb} +1 -1
  111. data/lib/badline/kernal_trap/load.rb +1 -1
  112. data/lib/badline/kernal_trap/save.rb +1 -1
  113. data/lib/badline/kernal_trap.rb +1 -1
  114. data/lib/badline/machine/clocking.rb +50 -0
  115. data/lib/badline/machine.rb +63 -6
  116. data/lib/badline/media/boot_disk.rb +50 -0
  117. data/lib/badline/media/disk_list.rb +3 -3
  118. data/lib/badline/media/disk_set.rb +1 -1
  119. data/lib/badline/media/extensions.rb +33 -0
  120. data/lib/badline/media/not_disk.rb +8 -0
  121. data/lib/badline/media/program.rb +28 -0
  122. data/lib/badline/media/true_drive.rb +38 -17
  123. data/lib/badline/media/tune.rb +35 -0
  124. data/lib/badline/media/vic20_cartridge.rb +1 -7
  125. data/lib/badline/media/vic20_media.rb +38 -80
  126. data/lib/badline/media.rb +90 -81
  127. data/lib/badline/options/event.rb +4 -2
  128. data/lib/badline/options/family.rb +3 -9
  129. data/lib/badline/options/help.rb +2 -1
  130. data/lib/badline/roms/README +9 -0
  131. data/lib/badline/roms/dos1581.rom +0 -0
  132. data/lib/badline/sdl.rb +37 -20
  133. data/lib/badline/sid.rb +1 -1
  134. data/lib/badline/snapshot/machine_state.rb +9 -1
  135. data/lib/badline/snapshot/state.rb +6 -4
  136. data/lib/badline/snapshot.rb +11 -9
  137. data/lib/badline/storage/d64_image.rb +15 -7
  138. data/lib/badline/storage/disk_image/writing.rb +2 -2
  139. data/lib/badline/storage/disk_image.rb +1 -1
  140. data/lib/badline/storage/host_directory.rb +1 -1
  141. data/lib/badline/storage/p00.rb +8 -0
  142. data/lib/badline/storage/t64.rb +1 -1
  143. data/lib/badline/time_of_day.rb +3 -3
  144. data/lib/badline/version.rb +1 -1
  145. data/lib/badline/vic/bank.rb +1 -1
  146. data/lib/badline/vic/border_mask.rb +1 -1
  147. data/lib/badline/vic/collisions.rb +1 -1
  148. data/lib/badline/vic/color_patches.rb +1 -1
  149. data/lib/badline/vic/display_state.rb +1 -1
  150. data/lib/badline/vic/graphics_mode.rb +1 -1
  151. data/lib/badline/vic/graphics_shifter.rb +1 -1
  152. data/lib/badline/vic/register_log.rb +1 -1
  153. data/lib/badline/vic/registers.rb +1 -1
  154. data/lib/badline/vic/sequencer.rb +1 -1
  155. data/lib/badline/vic/sequencer_output.rb +1 -1
  156. data/lib/badline/vic/sequencer_state.rb +1 -1
  157. data/lib/badline/vic/sprite/internal_bus.rb +1 -1
  158. data/lib/badline/vic/sprite/saved_state.rb +1 -1
  159. data/lib/badline/vic/sprite/shifter.rb +1 -1
  160. data/lib/badline/vic/sprite/timing.rb +1 -1
  161. data/lib/badline/vic/sprite.rb +1 -1
  162. data/lib/badline/vic/sprites.rb +1 -1
  163. data/lib/badline/vic/sprites_state.rb +1 -1
  164. data/lib/badline/vic.rb +6 -5
  165. data/lib/badline/vic20/attachments.rb +15 -71
  166. data/lib/badline/vic20/saved_state.rb +2 -30
  167. data/lib/badline/vic20.rb +4 -37
  168. data/lib/badline/z80/core.rb +2 -0
  169. data/lib/badline/z80/saved_state.rb +69 -0
  170. metadata +61 -23
  171. data/lib/badline/cycleable.rb +0 -38
  172. /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
  173. /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
  174. /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
  175. /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
data/lib/badline/cia.rb CHANGED
@@ -61,14 +61,16 @@ module Badline
61
61
  @tb.latch = value
62
62
  end
63
63
 
64
- 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)
65
67
  raise ArgumentError, "unknown CIA model #{model}" unless MODELS.include?(model)
66
68
 
67
69
  addressable_at(start, length: 2**8)
68
70
 
69
71
  @model = model
70
72
  @clock_hz = region.clock_hz
71
- @mains_hz = region.mains_hz
73
+ @mains_hz = tod_hz || region.mains_hz
72
74
 
73
75
  @peripheral = peripheral
74
76
  @port_b4_handler = nil
@@ -139,9 +141,10 @@ module Badline
139
141
 
140
142
  # Port A as driven by the data/direction registers alone, without
141
143
  # peripheral pulldown. Cheap path for the VIC bank lookup.
142
- def port_a_lines
143
- driven_lines(@data_port_a, @data_dir_a)
144
- 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)
145
148
 
146
149
  def read_port_b
147
150
  lines = driven_lines(@data_port_b, @data_dir_b)
@@ -2,10 +2,12 @@
2
2
 
3
3
  module Badline
4
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.
5
+ # What plugs into the machine besides its chips, the cartridge, its
6
+ # true drives (TrueDrives) and an REU, and their state for a snapshot.
7
+ # The disk device 8 serves through the traps is KernalTraps'.
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.
@@ -15,71 +17,13 @@ module Badline
15
17
  address_bus.attach_cartridge(cartridge)
16
18
  end
17
19
 
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. The traps go on the KERNAL the
21
- # machine runs (#trap_layout).
22
- def mount(storage)
23
- return @drive.insert(storage) if @drive
24
-
25
- @drive = KernalTrap::Drive.new(storage)
26
- install_kernal_traps(trap_layout)
27
- end
28
-
29
- # Takes device 8's mounted storage out, and with it the LOAD, SAVE and
30
- # serial traps, so the KERNAL's routines go out over the serial bus.
31
- # Mounting again starts a new drive, with its RAM cleared.
32
- def unmount
33
- return unless @drive
34
-
35
- remove_kernal_traps
36
- @drive = nil
37
- end
38
-
39
- # Whether device 8 serves a disk or directory through the traps.
40
- def mounted? = !@drive.nil?
41
-
42
20
  # The serial bus, which CIA 2's port A drives through the C64's own
43
21
  # inverters and reads back on PA6 and PA7, with or without a drive on
44
22
  # it.
45
23
  attr_reader :iec_bus
46
24
 
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
- # The true drive on the serial bus: the Drive1541, or nil.
61
- def true_drive = @drive1541
62
-
63
- # Plugs a Drive1541 in as device 8, and returns it.
64
- def plug_true_drive = Drive1541.new.tap { |drive| attach_drive1541(drive) }
65
-
66
- # Unplugs the Drive1541, leaving the serial bus with nothing on it.
67
- def detach_drive1541
68
- return unless @drive1541
69
-
70
- @iec_bus.detach(@drive1541)
71
- @drive1541 = nil
72
- @serial_trap&.device = serial_trap_device
73
- end
74
-
75
25
  private
76
26
 
77
- # The device number the serial traps answer: device 8, unless a true
78
- # drive is on the bus as device 8.
79
- def serial_trap_device
80
- @drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
81
- end
82
-
83
27
  # CIA 2's port A drives the serial bus, which reads CLK and DATA back
84
28
  # into it.
85
29
  def plug_serial_bus
@@ -110,46 +54,21 @@ module Badline
110
54
  end
111
55
  end
112
56
 
113
- # The disk device 8 serves through the traps, with the traps' own
114
- # state: open channels, the drive's status and RAM, a SAVE under way.
115
- def save_trap_drive(out)
116
- out.boolean(!@drive.nil?)
117
- return unless @drive
118
-
119
- @drive.save_state(out)
120
- @serial_trap.save_state(out)
121
- @save_trap.save_state(out)
122
- end
123
-
124
- def load_trap_drive(input)
125
- return unmount unless input.boolean?
126
-
127
- storage = Storage.reopen(input)
128
- unmount
129
- mount(storage)
130
- @drive.load_state(input)
131
- @serial_trap.load_state(input)
132
- @save_trap.load_state(input)
133
- end
134
-
135
- # A true 1541 on the serial bus. The leading flag is the bus itself,
136
- # which every machine now has, kept so older snapshots still load.
57
+ # A true 1541 on the serial bus, then a 1581. The leading flag is the
58
+ # bus itself, which every machine now has.
137
59
  def save_serial_bus(out)
138
60
  out.boolean(true).boolean(!@drive1541.nil?)
139
- return unless @drive1541
140
-
141
- out.int(@drive1541.device)
142
- @drive1541.save_state(out)
61
+ if @drive1541
62
+ out.int(@drive1541.device)
63
+ @drive1541.save_state(out)
64
+ end
65
+ save_drive1581(out)
143
66
  end
144
67
 
145
68
  def load_serial_bus(input)
146
69
  input.boolean?
147
- return detach_drive1541 unless input.boolean?
148
-
149
- device = input.int
150
- detach_drive1541 if @drive1541 && @drive1541.device != device
151
- attach_drive1541(Drive1541.new(device:)) unless @drive1541
152
- @drive1541.load_state(input)
70
+ load_drive1541(input)
71
+ load_drive1581(input)
153
72
  end
154
73
 
155
74
  # The REU, which the setup says the machine has, and whether it has
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Computer
5
+ # What the C128 does as the C64 does: the NMI line, a PRG put into RAM
6
+ # and the region's timing. Computer and C128 each include it.
7
+ module Common
8
+ # The clock, the raster and the crop of the machine's region, a pixel
9
+ # to a window pixel.
10
+ def timing
11
+ region = address_bus.region
12
+ Timing.new(clock_hz: region.clock_hz, cycles_per_line: region.cycles_per_line,
13
+ lines_per_frame: region.lines_per_frame, crop: region.crop, pixel_width: 1)
14
+ end
15
+
16
+ def load_prg(data)
17
+ uint16(data[0], data[1]).tap do |load_addr|
18
+ ram.write(load_addr, data[2..])
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
25
+ # key, and the CPU takes an interrupt on its falling edge.
26
+ def drive_nmi
27
+ nmi = @cia2.interrupted? || @cartridge_nmi || @restore_pulse
28
+ @restore_pulse = false
29
+ @cpu.nmi = true if nmi && !@nmi_asserted
30
+ @nmi_asserted = nmi
31
+ end
32
+ end
33
+ end
34
+ end
@@ -2,18 +2,56 @@
2
2
 
3
3
  module Badline
4
4
  class Computer
5
- # The LOAD, SAVE and serial traps on the KERNAL a KernalTrap::Layout
6
- # describes, which serve the disk in device 8 (Attachments#mount).
5
+ # The disk device 8 serves through the LOAD, SAVE and serial traps, and
6
+ # CHROUT capture, on the KERNAL the machine runs. Computer, Vic20 and
7
+ # C128 each include it and answer two private methods: #trap_layout,
8
+ # the KernalTrap::Layout of that KERNAL, and #trap_bus, the bus the
9
+ # traps read and write through.
7
10
  module KernalTraps
11
+ # Puts the storage in device 8. Mounting again swaps the disk at any
12
+ # point while the machine runs: the drive keeps its RAM, which only a
13
+ # drive reset clears, and its status. The traps go on the KERNAL the
14
+ # machine runs (#trap_layout).
15
+ def mount(storage)
16
+ return @drive.insert(storage) if @drive
17
+
18
+ @drive = KernalTrap::Drive.new(storage)
19
+ install_kernal_traps(trap_layout)
20
+ end
21
+
22
+ # Takes device 8's mounted storage out, and with it the LOAD, SAVE and
23
+ # serial traps, so the KERNAL's routines go out over the serial bus.
24
+ # Mounting again starts a new drive, with its RAM cleared.
25
+ def unmount
26
+ return unless @drive
27
+
28
+ remove_kernal_traps
29
+ @drive = nil
30
+ end
31
+
32
+ # Whether device 8 serves a disk or directory through the traps.
33
+ def mounted? = !@drive.nil?
34
+
35
+ # The path of the disk or directory device 8 serves through the
36
+ # traps, or an empty one.
37
+ def mounted_path = @drive.nil? ? "" : @drive.path
38
+
39
+ # Records what the KERNAL prints through CHROUT (ChroutTrap).
40
+ def capture_output
41
+ @capture_output ||= ChroutTrap.new(cpu:, bus: trap_bus, layout: trap_layout).tap do |trap|
42
+ cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
43
+ end
44
+ end
45
+
8
46
  private
9
47
 
10
48
  def install_kernal_traps(layout)
11
49
  @trap_layout = layout
12
- load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, layout:, drive: @drive)
50
+ load_trap = KernalTrap::Load.new(cpu:, bus: trap_bus, layout:, drive: @drive)
13
51
  cpu.install_trap(layout.load) { load_trap.call }
14
- @serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, layout:, drive: @drive,
52
+ @serial_trap = KernalTrap::Serial.new(cpu:, bus: trap_bus, layout:, drive: @drive,
15
53
  device: serial_trap_device).install
16
- save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, layout:, drive: @drive)
54
+ save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: trap_bus, layout:, drive: @drive)
17
55
  cpu.install_trap(layout.save) { save_trap.call }
18
56
  end
19
57
 
@@ -24,6 +62,28 @@ module Badline
24
62
  @serial_trap = nil
25
63
  @save_trap = nil
26
64
  end
65
+
66
+ # The disk device 8 serves through the traps, with the traps' own
67
+ # state: open channels, the drive's status and RAM, a SAVE under way.
68
+ def save_trap_drive(out)
69
+ out.boolean(!@drive.nil?)
70
+ return unless @drive
71
+
72
+ @drive.save_state(out)
73
+ @serial_trap.save_state(out)
74
+ @save_trap.save_state(out)
75
+ end
76
+
77
+ def load_trap_drive(input)
78
+ return unmount unless input.boolean?
79
+
80
+ storage = Storage.reopen(input)
81
+ unmount
82
+ mount(storage)
83
+ @drive.load_state(input)
84
+ @serial_trap.load_state(input)
85
+ @save_trap.load_state(input)
86
+ end
27
87
  end
28
88
  end
29
89
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Computer
5
+ # The true drives on the machine's serial bus: a Drive1541, and a
6
+ # Drive1581 (Drive1581::Slot), each in a field of its own. Computer,
7
+ # Vic20 and C128 each include it.
8
+ module TrueDrives
9
+ include Drive1581::Slot
10
+
11
+ # Plugs in a Drive1541, which then runs alongside the machine on its
12
+ # own 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 = timing.clock_hz
19
+ @drive1541 = drive
20
+ drive.connect(@iec_bus)
21
+ @serial_trap&.device = serial_trap_device
22
+ drive1541_attached
23
+ end
24
+
25
+ # The true drive on the serial bus: the Drive1581 ahead of the
26
+ # Drive1541, or nil.
27
+ def true_drive = @drive1581 || @drive1541
28
+
29
+ # Plugs a Drive1541 in as device 8, in place of a 1581 there, and
30
+ # returns it.
31
+ def plug_true_drive
32
+ detach_drive1581 if drive1581_on_device8?
33
+ Drive1541.new.tap { |drive| attach_drive1541(drive) }
34
+ end
35
+
36
+ # Unplugs the Drive1541, leaving the serial bus with nothing on it.
37
+ def detach_drive1541
38
+ return unless @drive1541
39
+
40
+ @iec_bus.detach(@drive1541)
41
+ @drive1541 = nil
42
+ @serial_trap&.device = serial_trap_device
43
+ end
44
+
45
+ private
46
+
47
+ # The device number the serial traps answer: device 8, unless a true
48
+ # drive is on the bus as device 8.
49
+ def serial_trap_device
50
+ device = KernalTrap::Routine::DEVICE
51
+ @drive1541&.device == device || drive1581_on_device8? ? nil : device
52
+ end
53
+
54
+ # What the machine does as a 1541 goes on its bus. The VIC-20 waits
55
+ # for its DOS to boot before typing.
56
+ def drive1541_attached = nil
57
+
58
+ # A true 1541 on the serial bus, with its device number.
59
+ def load_drive1541(input)
60
+ return detach_drive1541 unless input.boolean?
61
+
62
+ device = input.int
63
+ detach_drive1541 if @drive1541 && @drive1541.device != device
64
+ attach_drive1541(Drive1541.new(device:)) unless @drive1541
65
+ @drive1541.load_state(input)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/computer/common"
4
+ require "badline/computer/true_drives"
3
5
  require "badline/computer/attachments"
4
6
  require "badline/computer/kernal_traps"
5
7
  require "badline/computer/saved_state"
@@ -8,16 +10,14 @@ module Badline
8
10
  class Computer
9
11
  include IntegerHelper
10
12
  include KeyboardBuffer
13
+ include Machine::Clocking
14
+ include Common
11
15
  include Attachments
12
16
  include KernalTraps
13
17
  include SavedState
14
18
 
15
19
  attr_reader :address_bus, :cpu, :cycles, :drive1541
16
20
 
17
- # The path of the disk or directory device 8 serves through the traps
18
- # (Attachments), or an empty one.
19
- def mounted_path = @drive.nil? ? "" : @drive.path
20
-
21
21
  def family = :c64
22
22
 
23
23
  def region = address_bus.region
@@ -77,6 +77,7 @@ module Badline
77
77
  @serial_trap = nil
78
78
  @save_trap = nil
79
79
  @drive1541 = nil
80
+ @drive1581 = nil
80
81
  plug_serial_bus
81
82
  @reu = reu ? plug_reu(reu) : nil
82
83
  end
@@ -101,50 +102,22 @@ module Badline
101
102
  watch_freeze if @freezing
102
103
  clock_cpu
103
104
  @drive1541&.host_cycle!
105
+ @drive1581&.host_cycle!
104
106
 
105
107
  @cycles += 1
106
108
  end
107
109
 
108
- # Runs `count` cycles, one #cycle! after another.
109
- def run_cycles(count)
110
- i = 0
111
- while i < count
112
- cycle!
113
- i += 1
114
- end
115
- end
116
-
117
- # Runs cycles until the block returns true or the cycle count passes
118
- # `limit`, checking before each cycle.
119
- def run_until(limit)
120
- cycle! until yield || @cycles > limit
121
- end
122
-
123
110
  # The cycle at which #on_init's handlers run, once the KERNAL has booted.
124
111
  def init_threshold
125
112
  INIT_THRESHOLD
126
113
  end
127
114
 
128
- # The clock, the raster and the crop of the machine's region, a pixel
129
- # to a window pixel.
130
- def timing
131
- region = address_bus.region
132
- Timing.new(clock_hz: region.clock_hz, cycles_per_line: region.cycles_per_line,
133
- lines_per_frame: region.lines_per_frame, crop: region.crop, pixel_width: 1)
134
- end
135
-
136
115
  # The chip whose #display a front end shows.
137
116
  def video = @vic
138
117
 
139
118
  # The chip a front end records the machine's sound from.
140
119
  def sound_source = @sid
141
120
 
142
- def load_prg(data)
143
- uint16(data[0], data[1]).tap do |load_addr|
144
- ram.write(load_addr, data[2..])
145
- end
146
- end
147
-
148
121
  def attach_cartridge(cartridge)
149
122
  connect_cartridge(cartridge)
150
123
  power_cycle!
@@ -174,6 +147,7 @@ module Badline
174
147
  @dma = false
175
148
  @drive&.reset!
176
149
  @drive1541&.reset!
150
+ @drive1581&.reset!
177
151
  @freezing = false
178
152
  @nmi_asserted = false
179
153
  cpu.reset!
@@ -204,37 +178,12 @@ module Badline
204
178
  # The one-shot has pulsed already, so letting go does nothing.
205
179
  def release_restore; end
206
180
 
207
- def capture_output
208
- @capture_output ||= ChroutTrap.new(cpu:, bus: address_bus, layout: KernalTrap::C64_LAYOUT).tap do |trap|
209
- cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
210
- end
211
- end
212
-
213
- def inspect
214
- "#<#{self.class.name} cycles=#{@cycles} cpu=(#{@cpu.inspect})>"
215
- end
216
-
217
- def on_init(&block)
218
- if booting?
219
- @init_handlers << block
220
- else
221
- block.call
222
- end
223
- end
224
-
225
181
  private
226
182
 
227
- # The KERNAL the traps stand in for (Attachments#mount).
183
+ # The KERNAL the traps stand in for (KernalTraps#mount).
228
184
  def trap_layout = KernalTrap::C64_LAYOUT
229
185
 
230
- # The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
231
- # key, and the CPU takes an interrupt on its falling edge.
232
- def drive_nmi
233
- nmi = @cia2.interrupted? || @cartridge_nmi || @restore_pulse
234
- @restore_pulse = false
235
- @cpu.nmi = true if nmi && !@nmi_asserted
236
- @nmi_asserted = nmi
237
- end
186
+ def trap_bus = address_bus
238
187
 
239
188
  def plug_reu(size_kb)
240
189
  reu = REU.new(size_kb, bus: @address_bus, vic: @vic)
@@ -274,13 +223,5 @@ module Badline
274
223
  @freezing = false
275
224
  address_bus.cartridge.freeze!
276
225
  end
277
-
278
- def booting?
279
- @cycles < init_threshold
280
- end
281
-
282
- def handle_init
283
- @init_handlers.each(&:call)
284
- end
285
226
  end
286
227
  end
data/lib/badline/core.rb CHANGED
@@ -15,6 +15,7 @@ require "badline/memory"
15
15
  require "badline/color_memory"
16
16
  require "badline/rom"
17
17
  require "badline/ram_expansion"
18
+ require "badline/cpu_port"
18
19
  require "badline/address_bus"
19
20
  require "badline/instruction"
20
21
  require "badline/instruction_set"
@@ -23,7 +24,6 @@ require "badline/keyboard"
23
24
  require "badline/joystick"
24
25
  require "badline/control_ports"
25
26
  require "badline/input"
26
- require "badline/cycleable"
27
27
  require "badline/datasette"
28
28
  require "badline/time_of_day"
29
29
  require "badline/cia"
@@ -38,8 +38,11 @@ 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"
45
+ require "badline/machine"
43
46
  require "badline/computer"
44
47
  require "badline/vic20"
45
48
  require "badline/checkpoint"
@@ -48,4 +51,3 @@ require "badline/cartridge"
48
51
  require "badline/kernal_trap"
49
52
  require "badline/chrout_trap"
50
53
  require "badline/media"
51
- require "badline/machine"
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ # The I/O port of the 6510 and the 8502 at $00 (direction) and $01
5
+ # (data). An input pin reads its pull-up, P4 reads the datasette's sense
6
+ # line, and a pin without a pull-up holds the charge last driven onto
7
+ # it. P5 drives the datasette motor through an inverter: low runs it.
8
+ #
9
+ # The 6510 has pull-ups on P0-P2 and P4 and leaves P3, P6 and P7
10
+ # floating. The 8502 adds a CAPS LOCK pin on P6, high while the key is
11
+ # up.
12
+ class CPUPort
13
+ TAPE_SENSE = 0b0001_0000
14
+
15
+ # The direction and output registers, and the charge on the floating
16
+ # pins.
17
+ attr_reader :ddr, :data, :floating
18
+
19
+ # The port as the banking logic reads it (PortStatus).
20
+ attr_reader :status
21
+
22
+ # Whether CAPS LOCK is down, holding its pin low.
23
+ attr_reader :caps_lock
24
+
25
+ # +pullups+ and +floating+ are the masks of the pins with a pull-up and
26
+ # of the floating ones, and +caps_lock+ the CAPS LOCK pin's, 0 where
27
+ # there is none.
28
+ def initialize(datasette, pullups:, floating:, caps_lock: 0)
29
+ @datasette = datasette
30
+ @pullups = pullups
31
+ @floating_mask = floating
32
+ @caps_lock_pin = caps_lock
33
+ @caps_lock = false
34
+ @ddr = 0x00
35
+ @data = 0x00
36
+ @floating = 0x00
37
+ @change_handler = nil
38
+ @status = PortStatus.new(%i[basic kernal io tape_out tape_switch tape_motor], value: input_value)
39
+ end
40
+
41
+ # Calls the block after every write to the port, for the bus to remap
42
+ # its banks.
43
+ def on_change(&handler)
44
+ @change_handler = handler
45
+ end
46
+
47
+ # What a read of $01 sees.
48
+ def value = @status.value
49
+
50
+ def write_ddr(value)
51
+ @ddr = value
52
+ update
53
+ end
54
+
55
+ def write_data(value)
56
+ @data = value
57
+ update
58
+ end
59
+
60
+ # The RES line clears the direction and output registers. The floating
61
+ # pins keep their charge.
62
+ def reset!
63
+ @ddr = 0x00
64
+ @data = 0x00
65
+ update
66
+ end
67
+
68
+ # Sets the registers and the charge, as a snapshot restores them, and
69
+ # drives the motor and calls the change handler as a write does.
70
+ def restore(ddr, data, floating)
71
+ load(ddr, data, floating)
72
+ update
73
+ end
74
+
75
+ # Sets the registers and the charge, as a snapshot loads them, without
76
+ # driving the motor or calling the change handler.
77
+ def load(ddr, data, floating)
78
+ @ddr = ddr
79
+ @data = data
80
+ @floating = floating
81
+ refresh
82
+ end
83
+
84
+ # Holds CAPS LOCK down, or lets it up.
85
+ def caps_lock=(down)
86
+ @caps_lock = down
87
+ refresh
88
+ end
89
+
90
+ # Reads the inputs again, after the datasette's sense line changes.
91
+ def refresh
92
+ @status.value = input_value
93
+ end
94
+
95
+ private
96
+
97
+ def update
98
+ driven = @ddr & @floating_mask
99
+ @floating = (@floating & ~driven) | (@data & driven)
100
+ refresh
101
+ @datasette.motor = !@status.tape_motor?
102
+ @change_handler&.call
103
+ end
104
+
105
+ def input_value
106
+ input = @pullups | (@floating & @floating_mask)
107
+ input |= @caps_lock_pin unless @caps_lock
108
+ input &= ~TAPE_SENSE if @datasette.sense_low?
109
+ (@data & @ddr) | (input & ~@ddr & 0xff)
110
+ end
111
+ end
112
+ end