badline 0.17.0 → 0.19.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 (195) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +193 -0
  3. data/CLAUDE.md +19 -7
  4. data/CONTRIBUTING.md +11 -1
  5. data/README.md +178 -26
  6. data/doc/library.md +5 -1
  7. data/doc/media.md +3 -2
  8. data/doc/pinned-behaviour.md +169 -1
  9. data/doc/snapshot-format.md +302 -35
  10. data/doc/snapshots.md +97 -26
  11. data/doc/vice.md +344 -0
  12. data/exe/badline-ruby +5 -0
  13. data/lib/badline/audio/renderer.rb +4 -8
  14. data/lib/badline/audio/terminal.rb +34 -2
  15. data/lib/badline/c128/keyed_state.rb +2 -0
  16. data/lib/badline/c128.rb +5 -0
  17. data/lib/badline/c64/bus/fittings.rb +6 -0
  18. data/lib/badline/c64/keyed_state.rb +2 -0
  19. data/lib/badline/c64.rb +7 -0
  20. data/lib/badline/cbm2/keyed_state.rb +4 -0
  21. data/lib/badline/cbm2/p500.rb +9 -3
  22. data/lib/badline/cbm2.rb +10 -3
  23. data/lib/badline/chips/cia/timer.rb +2 -2
  24. data/lib/badline/chips/cpu/core.rb +29 -4
  25. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  26. data/lib/badline/chips/cpu/interrupts.rb +9 -0
  27. data/lib/badline/chips/cpu/keyed_state.rb +15 -9
  28. data/lib/badline/chips/cpu/microcode.rb +10 -0
  29. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  30. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  31. data/lib/badline/chips/m6809/core.rb +149 -0
  32. data/lib/badline/chips/m6809/indexed.rb +82 -0
  33. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  34. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  35. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  36. data/lib/badline/chips/m6809/operations.rb +258 -0
  37. data/lib/badline/chips/m6809/plans.rb +143 -0
  38. data/lib/badline/chips/m6809/registers.rb +82 -0
  39. data/lib/badline/chips/m6809/stack.rb +140 -0
  40. data/lib/badline/chips/m6809.rb +13 -0
  41. data/lib/badline/chips/riot/timer.rb +14 -3
  42. data/lib/badline/chips/rriot.rb +8 -3
  43. data/lib/badline/chips/sid/decimator.rb +1 -1
  44. data/lib/badline/chips/sid/filter.rb +4 -4
  45. data/lib/badline/chips/sid/keyed_parts.rb +51 -18
  46. data/lib/badline/chips/sid/waveform.rb +6 -6
  47. data/lib/badline/chips/sid.rb +6 -2
  48. data/lib/badline/chips/vic_i/keyed_parts.rb +12 -2
  49. data/lib/badline/chips/vic_i/light_pen.rb +89 -0
  50. data/lib/badline/chips/vic_i.rb +91 -23
  51. data/lib/badline/data_path.rb +3 -3
  52. data/lib/badline/drives/ieee_models.rb +54 -0
  53. data/lib/badline/drives/model1001.rb +37 -0
  54. data/lib/badline/drives/model1541/sector_reader.rb +20 -3
  55. data/lib/badline/drives/model1541.rb +1 -0
  56. data/lib/badline/drives/model1551.rb +1 -0
  57. data/lib/badline/drives/model1571.rb +1 -0
  58. data/lib/badline/drives/model1581.rb +1 -0
  59. data/lib/badline/drives/model2031.rb +7 -0
  60. data/lib/badline/drives/model4040/disk.rb +62 -0
  61. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  62. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  63. data/lib/badline/drives/model4040/mechanism.rb +195 -0
  64. data/lib/badline/drives/model4040/parts.rb +20 -0
  65. data/lib/badline/drives/model4040.rb +195 -0
  66. data/lib/badline/drives/model8050/board.rb +79 -0
  67. data/lib/badline/drives/model8050/dos_board.rb +134 -0
  68. data/lib/badline/drives/model8050/fdc_bus.rb +7 -0
  69. data/lib/badline/drives/model8050/idle.rb +148 -31
  70. data/lib/badline/drives/model8050/keyed_state.rb +5 -2
  71. data/lib/badline/drives/model8050.rb +32 -130
  72. data/lib/badline/drives/model9060/idle.rb +169 -0
  73. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  74. data/lib/badline/drives/model9060/parts.rb +98 -0
  75. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  76. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  77. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  78. data/lib/badline/drives/model9060.rb +197 -0
  79. data/lib/badline/frontend/app.rb +8 -0
  80. data/lib/badline/frontend/boot.rb +3 -2
  81. data/lib/badline/frontend/controls.rb +62 -6
  82. data/lib/badline/frontend/keys.rb +36 -0
  83. data/lib/badline/frontend/kim1_panel.rb +208 -0
  84. data/lib/badline/frontend/media_slots.rb +8 -1
  85. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  86. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  87. data/lib/badline/frontend/menu/menu_media.rb +42 -12
  88. data/lib/badline/frontend/menu/menu_pages.rb +97 -2
  89. data/lib/badline/frontend/menu/menu_ports.rb +38 -1
  90. data/lib/badline/frontend/menu/pause_menu.rb +5 -1
  91. data/lib/badline/frontend/pet_keyboard.rb +23 -1
  92. data/lib/badline/frontend/screen.rb +1 -1
  93. data/lib/badline/frontend/screens.rb +40 -2
  94. data/lib/badline/frontend/teletype_pane.rb +152 -0
  95. data/lib/badline/frontend/timeline.rb +31 -2
  96. data/lib/badline/frontend.rb +3 -0
  97. data/lib/badline/input/joystick.rb +15 -2
  98. data/lib/badline/kim1/bus.rb +117 -0
  99. data/lib/badline/kim1/cpu.rb +25 -0
  100. data/lib/badline/kim1/host_terminal.rb +74 -0
  101. data/lib/badline/kim1/key_entry.rb +74 -0
  102. data/lib/badline/kim1/keyed_modules.rb +53 -0
  103. data/lib/badline/kim1/keyed_state.rb +133 -0
  104. data/lib/badline/kim1/panel.rb +176 -0
  105. data/lib/badline/kim1/rriots.rb +28 -0
  106. data/lib/badline/kim1/sound.rb +29 -0
  107. data/lib/badline/kim1/tape.rb +118 -0
  108. data/lib/badline/kim1/teletype.rb +193 -0
  109. data/lib/badline/kim1.rb +194 -0
  110. data/lib/badline/machine/ieee_drives.rb +94 -117
  111. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  112. data/lib/badline/machine/line_sound.rb +122 -0
  113. data/lib/badline/machine.rb +49 -15
  114. data/lib/badline/media/extensions.rb +8 -4
  115. data/lib/badline/media/kim1_media.rb +150 -0
  116. data/lib/badline/media/pet_media.rb +3 -1
  117. data/lib/badline/media/true_drive.rb +52 -45
  118. data/lib/badline/media/vic20_media.rb +4 -2
  119. data/lib/badline/media.rb +16 -6
  120. data/lib/badline/memory.rb +2 -4
  121. data/lib/badline/options/family.rb +98 -8
  122. data/lib/badline/options/help.rb +44 -12
  123. data/lib/badline/options/table.rb +44 -12
  124. data/lib/badline/options/validation.rb +44 -8
  125. data/lib/badline/options.rb +27 -8
  126. data/lib/badline/pet/bus.rb +16 -2
  127. data/lib/badline/pet/keyboard_ports.rb +9 -2
  128. data/lib/badline/pet/keyed_state.rb +4 -0
  129. data/lib/badline/pet/model.rb +11 -2
  130. data/lib/badline/pet/rom_sets.rb +1 -0
  131. data/lib/badline/pet/sound.rb +5 -105
  132. data/lib/badline/pet/superpet/acia.rb +12 -0
  133. data/lib/badline/pet/superpet/board.rb +221 -0
  134. data/lib/badline/pet/superpet/dongle.rb +92 -0
  135. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  136. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  137. data/lib/badline/pet/superpet/m6809.rb +32 -0
  138. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  139. data/lib/badline/pet/superpet.rb +221 -0
  140. data/lib/badline/pet.rb +23 -3
  141. data/lib/badline/plus4/keyed_state.rb +2 -0
  142. data/lib/badline/plus4.rb +5 -0
  143. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  144. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  145. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  146. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  147. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  148. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  149. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  150. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  151. data/lib/badline/roms/README +176 -0
  152. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  153. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  154. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  155. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  156. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  157. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  158. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  159. data/lib/badline/snapshot/container.rb +8 -4
  160. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  161. data/lib/badline/snapshot/field_doc.rb +35 -10
  162. data/lib/badline/snapshot/image.rb +19 -7
  163. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  164. data/lib/badline/snapshot/pet_setup.rb +27 -11
  165. data/lib/badline/snapshot/storage_setup.rb +4 -1
  166. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  167. data/lib/badline/snapshot/vice/pet.rb +14 -7
  168. data/lib/badline/snapshot/vice/pet_cpu.rb +39 -5
  169. data/lib/badline/snapshot/vice/pet_crtc.rb +15 -2
  170. data/lib/badline/snapshot/vice/pet_dongle.rb +72 -0
  171. data/lib/badline/snapshot/vice/pet_drive8050.rb +188 -68
  172. data/lib/badline/snapshot/vice/pet_mem.rb +55 -5
  173. data/lib/badline/snapshot/vice/pet_restore.rb +40 -11
  174. data/lib/badline/snapshot/vice/vic20.rb +70 -0
  175. data/lib/badline/snapshot/vice/vic20_cpu.rb +61 -0
  176. data/lib/badline/snapshot/vice/vic20_mem.rb +85 -0
  177. data/lib/badline/snapshot/vice/vic20_restore.rb +124 -0
  178. data/lib/badline/snapshot/vice/vic20_vici.rb +200 -0
  179. data/lib/badline/snapshot/vice.rb +1 -0
  180. data/lib/badline/snapshot.rb +8 -4
  181. data/lib/badline/status.rb +30 -30
  182. data/lib/badline/storage/d67_image.rb +55 -0
  183. data/lib/badline/storage/d90_image.rb +180 -0
  184. data/lib/badline/storage/kim_tape.rb +254 -0
  185. data/lib/badline/storage/papertape.rb +90 -0
  186. data/lib/badline/storage.rb +4 -0
  187. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  188. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  189. data/lib/badline/version.rb +1 -1
  190. data/lib/badline/vic20/keyed_modules.rb +6 -3
  191. data/lib/badline/vic20/keyed_state.rb +6 -4
  192. data/lib/badline/vic20/port_wiring.rb +11 -0
  193. data/lib/badline/vic20.rb +30 -12
  194. data/lib/badline.rb +1 -0
  195. metadata +85 -1
@@ -86,16 +86,20 @@ 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 light pen's latch and line under pen.*,
92
+ # the painter's lines under line.*, the picture, and the sound
91
93
  # under sound.* What the registers decode to, where the line's fetches
92
94
  # end and the next event are worked out on load.
93
95
  module KeyedState
94
96
  def save_fields(out)
95
97
  out.bytes("registers", @registers).int("raster", @rasterline).int("column", @column)
98
+ out.int("frame_lines", @frame_lines).bool("long_field", @long_field)
96
99
  out.bool("window_open", @window_open).bool("window_done", @window_done).int("row_start", @row_start)
97
100
  out.int("matrix", @matrix).int("char_line", @char_line).int("text_rows", @text_rows)
98
101
  out.int("fetch_from", @fetch_from).int("fetched", @fetched).int("code", @code)
102
+ out.int("pen.x", @pen_x).int("pen.y", @pen_y).bool("pen.low", @pen_low)
99
103
  out.int("line.layout", LINE_LAYOUT)
100
104
  @painter.save_fields(out)
101
105
  @sound&.save_fields(out, "sound.")
@@ -106,6 +110,8 @@ module Badline
106
110
  input.bytes_into("registers", @registers)
107
111
  @rasterline = input.int("raster", @rasterline)
108
112
  @column = input.int("column", @column)
113
+ @frame_lines = input.int("frame_lines", @lines_per_frame)
114
+ @long_field = input.bool("long_field", true)
109
115
  @window_open = input.bool("window_open", @window_open)
110
116
  @window_done = input.bool("window_done", @window_done)
111
117
  @row_start = input.int("row_start", @row_start)
@@ -115,6 +121,9 @@ module Badline
115
121
  @fetch_from = input.int("fetch_from", @fetch_from)
116
122
  @fetched = input.int("fetched", @fetched)
117
123
  @code = input.byte("code", @code)
124
+ @pen_x = input.byte("pen.x", @pen_x)
125
+ @pen_y = input.byte("pen.y", @pen_y)
126
+ @pen_low = input.bool("pen.low", @pen_low)
118
127
  line = input.int("line.layout", 0) == LINE_LAYOUT
119
128
  input.adjust("line in progress from another renderer, restarted") unless line || input.missing?
120
129
  @painter.load_fields(input, line)
@@ -122,6 +131,7 @@ module Badline
122
131
  end
123
132
 
124
133
  def restored!
134
+ @frame_wrap = @frame_lines == @interlaced_lines ? @count_column : @wrap_column
125
135
  @origin_x = @registers[0] & 0x7f
126
136
  @columns = @registers[2] & 0x7f
127
137
  @origin_y = @registers[1]
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class VICI
6
+ # The light pen input, the line the VIC-20 shares with the joystick's
7
+ # fire button, and the position a falling edge on it latches into
8
+ # $9006 and $9007.
9
+ #
10
+ # $9006 reads 2h + 1, where h is the edge's column plus two, wrapping
11
+ # at the line's end, and $9007 bits 8-1 of the raster count in the
12
+ # cycle after the edge. Around the cycle where the count turns to 0
13
+ # the latch reads otherwise:
14
+ #
15
+ # - An edge from 8 cycles before that cycle to 1 after it latches as
16
+ # an edge in the cycle after it would, with $9007 at 0, but for an
17
+ # edge 2 cycles before it, which latches an even $9006, one less.
18
+ # - Before that, an edge latches nothing while the count read its
19
+ # last value of the frame in the cycle before the edge: line 311 on
20
+ # the 6561, and 261, past the last line, on the 6560.
21
+ module LightPen
22
+ # Where the window around the count's turn to 0 starts and ends,
23
+ # in cycles from that turn, and the edge in it that latches an even
24
+ # $9006.
25
+ WRAP_WINDOW = (-8..1)
26
+ WRAP_EVEN = -2
27
+
28
+ # The pen's line, low while the pen sees the beam or fire is held.
29
+ def lightpen_level(high)
30
+ low = !high
31
+ return if low == @pen_low
32
+
33
+ @pen_low = low
34
+ pen_fell if low
35
+ end
36
+
37
+ private
38
+
39
+ def power_on_pen!
40
+ @pen_x = @pen_y = 0
41
+ @pen_low = false
42
+ end
43
+
44
+ def pen_fell
45
+ line = @rasterline
46
+ column = @column
47
+ from_wrap = cycles_from_wrap(line, column)
48
+ if WRAP_WINDOW.cover?(from_wrap)
49
+ latch_pen(@frame_wrap + 1, 0, from_wrap == WRAP_EVEN)
50
+ elsif !pen_blocked?(line, column)
51
+ next_column = column + 1
52
+ if next_column == @cycles_per_line
53
+ next_column = 0
54
+ line = line == @frame_lines - 1 ? 0 : line + 1
55
+ end
56
+ latch_pen(column, count_at(line, next_column) >> 1, false)
57
+ end
58
+ end
59
+
60
+ # How many cycles the beam is past the cycle where the count turns
61
+ # to 0, negative before it. On the 6561 the count turns as the
62
+ # frame starts, so the frame's first cycles are past it.
63
+ def cycles_from_wrap(line, column)
64
+ position = (line * @cycles_per_line) + column
65
+ return position if position < 2 && @frame_wrap == @cycles_per_line
66
+
67
+ position - ((@frame_lines - 1) * @cycles_per_line) - @frame_wrap
68
+ end
69
+
70
+ # Whether the count read its last value of the frame in the cycle
71
+ # before this one.
72
+ def pen_blocked?(line, column)
73
+ column -= 1
74
+ if column.negative?
75
+ column = @cycles_per_line - 1
76
+ line = line.zero? ? @frame_lines - 1 : line - 1
77
+ end
78
+ last = @frame_wrap > @count_column ? @frame_lines : @frame_lines - 1
79
+ count_at(line, column) == last
80
+ end
81
+
82
+ def latch_pen(column, count, even)
83
+ @pen_x = (2 * ((column + 2) % @cycles_per_line)) + (even ? 0 : 1)
84
+ @pen_y = count
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -2,19 +2,22 @@
2
2
 
3
3
  require "badline/chips/vic_i/painter"
4
4
  require "badline/chips/vic_i/text_window"
5
+ require "badline/chips/vic_i/light_pen"
5
6
  require "badline/chips/vic_i/sound"
6
7
  require "badline/chips/vic_i/keyed_parts"
7
8
 
8
9
  module Badline
9
10
  module Chips
10
- # The VIC-I, the 6561 on PAL machines: its sixteen registers, the
11
+ # The VIC-I, the 6561 on PAL machines or the 6560 on NTSC ones: its
12
+ # sixteen registers, the
11
13
  # raster, the text window's fetches over the V-bus and the picture. The
12
14
  # writes to the sound's registers, $900A-$900E, go on to the Sound
13
15
  # attached as #sound.
14
16
  #
15
17
  # 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
- # light pen, which nothing triggers yet, and $9008 and $9009 the two
18
+ # the raster count, its bit 0 and bits 8-1, which on the 6561 is the
19
+ # raster line (see Profile for the 6560's). $9006 and $9007 read the
20
+ # light pen's latch (LightPen), and $9008 and $9009 the two
18
21
  # pots, which read $FF with nothing plugged in. The other registers
19
22
  # read back what was written to them.
20
23
  #
@@ -35,23 +38,49 @@ module Badline
35
38
  # of a text row where the row started, and the next row starts where
36
39
  # the last line of the row left it. It starts the frame at zero.
37
40
  #
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.
41
+ # The picture is 4 pixels a cycle, so 284 a line on the 6561 and 260 on
42
+ # the 6560. A character's 8 pixels come out in the two cycles after its
43
+ # pattern fetch. The display's lines start 6 cycles after the raster
44
+ # line moves, as xvic's view of the frame does, so the character
45
+ # fetched 16 cycles into the line lands 48 pixels in. Lines 0-27 are
46
+ # blanked.
43
47
  class VICI
44
48
  include TextWindow
49
+ include LightPen
45
50
 
46
51
  # 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)
52
+ # per line and lines per frame, how many lines at the top of the frame
53
+ # it blanks and how many it shows from there, and where in the line
54
+ # the raster count $9003 and $9004 read moves on, and how many lines
55
+ # the long field of an interlaced frame has, or 0 without interlace.
56
+ #
57
+ # The 6561's count is the raster line. The 6560's runs ahead of it: it
58
+ # reads the next line from +count_column+ on, and on the last line it
59
+ # reads one past the last line from there, then 0 from
60
+ # +wrap_column+ on, so the count's first and last values each last
61
+ # about half a line. The 6561's +count_column+ is past the end of the
62
+ # line, so it never moves early.
63
+ #
64
+ # With $9000 bit 7 set at the start of a frame, the 6560 interlaces:
65
+ # its frames alternate between the long field, +interlaced_lines+
66
+ # long, and the short one, a line shorter, the long one first. On the
67
+ # long field's last line the count wraps to 0 where it would move on,
68
+ # so its first and last values each last a whole line.
69
+ Profile = Data.define(:clock_hz, :cycles_per_line, :lines_per_frame, :blanked_lines, :shown_lines,
70
+ :count_column, :wrap_column, :interlaced_lines)
51
71
 
52
72
  # The 6561: 71 cycles by 312 lines, lines 0-27 blanked (Marko
53
73
  # 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)
74
+ PAL = Profile.new(clock_hz: 1_108_405, cycles_per_line: 71, lines_per_frame: 312, blanked_lines: 28,
75
+ shown_lines: 284, count_column: 71, wrap_column: 71, interlaced_lines: 0)
76
+
77
+ # The 6560: 65 cycles by 261 lines on a 14.31818 MHz crystal divided
78
+ # by 14. The count's columns and the lines shown are xvic's, measured
79
+ # as a black box: the count moves on 28 cycles into the line, and
80
+ # wraps to 0 on the last line 61 cycles in. Interlaced, its fields are
81
+ # 263 and 262 lines.
82
+ NTSC = Profile.new(clock_hz: 1_022_727, cycles_per_line: 65, lines_per_frame: 261, blanked_lines: 28,
83
+ shown_lines: 233, count_column: 28, wrap_column: 61, interlaced_lines: 263)
55
84
 
56
85
  PIXELS_PER_CYCLE = 4
57
86
 
@@ -71,6 +100,10 @@ module Badline
71
100
  0xb27240, 0xf2b27f, 0xc2655d, 0xbdffff, 0xf299ff, 0x8de57f, 0x806eda, 0xffffa5
72
101
  ].freeze
73
102
 
103
+ # The display's lines: the frame's, or the long interlaced field's
104
+ # where that is longer.
105
+ attr_reader :height
106
+
74
107
  attr_reader :rasterline, :column, :profile
75
108
  attr_accessor :sound
76
109
 
@@ -78,7 +111,11 @@ module Badline
78
111
  @profile = profile
79
112
  @cycles_per_line = profile.cycles_per_line
80
113
  @lines_per_frame = profile.lines_per_frame
81
- @painter = Painter.new(@lines_per_frame, @cycles_per_line * PIXELS_PER_CYCLE, profile.blanked_lines)
114
+ @count_column = profile.count_column
115
+ @wrap_column = profile.wrap_column
116
+ @interlaced_lines = profile.interlaced_lines
117
+ @height = [@lines_per_frame, @interlaced_lines].max
118
+ @painter = Painter.new(@height, @cycles_per_line * PIXELS_PER_CYCLE, profile.blanked_lines)
82
119
  @patterns = @painter.patterns
83
120
  @colors = @painter.colors
84
121
  @counts = @painter.counts
@@ -106,11 +143,15 @@ module Badline
106
143
  @origin_x = @origin_y = @columns = @rows = 0
107
144
  @char_shift = 3
108
145
  @screen_base = @char_base = 0
146
+ @long_field = true
147
+ @frame_lines = @lines_per_frame
148
+ @frame_wrap = @wrap_column
109
149
  new_frame
110
150
  @fetch_from = @fetch_to = 0
111
151
  @fetched = @slot = 0
112
152
  @code = 0
113
- @painter.power_on!(@lines_per_frame - 1)
153
+ @painter.power_on!(@frame_lines - 1)
154
+ power_on_pen!
114
155
  schedule
115
156
  end
116
157
 
@@ -131,14 +172,12 @@ module Badline
131
172
  # The sixteen registers as written.
132
173
  def register_file = @registers.dup
133
174
 
134
- # The display, a palette index a pixel, 284 to a line and every line
135
- # of the frame.
175
+ # The display, a palette index a pixel, 4 to a cycle of the line, and
176
+ # every line of the frame.
136
177
  def display = @painter.display
137
178
 
138
179
  def width = @painter.width
139
180
 
140
- def height = @lines_per_frame
141
-
142
181
  # Whether each line of the display has changed since the front end
143
182
  # last cleared them.
144
183
  def dirty_lines = @painter.dirty_lines
@@ -157,9 +196,10 @@ module Badline
157
196
  def peek(addr)
158
197
  register = addr & 0x0f
159
198
  case register
160
- when 0x03 then ((@rasterline & 0x01) << 7) | (@registers[3] & 0x7f)
161
- when 0x04 then @rasterline >> 1
162
- when 0x06, 0x07 then 0
199
+ when 0x03 then ((raster_count & 0x01) << 7) | (@registers[3] & 0x7f)
200
+ when 0x04 then raster_count >> 1
201
+ when 0x06 then @pen_x
202
+ when 0x07 then @pen_y
163
203
  when 0x08, 0x09 then 0xff
164
204
  else @registers[register]
165
205
  end
@@ -181,8 +221,19 @@ module Badline
181
221
  end
182
222
  end
183
223
 
224
+ # The raster count $9003 and $9004 read (see Profile).
225
+ def raster_count = count_at(@rasterline, @column)
226
+
184
227
  private
185
228
 
229
+ # What the count reads at +column+ of +line+ in this frame.
230
+ def count_at(line, column)
231
+ return line if column < @count_column
232
+ return 0 if line == @frame_lines - 1 && column >= @frame_wrap
233
+
234
+ line + 1
235
+ end
236
+
186
237
  def event(column)
187
238
  if column == @cycles_per_line
188
239
  column = @column = 0
@@ -205,9 +256,10 @@ module Badline
205
256
 
206
257
  def start_line
207
258
  line = @rasterline + 1
208
- if line == @lines_per_frame
259
+ if line == @frame_lines
209
260
  line = 0
210
261
  new_frame
262
+ next_field
211
263
  end
212
264
  @rasterline = line
213
265
  next_text_line if @window_open
@@ -215,6 +267,22 @@ module Badline
215
267
  place_fetches
216
268
  end
217
269
 
270
+ # How many lines the frame starting has, and where on its last line
271
+ # the count wraps: the field's, if $9000 bit 7 asks the 6560 to
272
+ # interlace, else the profile's.
273
+ def next_field
274
+ if @interlaced_lines.positive? && @registers[0] >= 0x80
275
+ long = @long_field
276
+ @frame_lines = long ? @interlaced_lines : @interlaced_lines - 1
277
+ @frame_wrap = long ? @count_column : @wrap_column
278
+ @long_field = !long
279
+ else
280
+ @frame_lines = @lines_per_frame
281
+ @frame_wrap = @wrap_column
282
+ @long_field = true
283
+ end
284
+ end
285
+
218
286
  def place_fetches
219
287
  line = @rasterline
220
288
  @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
@@ -13,12 +13,22 @@ module Badline
13
13
  # that sector's data block, whatever it holds.
14
14
  module SectorReader
15
15
  HEADER_BITS = 10 * 8
16
- DATA_BITS = 325 * 8
17
16
  SYNC = /1{10,}/
18
17
 
18
+ # A data block as far as its checksum: its ID, 256 bytes and the
19
+ # checksum. The DOS writes two more after them, and switches back to
20
+ # read mode a few bits into the last, so the gap that was there
21
+ # before finishes its last code.
22
+ CHECKED_BYTES = 258
23
+ CHECKED_BITS = CHECKED_BYTES * 10
24
+
25
+ # The codes of two $0 nibbles, which finish the last group of
26
+ # CHECKED_BITS.
27
+ PADDING = "01010" * 4
28
+
19
29
  # A sector as read: its decoded header, [ID, checksum, sector, track,
20
30
  # the second ID byte, the first, ...], and its decoded data block,
21
- # [ID, 256 data bytes, checksum, ...], nil when there isn't one that
31
+ # [ID, 256 data bytes, checksum], nil when there isn't one that
22
32
  # decodes.
23
33
  Sector = Struct.new(:header, :data)
24
34
 
@@ -62,10 +72,17 @@ module Badline
62
72
  next unless header && header[0] == Disk::HEADER_ID
63
73
 
64
74
  data_sync = SYNC.match(bits, pos + HEADER_BITS)
65
- yield header, data_sync && decode(bits, data_sync.end(0), DATA_BITS)
75
+ yield header, data_sync && decode_data(bits, data_sync.end(0))
66
76
  end
67
77
  end
68
78
 
79
+ def decode_data(bits, pos)
80
+ slice = bits[pos, CHECKED_BITS]
81
+ return unless slice.length == CHECKED_BITS
82
+
83
+ GCR.decode([slice + PADDING].pack("B*").bytes)&.first(CHECKED_BYTES)
84
+ end
85
+
69
86
  def decode(bits, pos, count)
70
87
  slice = bits[pos, count]
71
88
  return unless slice.length == count
@@ -39,6 +39,7 @@ module Badline
39
39
  @via2 = DiskVIA.new(start: 0x1c00, mechanism: @mechanism)
40
40
  @bus = Bus.new(rom: rom || ROM.load("dos1541.rom", 0xc000), via1: @via1, via2: @via2)
41
41
  @cpu = CPU.new(@bus, debug:)
42
+ @cpu.power_on!
42
43
  @clock_hz = CLOCK_HZ
43
44
  @idle_loop = IDLE_LOOP
44
45
  @host_clock_hz = host_clock_hz
@@ -97,6 +97,7 @@ module Badline
97
97
  @host_tia = Chips::TIA.new
98
98
  @bus = Bus.new(rom: rom || ROM.load("dos1551.rom", 0xc000), port: @port, tia: @tia)
99
99
  @cpu = CPU.new(@bus, debug:)
100
+ @cpu.power_on!
100
101
  @idle_loop = IDLE_LOOP
101
102
  @host_clock_hz = host_clock_hz
102
103
  @phase = 0
@@ -68,6 +68,7 @@ module Badline
68
68
  @fdc = WD1770.new
69
69
  @bus = Bus.new(rom: rom || ROM.load("dos1571.rom", 0x8000), via1: @via1, via2: @via2, cia: @cia, fdc: @fdc)
70
70
  @cpu = CPU.new(@bus, debug:)
71
+ @cpu.power_on!
71
72
  @fdc.cpu = @cpu
72
73
  @idle_loop = IDLE_LOOP
73
74
  @host_clock_hz = host_clock_hz
@@ -87,6 +87,7 @@ module Badline
87
87
  @cia = Chips::CIA.new(start: 0x4000, peripheral: @serial_port, tod_hz: 0)
88
88
  @bus = Bus.new(rom: rom || ROM.load("dos1581.rom", 0x8000), cia: @cia, fdc: @fdc, drive: self)
89
89
  @cpu = CPU.new(@bus, debug:)
90
+ @cpu.power_on!
90
91
  @idle_loop = IDLE_LOOP
91
92
  @host_clock_hz = host_clock_hz
92
93
  @phase = 0
@@ -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)
@@ -46,6 +52,7 @@ module Badline
46
52
  @via2 = Model1541::DiskVIA.new(start: 0x1c00, mechanism: @mechanism)
47
53
  @bus = Bus.new(rom: rom || ROM.load("dos2031.rom", 0xc000), via1: @via1, via2: @via2)
48
54
  @cpu = CPU.new(@bus, debug:)
55
+ @cpu.power_on!
49
56
  @clock_hz = CLOCK_HZ
50
57
  @idle_loop = IDLE_LOOP
51
58
  @host_clock_hz = host_clock_hz
@@ -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