badline 0.17.0 → 0.18.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 +153 -0
  3. data/CLAUDE.md +11 -7
  4. data/CONTRIBUTING.md +7 -1
  5. data/README.md +169 -20
  6. data/doc/pinned-behaviour.md +128 -0
  7. data/doc/snapshot-format.md +253 -18
  8. data/doc/snapshots.md +57 -23
  9. data/exe/badline-ruby +5 -0
  10. data/lib/badline/audio/terminal.rb +34 -2
  11. data/lib/badline/c128/keyed_state.rb +2 -0
  12. data/lib/badline/c128.rb +1 -0
  13. data/lib/badline/c64/keyed_state.rb +2 -0
  14. data/lib/badline/c64.rb +1 -0
  15. data/lib/badline/cbm2/keyed_state.rb +4 -0
  16. data/lib/badline/cbm2/p500.rb +5 -3
  17. data/lib/badline/cbm2.rb +6 -3
  18. data/lib/badline/chips/cia/timer.rb +2 -2
  19. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  20. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  21. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  22. data/lib/badline/chips/m6809/core.rb +149 -0
  23. data/lib/badline/chips/m6809/indexed.rb +82 -0
  24. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  25. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  26. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  27. data/lib/badline/chips/m6809/operations.rb +258 -0
  28. data/lib/badline/chips/m6809/plans.rb +143 -0
  29. data/lib/badline/chips/m6809/registers.rb +82 -0
  30. data/lib/badline/chips/m6809/stack.rb +140 -0
  31. data/lib/badline/chips/m6809.rb +13 -0
  32. data/lib/badline/chips/riot/timer.rb +14 -3
  33. data/lib/badline/chips/rriot.rb +4 -2
  34. data/lib/badline/chips/sid/filter.rb +4 -4
  35. data/lib/badline/chips/sid/waveform.rb +6 -6
  36. data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
  37. data/lib/badline/chips/vic_i.rb +84 -21
  38. data/lib/badline/data_path.rb +3 -3
  39. data/lib/badline/drives/ieee_models.rb +54 -0
  40. data/lib/badline/drives/model1001.rb +37 -0
  41. data/lib/badline/drives/model2031.rb +6 -0
  42. data/lib/badline/drives/model4040/disk.rb +62 -0
  43. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  44. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  45. data/lib/badline/drives/model4040/mechanism.rb +192 -0
  46. data/lib/badline/drives/model4040/parts.rb +20 -0
  47. data/lib/badline/drives/model4040.rb +179 -0
  48. data/lib/badline/drives/model8050/board.rb +77 -0
  49. data/lib/badline/drives/model8050/dos_board.rb +91 -0
  50. data/lib/badline/drives/model8050/idle.rb +11 -7
  51. data/lib/badline/drives/model8050/keyed_state.rb +5 -2
  52. data/lib/badline/drives/model8050.rb +18 -130
  53. data/lib/badline/drives/model9060/idle.rb +149 -0
  54. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  55. data/lib/badline/drives/model9060/parts.rb +98 -0
  56. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  57. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  58. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  59. data/lib/badline/drives/model9060.rb +195 -0
  60. data/lib/badline/frontend/app.rb +8 -0
  61. data/lib/badline/frontend/boot.rb +3 -2
  62. data/lib/badline/frontend/controls.rb +62 -6
  63. data/lib/badline/frontend/keys.rb +36 -0
  64. data/lib/badline/frontend/kim1_panel.rb +208 -0
  65. data/lib/badline/frontend/media_slots.rb +8 -1
  66. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  67. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  68. data/lib/badline/frontend/menu/menu_media.rb +42 -12
  69. data/lib/badline/frontend/menu/menu_pages.rb +97 -2
  70. data/lib/badline/frontend/menu/menu_ports.rb +38 -1
  71. data/lib/badline/frontend/menu/pause_menu.rb +5 -1
  72. data/lib/badline/frontend/pet_keyboard.rb +23 -1
  73. data/lib/badline/frontend/screen.rb +1 -1
  74. data/lib/badline/frontend/screens.rb +40 -2
  75. data/lib/badline/frontend/teletype_pane.rb +152 -0
  76. data/lib/badline/frontend/timeline.rb +31 -2
  77. data/lib/badline/frontend.rb +3 -0
  78. data/lib/badline/kim1/bus.rb +117 -0
  79. data/lib/badline/kim1/cpu.rb +25 -0
  80. data/lib/badline/kim1/host_terminal.rb +74 -0
  81. data/lib/badline/kim1/key_entry.rb +74 -0
  82. data/lib/badline/kim1/keyed_modules.rb +53 -0
  83. data/lib/badline/kim1/keyed_state.rb +133 -0
  84. data/lib/badline/kim1/panel.rb +176 -0
  85. data/lib/badline/kim1/rriots.rb +28 -0
  86. data/lib/badline/kim1/sound.rb +29 -0
  87. data/lib/badline/kim1/tape.rb +116 -0
  88. data/lib/badline/kim1/teletype.rb +193 -0
  89. data/lib/badline/kim1.rb +190 -0
  90. data/lib/badline/machine/ieee_drives.rb +94 -117
  91. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  92. data/lib/badline/machine/line_sound.rb +122 -0
  93. data/lib/badline/machine.rb +46 -15
  94. data/lib/badline/media/extensions.rb +8 -4
  95. data/lib/badline/media/kim1_media.rb +150 -0
  96. data/lib/badline/media/pet_media.rb +3 -1
  97. data/lib/badline/media/true_drive.rb +42 -45
  98. data/lib/badline/media.rb +12 -6
  99. data/lib/badline/options/family.rb +98 -8
  100. data/lib/badline/options/help.rb +44 -12
  101. data/lib/badline/options/table.rb +43 -11
  102. data/lib/badline/options/validation.rb +44 -8
  103. data/lib/badline/options.rb +27 -8
  104. data/lib/badline/pet/bus.rb +16 -2
  105. data/lib/badline/pet/keyboard_ports.rb +9 -2
  106. data/lib/badline/pet/keyed_state.rb +4 -0
  107. data/lib/badline/pet/model.rb +11 -2
  108. data/lib/badline/pet/rom_sets.rb +1 -0
  109. data/lib/badline/pet/sound.rb +5 -105
  110. data/lib/badline/pet/superpet/acia.rb +12 -0
  111. data/lib/badline/pet/superpet/board.rb +221 -0
  112. data/lib/badline/pet/superpet/dongle.rb +81 -0
  113. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  114. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  115. data/lib/badline/pet/superpet/m6809.rb +32 -0
  116. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  117. data/lib/badline/pet/superpet.rb +216 -0
  118. data/lib/badline/pet.rb +19 -3
  119. data/lib/badline/plus4/keyed_state.rb +2 -0
  120. data/lib/badline/plus4.rb +1 -0
  121. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  122. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  123. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  124. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  125. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  126. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  127. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  128. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  129. data/lib/badline/roms/README +166 -0
  130. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  131. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  132. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  133. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  134. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  135. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  136. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  137. data/lib/badline/snapshot/container.rb +2 -0
  138. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  139. data/lib/badline/snapshot/field_doc.rb +27 -6
  140. data/lib/badline/snapshot/image.rb +4 -1
  141. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  142. data/lib/badline/snapshot/pet_setup.rb +27 -11
  143. data/lib/badline/snapshot/storage_setup.rb +4 -1
  144. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  145. data/lib/badline/snapshot/vice/pet.rb +13 -7
  146. data/lib/badline/snapshot/vice/pet_cpu.rb +39 -5
  147. data/lib/badline/snapshot/vice/pet_crtc.rb +15 -2
  148. data/lib/badline/snapshot/vice/pet_drive8050.rb +188 -68
  149. data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
  150. data/lib/badline/snapshot/vice/pet_restore.rb +40 -11
  151. data/lib/badline/snapshot.rb +4 -0
  152. data/lib/badline/status.rb +30 -30
  153. data/lib/badline/storage/d67_image.rb +55 -0
  154. data/lib/badline/storage/d90_image.rb +180 -0
  155. data/lib/badline/storage/disk_image/writing.rb +2 -2
  156. data/lib/badline/storage/kim_tape.rb +254 -0
  157. data/lib/badline/storage/papertape.rb +90 -0
  158. data/lib/badline/storage.rb +4 -0
  159. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  160. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  161. data/lib/badline/version.rb +1 -1
  162. data/lib/badline/vic20/keyed_modules.rb +6 -3
  163. data/lib/badline/vic20/keyed_state.rb +6 -4
  164. data/lib/badline/vic20.rb +26 -12
  165. data/lib/badline.rb +1 -0
  166. metadata +77 -1
@@ -195,7 +195,7 @@ module Badline
195
195
  @unfiltered = 0
196
196
  route_voice(voice1.output >> 7, 0x1)
197
197
  route_voice(voice2.output >> 7, 0x2)
198
- if @routing & 0x4 != 0
198
+ if @routing.anybits?(0x4)
199
199
  @input += voice3.output >> 7
200
200
  elsif !@voice3_off
201
201
  @unfiltered += voice3.output >> 7
@@ -216,9 +216,9 @@ module Badline
216
216
 
217
217
  def filtered
218
218
  value = 0
219
- value += @lowpass if @mode & 0x1 != 0
220
- value += @bandpass if @mode & 0x2 != 0
221
- value += @highpass if @mode & 0x4 != 0
219
+ value += @lowpass if @mode.anybits?(0x1)
220
+ value += @bandpass if @mode.anybits?(0x2)
221
+ value += @highpass if @mode.anybits?(0x4)
222
222
  value
223
223
  end
224
224
  end
@@ -234,7 +234,7 @@ module Badline
234
234
  def compare_pulse(phase = @accumulator >> 12) = (@pulse = @test || phase >= @pulse_width ? 0xfff : 0x000)
235
235
 
236
236
  # More than one bit set in the selection.
237
- def combined?(selected) = selected & (selected - 1) != 0
237
+ def combined?(selected) = selected.anybits?(selected - 1)
238
238
 
239
239
  # The one point in the cycle where the waveform selector drives the DAC:
240
240
  # either it latches a fresh value, or nothing drives the line and the
@@ -274,8 +274,8 @@ module Badline
274
274
  def advance
275
275
  previous = @accumulator
276
276
  @accumulator = (previous + @frequency) & 0xffffff
277
- @msb_rising = @accumulator & ~previous & MSB != 0
278
- if @accumulator & ~previous & 0x080000 != 0
277
+ @msb_rising = (@accumulator & ~previous).anybits?(MSB)
278
+ if (@accumulator & ~previous).anybits?(0x080000)
279
279
  @shift_pipeline = 2
280
280
  elsif @shift_pipeline != 0 && (@shift_pipeline -= 1).zero?
281
281
  write_back_shift
@@ -292,12 +292,12 @@ module Badline
292
292
  # TriXOR = !Saw & ((!V3 & Ring) ^ bit23), where V3 is the modulating
293
293
  # voice's MSB (SID/ringmod).
294
294
  def inverted?(selected)
295
- return false if selected & 0x2 != 0
295
+ return false if selected.anybits?(0x2)
296
296
 
297
- msb = @accumulator & MSB != 0
297
+ msb = @accumulator.anybits?(MSB)
298
298
  return msb unless @ring
299
299
 
300
- msb == (@sync_source.accumulator & MSB != 0)
300
+ msb == @sync_source.accumulator.anybits?(MSB)
301
301
  end
302
302
  end
303
303
  end
@@ -86,13 +86,15 @@ module Badline
86
86
  LINE_LAYOUT = 1
87
87
 
88
88
  # The VIC-I's fields in a keyed snapshot module (Snapshot::Keyed): the
89
- # registers, the beam, the text window's counters and the fetch in
90
- # flight, the painter's lines under line.*, the picture, and the sound
89
+ # registers, the beam, the frame's length in lines and whether the
90
+ # next interlaced field is the long one, the text window's counters
91
+ # and the fetch in flight, the painter's lines under line.*, the picture, and the sound
91
92
  # under sound.* What the registers decode to, where the line's fetches
92
93
  # end and the next event are worked out on load.
93
94
  module KeyedState
94
95
  def save_fields(out)
95
96
  out.bytes("registers", @registers).int("raster", @rasterline).int("column", @column)
97
+ out.int("frame_lines", @frame_lines).bool("long_field", @long_field)
96
98
  out.bool("window_open", @window_open).bool("window_done", @window_done).int("row_start", @row_start)
97
99
  out.int("matrix", @matrix).int("char_line", @char_line).int("text_rows", @text_rows)
98
100
  out.int("fetch_from", @fetch_from).int("fetched", @fetched).int("code", @code)
@@ -106,6 +108,8 @@ module Badline
106
108
  input.bytes_into("registers", @registers)
107
109
  @rasterline = input.int("raster", @rasterline)
108
110
  @column = input.int("column", @column)
111
+ @frame_lines = input.int("frame_lines", @lines_per_frame)
112
+ @long_field = input.bool("long_field", true)
109
113
  @window_open = input.bool("window_open", @window_open)
110
114
  @window_done = input.bool("window_done", @window_done)
111
115
  @row_start = input.int("row_start", @row_start)
@@ -122,6 +126,7 @@ module Badline
122
126
  end
123
127
 
124
128
  def restored!
129
+ @frame_wrap = @frame_lines == @interlaced_lines ? @count_column : @wrap_column
125
130
  @origin_x = @registers[0] & 0x7f
126
131
  @columns = @registers[2] & 0x7f
127
132
  @origin_y = @registers[1]
@@ -7,13 +7,15 @@ require "badline/chips/vic_i/keyed_parts"
7
7
 
8
8
  module Badline
9
9
  module Chips
10
- # The VIC-I, the 6561 on PAL machines: its sixteen registers, the
10
+ # The VIC-I, the 6561 on PAL machines or the 6560 on NTSC ones: its
11
+ # sixteen registers, the
11
12
  # raster, the text window's fetches over the V-bus and the picture. The
12
13
  # writes to the sound's registers, $900A-$900E, go on to the Sound
13
14
  # attached as #sound.
14
15
  #
15
16
  # The registers repeat through $9000-$90FF. $9003 bit 7 and $9004 read
16
- # the raster line, its bit 0 and bits 8-1. $9006 and $9007 read the
17
+ # the raster count, its bit 0 and bits 8-1, which on the 6561 is the
18
+ # raster line (see Profile for the 6560's). $9006 and $9007 read the
17
19
  # light pen, which nothing triggers yet, and $9008 and $9009 the two
18
20
  # pots, which read $FF with nothing plugged in. The other registers
19
21
  # read back what was written to them.
@@ -35,23 +37,48 @@ module Badline
35
37
  # of a text row where the row started, and the next row starts where
36
38
  # the last line of the row left it. It starts the frame at zero.
37
39
  #
38
- # The picture is 4 pixels a cycle, so 284 a line. A character's 8
39
- # pixels come out in the two cycles after its pattern fetch. The
40
- # display's lines start 6 cycles after the raster count moves, as
41
- # xvic's view of the frame does, so the character fetched 16 cycles
42
- # into the line lands 48 pixels in. Lines 0-27 are blanked.
40
+ # The picture is 4 pixels a cycle, so 284 a line on the 6561 and 260 on
41
+ # the 6560. A character's 8 pixels come out in the two cycles after its
42
+ # pattern fetch. The display's lines start 6 cycles after the raster
43
+ # line moves, as xvic's view of the frame does, so the character
44
+ # fetched 16 cycles into the line lands 48 pixels in. Lines 0-27 are
45
+ # blanked.
43
46
  class VICI
44
47
  include TextWindow
45
48
 
46
49
  # What a VIC-I sets for the machine: the clock, the raster's cycles
47
- # per line and lines per frame, and how many lines at the top of the
48
- # frame it blanks. The 6560 of NTSC machines, with its half lines and
49
- # the interlace of $9000 bit 7, would be another.
50
- Profile = Data.define(:clock_hz, :cycles_per_line, :lines_per_frame, :blanked_lines)
50
+ # per line and lines per frame, how many lines at the top of the frame
51
+ # it blanks and how many it shows from there, and where in the line
52
+ # the raster count $9003 and $9004 read moves on, and how many lines
53
+ # the long field of an interlaced frame has, or 0 without interlace.
54
+ #
55
+ # The 6561's count is the raster line. The 6560's runs ahead of it: it
56
+ # reads the next line from +count_column+ on, and on the last line it
57
+ # reads one past the last line from there, then 0 from
58
+ # +wrap_column+ on, so the count's first and last values each last
59
+ # about half a line. The 6561's +count_column+ is past the end of the
60
+ # line, so it never moves early.
61
+ #
62
+ # With $9000 bit 7 set at the start of a frame, the 6560 interlaces:
63
+ # its frames alternate between the long field, +interlaced_lines+
64
+ # long, and the short one, a line shorter, the long one first. On the
65
+ # long field's last line the count wraps to 0 where it would move on,
66
+ # so its first and last values each last a whole line.
67
+ Profile = Data.define(:clock_hz, :cycles_per_line, :lines_per_frame, :blanked_lines, :shown_lines,
68
+ :count_column, :wrap_column, :interlaced_lines)
51
69
 
52
70
  # The 6561: 71 cycles by 312 lines, lines 0-27 blanked (Marko
53
71
  # Mäkelä's VIC-I.txt).
54
- PAL = Profile.new(clock_hz: 1_108_405, cycles_per_line: 71, lines_per_frame: 312, blanked_lines: 28)
72
+ PAL = Profile.new(clock_hz: 1_108_405, cycles_per_line: 71, lines_per_frame: 312, blanked_lines: 28,
73
+ shown_lines: 284, count_column: 71, wrap_column: 71, interlaced_lines: 0)
74
+
75
+ # The 6560: 65 cycles by 261 lines on a 14.31818 MHz crystal divided
76
+ # by 14. The count's columns and the lines shown are xvic's, measured
77
+ # as a black box: the count moves on 28 cycles into the line, and
78
+ # wraps to 0 on the last line 61 cycles in. Interlaced, its fields are
79
+ # 263 and 262 lines.
80
+ NTSC = Profile.new(clock_hz: 1_022_727, cycles_per_line: 65, lines_per_frame: 261, blanked_lines: 28,
81
+ shown_lines: 233, count_column: 28, wrap_column: 61, interlaced_lines: 263)
55
82
 
56
83
  PIXELS_PER_CYCLE = 4
57
84
 
@@ -71,6 +98,10 @@ module Badline
71
98
  0xb27240, 0xf2b27f, 0xc2655d, 0xbdffff, 0xf299ff, 0x8de57f, 0x806eda, 0xffffa5
72
99
  ].freeze
73
100
 
101
+ # The display's lines: the frame's, or the long interlaced field's
102
+ # where that is longer.
103
+ attr_reader :height
104
+
74
105
  attr_reader :rasterline, :column, :profile
75
106
  attr_accessor :sound
76
107
 
@@ -78,7 +109,11 @@ module Badline
78
109
  @profile = profile
79
110
  @cycles_per_line = profile.cycles_per_line
80
111
  @lines_per_frame = profile.lines_per_frame
81
- @painter = Painter.new(@lines_per_frame, @cycles_per_line * PIXELS_PER_CYCLE, profile.blanked_lines)
112
+ @count_column = profile.count_column
113
+ @wrap_column = profile.wrap_column
114
+ @interlaced_lines = profile.interlaced_lines
115
+ @height = [@lines_per_frame, @interlaced_lines].max
116
+ @painter = Painter.new(@height, @cycles_per_line * PIXELS_PER_CYCLE, profile.blanked_lines)
82
117
  @patterns = @painter.patterns
83
118
  @colors = @painter.colors
84
119
  @counts = @painter.counts
@@ -106,11 +141,14 @@ module Badline
106
141
  @origin_x = @origin_y = @columns = @rows = 0
107
142
  @char_shift = 3
108
143
  @screen_base = @char_base = 0
144
+ @long_field = true
145
+ @frame_lines = @lines_per_frame
146
+ @frame_wrap = @wrap_column
109
147
  new_frame
110
148
  @fetch_from = @fetch_to = 0
111
149
  @fetched = @slot = 0
112
150
  @code = 0
113
- @painter.power_on!(@lines_per_frame - 1)
151
+ @painter.power_on!(@frame_lines - 1)
114
152
  schedule
115
153
  end
116
154
 
@@ -131,14 +169,12 @@ module Badline
131
169
  # The sixteen registers as written.
132
170
  def register_file = @registers.dup
133
171
 
134
- # The display, a palette index a pixel, 284 to a line and every line
135
- # of the frame.
172
+ # The display, a palette index a pixel, 4 to a cycle of the line, and
173
+ # every line of the frame.
136
174
  def display = @painter.display
137
175
 
138
176
  def width = @painter.width
139
177
 
140
- def height = @lines_per_frame
141
-
142
178
  # Whether each line of the display has changed since the front end
143
179
  # last cleared them.
144
180
  def dirty_lines = @painter.dirty_lines
@@ -157,8 +193,8 @@ module Badline
157
193
  def peek(addr)
158
194
  register = addr & 0x0f
159
195
  case register
160
- when 0x03 then ((@rasterline & 0x01) << 7) | (@registers[3] & 0x7f)
161
- when 0x04 then @rasterline >> 1
196
+ when 0x03 then ((raster_count & 0x01) << 7) | (@registers[3] & 0x7f)
197
+ when 0x04 then raster_count >> 1
162
198
  when 0x06, 0x07 then 0
163
199
  when 0x08, 0x09 then 0xff
164
200
  else @registers[register]
@@ -181,6 +217,16 @@ module Badline
181
217
  end
182
218
  end
183
219
 
220
+ # The raster count $9003 and $9004 read (see Profile).
221
+ def raster_count
222
+ line = @rasterline
223
+ column = @column
224
+ return line if column < @count_column
225
+ return 0 if line == @frame_lines - 1 && column >= @frame_wrap
226
+
227
+ line + 1
228
+ end
229
+
184
230
  private
185
231
 
186
232
  def event(column)
@@ -205,9 +251,10 @@ module Badline
205
251
 
206
252
  def start_line
207
253
  line = @rasterline + 1
208
- if line == @lines_per_frame
254
+ if line == @frame_lines
209
255
  line = 0
210
256
  new_frame
257
+ next_field
211
258
  end
212
259
  @rasterline = line
213
260
  next_text_line if @window_open
@@ -215,6 +262,22 @@ module Badline
215
262
  place_fetches
216
263
  end
217
264
 
265
+ # How many lines the frame starting has, and where on its last line
266
+ # the count wraps: the field's, if $9000 bit 7 asks the 6560 to
267
+ # interlace, else the profile's.
268
+ def next_field
269
+ if @interlaced_lines.positive? && @registers[0] >= 0x80
270
+ long = @long_field
271
+ @frame_lines = long ? @interlaced_lines : @interlaced_lines - 1
272
+ @frame_wrap = long ? @count_column : @wrap_column
273
+ @long_field = !long
274
+ else
275
+ @frame_lines = @lines_per_frame
276
+ @frame_wrap = @wrap_column
277
+ @long_field = true
278
+ end
279
+ end
280
+
218
281
  def place_fetches
219
282
  line = @rasterline
220
283
  @slot = line * Painter::SLOTS
@@ -24,10 +24,10 @@ module Badline
24
24
  File.join(xdg, "badline")
25
25
  end
26
26
 
27
- # The quicksaves, autosaves or saves subfolder of data_path, created
28
- # along with data_path if it doesn't exist yet.
27
+ # The quicksaves, autosaves, saves or tapes subfolder of data_path,
28
+ # created along with data_path if it doesn't exist yet.
29
29
  def data_folder(name)
30
- raise ArgumentError, "unknown data folder: #{name}" unless %w[quicksaves autosaves saves].include?(name)
30
+ raise ArgumentError, "unknown data folder: #{name}" unless %w[quicksaves autosaves saves tapes].include?(name)
31
31
 
32
32
  path = File.join(data_path, name)
33
33
  FileUtils.mkdir_p(path)
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ # The true drives the PET's and the CBM-II's IEEE-488 bus takes, by the
6
+ # names --drive-type gives them, xpet's drive types, and the disk images
7
+ # each reads. Options reads it to check --drive-type, and
8
+ # Media::TrueDrive to pick the drive an image goes in, so it loads
9
+ # nothing else. Machine::IEEEDrives builds each.
10
+ module IEEEModels
11
+ # The images each model reads, by extension.
12
+ MEDIA = {
13
+ "2031" => %w[.d64 .g64], "8050" => %w[.d80], "8250" => %w[.d82], "1001" => %w[.d82], "4040" => %w[.d64 .d67],
14
+ "3040" => %w[.d64 .d67], "2040" => %w[.d67], "9060" => %w[.d90], "9090" => %w[.d90]
15
+ }.freeze
16
+
17
+ # The model --true-drive plugs in without --drive-type.
18
+ DEFAULT = "2031"
19
+
20
+ # The model an image goes in when neither the drive on device 8 nor
21
+ # the machine's own model reads it. A .d90 goes by its size
22
+ # (hard_disk_model).
23
+ FOR_IMAGES = { ".d64" => "2031", ".g64" => "2031", ".d80" => "8050", ".d82" => "8250", ".d67" => "2040" }.freeze
24
+
25
+ NAMES = MEDIA.keys.freeze
26
+
27
+ module_function
28
+
29
+ # Whether the image at path is a hard disk's, which only a true drive
30
+ # reads.
31
+ def hard_disk?(path) = File.extname(path).casecmp?(".d90")
32
+
33
+ # The hard disk a .d90 image belongs to, by its size: a D9090 for a
34
+ # D9090's, and otherwise a D9060.
35
+ def hard_disk_model(path) = Storage::D90Image.heads_for(File.size?(path).to_i) == 6 ? "9090" : "9060"
36
+
37
+ # Whether the model reads the image at path.
38
+ def reads?(model, path) = MEDIA.fetch(model, []).include?(File.extname(path).downcase)
39
+
40
+ # The model the image at path goes in: the drive's on device 8,
41
+ # `present`, if it reads it, or else the machine's own, `chosen`, or
42
+ # else the image's own, or nil for an image none reads. A .d90 goes
43
+ # in the machine's own model if that is a hard disk, and otherwise in
44
+ # the one its size names.
45
+ def taking(path, present, chosen)
46
+ return reads?(chosen, path) ? chosen : hard_disk_model(path) if hard_disk?(path)
47
+ return present if present && reads?(present, path)
48
+ return chosen if reads?(chosen, path)
49
+
50
+ FOR_IMAGES[File.extname(path).downcase]
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/model8050"
4
+
5
+ module Badline
6
+ module Drives
7
+ # The SFD-1001: the 8250LP's board redrawn as one PCB with one
8
+ # double-sided drive, a Matsushita JU-570-2 (schematic 251406). Both
9
+ # processors see an 8250. The interface processor runs DOS 2.7, which
10
+ # doesn't know there is one drive, and the controller runs 251257-02A
11
+ # from the EPROM on its 6530 adaptor (251252-01): the 8250LP's program
12
+ # with an interrupt every 3 ms for the JU-570's step rate, a longer head
13
+ # settle, and every job for drive 1 ended with an error. Drive 1's
14
+ # stepper, motor and LED aren't wired, so the unit takes no disk there,
15
+ # and its LED never lights.
16
+ class Model1001 < Model8250
17
+ # The LED bit of drive 1, which the board leaves unwired.
18
+ DRIVE1_LED = 0x01
19
+
20
+ def model_name = "1001"
21
+
22
+ # +fdc_rom+ defaults to 251257-02A, the rest as Model8050's.
23
+ def initialize(dos_rom: nil, fdc_rom: nil, host_clock_hz: CLOCK_HZ, device: 8)
24
+ super(dos_rom:, fdc_rom: fdc_rom || ROM.read("1001/fdc-251257-02a.rom"), host_clock_hz:, device:)
25
+ end
26
+
27
+ def leds = super & ~DRIVE1_LED
28
+
29
+ # Puts a Disk in drive 0, the only one. Drive 1 raises ArgumentError.
30
+ def insert(disk, drive: 0)
31
+ raise ArgumentError, "the 1001 has one drive, not drive #{drive}" unless drive.zero?
32
+
33
+ super
34
+ end
35
+ end
36
+ end
37
+ end
@@ -34,6 +34,12 @@ module Badline
34
34
 
35
35
  def model_name = "2031"
36
36
 
37
+ def boot_cycles = BOOT_CYCLES
38
+
39
+ # Whether the drive is where VICE saves it: its CPU between
40
+ # instructions.
41
+ def settled_for_vice? = cpu.boundary?
42
+
37
43
  # +rom+ covers $C000-$FFFF, and defaults to the DOS image in the ROM
38
44
  # path. +device+ is the number the jumpers set, 8 to 11.
39
45
  def initialize(rom: nil, host_clock_hz: CLOCK_HZ, device: 8, debug: false)
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model4040
6
+ # A .d64 image as the 4040's controller finds it on the disk, each
7
+ # track a ring of byte cells as on the 8050 (Model8050::Disk), laid
8
+ # out as DOS 2.1's format writes it, sector after sector:
9
+ #
10
+ # 4 sync, $08, checksum, sector, track, ID 2, ID 1,
11
+ # 9 x $00, 4 sync, $07, 256 data bytes, checksum, then $00
12
+ #
13
+ # nine cells being the header gap the interface processor gives the
14
+ # controller ($049D, from DOS 2.1's SECTRK table), past which a write
15
+ # puts its syncs. A .d67, DOS 1's disk, is laid out as DOS 1 writes a
16
+ # sector: 2 sync, the header, one $08, 2 sync, $07, the data and its
17
+ # checksum. DOS 1's format writes headers only, so this is the track as
18
+ # if every sector had been written. The disk's ID is the BAM's, at
19
+ # 18/0 $A2. A turn passes 80,000 / (16 - zone) cells, the 16 MHz bit
20
+ # clock's.
21
+ class Disk < Model8050::Disk
22
+ # How many tracks a disk holds: 35.
23
+ def side_tracks = 35
24
+
25
+ def sides = 1
26
+
27
+ # The cells a track of the zone holds in one turn at 300 rpm: the
28
+ # 200,000 µs of a turn over 2.5 µs times 16 less the zone a byte.
29
+ def self.cells_in(zone) = 80_000 / (16 - zone)
30
+
31
+ # The zone a track is recorded in, 3 outermost to 0 innermost.
32
+ def self.zone_of(track) = Storage::D64Image.zone(track)
33
+
34
+ private
35
+
36
+ def build(number)
37
+ id1, id2 = disk_id
38
+ cells = []
39
+ sectors = @image.sectors_in(number)
40
+ dos1 = @image.is_a?(Storage::D67Image)
41
+ sectors.times do |sector|
42
+ data = @image.read_block(number, sector)
43
+ header = [0x08, number ^ sector ^ id1 ^ id2, sector, number, id2, id1]
44
+ if dos1
45
+ cells.push(SYNC, SYNC, *header, 0x08, SYNC, SYNC, 0x07)
46
+ else
47
+ cells.push(SYNC, SYNC, SYNC, SYNC, *header, *Array.new(9, 0x00), SYNC, SYNC, SYNC, SYNC, 0x07)
48
+ end
49
+ cells.concat(data)
50
+ cells << checksum(data)
51
+ end
52
+ pad(cells, sectors, self.class.cells_in(self.class.zone_of(number)))
53
+ end
54
+
55
+ def disk_id
56
+ bam = @image.read_block(18, 0)
57
+ [bam[0xa2], bam[0xa3]]
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model4040
6
+ # The interface processor's bus (schematic 320806, sheet 1). UB3
7
+ # decodes A12-A14 only, so the map repeats every 32 KB:
8
+ #
9
+ # $0000-$0FFF - the two 6532s, as on the 8050 (Model8050::IPBus)
10
+ # $1000-$4FFF - the common RAM, 1 KB in each block, mirrored through it
11
+ # $5000-$7FFF - the ROM sockets UJ1, UL1 and UH1, seen again at
12
+ # $D000-$FFFF, where the processor finds its vectors
13
+ #
14
+ # A socket left empty, as UJ1 is under DOS 1, reads as open bus: the
15
+ # last byte on the data bus.
16
+ class IPBus < Model8050::IPBus
17
+ def peek(addr)
18
+ addr &= 0x7fff
19
+ return @data = rom_peek(addr | 0x8000) if addr >= 0x5000
20
+
21
+ @data = if addr < 0x1000
22
+ peek_riots(addr)
23
+ else
24
+ @common.peek((((addr >> 12) - 1) << 10) | (addr & 0x03ff))
25
+ end
26
+ end
27
+
28
+ def poke(addr, value)
29
+ @data = value
30
+ addr &= 0x7fff
31
+ return if addr >= 0x5000
32
+
33
+ if addr < 0x1000
34
+ poke_riots(addr, value)
35
+ else
36
+ poke_common((((addr >> 12) - 1) << 10) | (addr & 0x03ff), value)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def rom_peek(addr) = addr >= @rom.start ? @rom.peek(addr) : @data
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model4040
6
+ class Disk
7
+ def image_format = @image.is_a?(Storage::D67Image) ? "d67" : "d64"
8
+
9
+ # The disk a BL-D8-DISK module holds, as Model8050::Disk's, from a
10
+ # .d64 or a .d67.
11
+ def self.from_fields(input, detached: false)
12
+ path = input.text("path", "")
13
+ read_only = input.bool("read_only", false) || detached
14
+ kind = input.choice("image.format", %w[d64 d67], "d64")
15
+ bytes = input.bytes("image", "".b).bytes
16
+ path = "" if detached || !File.file?(path)
17
+ image = if kind == "d67"
18
+ Storage::D67Image.new(path, read_only:, bytes:)
19
+ else
20
+ Storage::D64Image.new(path, read_only:, bytes:)
21
+ end
22
+ new(image).load_track_fields(input)
23
+ end
24
+ end
25
+
26
+ class Mechanism
27
+ # The drives' fields in a keyed snapshot module (BL-D8-MECH), a value
28
+ # a drive in each list: the heads' half tracks, the stepper counts,
29
+ # each disk's place in its turn and its bit clock and the motors,
30
+ # then byte ready, the byte on VIA port A and SYNC. The disks load
31
+ # from their own modules.
32
+ module KeyedState
33
+ def save_fields(out)
34
+ out.ints("half_tracks", @half_tracks).ints("phases", @phases).ints("positions", @positions)
35
+ out.ints("bit_clocks", @clocks)
36
+ out.ints("motors", @motors.map { |on| on ? 1 : 0 }).ints("syncs", @syncs.map { |low| low ? 1 : 0 })
37
+ out.bool("ca1_low", @ca1_low).int("data_in", @data_in)
38
+ end
39
+
40
+ def load_fields(input, disks)
41
+ @disks = disks
42
+ input.ints_into("half_tracks", @half_tracks)
43
+ input.ints_into("phases", @phases)
44
+ input.ints_into("positions", @positions)
45
+ input.ints_into("bit_clocks", @clocks)
46
+ @motors = input.ints("motors", [0, 0]).map { |on| on == 1 }
47
+ @syncs = input.ints("syncs", [0, 0]).map { |low| low == 1 }
48
+ @ca1_low = input.bool("ca1_low", @ca1_low)
49
+ @data_in = input.byte("data_in", @data_in)
50
+ end
51
+ end
52
+
53
+ include KeyedState
54
+ end
55
+ end
56
+ end
57
+ end