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
@@ -4,70 +4,96 @@ module Badline
4
4
  module Snapshot
5
5
  module Vice
6
6
  module PET
7
- # A true 8050 or 8250 on device 8, as xpet 3.10, xcbm2 and xcbm5x0 save it with true
8
- # drive emulation: DRIVE8 2.0 with both drives, DRIVE9 to DRIVE11
9
- # with none, DRIVECPU0 1.3, the interface processor and its RAM,
10
- # RIOT1D0 and RIOT2D0 0.0, its two 6532s, and FDC0 0.0. xpet runs
11
- # the floppy controller at a high level, so its 6504, VIA and 6530
12
- # aren't in the snapshot: FDC0 holds the controller's state, its
13
- # last track in byte 6.
7
+ # A true unit with an interface processor on device 8, as xpet 3.10,
8
+ # xcbm2 and xcbm5x0 save it with true drive emulation: an 8050, an
9
+ # 8250, an SFD-1001, a 4040, a 3040, a 2040, or a D9060 or D9090,
10
+ # which xpet's type 9000 stands for. Each is DRIVE8 2.0 with the
11
+ # unit's drives, DRIVE9 to DRIVE11 with none, DRIVECPU0 1.3, the
12
+ # interface processor and its RAM, RIOT1D0 and RIOT2D0 0.0, its two
13
+ # 6532s, and FDC0 0.0. xpet runs the floppy controller and the hard
14
+ # disk's SASI host at a high level, so their processors, VIAs and
15
+ # 6530s aren't in the snapshot: FDC0 holds the controller's state,
16
+ # its last track in byte 6.
14
17
  #
15
- # DRIVE8 holds a block of 139 bytes for each drive, then 6 for the
16
- # unit: each block has its head's half track, twice the track, at
17
- # $10 and the drive's type as a word at $28 (8050 or 8250).
18
- # DRIVECPU0 is laid out as the 2031's (Drive2031), its RAM the IP's
19
- # page 0, the 6532s' 128 bytes each, then the 4K of common RAM.
20
- # A RIOT module holds port A's output and direction registers, port
21
- # B's, the edge control (bit 0 the PA7 interrupt, bit 1 a rising
22
- # edge), the flags (bit 7 the timer's interrupt, which xpet raises
23
- # IRQ for, bit 6 PA7's), the timer's
18
+ # DRIVE8 holds a block of 139 bytes for each of the unit's drives,
19
+ # then 6 for the unit: each block has its head's half track, twice
20
+ # the track, at $10 and the unit's type as a word at $28 (TYPES).
21
+ # The 1001 and the hard disks have one block, and a hard disk's head
22
+ # rests at HARD_DISK_TRACK. DRIVECPU0 is laid out as the 2031's
23
+ # (Drive2031), its RAM the IP's page 0, the 6532s' 128 bytes each,
24
+ # then the 4K of common RAM. A RIOT module holds port A's output and
25
+ # direction registers, port B's, the edge control (bit 0 the PA7
26
+ # interrupt, bit 1 a rising edge), the flags (bit 7 the timer's
27
+ # interrupt, which xpet raises IRQ for, bit 6 PA7's), the timer's
24
28
  # counter, its divider as a word, two bytes xpet leaves 0, and
25
29
  # whether the timer interrupts.
26
30
  #
27
- # Written, once the controller is in its idle loop between
28
- # instructions (PET#settle_for_vice!): the IP's registers and RAM,
29
- # the 6532s, the heads' tracks, and FDC0 as xpet leaves an idle
30
- # controller with the track badline's controller last stepped to.
31
- # A unit whose controller is still busy is left out of xpet's
32
- # modules. Read: the IP's registers and RAM and the 6532s, with the
33
- # controller as a unit switched on has it in its idle loop, its
34
- # heads on track 1, from where its next job steps them. A job under
35
- # way in xpet's controller starts again from there. The disks in
36
- # the drives stay.
31
+ # Written, once the controller, or the SASI host, is in its idle
32
+ # loop between instructions (PET#settle_for_vice!): the IP's
33
+ # registers and RAM, the 6532s, the heads' tracks, and FDC0 as xpet
34
+ # leaves an idle controller with the track badline's controller last
35
+ # stepped to. A unit whose controller is still busy is left out of
36
+ # xpet's modules. Read: the IP's registers and RAM and the 6532s,
37
+ # with the controller as a unit switched on has it in its idle loop,
38
+ # its heads on track 1, from where its next job steps them. A job
39
+ # under way in xpet's controller starts again from there. The disks
40
+ # in the drives stay, and a hard disk's image with them, which also
41
+ # says whether the unit is a D9060 or a D9090.
37
42
  module Drive8050
38
43
  UNIT = "DRIVE8"
39
44
  CPU = Drive2031::CPU
40
45
  RIOTS = %w[RIOT1D0 RIOT2D0].freeze
41
46
  FDC = "FDC0"
42
47
  NAMES = [UNIT, CPU, *RIOTS, FDC].freeze
43
- TYPES = { 0x1f72 => "8050", 0x203a => "8250" }.freeze
44
- # Where each drive's block starts in DRIVE8, and its fields.
45
- DRIVES = [0x00, 0x8b].freeze
48
+ # xpet's drive types by DRIVE8's type word, for the units written
49
+ # and read here.
50
+ TYPES = { 0x1f72 => "8050", 0x203a => "8250", 0x03e9 => "1001", 0x0fc8 => "4040", 0x0be0 => "3040",
51
+ 0x07f8 => "2040", 0x2328 => "9060" }.freeze
52
+ # The type word each model is written with.
53
+ WORDS = TYPES.invert.merge("9090" => 0x2328).freeze
54
+ # The units that are hard disks, and the DOS 2.1 and DOS 1 units
55
+ # on the 4040's board.
56
+ HARD_DISKS = %w[9060 9090].freeze
57
+ DOS2_BOARD = %w[4040 3040 2040].freeze
58
+ # The drives each unit has, a block of DRIVE8 each.
59
+ DRIVE_COUNTS = { "8050" => 2, "8250" => 2, "1001" => 1, "4040" => 2, "3040" => 2, "2040" => 2, "9060" => 1,
60
+ "9090" => 1 }.freeze
61
+ # A drive's block in DRIVE8, and its fields.
62
+ BLOCK = 0x8b
46
63
  HALF_TRACK = 0x10
47
64
  TYPE = 0x28
48
- SIZE = 284
65
+ # The half track xpet keeps a hard disk's head on.
66
+ HARD_DISK_TRACK = 0x98
67
+ # The unit's bytes after the drives' blocks.
68
+ TAIL = 6
49
69
  # DRIVE8 as xpet saves an idle 8050, by the bytes that aren't 0,
50
- # but for the type.
51
- IDLE = { 0x00 => 0x01, 0x01 => 0x02, 0x02 => 0x01, 0x0e => 0x01, 0x0f => 0x01, 0x10 => 0x4c, 0x1a => 0x01,
52
- 0x20 => 0x55, 0x30 => 0x06, 0x6f => 0xcd, 0x70 => 0xab, 0x71 => 0x34, 0x72 => 0x12, 0x99 => 0x01,
53
- 0x9a => 0x01, 0x9b => 0x24, 0xa5 => 0x01, 0xab => 0x55, 0x11a => 0x01 }.freeze
70
+ # but for the type and the drive count at COUNT: the first drive's
71
+ # block, and from BLOCK on the second's with a byte past it.
72
+ IDLE = { 0x00 => 0x01, 0x02 => 0x01, 0x0e => 0x01, 0x0f => 0x01, 0x10 => 0x4c, 0x1a => 0x01, 0x20 => 0x55,
73
+ 0x30 => 0x06, 0x6f => 0xcd, 0x70 => 0xab, 0x71 => 0x34, 0x72 => 0x12 }.freeze
74
+ IDLE_SECOND = { 0x0e => 0x01, 0x0f => 0x01, 0x10 => 0x24, 0x1a => 0x01, 0x20 => 0x55, 0x8f => 0x01 }.freeze
75
+ COUNT = 0x01
54
76
  ZERO_PAGE = 0x80
55
77
  COMMON = 0x1000
56
78
  # FDC0 as xpet leaves an idle controller, and where the track goes.
57
79
  FDC_IDLE = [0x04, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00].freeze
58
80
  FDC_TRACK = 6
59
81
  # Where badline's controller keeps drive 0's track, in the 6530's
60
- # RAM.
82
+ # RAM: the 8050's, and the 4040's in the low six bits.
61
83
  TRACK_AT = 0x21
84
+ DOS2_TRACK_AT = 0x03
85
+ DOS2_TRACK = 0x3f
62
86
  DIVIDERS = [1, 8, 64, 1024].freeze
63
87
  IRQ_EDGE = 0x01
64
88
  RISING = 0x02
65
89
  TIMER_FLAG = 0x80
66
90
  PA7_FLAG = 0x40
91
+ # The hard disk's controller, which holds the image's path.
92
+ SASI = "BL-D8-SASI"
67
93
 
68
94
  module_function
69
95
 
70
- # Whether the snapshot has a true 8050 or 8250.
96
+ # Whether the snapshot has a true unit of these.
71
97
  def present?(container) = !container[FDC].nil? && !container[CPU].nil?
72
98
 
73
99
  def reads?(section)
@@ -78,48 +104,93 @@ module Badline
78
104
  end
79
105
  end
80
106
 
81
- # The model DRIVE8 names, or nil.
107
+ # The model DRIVE8 names, or nil. Type 9000 reads as the D9060.
82
108
  def model(container)
83
109
  unit = container[UNIT]
84
- return nil if unit.nil? || unit.data.bytesize < SIZE
110
+ return nil if unit.nil? || unit.data.bytesize < BLOCK
85
111
 
86
- TYPES[FieldReader.new(unit).skip(TYPE).word]
112
+ model = TYPES[FieldReader.new(unit).skip(TYPE).word]
113
+ model if model && unit.data.bytesize >= size(model)
87
114
  end
88
115
 
89
- # xpet's modules for the 8050 or 8250 whose BL-D8 modules `readers`
90
- # holds, and none without one, or with its controller away from
91
- # its idle loop.
116
+ # Whether xpet saves the two models as one type.
117
+ def same_type?(model, other) = WORDS.key?(model) && WORDS[model] == WORDS[other]
118
+
119
+ def hard_disk?(model) = HARD_DISKS.include?(model)
120
+
121
+ # The hard disk whose image the kept modules `disks` hold: a
122
+ # D9090 for one with six heads, and otherwise a D9060.
123
+ def hard_disk_model(disks)
124
+ own = disks.find { |section| section.name == SASI }
125
+ own && Keyed::Reader.parse(own).int("disk_heads", 4) == 6 ? "9090" : "9060"
126
+ end
127
+
128
+ # DRIVE8's size for the model.
129
+ def size(model) = (BLOCK * DRIVE_COUNTS.fetch(model)) + TAIL
130
+
131
+ # xpet's modules for the unit whose BL-D8 modules `readers` holds,
132
+ # and none without one, or with its controller away from its idle
133
+ # loop.
92
134
  def export(machine, readers)
93
135
  drive = readers["BL-D8"]
94
136
  model = drive&.text("model", "")
95
- return [] unless TYPES.value?(model) && idle?(readers)
137
+ return [] unless WORDS.key?(model) && idle?(model, readers)
96
138
 
97
- [unit(model, readers.fetch("BL-D8-MECH")), *Drive2031::UNITS.drop(1).map { |name| none(name) },
139
+ [unit(model, readers), *Drive2031::UNITS.drop(1).map { |name| none(name) },
98
140
  Drive2031.cpu_section(machine, readers.fetch("BL-D8-IPCPU"), ram(readers)),
99
141
  riot(RIOTS[0], readers.fetch("BL-D8-RIOT1")), riot(RIOTS[1], readers.fetch("BL-D8-RIOT2")),
100
- fdc(readers.fetch("BL-D8-RRIOT"))]
142
+ fdc(model, readers)]
101
143
  end
102
144
 
103
145
  # Whether both processors are between instructions, the
104
146
  # controller at the start of its idle loop.
105
- def idle?(readers)
106
- fdc = readers.fetch("BL-D8-FDCCPU")
107
- !position?(fdc) && !position?(readers.fetch("BL-D8-IPCPU")) &&
108
- fdc.int("pc", 0) == Drives::Model8050::Idle::FDC_LOOP
147
+ def idle?(model, readers)
148
+ controller = readers.fetch(hard_disk?(model) ? "BL-D8-SASICPU" : "BL-D8-FDCCPU")
149
+ !position?(controller) && !position?(readers.fetch("BL-D8-IPCPU")) &&
150
+ controller.int("pc", 0) == controller_loop(model)
151
+ end
152
+
153
+ # Where the model's controller, or its SASI host, waits for jobs.
154
+ def controller_loop(model)
155
+ case model
156
+ when "9060", "9090" then Drives::Model9060::Idle::SASI_LOOP
157
+ when "2040" then Drives::Model2040::FDC_LOOP
158
+ when "4040", "3040" then Drives::Model4040::FDC_LOOP
159
+ else Drives::Model8050::Idle::FDC_LOOP
160
+ end
109
161
  end
110
162
 
111
163
  def position?(cpu) = !cpu.text("pos.seq", "").empty?
112
164
 
113
- def unit(model, mechanism)
114
- bytes = Array.new(SIZE) { |offset| IDLE.fetch(offset, 0) }
115
- cylinders = mechanism.ints("cylinders", [0, 0])
116
- DRIVES.each_with_index do |at, drive|
117
- bytes[at + HALF_TRACK] = ((cylinders[drive] || 0) + 1) * 2
118
- bytes[at + TYPE, 2] = [TYPES.key(model)].pack("v").bytes
165
+ def unit(model, readers)
166
+ count = DRIVE_COUNTS.fetch(model)
167
+ bytes = Array.new(size(model), 0)
168
+ IDLE.each { |offset, value| bytes[offset] = value }
169
+ IDLE_SECOND.each { |offset, value| bytes[BLOCK + offset] = value } if count > 1
170
+ bytes[COUNT] = count
171
+ tracks = half_tracks(model, readers)
172
+ count.times do |drive|
173
+ at = BLOCK * drive
174
+ bytes[at + HALF_TRACK] = tracks[drive]
175
+ bytes[at + TYPE, 2] = [WORDS.fetch(model)].pack("v").bytes
119
176
  end
120
177
  FieldWriter.new.bytes(bytes).section(UNIT, Drive2031::DRIVE_MAJOR, Drive2031::DRIVE_MINOR)
121
178
  end
122
179
 
180
+ # Each drive's head as xpet's half track: the 8050's cylinder on
181
+ # from 1, the 4040's half track on from track 1, and a hard
182
+ # disk's where xpet keeps it.
183
+ def half_tracks(model, readers)
184
+ return [HARD_DISK_TRACK] if hard_disk?(model)
185
+
186
+ mechanism = readers.fetch("BL-D8-MECH")
187
+ if DOS2_BOARD.include?(model)
188
+ mechanism.ints("half_tracks", [0, 0]).map { |half_track| half_track + 2 }
189
+ else
190
+ mechanism.ints("cylinders", [0, 0]).map { |cylinder| (cylinder + 1) * 2 }
191
+ end
192
+ end
193
+
123
194
  def none(name) = Drive2031.unit(Drive2031::NONE, name)
124
195
 
125
196
  # The IP's RAM as xpet lays it out: page 0 from both 6532s, then
@@ -143,16 +214,25 @@ module Badline
143
214
  fields.zeros(2).flag(input.bool("timer.irq_enabled", false)).section(name, 0, 0)
144
215
  end
145
216
 
146
- def fdc(rriot)
217
+ # FDC0 with the controller's track, and a hard disk's with none.
218
+ def fdc(model, readers)
147
219
  bytes = FDC_IDLE.dup
148
- bytes[FDC_TRACK] = rriot.bytes("ram", "".b).getbyte(TRACK_AT).to_i
220
+ unless hard_disk?(model)
221
+ ram = readers.fetch("BL-D8-RRIOT").bytes("ram", "".b)
222
+ bytes[FDC_TRACK] = if DOS2_BOARD.include?(model)
223
+ ram.getbyte(DOS2_TRACK_AT).to_i & DOS2_TRACK
224
+ else
225
+ ram.getbyte(TRACK_AT).to_i
226
+ end
227
+ end
149
228
  FieldWriter.new.bytes(bytes).section(FDC, 0, 0)
150
229
  end
151
230
 
152
231
  # The unit's keyed modules, named for device 8, from the
153
232
  # snapshot's: the IP's side from xpet's, the controller's side
154
233
  # from `reference`, a unit's in its idle loop, and `disks`, the
155
- # drives' disk modules badline keeps.
234
+ # drives' disk modules badline keeps, or a hard disk's controller
235
+ # with its image.
156
236
  def import(container, reference, disks)
157
237
  cpu = container[CPU]
158
238
  page = FieldReader.new(cpu).skip(Drive2031::RAM_AT).string(ZERO_PAGE * 2)
@@ -162,16 +242,48 @@ module Badline
162
242
  riot_section("BL-D8-RIOT1", container[RIOTS[0]], page.byteslice(0, ZERO_PAGE)),
163
243
  riot_section("BL-D8-RIOT2", container[RIOTS[1]], page.byteslice(ZERO_PAGE, ZERO_PAGE)),
164
244
  Keyed.section("BL-D8-IPCPU") { |out| Drive2031.registers(cpu, out) }]
165
- sections + reference.select { |section| CONTROLLER.include?(section.name) } + disks
245
+ sections + controller_side(reference, disks)
166
246
  end
167
247
 
168
- # The controller's side, which comes from a unit in its idle loop.
248
+ # The controller's side, which comes from a unit in its idle loop:
249
+ # a floppy unit's, and a hard disk's SASI host.
169
250
  CONTROLLER = %w[BL-D8-FDCCPU BL-D8-VIA BL-D8-RRIOT BL-D8-MECH].freeze
251
+ HOST = %w[BL-D8-SASICPU BL-D8-SASIRAM BL-D8-VIA].freeze
252
+
253
+ def controller_side(reference, disks)
254
+ base = reference.find { |section| section.name == SASI }
255
+ return reference.select { |section| CONTROLLER.include?(section.name) } + disks if base.nil?
256
+
257
+ reference.select { |section| HOST.include?(section.name) } + [controller(base, disks)]
258
+ end
259
+
260
+ # The hard disk's controller as `base` has it, with the image
261
+ # badline's kept in `disks`.
262
+ def controller(base, disks)
263
+ kept = disks.find { |section| section.name == SASI }
264
+ return base if kept.nil?
170
265
 
266
+ target = Drives::Model9060::SASITarget.new
267
+ target.load_fields(Keyed::Reader.parse(base))
268
+ own = Keyed::Reader.parse(kept)
269
+ path = own.text("path", "")
270
+ if File.file?(path)
271
+ target.disk = Storage::D90Image.new(path, read_only: own.bool("read_only", false),
272
+ heads: own.int("disk_heads", 4))
273
+ end
274
+ Keyed.section(SASI) { |out| target.save_fields(out) }
275
+ end
276
+
277
+ # The unit's own fields: its model and clock, with the controller's
278
+ # last byte from `reference`, or a hard disk's SASI lines.
171
279
  def unit_fields(container, reference, out)
172
280
  own = Keyed::Reader.parse(reference.find { |section| section.name == "BL-D8" })
173
- out.text("model", own.text("model", "8050")).int("phase", 0)
174
- out.int("cycles", FieldReader.new(container[CPU]).qword).int("fdc_data", own.int("fdc_data", 0))
281
+ model = own.text("model", "8050")
282
+ out.text("model", model).int("phase", 0).int("cycles", FieldReader.new(container[CPU]).qword)
283
+ return out.int("fdc_data", own.int("fdc_data", 0)) unless hard_disk?(model)
284
+
285
+ out.int("sasi_data", own.int("sasi_data", 0)).bool("sasi.ack_high", own.bool("sasi.ack_high", true))
286
+ out.bool("sasi.reset_high", own.bool("sasi.reset_high", false))
175
287
  end
176
288
 
177
289
  def riot_section(name, section, ram)
@@ -199,15 +311,23 @@ module Badline
199
311
  end
200
312
 
201
313
  def booted(model)
202
- unit = model == "8250" ? Drives::Model8250.new : Drives::Model8050.new
203
- Drives::Model8050::BOOT_CYCLES.times { unit.host_cycle! }
204
- unit.cycle! until idle_loop?(unit)
314
+ unit = new_unit(model)
315
+ unit.boot_cycles.times { unit.host_cycle! }
316
+ unit.cycle! until unit.settled_for_vice?
205
317
  unit.save_modules([], "BL-D8")
206
318
  end
207
319
 
208
- def idle_loop?(unit)
209
- cpu = unit.fdc_cpu
210
- unit.ip_cpu.boundary? && cpu.boundary? && cpu.program_counter == Drives::Model8050::Idle::FDC_LOOP
320
+ def new_unit(model)
321
+ case model
322
+ when "8250" then Drives::Model8250.new
323
+ when "1001" then Drives::Model1001.new
324
+ when "4040" then Drives::Model4040.new
325
+ when "3040" then Drives::Model3040.new
326
+ when "2040" then Drives::Model2040.new
327
+ when "9060" then Drives::Model9060.new
328
+ when "9090" then Drives::Model9090.new
329
+ else Drives::Model8050.new
330
+ end
211
331
  end
212
332
  end
213
333
  end
@@ -5,14 +5,24 @@ module Badline
5
5
  module Vice
6
6
  module PET
7
7
  # PETMEM 1.3: the board, the RAM below $8000 in K and the 8096's
8
- # control register at $FFF0, then the RAM, the screen RAM, 1K or 2K
9
- # as the board has, or on the 8296 its 32K from $8000, an 8096's or
10
- # 8296's 64K board, and 24 bytes of xpet's own settings, which name
11
- # the 2001.
8
+ # control register at $FFF0, a byte for the SuperPET's latches, then
9
+ # the RAM, the screen RAM, 1K or 2K as the board has, or on the 8296
10
+ # its 32K from $8000, an 8096's, 8296's or SuperPET's 64K board, and
11
+ # 24 bytes of xpet's own settings, which name the 2001, and on the
12
+ # SuperPET the CPU that has the bus.
12
13
  #
13
14
  # The board is 0 for the discrete video, 1 for a 40-column CRTC, 2
14
15
  # for an 80-column one, 3 for the SuperPET, 4 for the 8096 and 5 for
15
16
  # the 8296. xpet's ROMs aren't in the snapshot.
17
+ #
18
+ # The SuperPET's latch byte holds the bank at $9000 in its high
19
+ # nibble, and sets bit 0 while the bank RAM is there rather than the
20
+ # empty socket, and bit 2 always. Its settings hold 1 at offset 2
21
+ # while the 6809 has the bus. Both were inferred from xpet 3.10's
22
+ # snapshots after writes to the bank latch and $EFFE, and in both
23
+ # modes. xpet has no system latch, so the bank latch's bit 7 and the
24
+ # system latch aren't saved, and a restore clears them. Nor is the
25
+ # 6702: a restore leaves it as the RES line does.
16
26
  module PETMem
17
27
  NAME = "PETMEM"
18
28
  MAJOR = 1
@@ -30,6 +40,14 @@ module Badline
30
40
  SETTINGS = [0x00, 0x00, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00, 0x40, 0x00, 0x00, 0x00,
31
41
  0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00].freeze
32
42
  SETTINGS_2001 = 0x03
43
+ # The SuperPET's settings byte for the CPU, and its value while the
44
+ # 6809 has the bus.
45
+ SETTINGS_CPU = 2
46
+ SETTINGS_M6809 = 0x01
47
+ # The SuperPET's latch byte: the bit always set, and the one for
48
+ # the bank RAM at $9000.
49
+ LATCHES = 0x04
50
+ BANK_RAM = 0x01
33
51
  EXPANSION = 0x10000
34
52
 
35
53
  module_function
@@ -37,6 +55,7 @@ module Badline
37
55
  def reads?(section) = section.major == MAJOR && section.minor == MINOR
38
56
 
39
57
  def board(model)
58
+ return SUPERPET if model.superpet?
40
59
  return CBM8296 if model.cbm8296?
41
60
  return CBM8096 if model.expansion
42
61
 
@@ -50,15 +69,40 @@ module Badline
50
69
  def export(machine)
51
70
  bus = machine.bus
52
71
  expansion = bus.expansion
72
+ superpet = machine.superpet?
53
73
  fields = FieldWriter.new.byte(board(machine.model)).byte(0).byte(machine.ram_kb)
54
- fields.byte(expansion ? expansion.control : 0).byte(HEADER_END)
74
+ fields.byte(expansion ? expansion.control : 0).byte(superpet ? latches(machine.board) : HEADER_END)
55
75
  fields.bytes(machine.ram.snapshot).bytes(bus.screen.cells)
56
76
  fields.bytes(expansion.cells) if expansion
77
+ fields.bytes(machine.board.cells) if superpet
57
78
  settings = SETTINGS.dup
58
79
  settings[1] = SETTINGS_2001 if machine.model.basic1?
80
+ settings[SETTINGS_CPU] = SETTINGS_M6809 if superpet && machine.m6809_running?
59
81
  fields.bytes(settings).section(NAME, MAJOR, MINOR)
60
82
  end
61
83
 
84
+ def latches(board) = (board.bank << 4) | LATCHES | (board.socket.zero? ? BANK_RAM : 0)
85
+
86
+ # Whether a SuperPET's settings say the 6809 has the bus.
87
+ def m6809?(section)
88
+ data = section.data
89
+ offset = data.bytesize - SETTINGS.length + SETTINGS_CPU
90
+ offset >= 0 && data.getbyte(offset).to_i.anybits?(SETTINGS_M6809)
91
+ end
92
+
93
+ # A SuperPET's bank latch, socket bit and 64K board by their
94
+ # BL-SUPERPET keys, in a keyed module's writer.
95
+ def import_superpet(section, out)
96
+ fields = FieldReader.new(section).skip(4)
97
+ latches = fields.byte
98
+ fields.skip(fields_before_board(section))
99
+ out.int("bank_latch", latches >> 4).int("socket", latches.anybits?(BANK_RAM) ? 0 : 1)
100
+ out.int("system_latch", 0).bytes("ram", fields.string(EXPANSION))
101
+ end
102
+
103
+ # The RAM and the screen RAM before a SuperPET's board.
104
+ def fields_before_board(section) = (ram_kb(section) * 1024) + 0x800
105
+
62
106
  # The board's number in the header.
63
107
  def board_of(section) = FieldReader.new(section).byte
64
108
 
@@ -8,18 +8,23 @@ module Badline
8
8
  # reads become the PET's own keyed modules (Keyed), which it loads
9
9
  # as it loads its own snapshot. Whatever they don't set stays as the
10
10
  # machine has it at power-on, and the datasettes' tapes and the
11
- # disk device 8 serves stay as they are. A snapshot with a true 2031,
12
- # 8050 or 8250 puts one on the bus, with the disks badline's drive
13
- # of that model had in it, and one without takes badline's off.
11
+ # disk device 8 serves stay as they are. A snapshot with a true drive
12
+ # puts one on the bus, with the disks badline's drive of that model
13
+ # had in it (Drive2031, Drive8050), and one without takes badline's
14
+ # off.
14
15
  module Restore
15
16
  # MAINCPU and PETMEM hold the machine itself, so a version badline
16
17
  # doesn't read fails the restore. The others are left out and
17
18
  # reported.
18
19
  READ = [MainCPU::NAME, PETMem::NAME, CRTC::NAME, "PIA1", "PIA2", VIA::NAME].freeze
20
+ # The SuperPET's too: its 6809 and its ACIA. A SuperPET's board
21
+ # comes from PETMEM.
22
+ SUPERPET_READ = [CPU6809::NAME, CBM2::ACIA::NAME].freeze
19
23
  # The host's media, which a restore keeps.
20
24
  KEPT = %w[BL-TAPE1 BL-TAPE2 BL-TRAPDOS].freeze
21
- # The disks in a true drive's drives, which a restore keeps.
22
- DISKS = %w[BL-D8-DISK BL-D8-DISK1].freeze
25
+ # The disks in a true drive's drives, and a hard disk's controller
26
+ # with its image, which a restore keeps.
27
+ DISKS = %w[BL-D8-DISK BL-D8-DISK1 BL-D8-SASI].freeze
23
28
 
24
29
  module_function
25
30
 
@@ -53,7 +58,8 @@ module Badline
53
58
  applied = []
54
59
  ignored = []
55
60
  sections = []
56
- READ.each do |name|
61
+ names = read_names(computer)
62
+ names.each do |name|
57
63
  section = container[name]
58
64
  next if section.nil?
59
65
 
@@ -72,17 +78,20 @@ module Badline
72
78
  end
73
79
  sections += drive(container, applied, ignored, kept_disks(container, computer, disks))
74
80
  computer.load_modules(Keyed::Modules.new(modules(container, computer, applied) + sections))
75
- Report.new(applied:, ignored: ignored + left_out(container, applied))
81
+ Report.new(applied:, ignored: ignored + left_out(container, names, applied))
76
82
  end
77
83
 
84
+ def read_names(computer) = computer.superpet? ? READ + SUPERPET_READ : READ
85
+
78
86
  # The disks badline's true drive has, which stay in the drive the
79
- # snapshot puts on the bus when it is of the same model.
87
+ # snapshot puts on the bus when it is of the same model, or of the
88
+ # same xpet type.
80
89
  def kept_disks(container, computer, disks)
81
90
  drive = computer.true_drive
82
91
  return [] if drive.nil?
83
92
 
84
93
  if Drive8050.present?(container)
85
- Drive8050.model(container) == drive.model_name ? disks : []
94
+ Drive8050.same_type?(Drive8050.model(container), drive.model_name) ? disks : []
86
95
  else
87
96
  drive.model_name == "2031" ? disks.first(1) : []
88
97
  end
@@ -113,6 +122,7 @@ module Badline
113
122
  end
114
123
 
115
124
  applied.concat(Drive8050::NAMES)
125
+ model = Drive8050.hard_disk_model(disks) if Drive8050.hard_disk?(model)
116
126
  Drive8050.import(container, Drive8050.reference(model), disks)
117
127
  end
118
128
 
@@ -134,6 +144,8 @@ module Badline
134
144
  when PETMem::NAME then PETMem.reads?(section)
135
145
  when CRTC::NAME then CRTC.reads?(section)
136
146
  when VIA::NAME then VIA.reads?(section)
147
+ when CPU6809::NAME then CPU6809.reads?(section)
148
+ when CBM2::ACIA::NAME then CBM2::ACIA.reads?(section)
137
149
  else PIAs.reads?(section)
138
150
  end
139
151
  end
@@ -160,9 +172,26 @@ module Badline
160
172
  if applied.include?(VIA::NAME)
161
173
  sections << Keyed.section("BL-VIA") { |out| VIA.import(container[VIA::NAME], out) }
162
174
  end
175
+ sections += superpet(container, applied) if computer.superpet?
163
176
  sections + video
164
177
  end
165
178
 
179
+ # The SuperPET's board, and its 6809 while it has the bus and its
180
+ # ACIA where the snapshot has them. The 6809 the 6502 has the bus
181
+ # from, and a module left out, stay as the machine has them at
182
+ # power-on.
183
+ def superpet(container, applied)
184
+ cycles = FieldReader.new(container[MainCPU::NAME]).qword
185
+ memory = container[PETMem::NAME]
186
+ sections = [Keyed.section("BL-SUPERPET") { |out| PETMem.import_superpet(memory, out) }]
187
+ if applied.include?(CPU6809::NAME) && PETMem.m6809?(memory)
188
+ sections << Keyed.section("BL-M6809") { |out| CPU6809.import(container[CPU6809::NAME], out, cycles) }
189
+ end
190
+ return sections unless applied.include?(CBM2::ACIA::NAME)
191
+
192
+ sections << Keyed.section("BL-ACIA") { |out| CBM2::ACIA.import(container[CBM2::ACIA::NAME], out, cycles) }
193
+ end
194
+
166
195
  def machine(out, container, computer, drive)
167
196
  out.text("family", "pet")
168
197
  PETSetup.of(computer).write_fields(out)
@@ -203,8 +232,8 @@ module Badline
203
232
  [Keyed.section("BL-CRTC") { |out| CRTC.copy(fields, out) }]
204
233
  end
205
234
 
206
- def left_out(container, applied)
207
- container.sections.reject { |section| READ.include?(section.name) || applied.include?(section.name) }
235
+ def left_out(container, names, applied)
236
+ container.sections.reject { |section| names.include?(section.name) || applied.include?(section.name) }
208
237
  .map { |section| "#{section}: badline doesn't read this module, left out" }
209
238
  end
210
239
  end
@@ -100,6 +100,10 @@ module Badline
100
100
  include Snapshot::Files
101
101
  end
102
102
 
103
+ class KIM1
104
+ include Snapshot::Files
105
+ end
106
+
103
107
  class CBM2
104
108
  include Snapshot::Files
105
109