badline 0.16.1 → 0.17.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +102 -12
  5. data/doc/media.md +5 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +376 -69
  8. data/doc/snapshots.md +148 -14
  9. data/lib/badline/cbm2/acia.rb +10 -0
  10. data/lib/badline/cbm2/bus.rb +263 -0
  11. data/lib/badline/cbm2/cpu.rb +58 -0
  12. data/lib/badline/cbm2/crtc_video.rb +133 -0
  13. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  14. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  15. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  16. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  17. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  18. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  19. data/lib/badline/cbm2/keyed_state.rb +91 -0
  20. data/lib/badline/cbm2/model.rb +44 -0
  21. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  22. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  23. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  24. data/lib/badline/cbm2/p500/model.rb +39 -0
  25. data/lib/badline/cbm2/p500/pages.rb +57 -0
  26. data/lib/badline/cbm2/p500.rb +262 -0
  27. data/lib/badline/cbm2/sid_page.rb +19 -0
  28. data/lib/badline/cbm2.rb +267 -0
  29. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  30. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  31. data/lib/badline/chips/acia.rb +307 -0
  32. data/lib/badline/chips/crtc.rb +3 -2
  33. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  34. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  35. data/lib/badline/chips/riot/timer.rb +80 -0
  36. data/lib/badline/chips/riot.rb +162 -0
  37. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  38. data/lib/badline/chips/rriot.rb +132 -0
  39. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  40. data/lib/badline/chips/tpi.rb +233 -0
  41. data/lib/badline/chips/vic/bank.rb +60 -5
  42. data/lib/badline/chips/vic/sprite.rb +4 -3
  43. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  44. data/lib/badline/drives/datasette.rb +27 -3
  45. data/lib/badline/drives/model8050/disk.rb +163 -0
  46. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  47. data/lib/badline/drives/model8050/idle.rb +177 -0
  48. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  49. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  50. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  51. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  52. data/lib/badline/drives/model8050/parts.rb +37 -0
  53. data/lib/badline/drives/model8050.rb +243 -0
  54. data/lib/badline/frontend/app.rb +10 -3
  55. data/lib/badline/frontend/boot.rb +1 -1
  56. data/lib/badline/frontend/controls.rb +51 -15
  57. data/lib/badline/frontend/gamepads.rb +3 -3
  58. data/lib/badline/frontend/keys.rb +61 -2
  59. data/lib/badline/frontend/media_slots.rb +1 -1
  60. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  61. data/lib/badline/frontend/menu/menu_pages.rb +18 -4
  62. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  63. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  64. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  65. data/lib/badline/frontend/timeline.rb +19 -10
  66. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  67. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  68. data/lib/badline/kernal_trap/dos.rb +18 -5
  69. data/lib/badline/kernal_trap/ieee.rb +4 -2
  70. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  71. data/lib/badline/machine/ieee_drives.rb +194 -0
  72. data/lib/badline/machine/settling.rb +4 -0
  73. data/lib/badline/machine.rb +23 -6
  74. data/lib/badline/media/cbm2_media.rb +29 -0
  75. data/lib/badline/media/program.rb +6 -3
  76. data/lib/badline/media/true_drive.rb +38 -9
  77. data/lib/badline/media.rb +15 -6
  78. data/lib/badline/options/family.rb +15 -3
  79. data/lib/badline/options/help.rb +16 -2
  80. data/lib/badline/options/table.rb +18 -4
  81. data/lib/badline/options/validation.rb +27 -3
  82. data/lib/badline/options.rb +21 -7
  83. data/lib/badline/pet/cassettes.rb +22 -1
  84. data/lib/badline/pet/ieee_drive.rb +5 -64
  85. data/lib/badline/pet/ieee_ports.rb +7 -0
  86. data/lib/badline/pet.rb +15 -4
  87. data/lib/badline/plus4/acia.rb +4 -298
  88. data/lib/badline/plus4/keyed_parts.rb +0 -44
  89. data/lib/badline/plus4.rb +1 -0
  90. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  91. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  92. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  93. data/lib/badline/roms/README +111 -0
  94. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  95. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  96. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  97. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  98. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  99. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  100. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  101. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  102. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  103. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  104. data/lib/badline/snapshot/container.rb +7 -2
  105. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  106. data/lib/badline/snapshot/field_doc.rb +38 -3
  107. data/lib/badline/snapshot/image.rb +62 -9
  108. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  109. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  110. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  111. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  112. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  113. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  114. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  115. data/lib/badline/snapshot/vice/cias.rb +13 -9
  116. data/lib/badline/snapshot/vice/p500.rb +96 -0
  117. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  118. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  119. data/lib/badline/snapshot/vice/pet.rb +7 -3
  120. data/lib/badline/snapshot/vice/pet_cpu.rb +5 -2
  121. data/lib/badline/snapshot/vice/pet_crtc.rb +10 -15
  122. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  123. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  124. data/lib/badline/snapshot/vice/pet_restore.rb +70 -6
  125. data/lib/badline/snapshot/vice/pet_via.rb +6 -5
  126. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  127. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  128. data/lib/badline/snapshot/vice.rb +3 -0
  129. data/lib/badline/snapshot.rb +11 -3
  130. data/lib/badline/storage/crt_file.rb +7 -1
  131. data/lib/badline/storage/d64_image.rb +7 -0
  132. data/lib/badline/storage/d80_image.rb +17 -1
  133. data/lib/badline/storage/d81_image.rb +14 -0
  134. data/lib/badline/storage/disk_image/bam.rb +18 -3
  135. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  136. data/lib/badline/storage/disk_image/writing.rb +35 -0
  137. data/lib/badline/storage/disk_image.rb +2 -0
  138. data/lib/badline/storage.rb +1 -0
  139. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  140. data/lib/badline/support/p500_checkpoint.rb +35 -0
  141. data/lib/badline/version.rb +1 -1
  142. data/lib/badline.rb +1 -0
  143. metadata +72 -2
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CBM2
5
+ # Where the B series KERNALs, 901244-04a and -03b, keep the eight
6
+ # IEEE-488 primitives, their file variables in segment 15, and the
7
+ # steps they take on the bus's lines (KernalTrap::IEEELayout). TALK and
8
+ # LISTEN take the device in A.
9
+ #
10
+ # The KERNAL drives the bus from TPI1's port A at $DE00 (PA3 ATN, PA4
11
+ # DAV, PA5 EOI, PA6 NDAC, PA7 NRFD, PA0-PA2 the transceivers'
12
+ # directions) and the CIA's port A at $DC00, the data lines inverted.
13
+ # TALK and LISTEN set both ports' directions for sending and pull ATN
14
+ # low, TKSA and ACPTR turn NRFD, NDAC and the data lines round for
15
+ # listening, and each handshake times out on CIA timer B, which the
16
+ # ROM starts with $FF to $DC07 and $11 to $DC0F and acknowledges by
17
+ # reading $DC0D. ACPTR leaves port A as TKSA does, without touching
18
+ # the directions. 901244-03b differs from -04a in setting PA0 where -04a
19
+ # clears it, at the end of UNTALK, UNLISTEN and ACPTR.
20
+ module IEEELayout
21
+ LineStep = KernalTrap::IEEELineStep
22
+
23
+ # A write of +value+ to +address+.
24
+ def self.write(address, value) = LineStep.new(address:, keep: 0, set: value)
25
+
26
+ # +address+'s bits in +keep+, ORed with +set+.
27
+ def self.update(address, keep, set) = LineStep.new(address:, keep:, set:)
28
+
29
+ TIMER = [write(0xdc07, 0xff), write(0xdc0f, 0x11),
30
+ LineStep.new(address: 0xdc0d, keep: KernalTrap::IEEE_LINE_READ, set: 0)].freeze
31
+ ATTENTION = [write(0xde03, 0x3f), write(0xdc00, 0xff), write(0xdc02, 0xff), write(0xde00, 0xfa),
32
+ update(0xde00, 0xf7, 0)].freeze
33
+ BYTE_SENT = [update(0xde00, 0xff, 0x12), write(0xdc00, 0xff)].freeze
34
+ RELEASE_ATTENTION = [update(0xde00, 0xff, 0x08)].freeze
35
+ LISTEN_TO_TALKER = [update(0xde00, 0x39, 0), write(0xde03, 0xc7), write(0xdc02, 0x00)].freeze
36
+
37
+ # The end of UNTALK and UNLISTEN, which leaves PA $F8, or $F9 on
38
+ # 901244-03b, before letting ATN go, and the end of ACPTR, which
39
+ # leaves PA0 clear, or set on 901244-03b.
40
+ def self.lines(release)
41
+ KernalTrap::IEEELines.new(
42
+ timer: TIMER, attention: ATTENTION, byte_sent: BYTE_SENT, release_attention: RELEASE_ATTENTION,
43
+ listen_to_talker: LISTEN_TO_TALKER, byte_received: [update(0xde00, 0x39, release & 0x01)].freeze,
44
+ release: (ATTENTION + BYTE_SENT + [write(0xde00, release)] + LISTEN_TO_TALKER.drop(1) +
45
+ RELEASE_ATTENTION).freeze
46
+ )
47
+ end
48
+
49
+ # The file variables in segment 15's zero page: the file name's
50
+ # pointer at $90, SAVE's start at $99 and its end at $96, ST at $9C,
51
+ # FNLEN at $9D, FA at $9F, SA at $A0 and VERIFY at $035F.
52
+ ZERO_PAGE = KernalTrap::ZeroPage.new(
53
+ status: 0x9c, verify: 0x035f, start_address: 0x99, end_address: 0x96, filename_length: 0x9d,
54
+ secondary_address: 0xa0, device: 0x9f, filename_address: 0x90, save_start: 0x99, load_address: 0x99
55
+ )
56
+
57
+ def self.layout(release)
58
+ KernalTrap::IEEELayout.new(
59
+ talk: 0xf230, listen: 0xf234, second: 0xf274, tksa: 0xf280, ciout: 0xf297, untalk: 0xf2ab,
60
+ unlisten: 0xf2af, acptr: 0xf30a, overwrites_device: false, device_in_a: true, zero_page: ZERO_PAGE,
61
+ lines: lines(release)
62
+ )
63
+ end
64
+
65
+ # By the KERNAL's name in Model::KERNALS.
66
+ LAYOUTS = { "04a" => layout(0xf8), "03b" => layout(0xf9) }.freeze
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CBM2
5
+ # The CBM-II's side of the IEEE-488 bus (Drives::IEEEBus): the data
6
+ # lines DIO1-8 on the CIA's port A at $DC00, and the handshake on
7
+ # TPI1's port A at $DE00:
8
+ #
9
+ # PA3 ATN PA4 DAV PA5 EOI PA6 NDAC PA7 NRFD
10
+ #
11
+ # PA0-PA2 set the transceivers' directions, which follow the ports'
12
+ # direction registers as the KERNAL sets them. Each line goes through
13
+ # its transceiver as it is: an output bit at 0 pulls its line low, and
14
+ # an input bit reads its line, 0 while it is low. It is TPI1's
15
+ # peripheral, and Data the CIA's.
16
+ #
17
+ # The machine pushes the lines it pulls into the bus for the drives on
18
+ # it once a cycle while a drive is on the bus (#cycle_lines), and only
19
+ # when they moved.
20
+ class IEEEPorts
21
+ ATN = 0x08
22
+ DAV = 0x10
23
+ EOI = 0x20
24
+ NDAC = 0x40
25
+ NRFD = 0x80
26
+
27
+ # The CIA's port A: the data lines.
28
+ class Data
29
+ def initialize(ports)
30
+ @ports = ports
31
+ end
32
+
33
+ def read_a(_port_a, _port_b) = ~@ports.low_lines & 0xff
34
+
35
+ def read_b(_port_a, _port_b) = 0xff
36
+
37
+ def port_b4_high? = true
38
+ end
39
+
40
+ attr_reader :bus, :data
41
+
42
+ def initialize(bus)
43
+ @bus = bus
44
+ @data = Data.new(self)
45
+ @cia = nil
46
+ @tpi = nil
47
+ @pushed = 0
48
+ end
49
+
50
+ def connect(cia, tpi)
51
+ @cia = cia
52
+ @tpi = tpi
53
+ end
54
+
55
+ # TPI1's port A: the handshake lines and ATN.
56
+ def read_a(_lines)
57
+ low = low_lines
58
+ value = 0xff
59
+ value &= ~ATN if low.anybits?(Drives::IEEEBus::ATN)
60
+ value &= ~DAV if low.anybits?(Drives::IEEEBus::DAV)
61
+ value &= ~EOI if low.anybits?(Drives::IEEEBus::EOI)
62
+ value &= ~NDAC if low.anybits?(Drives::IEEEBus::NDAC)
63
+ value &= ~NRFD if low.anybits?(Drives::IEEEBus::NRFD)
64
+ value & 0xff
65
+ end
66
+
67
+ def read_b(_lines) = 0xff
68
+
69
+ def read_c(_lines) = 0xff
70
+
71
+ # The lines low on the bus.
72
+ def low_lines = @bus.low_lines(controller_lines)
73
+
74
+ # The drives on the bus see the lines the machine pulls now, if they
75
+ # moved.
76
+ def cycle_lines
77
+ lines = controller_lines
78
+ push(lines) unless lines == @pushed
79
+ end
80
+
81
+ # The drives on the bus see the lines the machine pulls now.
82
+ def push_lines = push(controller_lines)
83
+
84
+ private
85
+
86
+ def push(lines)
87
+ @pushed = lines
88
+ @bus.host_lines = lines
89
+ end
90
+
91
+ # The lines the machine pulls low.
92
+ def controller_lines
93
+ low = ~@cia.port_a_lines & Drives::IEEEBus::DIO
94
+ port_a = @tpi.port_a_output
95
+ low |= Drives::IEEEBus::ATN if port_a.nobits?(ATN)
96
+ low |= Drives::IEEEBus::DAV if port_a.nobits?(DAV)
97
+ low |= Drives::IEEEBus::EOI if port_a.nobits?(EOI)
98
+ low |= Drives::IEEEBus::NDAC if port_a.nobits?(NDAC)
99
+ low |= Drives::IEEEBus::NRFD if port_a.nobits?(NRFD)
100
+ low
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CBM2
5
+ # The disk device 8 serves through the traps on the KERNAL's IEEE-488
6
+ # primitives (KernalTrap::IEEE, IEEELayout), and CHROUT capture. There
7
+ # is no LOAD or SAVE trap: the ROM's own LOAD and SAVE, which move the
8
+ # bytes into the segment they name, and BASIC's disk commands run
9
+ # through the primitives. Other devices find only a true drive on the
10
+ # bus (IEEEDrive), and the KERNAL reports the rest not present.
11
+ module IEEETraps
12
+ # Puts the storage in device 8. Mounting again swaps the disk while
13
+ # the machine runs, and the drive keeps its RAM and its status. The
14
+ # traps leave device 8 to a true drive with that number.
15
+ def mount(storage)
16
+ return @dos.insert(storage) if @dos
17
+
18
+ @dos = dos = KernalTrap::DOS.new(storage, channel_saves: true)
19
+ @ieee_trap = KernalTrap::IEEE.new(cpu:, bus:, layout: trap_layout, dos:).install
20
+ @ieee_trap.device = trap_device
21
+ @ieee_trap
22
+ end
23
+
24
+ # Takes device 8's storage out, and the traps with it.
25
+ def unmount
26
+ trap = @ieee_trap
27
+ return unless trap
28
+
29
+ trap.uninstall
30
+ @ieee_trap = nil
31
+ @dos = nil
32
+ end
33
+
34
+ # Whether device 8 serves a disk or directory through the traps.
35
+ def mounted? = !@dos.nil?
36
+
37
+ # The path of the disk or directory device 8 serves, or an empty one.
38
+ def mounted_path = @dos.nil? ? "" : @dos.path
39
+
40
+ # Device 8 through the traps in a keyed snapshot module (BL-TRAPDOS):
41
+ # the storage under storage.*, the DOS, and the IEEE-488 trap under
42
+ # ieee.*. Nothing mounted writes nothing.
43
+ def save_trap_dos_fields(out)
44
+ dos = @dos
45
+ return unless dos
46
+
47
+ Snapshot::StorageSetup.write_fields(dos.storage, out)
48
+ dos.save_fields(out)
49
+ @ieee_trap&.save_fields(out, "ieee.")
50
+ end
51
+
52
+ # `detached` mounts a disk image write-protected and no directory.
53
+ def load_trap_dos_fields(input, detached: false)
54
+ unmount
55
+ return if input.missing?
56
+
57
+ mount(Snapshot::StorageSetup.read_fields(input, detached:))
58
+ @dos&.load_fields(input)
59
+ @ieee_trap&.load_fields(input, "ieee.")
60
+ end
61
+
62
+ # Records what the KERNAL prints through CHROUT (KernalTrap::Chrout).
63
+ def capture_output
64
+ @capture_output ||= KernalTrap::Chrout.new(cpu:, bus:, layout: trap_layout).tap do |trap|
65
+ cpu.install_trap(KernalTrap::Chrout::ADDRESS) { trap.call }
66
+ end
67
+ end
68
+
69
+ # The IEEELayout of the KERNAL the machine runs.
70
+ def trap_layout = IEEELayout::LAYOUTS.fetch(@kernal)
71
+
72
+ private
73
+
74
+ # Device 8, unless a true drive is device 8.
75
+ def trap_device
76
+ device = KernalTrap::Routine::DEVICE
77
+ true_drive&.device == device ? nil : device
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CBM2
5
+ # What TPI2's ports read from outside the chip: the key matrix and the
6
+ # board's jumpers. The KERNAL drives the matrix's sixteen column lines
7
+ # low one at a time from PB0-PB7 and PA0-PA7 and reads its six row
8
+ # lines on PC0-PC5, a 0 for each key down. PC6 and PC7 read the
9
+ # jumpers: PC7 high on a 700, whose editor then loads its own CRTC
10
+ # table, and PC6 high on a 600 at 60 Hz.
11
+ #
12
+ # MATRIX lists the keys on each column line, PB0 first, in row order,
13
+ # :none where no key sits, as the KERNAL's key table at $EA29 places
14
+ # them, named as the B series' keyboard matrix sheet labels them.
15
+ class KeyboardPorts
16
+ MATRIX = [
17
+ %i[f1 escape tab none shift control],
18
+ %i[f2 1 q a z none],
19
+ %i[f3 2 w s x c],
20
+ %i[f4 3 e d f v],
21
+ %i[f5 4 r t g b],
22
+ %i[f6 5 6 y h n],
23
+ %i[f7 7 u j m space],
24
+ %i[f8 8 i k , .],
25
+ %i[f9 9 o l ; /],
26
+ [:f10, :"0", :-, :p, :"[", :"'"],
27
+ [:cursor_down, :"=", :left_arrow, :"]", :return, :pi],
28
+ %i[cursor_up cursor_left cursor_right delete cbm none],
29
+ %i[clr_home kp_question kp7 kp4 kp1 kp0],
30
+ %i[off_rvs ce kp8 kp5 kp2 kp_period],
31
+ %i[norm_graph kp_multiply kp9 kp6 kp3 kp00],
32
+ %i[run_stop kp_divide kp_minus kp_plus kp_enter none]
33
+ ].freeze
34
+
35
+ attr_reader :keyboard
36
+
37
+ # +jumpers+ is PC6 and PC7 as the board's jumpers drive them.
38
+ def initialize(keyboard, jumpers)
39
+ @keyboard = keyboard
40
+ @jumpers = jumpers
41
+ @tpi = nil
42
+ end
43
+
44
+ # The TPI whose ports drive the column lines.
45
+ def connect(tpi)
46
+ @tpi = tpi
47
+ end
48
+
49
+ def read_a(lines) = lines
50
+
51
+ def read_b(lines) = lines
52
+
53
+ def read_c(lines)
54
+ columns = (@tpi.port_a_output << 8) | @tpi.port_b_output
55
+ rows = @keyboard.scan(columns, lines | 0x3f).last
56
+ (rows & 0x3f) | (@jumpers & lines & 0xc0)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CBM2
5
+ # The whole machine as keyed snapshot modules (Snapshot::Keyed), in the
6
+ # order a load applies them: BL-MACHINE with the setup, the RAM with
7
+ # the segment registers, the CRTC, the picture, the SID, the CIA, the ACIA, both
8
+ # TPIs, the CPU, device 8 through the traps and a true 2031 on the
9
+ # IEEE-488 bus.
10
+ #
11
+ # A load lays the pages out before the chips load, and works out what
12
+ # each unit derives from its fields once every unit is loaded.
13
+ module KeyedModules
14
+ def save_modules
15
+ bus = @bus
16
+ sections = [Snapshot::Keyed.section("BL-MACHINE") { |out| save_machine_fields(out) }]
17
+ sections << Snapshot::Keyed.section("BL-CPU") { |out| @cpu.save_fields(out) }
18
+ sections << Snapshot::Keyed.section("BL-RAM") { |out| bus.save_fields(out) }
19
+ sections << Snapshot::Keyed.section("BL-CRTC") { |out| @crtc.save_fields(out) }
20
+ sections << Snapshot::Keyed.section("BL-VIDEO") { |out| @video.save_fields(out) }
21
+ sections << Snapshot::Keyed.section("BL-SID") { |out| @sid.save_fields(out) }
22
+ sections << Snapshot::Keyed.section("BL-CIA") { |out| @cia.save_fields(out) }
23
+ sections << Snapshot::Keyed.section("BL-ACIA") { |out| @acia.save_fields(out) }
24
+ sections << Snapshot::Keyed.section("BL-TPI1") { |out| @tpi1.save_fields(out) }
25
+ sections << Snapshot::Keyed.section("BL-TPI2") { |out| @tpi2.save_fields(out) }
26
+ sections << Snapshot::Keyed.section("BL-TRAPDOS") { |out| save_trap_dos_fields(out) } if mounted?
27
+ save_drive_modules(sections)
28
+ end
29
+
30
+ # Raises Snapshot::FormatError for the modules of a machine built
31
+ # another way.
32
+ def load_modules(modules)
33
+ check_snapshot_setup(Snapshot::CBM2Setup.read_fields(modules.machine))
34
+ load_machine_fields(modules.machine)
35
+ @bus.load_fields(modules.required("BL-RAM"))
36
+ @bus.restored!
37
+ @crtc.load_fields(modules.required("BL-CRTC"))
38
+ @video.load_fields(modules.required("BL-VIDEO"))
39
+ @sid.load_fields(modules.required("BL-SID"))
40
+ @cia.load_fields(modules.required("BL-CIA"))
41
+ @acia.load_fields(modules.required("BL-ACIA"))
42
+ @tpi1.load_fields(modules.required("BL-TPI1"))
43
+ @tpi2.load_fields(modules.required("BL-TPI2"))
44
+ @cpu.load_fields(modules.required("BL-CPU"))
45
+ trap_dos = modules.optional("BL-TRAPDOS")
46
+ trap_dos ? load_trap_dos_fields(trap_dos, detached: modules.detached?) : unmount
47
+ load_drive_modules(modules)
48
+ @cpu.restored!
49
+ @sid.restored!
50
+ @cia.restored!
51
+ @acia.restored!
52
+ self
53
+ end
54
+
55
+ private
56
+
57
+ def check_snapshot_setup(setup)
58
+ ours = Snapshot::CBM2Setup.of(self)
59
+ return if setup == ours
60
+
61
+ raise Snapshot::FormatError, "the snapshot is of a #{setup}, not a #{ours}"
62
+ end
63
+ end
64
+
65
+ include KeyedModules
66
+ end
67
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CBM2
5
+ class Bus
6
+ # Segment 15's RAM, the screen RAM, the RAM board's segments, both
7
+ # segment registers, the last byte on the bus and a cartridge's ROMs
8
+ # by load address, in a keyed snapshot module (BL-RAM). The pages are
9
+ # laid out again once loaded (#restored!). The BASIC and KERNAL ROMs
10
+ # come with the machine.
11
+ module KeyedState
12
+ def save_fields(out)
13
+ @ram.save_fields(out, "system")
14
+ @screen.save_fields(out, "screen")
15
+ @segment_ram.each_with_index { |memory, segment| memory&.save_fields(out, "segment#{segment}") }
16
+ out.int("execution_segment", @execution_segment).int("indirect_segment", @indirect_segment)
17
+ out.int("data", @data)
18
+ out.ints("cartridge.addresses", @cartridge.map(&:start))
19
+ @cartridge.each { |rom| out.bytes("cartridge.#{rom.start}", rom.snapshot) }
20
+ end
21
+
22
+ def load_fields(input)
23
+ @ram.load_fields(input, "system")
24
+ @screen.load_fields(input, "screen")
25
+ @segment_ram.each_with_index { |memory, segment| memory&.load_fields(input, "segment#{segment}") }
26
+ @execution_segment = input.masked("execution_segment", @execution_segment, 0x0f)
27
+ @indirect_segment = input.masked("indirect_segment", @indirect_segment, 0x0f)
28
+ @data = input.byte("data", @data)
29
+ @cartridge = input.ints("cartridge.addresses", []).filter_map { |address| cartridge_rom(input, address) }
30
+ end
31
+
32
+ def restored! = map_pages
33
+
34
+ private
35
+
36
+ # The cartridge ROM saved for +address+, in $1000-$7FFF of segment
37
+ # 15, or nil.
38
+ def cartridge_rom(input, address)
39
+ data = input.bytes("cartridge.#{address}", "".b).bytes
40
+ return nil if data.empty? || address < 0x1000 || address + data.length > 0x8000
41
+
42
+ ROM.new(data, length: data.length, start: address)
43
+ end
44
+ end
45
+
46
+ include KeyedState
47
+ end
48
+
49
+ # The machine's own fields in a keyed snapshot module (Snapshot::Keyed,
50
+ # BL-MACHINE): the family, its setup by name (Snapshot::CBM2Setup), the
51
+ # cycle count, where the mains is in its period, the refresh counter,
52
+ # and the host's typing
53
+ # still to feed and on_init handlers still to run under host.*. The
54
+ # badline version that wrote it is written for reports and never read.
55
+ module KeyedState
56
+ # How many more on_init handlers the machine a snapshot was taken of had
57
+ # yet to run than this one has. Nil until the machine is restored.
58
+ attr_reader :init_handlers_lost
59
+
60
+ # Whether a snapshot saves now (Machine::Settling): the CPU between
61
+ # instructions or jammed.
62
+ def settled_for_snapshot? = @cpu.boundary? || @cpu.jammed?
63
+
64
+ # The SID's deferred cycles and writes, run up to this cycle.
65
+ def settle_chips_for_snapshot = @sid.catch_up!
66
+
67
+ def save_machine_fields(out)
68
+ out.text("family", "cbm2").text("written_by", VERSION)
69
+ Snapshot::CBM2Setup.of(self).write_fields(out)
70
+ out.int("cycles", @cycles).int("mains_countdown", @mains_countdown).bool("mains_high", @mains_high)
71
+ out.int("refresh", @refresh)
72
+ pending = @pending_keys
73
+ out.ints("host.pending_keys", pending) if pending
74
+ out.int("host.init_pending", @cycles <= @init_threshold ? @init_handlers.length : 0)
75
+ end
76
+
77
+ def load_machine_fields(input)
78
+ @cycles = input.int("cycles", @cycles)
79
+ countdown = input.int("mains_countdown", @mains_countdown)
80
+ @mains_countdown = countdown.between?(1, @mains_half_period) ? countdown : @mains_half_period
81
+ @mains_high = input.bool("mains_high", @mains_high)
82
+ @refresh = input.int("refresh", 0).clamp(0, REFRESH_COUNT)
83
+ pending = input.ints("host.pending_keys", [])
84
+ @pending_keys = pending.empty? ? nil : pending.dup
85
+ @init_handlers_lost = [input.int("host.init_pending", 0) - @init_handlers.length, 0].max
86
+ end
87
+ end
88
+
89
+ include KeyedState
90
+ end
91
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CBM2
5
+ # The B series models badline builds by name, after xcbm2's -model
6
+ # names: the segments the RAM board fills, the BASIC that fits it, the
7
+ # character ROM, and whether the board's PC7 jumper tells the editor
8
+ # it drives the 700's monitor.
9
+ module Model
10
+ Profile = Data.define(:name, :ram_segments, :basic, :characters, :high_profile) do
11
+ def ram_kb = ram_segments.length * 64
12
+ end
13
+
14
+ M610 = Profile.new(name: "610", ram_segments: [1, 2].freeze, basic: "cbm2/basic128.rom",
15
+ characters: "cbm2/characters-600.rom", high_profile: false)
16
+ M620 = Profile.new(name: "620", ram_segments: [1, 2, 3, 4].freeze, basic: "cbm2/basic256.rom",
17
+ characters: "cbm2/characters-600.rom", high_profile: false)
18
+ M710 = Profile.new(name: "710", ram_segments: [1, 2].freeze, basic: "cbm2/basic128.rom",
19
+ characters: "cbm2/characters-700.rom", high_profile: true)
20
+ M720 = Profile.new(name: "720", ram_segments: [1, 2, 3, 4].freeze, basic: "cbm2/basic256.rom",
21
+ characters: "cbm2/characters-700.rom", high_profile: true)
22
+
23
+ ALL = [M610, M620, M710, M720].freeze
24
+ DEFAULT = "610"
25
+
26
+ # The KERNALs a B series board takes: 901244-04a, VICE's, and
27
+ # 901244-03b, the 1983 release.
28
+ KERNALS = { "04a" => "cbm2/kernal-04a.rom", "03b" => "cbm2/kernal-03b.rom" }.freeze
29
+
30
+ def self.named(name)
31
+ model = ALL.find { |candidate| candidate.name == name }
32
+ raise ArgumentError, "no CBM-II model named #{name}" if model.nil?
33
+
34
+ model
35
+ end
36
+
37
+ # The BASIC +model+ runs with +kernal+: BASIC 128 of the 1983
38
+ # release, 02b, with 03b.
39
+ def self.basic(model, kernal)
40
+ kernal == "03b" && model.basic == "cbm2/basic128.rom" ? "cbm2/basic128-02b.rom" : model.basic
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CBM2
5
+ class P500
6
+ # The P500's two control ports on the CIA at $DC00, as its schematic's
7
+ # game port interface wires them (sheets 9 and 10): port 1's
8
+ # directions on PB0-PB3 and its fire button on PA6, which is the VIC's
9
+ # light pen input too, and port 2's on PB4-PB7 and PA7. Every switch
10
+ # pulls its line low. Port A carries the IEEE-488 bus's data lines as
11
+ # well (IEEEPorts::Data), which pull it low too.
12
+ #
13
+ # A device on a port, a pair of paddles or a mouse, adds its buttons
14
+ # to the port's lines, and its pots reach the SID through the analog
15
+ # switches PA0 (port 1) and PA1 (port 2) close, as PA6 and PA7 do on
16
+ # the C64 (Input::ControlPorts).
17
+ class ControlPorts
18
+ DIRECTIONS = 0x0f
19
+ FIRE = 0x10
20
+ PORT1_POTS = 0x01
21
+ PORT2_POTS = 0x02
22
+
23
+ attr_reader :joystick1, :joystick2
24
+ attr_accessor :device1, :device2
25
+
26
+ def initialize(joystick1, joystick2, ieee)
27
+ @joystick1 = joystick1
28
+ @joystick2 = joystick2
29
+ @ieee = ieee
30
+ @device1 = nil
31
+ @device2 = nil
32
+ @cia = nil
33
+ end
34
+
35
+ # The CIA whose PA0 and PA1 close the pots' switches.
36
+ def connect(cia)
37
+ @cia = cia
38
+ end
39
+
40
+ def read_a(port_a, port_b)
41
+ fire1 = port1.anybits?(FIRE) ? 0x40 : 0
42
+ fire2 = port2.anybits?(FIRE) ? 0x80 : 0
43
+ (0x3f | fire1 | fire2) & @ieee.read_a(port_a, port_b)
44
+ end
45
+
46
+ def read_b(_port_a, _port_b)
47
+ (port1 & DIRECTIONS) | ((port2 & DIRECTIONS) << 4)
48
+ end
49
+
50
+ # PB4 is port 2's up line here, not a light pen input.
51
+ def port_b4_high? = true
52
+
53
+ # Whether port 1's fire line is high, the light pen input's.
54
+ def fire1_high? = port1.anybits?(FIRE)
55
+
56
+ def pot_x = selected_devices.map(&:pot_x).min || 0xff
57
+
58
+ def pot_y = selected_devices.map(&:pot_y).min || 0xff
59
+
60
+ private
61
+
62
+ def port1 = port_bits(@joystick1, @device1)
63
+
64
+ def port2 = port_bits(@joystick2, @device2)
65
+
66
+ def port_bits(joystick, device)
67
+ device ? joystick.port_bits & device.port_bits : joystick.port_bits
68
+ end
69
+
70
+ def selected_devices
71
+ lines = @cia&.port_a_lines || 0x00
72
+ devices = []
73
+ devices << @device1 if @device1 && lines.anybits?(PORT1_POTS)
74
+ devices << @device2 if @device2 && lines.anybits?(PORT2_POTS)
75
+ devices
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CBM2
5
+ class P500
6
+ # Where the P500's KERNAL, 901234-02, keeps the eight IEEE-488
7
+ # primitives: each seven bytes above the B series' (CBM2::IEEELayout),
8
+ # with the same file variables in segment 15's zero page. It drives
9
+ # the bus from the same ports, but leaves TPI1's PA2 an input, so its
10
+ # writes to TPI1's port A and its direction register differ in that
11
+ # bit: TALK and LISTEN set DDRA to $3B and PA to $FE, TKSA and ACPTR
12
+ # keep $3D of PA and set DDRA to $C3, ACPTR keeps $3D of PA and sets
13
+ # PA0, and UNTALK and UNLISTEN leave PA $FD before letting ATN go.
14
+ module IEEELayout
15
+ LineStep = KernalTrap::IEEELineStep
16
+
17
+ def self.write(address, value) = CBM2::IEEELayout.write(address, value)
18
+
19
+ def self.update(address, keep, set) = CBM2::IEEELayout.update(address, keep, set)
20
+
21
+ ATTENTION = [write(0xde03, 0x3b), write(0xdc00, 0xff), write(0xdc02, 0xff), write(0xde00, 0xfe),
22
+ update(0xde00, 0xf7, 0)].freeze
23
+ LISTEN_TO_TALKER = [update(0xde00, 0x3d, 0), write(0xde03, 0xc3), write(0xdc02, 0x00)].freeze
24
+
25
+ LINES = KernalTrap::IEEELines.new(
26
+ timer: CBM2::IEEELayout::TIMER, attention: ATTENTION, byte_sent: CBM2::IEEELayout::BYTE_SENT,
27
+ release_attention: CBM2::IEEELayout::RELEASE_ATTENTION, listen_to_talker: LISTEN_TO_TALKER,
28
+ byte_received: [update(0xde00, 0x3d, 0x01)].freeze,
29
+ release: (ATTENTION + CBM2::IEEELayout::BYTE_SENT + [write(0xde00, 0xfd)] + LISTEN_TO_TALKER.drop(1) +
30
+ CBM2::IEEELayout::RELEASE_ATTENTION).freeze
31
+ )
32
+
33
+ LAYOUT = KernalTrap::IEEELayout.new(
34
+ talk: 0xf237, listen: 0xf23b, second: 0xf27b, tksa: 0xf287, ciout: 0xf29e, untalk: 0xf2b2,
35
+ unlisten: 0xf2b6, acptr: 0xf311, overwrites_device: false, device_in_a: true,
36
+ zero_page: CBM2::IEEELayout::ZERO_PAGE, lines: LINES
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end