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
@@ -15,7 +15,17 @@ module Badline
15
15
  # A drive drives CLK and DATA from VIA 1's PB3 and PB1, the same way. It
16
16
  # also pulls DATA while ATN IN differs from its ATN acknowledge on PB4
17
17
  # (an XOR gate), which answers ATN in hardware before the DOS gets to it.
18
- # SerialPort reads the lines back into VIA 1.
18
+ # SerialPort reads the lines back into VIA 1. A drive whose
19
+ # serial_output sets DRIVE_ATN_GATED pulls DATA that way only while
20
+ # ATN is low, as the 1581's NAND does.
21
+ #
22
+ # Fast serial adds SRQ, which the C64 leaves alone. A C128 and a 1571
23
+ # clock bytes over it from a CIA's CNT, with the bits on DATA from its
24
+ # SP, through buffers each side turns towards the bus to send. A pin low
25
+ # pulls its line low. Each side pushes the lines it pulls when they
26
+ # move: the host through host_fast_lines=, which tells the drives
27
+ # (fast_lines_moved), and a drive through drives_fast_moved!, which
28
+ # tells the host (on_fast_change).
19
29
  class IECBus
20
30
  # CIA 2 port A
21
31
  HOST_ATN_OUT = 0x08
@@ -29,16 +39,60 @@ module Badline
29
39
  DRIVE_CLK_OUT = 0x08
30
40
  DRIVE_ATNA = 0x10
31
41
 
32
- # The lines pulled low, as ATN, CLK and DATA bits
42
+ # The lines pulled low, as ATN, CLK, DATA and SRQ bits
33
43
  ATN = 0x01
34
44
  CLK = 0x02
35
45
  DATA = 0x04
46
+ SRQ = 0x08
47
+
48
+ # A drive's serial_output bits above VIA 1's port B: its fast serial
49
+ # pins pulling DATA and SRQ.
50
+ DRIVE_FAST_DATA = 0x100
51
+ DRIVE_FAST_SRQ = 0x200
52
+
53
+ # A drive's serial_output bit whose ATN acknowledge pulls DATA only
54
+ # while ATN is low.
55
+ DRIVE_ATN_GATED = 0x400
36
56
 
37
57
  attr_reader :drives, :host_lines
38
58
 
59
+ # The lines the host's fast serial pins pull, as DATA and SRQ bits.
60
+ attr_reader :host_fast_lines
61
+
39
62
  def initialize
40
63
  @drives = []
41
64
  @host_lines = 0
65
+ @host_fast_lines = 0
66
+ @drive_fast_lines = 0
67
+ @on_fast_change = nil
68
+ end
69
+
70
+ # The host's fast serial pins moved: +low+ is the lines they pull, as
71
+ # DATA and SRQ bits.
72
+ def host_fast_lines=(low)
73
+ return if low == @host_fast_lines
74
+
75
+ @host_fast_lines = low
76
+ @drives.each(&:fast_lines_moved)
77
+ end
78
+
79
+ # A drive's fast serial pins moved (serial_output's fast bits).
80
+ def drives_fast_moved!
81
+ low = 0
82
+ @drives.each do |drive|
83
+ output = drive.serial_output
84
+ low |= DATA if output.anybits?(DRIVE_FAST_DATA)
85
+ low |= SRQ if output.anybits?(DRIVE_FAST_SRQ)
86
+ end
87
+ return if low == @drive_fast_lines
88
+
89
+ @drive_fast_lines = low
90
+ @on_fast_change&.call
91
+ end
92
+
93
+ # Calls the block whenever the drives move their fast serial pins.
94
+ def on_fast_change(&block)
95
+ @on_fast_change = block
42
96
  end
43
97
 
44
98
  # The machine pushes the lines it pulls, as port A bits, whenever they
@@ -58,18 +112,20 @@ module Badline
58
112
  end
59
113
 
60
114
  # One pass over everything on the bus, for a reader that wants more
61
- # than one line. +host+ is the C64's port A lines as the reader sees
62
- # them, which a drive takes a cycle late (see Drive1541::SerialPort).
115
+ # than one line, SRQ included. +host+ is the C64's port A lines as the
116
+ # reader sees them, which a drive takes a cycle late (see
117
+ # Drive::SerialLines).
63
118
  def low_lines(host = @host_lines)
64
119
  atn = host.anybits?(HOST_ATN_OUT)
65
- low = atn ? ATN : 0
120
+ low = @host_fast_lines | @drive_fast_lines
121
+ low |= ATN if atn
66
122
  low |= CLK if host.anybits?(HOST_CLK_OUT)
67
123
  low |= DATA if host.anybits?(HOST_DATA_OUT)
68
124
  i = 0
69
125
  while i < @drives.length
70
126
  drive = @drives[i].serial_output
71
127
  low |= CLK if drive.anybits?(DRIVE_CLK_OUT)
72
- low |= DATA if drive.anybits?(DRIVE_DATA_OUT) || atn != drive.anybits?(DRIVE_ATNA)
128
+ low |= DATA if drive.anybits?(DRIVE_DATA_OUT) || acknowledging?(drive, atn)
73
129
  i += 1
74
130
  end
75
131
  low
@@ -77,6 +133,12 @@ module Badline
77
133
 
78
134
  def atn_low?(host = @host_lines) = host.anybits?(HOST_ATN_OUT)
79
135
 
136
+ # Whether a drive's ATN acknowledge pulls DATA, with its serial_output
137
+ # +drive+, while ATN is +atn+.
138
+ def acknowledging?(drive, atn)
139
+ atn != drive.anybits?(DRIVE_ATNA) && (atn || drive.nobits?(DRIVE_ATN_GATED))
140
+ end
141
+
80
142
  def clk_low? = low_lines.anybits?(CLK)
81
143
 
82
144
  def data_low? = low_lines.anybits?(DATA)
@@ -24,8 +24,10 @@ module Badline
24
24
  # - Media: +ram+, +load_prg+, +type_text+, +mount+, +unmount+,
25
25
  # +mounted?+, +datasette+, +attach_cartridge+,
26
26
  # +press_cartridge_button+, +release_cartridge_button+,
27
- # +attach_drive1541+, +drive1541+, and +address_bus+ for its
28
- # +cartridge+ and +detach_cartridge+.
27
+ # +attach_drive1541+, +drive1541+, +true_drive+ and +plug_true_drive+
28
+ # for the drive Media::TrueDrive plugs in, +attach_drive1581+,
29
+ # +drive1581+ and +plug_drive1581+ for a 1581 (Drive1581::Slot), and
30
+ # +address_bus+ for its +cartridge+ and +detach_cartridge+.
29
31
  # - Snapshots and tests: +snapshot+, +restore+, +save_snapshot+,
30
32
  # +install_debug_register+ and +capture_output+.
31
33
  #
@@ -33,14 +35,15 @@ module Badline
33
35
  # input parts, with its one joystick as both +joystick1+ and +joystick2+
34
36
  # and nil for +control_ports+, and of the rest +ram+, +load_prg+,
35
37
  # +type_text+, +mount+, +unmount+, +mounted?+, +datasette+, +attach_cartridge+,
36
- # +attach_drive1541+, +drive1541+, +install_debug_register+ and
37
- # +capture_output+.
38
+ # +attach_drive1541+, +drive1541+, +true_drive+, +plug_true_drive+, the
39
+ # 1581's, +install_debug_register+ and +capture_output+.
38
40
  #
39
41
  # The C128 answers all but the cartridge button, and besides +mode+,
40
42
  # :c64, as Machine.build builds it, or :c128, +vdc+, the VDC a front
41
43
  # end can show in place of +video+ once +vdc_shown=+ renders it,
42
- # +press_caps_lock+, +release_caps_lock+, +press_display_key+ and
43
- # +release_display_key+.
44
+ # +press_caps_lock+, +release_caps_lock+, +press_display_key+,
45
+ # +release_display_key+, and +attach_drive1571+ and +drive1571+ for the
46
+ # C128D's 1571, its true drive.
44
47
  module Machine
45
48
  # A new machine of `family`, :c64, :vic20 or :c128, the model of it
46
49
  # `model` names. A C64's model is one of Model::ALL, with its SID
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # A C128 boot disk. At power-on the C128 KERNAL reads track 1, sector 0
6
+ # of the disk in device 8 and runs it when it starts with "CBM", as boot
7
+ # games have it. The 1581's DOS first loads and runs a USR file named
8
+ # COPYRIGHT CBM 86 when the disk has one, which is how CP/M's 1581 disks
9
+ # hand the KERNAL their boot sector, so a .d81 with that file boots too.
10
+ #
11
+ # On a C128 in C128 mode a boot disk goes in a true drive, a 1581 for a
12
+ # .d81 and the C128D's 1571 otherwise, and nothing is typed, so the
13
+ # KERNAL boots it. Other disks, and other machines, keep the traps and
14
+ # the autostart.
15
+ module BootDisk
16
+ SIGNATURE = "CBM".bytes.freeze
17
+ AUTO_BOOT = "copyright cbm 86"
18
+ TYPES = %w[.d64 .d71 .d81].freeze
19
+
20
+ class << self
21
+ def takes?(computer, path)
22
+ computer.family == :c128 && computer.mode == :c128 && boot?(path)
23
+ end
24
+
25
+ # Puts a disk that boots on the computer in a true drive as device
26
+ # 8, write-protected with `disk: { read_only: true }`, for the KERNAL
27
+ # to boot at power-on, and says so. Nil for any other disk.
28
+ def attach(computer, path, disk: {}, **)
29
+ return unless takes?(computer, path)
30
+
31
+ read_only = disk.fetch(:read_only, false)
32
+ "#{TrueDrive.insert(computer, path, read_only:)}, to boot at power-on"
33
+ end
34
+
35
+ # Whether the disk image at path boots: its boot sector, or on a
36
+ # .d81 the 1581's auto-boot file.
37
+ def boot?(path)
38
+ type = File.extname(path).downcase
39
+ return false unless TYPES.include?(type) && File.file?(path)
40
+
41
+ image = MOUNT_TYPES.fetch(type).new(path, read_only: true)
42
+ block = image.read_block(1, 0)
43
+ return true if block && block[0, 3] == SIGNATURE
44
+
45
+ type == ".d81" && !image.read_file(AUTO_BOOT, type: :usr).nil?
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # A file that goes in device 8 but isn't a disk image or a directory.
6
+ class NotDisk < ArgumentError; end
7
+ end
8
+ end
@@ -2,51 +2,74 @@
2
2
 
3
3
  module Badline
4
4
  module Media
5
- # Device 8 as a true 1541 instead of the KERNAL traps. Once a Drive1541
6
- # is plugged in as device 8, disk media go into it rather than into the
7
- # traps, so a LOAD runs the DOS and every byte crosses the serial bus.
8
- # A .g64 holds the disk's raw GCR, which only a true drive reads, so it
9
- # plugs one in when there's none yet. The 1541 reads only .d64 and .g64
10
- # images, so the traps' other disk media, a .d71, .d81 or .t64 or a host
11
- # directory, raise Error.
5
+ # Device 8 as a true drive instead of the KERNAL traps: a 1541, or on
6
+ # the C128 its 1571, or a 1581. Once a true drive is plugged in as
7
+ # device 8, disk media go into it rather than into the traps, so a LOAD
8
+ # runs the DOS and every byte crosses the serial bus. A .g64 holds the
9
+ # disk's raw GCR, which only a true drive reads, so it plugs one in
10
+ # when there's none yet. The 1541 reads only .d64 and .g64 images, the
11
+ # 1571 .d71 and .g71 as well, and the 1581 only .d81, so a .d81 swaps
12
+ # the drive on device 8 for a 1581 (Drive1581::Slot), and another disk
13
+ # image swaps a 1581 back for the machine's own drive. The traps' other
14
+ # disk media, a .t64 or a host directory, raise Error.
12
15
  module TrueDrive
13
16
  class Error < ArgumentError; end
14
17
 
15
18
  class << self
16
- # Plugs a Drive1541 in as device 8, unless one is there already, and
17
- # returns it.
19
+ # Plugs the machine's true drive in as device 8, unless one is there
20
+ # already, and returns it.
18
21
  def plug(computer)
19
- drive(computer) || Drive1541.new.tap { |drive| computer.attach_drive1541(drive) }
22
+ drive(computer) || computer.plug_true_drive
20
23
  end
21
24
 
22
- # The Drive1541 on device 8, or nil.
25
+ # The true drive on device 8, or nil.
23
26
  def drive(computer)
24
- drive = computer.drive1541
27
+ drive = computer.true_drive
25
28
  drive if drive && drive.device == KernalTrap::Routine::DEVICE
26
29
  end
27
30
 
28
- # Whether the path goes in a true drive: a .g64 always, and other
29
- # disk media, which the traps would mount, when a true drive is
30
- # device 8.
31
+ # Whether the path goes in a true drive: a .g64 or .g71 always, and
32
+ # other disk media, which the traps would mount, when a true drive
33
+ # is device 8.
31
34
  def takes?(computer, path)
32
- return true if File.extname(path).casecmp?(".g64")
35
+ return true if %w[.g64 .g71].include?(File.extname(path).downcase)
33
36
 
34
37
  !drive(computer).nil? && (File.directory?(path) || MOUNT_TYPES.key?(File.extname(path).downcase))
35
38
  end
36
39
 
37
- # Puts the .d64 or .g64 in the drive on device 8, plugging one in
38
- # when there's none, and takes out what was mounted through the
39
- # traps, so LOAD and SAVE reach the drive too. `read_only` puts it
40
- # in write-protected.
40
+ # Puts the image in the drive on device 8, plugging one in that
41
+ # reads it when the drive there doesn't, and takes out what was
42
+ # mounted through the traps, so LOAD and SAVE reach the drive too.
43
+ # `read_only` puts it in write-protected.
41
44
  def insert(computer, path, read_only: false)
42
- unless %w[.d64 .g64].include?(File.extname(path).downcase)
43
- raise Error, "#{path} is not a .d64 or .g64 image, which the 1541 reads"
45
+ return insert_d81(computer, path, read_only) if File.extname(path).casecmp?(".d81")
46
+
47
+ drive = own_drive(computer)
48
+ name = drive&.model_name || (computer.family == :c128 ? "1571" : "1541")
49
+ types = name == "1571" ? %w[.d64 .g64 .d71 .g71] : %w[.d64 .g64]
50
+ unless types.include?(File.extname(path).downcase)
51
+ raise Error, "#{path} is not a #{types.join(', ')} or .d81 image, which the #{name} and the 1581 read"
44
52
  end
45
53
 
46
- disk = Drive1541::Disk.open(path, read_only:)
54
+ (drive || computer.plug_true_drive).insert_image(path, read_only:)
55
+ computer.unmount
56
+ "Inserted #{path} in the #{name} as device 8"
57
+ end
58
+
59
+ private
60
+
61
+ # The drive on device 8 unless it's a 1581.
62
+ def own_drive(computer)
63
+ drive = drive(computer)
64
+ drive unless drive&.model_name == "1581"
65
+ end
66
+
67
+ def insert_d81(computer, path, read_only)
68
+ drive = drive(computer)
69
+ drive = computer.plug_drive1581 unless drive&.model_name == "1581"
70
+ drive.insert_image(path, read_only:)
47
71
  computer.unmount
48
- plug(computer).insert(disk)
49
- "Inserted #{path} in the 1541 as device 8"
72
+ "Inserted #{path} in the 1581 as device 8"
50
73
  end
51
74
  end
52
75
  end
data/lib/badline/media.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/media/true_drive"
4
+ require "badline/media/boot_disk"
5
+ require "badline/media/not_disk"
4
6
  require "badline/media/queue"
5
7
  require "badline/media/disk_set"
6
8
  require "badline/media/disk_list"
@@ -32,6 +34,9 @@ module Badline
32
34
  # there, a .d64 goes into it instead of the KERNAL traps too. The
33
35
  # autostart then loads through it.
34
36
  #
37
+ # On a C128 in C128 mode, a boot disk goes in a true drive for the
38
+ # KERNAL to boot at power-on, with nothing typed (BootDisk).
39
+ #
35
40
  # An .m3u or .vfl list of disks attaches the first disk it lists
36
41
  # (DiskList). The VIC-20 takes its own media (Vic20Media), and the
37
42
  # C128 in C64 mode a C64's.
@@ -39,7 +44,7 @@ module Badline
39
44
  path = DiskList.disk(path)
40
45
  return Vic20Media.attach(computer, path, autostart:, **) if computer.family == :vic20
41
46
 
42
- attach_c64(computer, path, autostart:, subtune:, **)
47
+ BootDisk.attach(computer, path, **) || attach_c64(computer, path, autostart:, subtune:, **)
43
48
  end
44
49
 
45
50
  # Swaps the disk in device 8 for a disk image or a host directory,
@@ -50,11 +55,12 @@ module Badline
50
55
  # A .g64 goes in a true 1541, which is plugged in as device 8 when
51
56
  # there's none yet (TrueDrive). Once a true 1541 is device 8, a .d64
52
57
  # goes in its drive as well, and other disks, which a 1541 can't
53
- # read, raise TrueDrive::Error, an ArgumentError.
58
+ # read, raise TrueDrive::Error, an ArgumentError. Anything else
59
+ # raises NotDisk.
54
60
  def insert_disk(computer, path, read_only: false)
55
61
  return TrueDrive.insert(computer, path, read_only:) if TrueDrive.takes?(computer, path)
56
62
 
57
- raise ArgumentError, "#{path} is not a disk image or a directory" unless disk?(path)
63
+ raise NotDisk, "#{path} is not a disk image or a directory" unless disk?(path)
58
64
 
59
65
  computer.mount(open_storage(path, { read_only: }))
60
66
  "Inserted #{path} in device 8"
@@ -23,7 +23,7 @@ module Badline
23
23
 
24
24
  # What insert takes: disk images, tapes and cartridges, or an .m3u
25
25
  # or .vfl list of disks or a directory to mount as device 8.
26
- INSERTABLE = %w[.d64 .d71 .d81 .g64 .t64 .tap .crt].freeze
26
+ INSERTABLE = %w[.d64 .d71 .d81 .g64 .t64 .tap .crt .m3u .vfl].freeze
27
27
 
28
28
  attr_reader :frame, :action, :argument
29
29
 
@@ -95,7 +95,7 @@ module Badline
95
95
  def self.insertable?(path)
96
96
  return true if File.directory?(path)
97
97
 
98
- File.exist?(path) && (INSERTABLE.include?(File.extname(path).downcase) || Media::DiskList.list?(path))
98
+ File.exist?(path) && INSERTABLE.include?(File.extname(path).downcase)
99
99
  end
100
100
  end
101
101
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/storage/p00"
4
+
3
5
  module Badline
4
6
  class Options
5
7
  # The machine family: the subcommand that names it, or else the media,
@@ -37,15 +39,7 @@ module Badline
37
39
  return if @family_named || @sid_command || @media_path.nil?
38
40
  return unless %w[.prg .p00].include?(File.extname(@media_path).downcase) && File.file?(@media_path)
39
41
 
40
- @family = :c128 if load_address(@media_path) == C128_BASIC_START
41
- end
42
-
43
- # The load address of a .prg, or of the program a .p00 wraps after its
44
- # 26-byte header.
45
- def load_address(path)
46
- offset = File.extname(path).casecmp?(".p00") ? 26 : 0
47
- bytes = File.binread(path).bytes
48
- bytes.size >= offset + 2 ? bytes[offset] | (bytes[offset + 1] << 8) : -1
42
+ @family = :c128 if Storage::P00.load_address(@media_path) == C128_BASIC_START
49
43
  end
50
44
  end
51
45
  end
@@ -60,7 +60,9 @@ module Badline
60
60
  window also autosaves every two minutes of play, which only the
61
61
  pause menu loads. Device 8 answers through traps on the KERNAL's
62
62
  disk routines, unless --true-drive puts an emulated 1541 there,
63
- which runs its own DOS and reads .d64 and .g64 images only. A .sid
63
+ which runs its own DOS and reads .d64 and .g64 images only, or on
64
+ the C128 the C128D's 1571, which reads .d71 and .g71 too. A .d81
65
+ then goes in an emulated 1581 in its place. A .sid
64
66
  tune opens in the SID player, as `sid` plays it, unless an option
65
67
  of the emulator's window asks for the machine.
66
68
 
@@ -35,7 +35,7 @@ module Badline
35
35
  Option.new("--sound", "Play the SID through the host's audio device (F10 mutes)",
36
36
  section: "Window options", needs: :window),
37
37
  Option.new("--no-sound", "Don't play the SID", section: "Window options", needs: :window, build: :native),
38
- Option.new("--true-drive", "Put a true 1541 on device 8 instead of the KERNAL traps",
38
+ Option.new("--true-drive", "Put a true 1541 on device 8 instead of the KERNAL traps, a 1571 on the C128",
39
39
  section: "Window options", needs: :window),
40
40
  Option.new("--reu SIZE", "Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384",
41
41
  section: "Window options", needs: :window),
@@ -61,6 +61,24 @@ https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1541/
61
61
  (1541-c000.325302-01.bin and 1541-e000.901229-05.bin) and concatenated.
62
62
  SHA-1 ab16f56989b27d89babe5f89c5a8cb3da71a82f0.
63
63
 
64
+ dos1571.rom
65
+ -----------
66
+ The 1571 disk drive's DOS 3.0, 310654-05, 32 KB for $8000-$FFFF.
67
+ Drive1571 loads it.
68
+
69
+ Downloaded from
70
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1571/1571-rom.310654-05.bin
71
+ SHA-1 f1be619c106641a685f6609e4d43d6fc9eac1e70.
72
+
73
+ dos1581.rom
74
+ -----------
75
+ The 1581 disk drive's DOS, 318045-02, 32 KB for $8000-$FFFF.
76
+ Drive1581 loads it.
77
+
78
+ Downloaded from
79
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1581/1581-rom.318045-02.bin
80
+ SHA-1 01228eae6f066bd9b7b2b6a7fa3f667e41dad393.
81
+
64
82
  Commodore VIC-20 ROMs
65
83
  =====================
66
84
 
Binary file
Binary file
data/lib/badline/sdl.rb CHANGED
@@ -59,23 +59,6 @@ module Badline
59
59
  ffi_func :SDL_PushEvent, [:ptr], :int
60
60
  ffi_func :SDL_GetKeyName, [:int], :str
61
61
 
62
- ffi_buffer :event, 56
63
- ffi_read_u32 :event_type, 0
64
- ffi_read_u8 :event_repeat, 13
65
- ffi_read_i32 :event_scancode, 16
66
- ffi_read_i32 :event_sym, 20
67
- ffi_read_u16 :event_mod, 24
68
- # SDL_MouseMotionEvent's xrel and yrel, and SDL_MouseButtonEvent's
69
- # button.
70
- ffi_read_i32 :event_xrel, 28
71
- ffi_read_i32 :event_yrel, 32
72
- ffi_read_u8 :event_button, 16
73
- # Where the pointer is, in a motion or button event.
74
- ffi_read_i32 :event_x, 20
75
- ffi_read_i32 :event_y, 24
76
- # SDL_DropEvent's file, which the receiver frees.
77
- ffi_read_ptr :event_file, 8
78
-
79
62
  ffi_buffer :rect, 16
80
63
  ffi_write_i32 :rect_x, 0
81
64
  ffi_write_i32 :rect_y, 4
@@ -115,19 +98,53 @@ module Badline
115
98
  ffi_const :PIXELFORMAT_ARGB8888, 0x16362004
116
99
  ffi_const :BLENDMODE_BLEND, 0x01
117
100
  ffi_const :TEXTUREACCESS_STREAMING, 1
101
+ ffi_const :AUDIO_S16LSB, 0x8010
102
+ ffi_const :ALLOW_FREQUENCY_CHANGE, 0x01
103
+ end
104
+
105
+ # The events: the SDL_Event that SDL_PollEvent fills, the fields the
106
+ # front end reads from it, the event types, the key modifier and
107
+ # keycodes, and the dropped file's path.
108
+ module SDL
109
+ ffi_buffer :event, 56
110
+ ffi_read_u32 :event_type, 0
111
+ ffi_read_u8 :event_repeat, 13
112
+ ffi_read_i32 :event_scancode, 16
113
+ ffi_read_i32 :event_sym, 20
114
+ ffi_read_u16 :event_mod, 24
115
+ # SDL_MouseMotionEvent's xrel and yrel, and SDL_MouseButtonEvent's
116
+ # button.
117
+ ffi_read_i32 :event_xrel, 28
118
+ ffi_read_i32 :event_yrel, 32
119
+ ffi_read_u8 :event_button, 16
120
+ # Where the pointer is, in a motion or button event.
121
+ ffi_read_i32 :event_x, 20
122
+ ffi_read_i32 :event_y, 24
123
+ # SDL_DropEvent's file, which the receiver frees.
124
+ ffi_read_ptr :event_file, 8
125
+
118
126
  ffi_const :QUIT, 0x100
119
127
  ffi_const :KEYDOWN, 0x300
120
128
  ffi_const :KEYUP, 0x301
121
129
  ffi_const :MOUSEMOTION, 0x400
122
130
  ffi_const :MOUSEBUTTONDOWN, 0x401
123
131
  ffi_const :MOUSEBUTTONUP, 0x402
124
- ffi_const :KMOD_SHIFT, 0x0003
132
+ ffi_const :MOUSEWHEEL, 0x403
125
133
  ffi_const :CONTROLLERDEVICEADDED, 0x653
126
134
  ffi_const :CONTROLLERDEVICEREMOVED, 0x654
127
- ffi_const :AUDIO_S16LSB, 0x8010
128
- ffi_const :ALLOW_FREQUENCY_CHANGE, 0x01
135
+ ffi_const :DROPFILE, 0x1000
136
+ ffi_const :KMOD_SHIFT, 0x0003
129
137
  ffi_const :KEY_TAB, 0x09
130
138
  ffi_const :KEY_F10, 0x4000_0043
139
+
140
+ # The path of the file the DROPFILE event in SDL.event carries, as a
141
+ # String, freeing SDL's copy.
142
+ def self.dropped_file
143
+ file = SDL.event_file(SDL.event)
144
+ path = LibC.strstr(file, "")
145
+ LibC.free(file)
146
+ path
147
+ end
131
148
  end
132
149
 
133
150
  # The few C library calls the native front end makes.
@@ -15,10 +15,11 @@ module Badline
15
15
  # save_state writes something else, so a State in another layout
16
16
  # fails before anything is read into a machine. Layout 3, without the
17
17
  # KERNAL and datasette in the setup, layout 4, without the VIA shift
18
- # register's clock delay, and layout 5, without the board in the
19
- # setup, still read.
20
- SCHEMA = 6
21
- READABLE = [3, 4, 5, SCHEMA].freeze
18
+ # register's clock delay, layout 5, without the board in the setup,
19
+ # layout 6, without the C128's Z80, and layout 7, without a 1581 on the
20
+ # serial bus, still read.
21
+ SCHEMA = 8
22
+ READABLE = [3, 4, 5, 6, 7, SCHEMA].freeze
22
23
 
23
24
  attr_reader :values, :strings
24
25
 
@@ -17,6 +17,9 @@ module Badline
17
17
 
18
18
  def storage_kind = D64
19
19
 
20
+ # The sides of a disk the image's tracks fill.
21
+ def sides = 1
22
+
20
23
  def sectors_in(track)
21
24
  case track
22
25
  when 1..17 then 21
@@ -7,6 +7,15 @@ module Badline
7
7
 
8
8
  def storage_kind = D71
9
9
 
10
+ # Tracks 1-35 on the first side and 36-70 on the second.
11
+ def track_count = 70
12
+
13
+ def sides = 2
14
+
15
+ def sectors_in(track)
16
+ track > 35 ? super(track - 35) : super
17
+ end
18
+
10
19
  private
11
20
 
12
21
  def error_tables = ERROR_TABLES
@@ -22,10 +31,6 @@ module Badline
22
31
  def bam_bitmap(track) = track > 35 ? [53, 0, 3 * (track - 36)] : super
23
32
 
24
33
  def double_sided? = @bytes[sector_offset(18, 0) + 3].anybits?(0x80)
25
-
26
- def sectors_in(track)
27
- track > 35 ? super(track - 35) : super
28
- end
29
34
  end
30
35
  end
31
36
  end
@@ -5,7 +5,9 @@ module Badline
5
5
  # A .g64 image: a 1541 disk's flux as raw GCR, track by track, for the
6
6
  # true drive to read the way it reads a real disk, copy protection and
7
7
  # all. It has no file system of its own, so only the true drive
8
- # (Drive1541::Disk.from_g64) can use it.
8
+ # (Drive1541::Disk.from_g64) can use it. A .g71 holds a 1571 disk the
9
+ # same way, with "GCR-1571" for its signature and 168 half tracks,
10
+ # the first side's 84 and then the second's.
9
11
  #
10
12
  # The header is the signature "GCR-1541", a version byte ($00), the
11
13
  # number of half tracks the tables cover (84, tracks 1 to 42.5) and
@@ -36,6 +38,7 @@ module Badline
36
38
  class FormatError < StandardError; end
37
39
 
38
40
  SIGNATURE = "GCR-1541"
41
+ SIGNATURE_1571 = "GCR-1571"
39
42
  HEADER_SIZE = 12
40
43
  HALF_TRACKS = 84
41
44
  MAX_TRACK_SIZE = 7928
@@ -124,7 +127,9 @@ module Badline
124
127
  private
125
128
 
126
129
  def parse
127
- raise FormatError, "Missing G64 signature" unless @data.start_with?(SIGNATURE)
130
+ unless @data.start_with?(SIGNATURE) || @data.start_with?(SIGNATURE_1571)
131
+ raise FormatError, "Missing G64 signature"
132
+ end
128
133
  raise FormatError, "G64 header is cut short" if @data.bytesize < HEADER_SIZE
129
134
 
130
135
  version, @half_tracks, @max_track_size = @data.byteslice(8, 4).unpack("CCv")
@@ -18,6 +18,14 @@ module Badline
18
18
  def data(bytes)
19
19
  bytes[HEADER_SIZE..]
20
20
  end
21
+
22
+ # The load address of the program in a .prg file, or wrapped in a
23
+ # .p00 one, or -1 when the file is too short to have one.
24
+ def load_address(path)
25
+ bytes = File.binread(path).bytes
26
+ offset = wraps?(bytes) ? HEADER_SIZE : 0
27
+ bytes.size >= offset + 2 ? bytes[offset] | (bytes[offset + 1] << 8) : -1
28
+ end
21
29
  end
22
30
  end
23
31
  end
@@ -52,6 +52,15 @@ module Badline
52
52
  @latch = input.boolean? ? FIELDS.to_h { |field| [field, input.int] } : nil
53
53
  end
54
54
 
55
+ # Whether the clock is stopped, or gets no pulses on its pin, with no
56
+ # alarm to raise, so its cycles move only the divider's phase.
57
+ def quiet? = (@stopped || @mains_hz.zero?) && !@alarm_pending
58
+
59
+ # Runs +cycles+ quiet cycles at once.
60
+ def fast_forward(cycles)
61
+ @accumulator = (@accumulator + (cycles * @mains_hz)) % @cycles_per_pulse
62
+ end
63
+
55
64
  def cycle!
56
65
  @accumulator += @mains_hz
57
66
  pulse! if @accumulator >= @cycles_per_pulse
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.11.0"
4
+ VERSION = "0.12.0"
5
5
  end