badline 0.15.1 → 0.16.1

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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/CLAUDE.md +8 -7
  4. data/README.md +128 -9
  5. data/doc/media.md +9 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +215 -18
  8. data/doc/snapshots.md +55 -0
  9. data/lib/badline/c64/common.rb +1 -1
  10. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  11. data/lib/badline/chips/crtc.rb +163 -0
  12. data/lib/badline/chips/pia/control_lines.rb +138 -0
  13. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  14. data/lib/badline/chips/pia.rb +138 -0
  15. data/lib/badline/chips/vic/register_log.rb +1 -4
  16. data/lib/badline/core.rb +3 -0
  17. data/lib/badline/drives/ieee_bus.rb +74 -0
  18. data/lib/badline/drives/model1551/cable.rb +5 -2
  19. data/lib/badline/drives/model1551/tia.rb +13 -34
  20. data/lib/badline/drives/model1551.rb +26 -4
  21. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  22. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  23. data/lib/badline/drives/model2031/parts.rb +25 -0
  24. data/lib/badline/drives/model2031.rb +92 -0
  25. data/lib/badline/drives.rb +1 -0
  26. data/lib/badline/frontend/app.rb +1 -1
  27. data/lib/badline/frontend/controls.rb +23 -3
  28. data/lib/badline/frontend/gamepads.rb +10 -3
  29. data/lib/badline/frontend/keys.rb +88 -0
  30. data/lib/badline/frontend/media_slots.rb +1 -0
  31. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  32. data/lib/badline/frontend/menu/menu_media.rb +27 -28
  33. data/lib/badline/frontend/menu/menu_pages.rb +24 -2
  34. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  35. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  36. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  37. data/lib/badline/frontend/screens.rb +2 -1
  38. data/lib/badline/frontend/timeline.rb +16 -9
  39. data/lib/badline/frontend.rb +1 -0
  40. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  41. data/lib/badline/kernal_trap/dos.rb +6 -1
  42. data/lib/badline/kernal_trap/frames.rb +117 -0
  43. data/lib/badline/kernal_trap/ieee.rb +151 -0
  44. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  45. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  46. data/lib/badline/kernal_trap/routine.rb +3 -53
  47. data/lib/badline/kernal_trap/serial.rb +4 -102
  48. data/lib/badline/kernal_trap/steps.rb +64 -0
  49. data/lib/badline/kernal_trap.rb +4 -0
  50. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  51. data/lib/badline/machine/settling.rb +1 -1
  52. data/lib/badline/machine.rb +29 -6
  53. data/lib/badline/media/extensions.rb +5 -2
  54. data/lib/badline/media/pet_media.rb +40 -0
  55. data/lib/badline/media/program.rb +14 -2
  56. data/lib/badline/media/true_drive.rb +19 -6
  57. data/lib/badline/media/vic20_media.rb +2 -2
  58. data/lib/badline/media.rb +41 -17
  59. data/lib/badline/options/family.rb +17 -6
  60. data/lib/badline/options/help.rb +23 -3
  61. data/lib/badline/options/table.rb +22 -4
  62. data/lib/badline/options/validation.rb +84 -5
  63. data/lib/badline/options.rb +47 -16
  64. data/lib/badline/pet/bus/expansion.rb +200 -0
  65. data/lib/badline/pet/bus.rb +222 -0
  66. data/lib/badline/pet/canvas.rb +97 -0
  67. data/lib/badline/pet/cassettes.rb +63 -0
  68. data/lib/badline/pet/characters.rb +32 -0
  69. data/lib/badline/pet/cpu.rb +10 -0
  70. data/lib/badline/pet/crtc_video.rb +155 -0
  71. data/lib/badline/pet/discrete_video.rb +83 -0
  72. data/lib/badline/pet/ieee_drive.rb +76 -0
  73. data/lib/badline/pet/ieee_ports.rb +77 -0
  74. data/lib/badline/pet/ieee_traps.rb +80 -0
  75. data/lib/badline/pet/keyboard_ports.rb +74 -0
  76. data/lib/badline/pet/keyed_modules.rb +88 -0
  77. data/lib/badline/pet/keyed_parts.rb +127 -0
  78. data/lib/badline/pet/keyed_state.rb +42 -0
  79. data/lib/badline/pet/model.rb +163 -0
  80. data/lib/badline/pet/rom_sets.rb +89 -0
  81. data/lib/badline/pet/sound.rb +128 -0
  82. data/lib/badline/pet/via.rb +10 -0
  83. data/lib/badline/pet/via_ports.rb +19 -0
  84. data/lib/badline/pet.rb +244 -0
  85. data/lib/badline/plus4/keyed_state.rb +5 -3
  86. data/lib/badline/plus4/tcbm_drive.rb +14 -0
  87. data/lib/badline/plus4.rb +1 -1
  88. data/lib/badline/roms/README +169 -0
  89. data/lib/badline/roms/dos2031.rom +0 -0
  90. data/lib/badline/roms/pet/basic1.rom +0 -0
  91. data/lib/badline/roms/pet/basic2.rom +0 -0
  92. data/lib/badline/roms/pet/basic4.rom +0 -0
  93. data/lib/badline/roms/pet/characters-din.rom +0 -0
  94. data/lib/badline/roms/pet/characters1.rom +0 -0
  95. data/lib/badline/roms/pet/characters2.rom +0 -0
  96. data/lib/badline/roms/pet/editor1.rom +0 -0
  97. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  98. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  99. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  100. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  101. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  102. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  103. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  104. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  105. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  106. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  107. data/lib/badline/roms/pet/kernal1.rom +0 -0
  108. data/lib/badline/roms/pet/kernal2.rom +0 -0
  109. data/lib/badline/roms/pet/kernal4.rom +0 -0
  110. data/lib/badline/snapshot/container.rb +6 -1
  111. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  112. data/lib/badline/snapshot/field_doc.rb +17 -3
  113. data/lib/badline/snapshot/image.rb +28 -10
  114. data/lib/badline/snapshot/pet_setup.rb +88 -0
  115. data/lib/badline/snapshot/storage_setup.rb +7 -1
  116. data/lib/badline/snapshot/vice/pet.rb +117 -0
  117. data/lib/badline/snapshot/vice/pet_cpu.rb +67 -0
  118. data/lib/badline/snapshot/vice/pet_crtc.rb +246 -0
  119. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  120. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  121. data/lib/badline/snapshot/vice/pet_restore.rb +150 -0
  122. data/lib/badline/snapshot/vice/pet_via.rb +164 -0
  123. data/lib/badline/snapshot/vice.rb +1 -0
  124. data/lib/badline/snapshot.rb +9 -5
  125. data/lib/badline/storage/d80_image.rb +48 -0
  126. data/lib/badline/storage/d82_image.rb +21 -0
  127. data/lib/badline/storage/tap.rb +16 -3
  128. data/lib/badline/storage.rb +2 -0
  129. data/lib/badline/support/pet_checkpoint.rb +38 -0
  130. data/lib/badline/timing.rb +5 -4
  131. data/lib/badline/version.rb +1 -1
  132. data/lib/badline/vic20/attachments.rb +7 -2
  133. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  134. data/lib/badline/vic20/bus.rb +5 -0
  135. data/lib/badline/vic20.rb +1 -1
  136. metadata +70 -1
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ # The PET's two cassette ports, each with a datasette:
6
+ #
7
+ # read motor, low runs it sense, low with a key down
8
+ # #1 PIA 1 CA1 PIA 1 CB2 PIA 1 PA4
9
+ # #2 VIA CB1 VIA PB4 PIA 1 PA5
10
+ #
11
+ # Both record from VIA PB3. The KERNAL reads each tape as the falling
12
+ # edges its read line makes, as the C64 and VIC-20 do. The machine
13
+ # passes the motor and write lines on after each write to the I/O
14
+ # chips (#lines_written).
15
+ class Cassettes
16
+ attr_reader :datasette1, :datasette2
17
+
18
+ def initialize(pia1, via)
19
+ @pia1 = pia1
20
+ @via = via
21
+ @datasette1 = Drives::Datasette.new
22
+ @datasette2 = Drives::Datasette.new
23
+ @datasette1.on_flag { pulse1 }
24
+ @datasette2.on_flag { pulse2 }
25
+ end
26
+
27
+ def cycle!
28
+ @datasette1.cycle!
29
+ @datasette2.cycle!
30
+ end
31
+
32
+ # PA4 and PA5 of PIA 1's port A, low for a deck with a key down.
33
+ def sense_input
34
+ value = 0xff
35
+ value &= 0xef if @datasette1.sense_low?
36
+ value &= 0xdf if @datasette2.sense_low?
37
+ value
38
+ end
39
+
40
+ def lines_written
41
+ port_b = @via.port_b_output
42
+ @datasette1.motor = !@pia1.cb2_output
43
+ @datasette2.motor = port_b.nobits?(0x10)
44
+ write_high = port_b.anybits?(0x08)
45
+ @datasette1.write_line = write_high
46
+ @datasette2.write_line = write_high
47
+ end
48
+
49
+ private
50
+
51
+ # A falling edge on a read line, and the line back up.
52
+ def pulse1
53
+ @pia1.ca1 = false
54
+ @pia1.ca1 = true
55
+ end
56
+
57
+ def pulse2
58
+ @via.cb1 = false
59
+ @via.cb1 = true
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ # The character ROM as the video reads it: 2K of two sets, graphics
6
+ # (upper case and graphics) from $000 and text (lower and upper case)
7
+ # from $400, 128 characters of 8 rows each. Bit 7 of a screen code
8
+ # inverts the character's rows, as the hardware does, so #rows holds
9
+ # 256 codes a set, each row at (set << 11) | (code << 3) | row. A 4K
10
+ # ROM, the 8296's, holds two such halves, the second at HALF.
11
+ class Characters
12
+ TEXT = 0x800
13
+
14
+ HALF = 0x1000
15
+
16
+ attr_reader :rows
17
+
18
+ def initialize(file)
19
+ rom = ROM.read(file)
20
+ @rows = Array.new(rom.length << 1) do |index|
21
+ set = index >> 11
22
+ code = (index >> 3) & 0xff
23
+ bits = rom[(set << 10) | ((code & 0x7f) << 3) | (index & 0x07)]
24
+ code >= 0x80 ? bits ^ 0xff : bits
25
+ end
26
+ end
27
+
28
+ # Whether the ROM holds two halves.
29
+ def halves? = @rows.length > HALF
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ # The PET's 6502, on the PET::Bus.
6
+ class CPU
7
+ include Badline::Chips::CPU::Core
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ # What the CRTC boards make of the CRTC's outputs. Each character the
6
+ # CRTC displays reads the screen RAM at its memory address, MA0-MA9,
7
+ # and the character ROM's row RA0-RA2 from the half VIA CA2 selects.
8
+ # On the 80-column boards each character reads two bytes, at twice the
9
+ # address and the one after, and shows them side by side, 16 dots to a
10
+ # cycle. MA12 low inverts the picture, and RA3 or RA4 high blanks the
11
+ # line (NO ROW on the 8032029 and 8032087 schematics' sheet 10), so a
12
+ # row taller than eight lines shows its first eight alone.
13
+ #
14
+ # The 8296's board (+board8296+) reads the screen from the 32K of RAM
15
+ # from $8000 on: MA0-MA11 count two bytes each from $8000, RA3 alone
16
+ # blanks the line, and RA4 adds $4000, so lines 16 to 23 of a row
17
+ # taller than 16 show the RAM under BASIC at $C000. MA13 picks the half
18
+ # of a 4K character ROM.
19
+ #
20
+ # The picture is a Canvas 273 lines high, 704 dots wide on 80 columns
21
+ # and 384 on 40, framed as xpet frames it. Across, R1's characters sit
22
+ # in the middle as R1, R0 and R2 stand at the start of the frame, and
23
+ # each character shows as far right as it comes after its line's
24
+ # horizontal sync, so a line whose sync moves moves with it. Down, the
25
+ # frame's first line sits where the lines the last frame showed until
26
+ # the display stopped at R6 would sit in the middle (a frame where it
27
+ # never stopped leaves that alone), and each horizontal sync starts the
28
+ # next line of the picture. Nothing shows within MARGIN dots of either
29
+ # side.
30
+ class CRTCVideo
31
+ HEIGHT = 273
32
+
33
+ # The dots at each side that xpet leaves black even where the display
34
+ # runs on through horizontal sync.
35
+ MARGIN = 8
36
+
37
+ attr_reader :canvas
38
+
39
+ # The screen RAM's bytes.
40
+ attr_writer :cells
41
+
42
+ def initialize(characters, via, crtc_registers, columns80:, board8296: false)
43
+ @cells = []
44
+ @rows = characters.rows
45
+ @cell_mask = board8296 ? 0xfff : 0x3ff
46
+ @ra4 = board8296 ? 0x10 : 0
47
+ @no_row = board8296 ? 0x08 : 0x18
48
+ @half = characters.halves? ? 0x2000 : 0
49
+ @via = via
50
+ @registers = crtc_registers
51
+ @wide = columns80
52
+ @char_shift = columns80 ? 4 : 3
53
+ @canvas = Canvas.new(columns80 ? 704 : 384, HEIGHT, margin: MARGIN)
54
+ @displayed = 200
55
+ @frame_displayed = 0
56
+ @line_shown = false
57
+ @y = 0
58
+ @from_y = 0
59
+ @to_y = 0
60
+ @shown_from = 0
61
+ @shown_to = HEIGHT
62
+ @on_canvas = false
63
+ @origin = 0
64
+ @from = 0
65
+ @to = 0
66
+ frame_start
67
+ end
68
+
69
+ def character(address, raster, since_hsync)
70
+ @line_shown = true
71
+ return unless @on_canvas
72
+
73
+ x = @origin + (since_hsync << @char_shift)
74
+ @from = x if x < @from
75
+ @to = x if x > @to
76
+ bits = raster.anybits?(@no_row) ? 0 : row_bits(address, raster)
77
+ if @wide
78
+ @canvas.paint(x, @y, bits >> 8)
79
+ @canvas.paint(x + 8, @y, bits & 0xff)
80
+ else
81
+ @canvas.paint(x, @y, bits)
82
+ end
83
+ end
84
+
85
+ # Horizontal sync takes the beam to the next line of the picture.
86
+ def hsync
87
+ end_canvas_line
88
+ @y += 1
89
+ @on_canvas = @y >= 0 && @y < HEIGHT
90
+ end
91
+
92
+ def line_end
93
+ @frame_displayed += 1 if @line_shown
94
+ @line_shown = false
95
+ end
96
+
97
+ # The display stops for the rest of the frame after the lines it has
98
+ # shown, which frame the next one.
99
+ def rows_end
100
+ @displayed = @frame_displayed
101
+ end
102
+
103
+ def frame_start
104
+ end_canvas_line
105
+ sweep(@shown_from, [@from_y, @shown_to].min)
106
+ sweep([@to_y, @shown_from].max, @shown_to)
107
+ @shown_from = @from_y
108
+ @shown_to = @to_y
109
+ @frame_displayed = 0
110
+ regs = @registers
111
+ @origin = ((@canvas.width - (regs[1] << @char_shift)) >> 1) - ((regs[0] + 1 - regs[2]) << @char_shift)
112
+ @y = (HEIGHT - (@displayed + 1)) / 2
113
+ @on_canvas = @y >= 0 && @y < HEIGHT
114
+ @from_y = @y.clamp(0, HEIGHT)
115
+ @to_y = @from_y
116
+ end
117
+
118
+ private
119
+
120
+ # The row's dots, both bytes' on 80 columns, high byte left.
121
+ def row_bits(address, raster)
122
+ set = @via.ca2_output ? Characters::TEXT : 0
123
+ invert = address.anybits?(0x1000) ? 0 : 0xffff
124
+ line = set | (raster & 0x07) | ((address & @half) >> 1)
125
+ cell = address & @cell_mask
126
+ if @wide
127
+ base = (cell << 1) | ((raster & @ra4) << 10)
128
+ left = @rows[line | (@cells[base] << 3)]
129
+ right = @rows[line | (@cells[base | 1] << 3)]
130
+ ((left << 8) | right) ^ invert
131
+ else
132
+ (@rows[line | (@cells[cell] << 3)] ^ invert) & 0xff
133
+ end
134
+ end
135
+
136
+ # Sets the line's span to the characters painted on it.
137
+ def end_canvas_line
138
+ if @on_canvas
139
+ @canvas.span(@y, @from, @to + (1 << @char_shift))
140
+ @to_y = @y + 1
141
+ end
142
+ @from = @canvas.width
143
+ @to = -@canvas.width
144
+ end
145
+
146
+ def sweep(from, to)
147
+ y = from
148
+ while y < to
149
+ @canvas.span(y, 0, 0)
150
+ y += 1
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ # The 2001's and 3000 series' video, which runs at 60 Hz on every
6
+ # board: 260 lines of 64 cycles, 16,640 cycles a frame. The first 200
7
+ # lines show the 25 rows of characters, one character a cycle from
8
+ # cycle FIRST_COLUMN of each line on, and the vertical drive line goes
9
+ # low for the other 60, which the editor's IRQ fires on and its screen
10
+ # writes wait for. xpet's 2001 and 3032 take 16,640 cycles from one
11
+ # falling edge of the line to the next, and keep it low for 3,840.
12
+ #
13
+ # Each character reads its screen code from the 1K of screen RAM, as
14
+ # it stands before the CPU's access that cycle, and its row from the
15
+ # character ROM's half that VIA CA2 selects: graphics while CA2 is low,
16
+ # text while it is high. The picture is the Canvas, DISPLAY_WIDTH by
17
+ # DISPLAY_HEIGHT dots with the characters from (BORDER_LEFT,
18
+ # BORDER_TOP) on, as xpet's screenshots frame it.
19
+ class DiscreteVideo
20
+ CYCLES_PER_LINE = 64
21
+ LINES_PER_FRAME = 260
22
+ DISPLAYED_LINES = 200
23
+ COLUMNS = 40
24
+ FIRST_COLUMN = 24
25
+ DISPLAY_WIDTH = 384
26
+ DISPLAY_HEIGHT = 217
27
+ BORDER_LEFT = 32
28
+ BORDER_TOP = 8
29
+
30
+ attr_reader :column, :line, :canvas
31
+
32
+ def initialize(cells, characters, via)
33
+ @cells = cells
34
+ @rows = characters.rows
35
+ @via = via
36
+ @canvas = Canvas.new(DISPLAY_WIDTH, DISPLAY_HEIGHT)
37
+ @column = 0
38
+ @line = 0
39
+ @row_base = 0
40
+ @row_line = 0
41
+ @y = BORDER_TOP
42
+ @shown = true
43
+ end
44
+
45
+ def cycle!
46
+ column = @column
47
+ fetch(column - FIRST_COLUMN) if column >= FIRST_COLUMN && @shown
48
+ if column < CYCLES_PER_LINE - 1
49
+ @column = column + 1
50
+ else
51
+ @column = 0
52
+ next_line
53
+ end
54
+ end
55
+
56
+ # The vertical drive line, high while the lines with characters show.
57
+ def vertical_drive? = @shown
58
+
59
+ private
60
+
61
+ def fetch(char)
62
+ set = @via.ca2_output ? Characters::TEXT : 0
63
+ bits = @rows[set | (@cells[@row_base + char] << 3) | @row_line]
64
+ @canvas.paint(BORDER_LEFT + (char << 3), @y, bits)
65
+ end
66
+
67
+ def next_line
68
+ @line += 1
69
+ @line = 0 if @line == LINES_PER_FRAME
70
+ follow_line
71
+ end
72
+
73
+ # Where the line puts the beam: the character row and its ROM row,
74
+ # the line of the picture, and whether characters show on it.
75
+ def follow_line
76
+ @row_line = @line & 0x07
77
+ @row_base = (@line >> 3) * COLUMNS
78
+ @y = BORDER_TOP + @line
79
+ @shown = @line < DISPLAYED_LINES
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/model2031"
4
+
5
+ module Badline
6
+ class PET
7
+ # The PET's true drive: a Drives::Model2031 on the IEEE-488 bus
8
+ # (IEEEPorts), clocked after the CPU each cycle and reset with it, as
9
+ # the PET's RES line reaches IFC. The traps on the KERNAL's primitives
10
+ # (IEEETraps) leave device 8 to the drive while it is device 8.
11
+ module IEEEDrive
12
+ # The 2031 on the bus, or nil.
13
+ attr_reader :drive2031
14
+
15
+ # Plugs in a Drives::Model2031, in place of the one there.
16
+ def attach_drive2031(drive)
17
+ detach_drive2031
18
+ drive.host_clock_hz = 1_000_000
19
+ drive.connect(@ieee.bus)
20
+ @drive2031 = drive
21
+ @ieee.lines_written
22
+ @ieee_trap&.device = trap_device
23
+ @init_threshold = [@init_threshold, Badline::Drives::Model2031::BOOT_CYCLES].max if @cycles < @init_threshold
24
+ end
25
+
26
+ # Unplugs the 2031, leaving it on a bus of its own.
27
+ def detach_drive2031
28
+ drive = @drive2031
29
+ return unless drive
30
+
31
+ drive.connect(Badline::Drives::IEEEBus.new)
32
+ @drive2031 = nil
33
+ @ieee_trap&.device = trap_device
34
+ end
35
+
36
+ # The true drive on the bus, or nil.
37
+ def true_drive = @drive2031
38
+
39
+ # Plugs in a 2031 as device 8, in place of the drive there, and
40
+ # returns it.
41
+ def plug_true_drive
42
+ Badline::Drives::Model2031.new.tap { |drive| attach_drive2031(drive) }
43
+ end
44
+
45
+ private
46
+
47
+ # The 2031 as keyed snapshot modules named from its device number:
48
+ # BL-D8, BL-D8-CPU and so on for device 8.
49
+ def save_drive_modules(sections)
50
+ drive = @drive2031
51
+ drive&.save_modules(sections, "BL-D#{drive.device}")
52
+ sections
53
+ end
54
+
55
+ # The 2031 a snapshot's modules hold, built afresh unless one is on
56
+ # the bus as the same device, or none when they hold no drive. The
57
+ # bus then carries the PET's lines as its chips left them.
58
+ def load_drive_modules(modules)
59
+ device = 8
60
+ input = modules.optional("BL-D8")
61
+ while input.nil? && device < 11
62
+ device += 1
63
+ input = modules.optional("BL-D#{device}")
64
+ end
65
+ if input
66
+ detach_drive2031 if @drive2031 && @drive2031.device != device
67
+ attach_drive2031(Badline::Drives::Model2031.new(device:)) unless @drive2031
68
+ @drive2031.load_modules(modules, input, "BL-D#{device}")
69
+ else
70
+ detach_drive2031
71
+ end
72
+ @ieee.lines_written
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ # The PET's side of the IEEE-488 bus (Drives::IEEEBus), spread over
6
+ # three chips:
7
+ #
8
+ # PIA 2 port A DIO1-8 in PIA 2 port B DIO1-8 out
9
+ # PIA 2 CA2 NDAC out PIA 2 CB2 DAV out
10
+ # PIA 1 PA6 EOI in PIA 1 CA2 EOI out
11
+ # VIA PB0 NDAC in VIA PB1 NRFD out
12
+ # VIA PB6 NRFD in VIA PB2 ATN out
13
+ # VIA PB7 DAV in
14
+ #
15
+ # The outputs go through inverting drivers onto the bus, so a 0 on a
16
+ # data line's port bit pulls the line low, and the inputs read the
17
+ # lines as they are, a 0 for a low line. It is PIA 2's peripheral, and
18
+ # answers what PIA 1 and the VIA read of the bus. It pushes the lines
19
+ # the PET pulls into the bus for the drives on it after every write to
20
+ # the chips (lines_written).
21
+ class IEEEPorts
22
+ attr_reader :bus
23
+
24
+ def initialize(bus)
25
+ @bus = bus
26
+ @pia1 = @pia2 = @via = nil
27
+ end
28
+
29
+ def connect(pia1, pia2, via)
30
+ @pia1 = pia1
31
+ @pia2 = pia2
32
+ @via = via
33
+ end
34
+
35
+ # The data lines.
36
+ def read_a(_lines) = ~low_lines & 0xff
37
+
38
+ def read_b(_lines) = 0xff
39
+
40
+ # PIA 1's PA6 reads EOI.
41
+ def eoi_input = low_lines.anybits?(Drives::IEEEBus::EOI) ? 0xbf : 0xff
42
+
43
+ # The VIA's PB0, PB6 and PB7 read NDAC, NRFD and DAV.
44
+ def via_inputs
45
+ low = low_lines
46
+ value = 0xff
47
+ value &= 0xfe if low.anybits?(Drives::IEEEBus::NDAC)
48
+ value &= 0xbf if low.anybits?(Drives::IEEEBus::NRFD)
49
+ value &= 0x7f if low.anybits?(Drives::IEEEBus::DAV)
50
+ value
51
+ end
52
+
53
+ # The PET wrote one of its chips: the drives on the bus see the lines
54
+ # it pulls now.
55
+ def lines_written
56
+ bus = @bus
57
+ bus.host_lines = controller_lines unless bus.drives.empty?
58
+ end
59
+
60
+ private
61
+
62
+ def low_lines = @bus.low_lines(controller_lines)
63
+
64
+ # The lines the PET pulls low.
65
+ def controller_lines
66
+ low = ~@pia2.port_b_output & Drives::IEEEBus::DIO
67
+ low |= Drives::IEEEBus::EOI unless @pia1.ca2_output
68
+ low |= Drives::IEEEBus::NDAC unless @pia2.ca2_output
69
+ low |= Drives::IEEEBus::DAV unless @pia2.cb2_output
70
+ port_b = @via.port_b_output
71
+ low |= Drives::IEEEBus::NRFD if port_b.nobits?(0x02)
72
+ low |= Drives::IEEEBus::ATN if port_b.nobits?(0x04)
73
+ low
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ # The disk device 8 serves through the traps on the KERNAL's IEEE-488
6
+ # primitives (KernalTrap::IEEE), and CHROUT capture. There is no LOAD
7
+ # or SAVE trap: the ROM's own LOAD and SAVE, and BASIC 4's disk
8
+ # commands, run through the primitives.
9
+ module IEEETraps
10
+ # Puts the storage in device 8. Mounting again swaps the disk while
11
+ # the machine runs, and the drive keeps its RAM and its status. The
12
+ # traps leave device 8 to a true drive with that number.
13
+ def mount(storage)
14
+ return @dos.insert(storage) if @dos
15
+
16
+ @dos = dos = KernalTrap::DOS.new(storage, channel_saves: true)
17
+ @ieee_trap = KernalTrap::IEEE.new(cpu:, bus:, layout: trap_layout, dos:).install
18
+ @ieee_trap.device = trap_device
19
+ @ieee_trap
20
+ end
21
+
22
+ # Takes device 8's storage out, and the traps with it, so the KERNAL
23
+ # finds no device 8 on the bus.
24
+ def unmount
25
+ trap = @ieee_trap
26
+ return unless trap
27
+
28
+ trap.uninstall
29
+ @ieee_trap = nil
30
+ @dos = nil
31
+ end
32
+
33
+ # Whether device 8 serves a disk or directory through the traps.
34
+ def mounted? = !@dos.nil?
35
+
36
+ # The path of the disk or directory device 8 serves, or an empty one.
37
+ def mounted_path = @dos.nil? ? "" : @dos.path
38
+
39
+ # Device 8 through the traps in a keyed snapshot module (BL-TRAPDOS):
40
+ # the storage under storage.*, the DOS, and the IEEE-488 trap under
41
+ # ieee.*. Nothing mounted writes nothing.
42
+ def save_trap_dos_fields(out)
43
+ dos = @dos
44
+ return unless dos
45
+
46
+ Snapshot::StorageSetup.write_fields(dos.storage, out)
47
+ dos.save_fields(out)
48
+ @ieee_trap&.save_fields(out, "ieee.")
49
+ end
50
+
51
+ # `detached` mounts a disk image write-protected and no directory.
52
+ def load_trap_dos_fields(input, detached: false)
53
+ unmount
54
+ return if input.missing?
55
+
56
+ mount(Snapshot::StorageSetup.read_fields(input, detached:))
57
+ @dos&.load_fields(input)
58
+ @ieee_trap&.load_fields(input, "ieee.")
59
+ end
60
+
61
+ # Records what the KERNAL prints through CHROUT (KernalTrap::Chrout).
62
+ def capture_output
63
+ @capture_output ||= KernalTrap::Chrout.new(cpu:, bus:, layout: trap_layout).tap do |trap|
64
+ cpu.install_trap(KernalTrap::Chrout::ADDRESS) { trap.call }
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # The IEEELayout of the KERNAL the model runs.
71
+ def trap_layout = @model.trap_layout
72
+
73
+ # Device 8, unless a true drive is device 8.
74
+ def trap_device
75
+ device = KernalTrap::Routine::DEVICE
76
+ true_drive&.device == device ? nil : device
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ # What PIA 1's port B reads from outside the chip: the key matrix's
6
+ # columns, for the row PA0-PA3 select through a 4-to-10 decoder. The
7
+ # editor writes a row number to $E810 and reads the row's keys from
8
+ # $E812, a 0 for each key down. A row number past 9 selects no row.
9
+ #
10
+ # The graphics keyboard (N) and the business keyboard (B) are wired
11
+ # differently, and the editor ROM scans the one its model has. Each
12
+ # matrix lists its rows in port B bit order, :none where no key sits,
13
+ # as the editors' key tables place them. The graphics keyboard's
14
+ # digits are on its keypad; the business keyboard has a row of them
15
+ # and a keypad (kp0 to kp9 and kp_period).
16
+ class KeyboardPorts
17
+ GRAPHICS_MATRIX = [
18
+ %i[! # % & ( left clr_home cursor_h],
19
+ [:'"', :"$", :"'", :"\\", :")", :none, :cursor_v, :delete],
20
+ %i[q e t u o up 7 9],
21
+ %i[w r y i p none 8 /],
22
+ %i[a d g j l none 4 6],
23
+ %i[s f h k : none 5 *],
24
+ %i[z c b m ; return 1 3],
25
+ %i[x v n , ? none 2 +],
26
+ [:lshift, :"@", :"]", :none, :>, :rshift, :"0", :-],
27
+ [:rvs, :"[", :space, :<, :run_stop, :none, :".", :"="]
28
+ ].freeze
29
+
30
+ BUSINESS_MATRIX = [
31
+ %i[2 5 8 - kp8 cursor_h none none],
32
+ %i[1 4 7 0 kp7 up none kp9],
33
+ [:escape, :s, :f, :h, :"]", :k, :";", :kp5],
34
+ %i[a d g j return l @ kp6],
35
+ %i[tab w r y \\ i p delete],
36
+ [:q, :e, :t, :u, :cursor_v, :o, :"[", :kp4],
37
+ %i[lshift c b . kp_period none rshift kp3],
38
+ %i[z v n , kp0 none repeat kp2],
39
+ %i[rvs x space m clr_home none / kp1],
40
+ %i[left 3 6 9 run_stop : none none]
41
+ ].freeze
42
+
43
+ attr_reader :keyboard
44
+
45
+ def initialize(keyboard)
46
+ @keyboard = keyboard
47
+ @pia = nil
48
+ @ieee = nil
49
+ @cassettes = nil
50
+ end
51
+
52
+ # The PIA these ports hang off, whose port A selects the row, the
53
+ # PET's IEEEPorts, which answer PA6, and its Cassettes, which answer
54
+ # PA4 and PA5.
55
+ def connect(pia, ieee, cassettes)
56
+ @pia = pia
57
+ @ieee = ieee
58
+ @cassettes = cassettes
59
+ end
60
+
61
+ # Port A's other lines: PA4 and PA5 read the cassette sense switches,
62
+ # PA6 the IEEE bus's EOI, and the diagnostic sense on PA7 floats high
63
+ # with nothing on it.
64
+ def read_a(_lines) = @ieee.eoi_input & @cassettes.sense_input
65
+
66
+ def read_b(lines)
67
+ row = @pia.port_a_output & 0x0f
68
+ return lines if row > 9
69
+
70
+ @keyboard.scan(0x3ff & ~(1 << row), lines).last
71
+ end
72
+ end
73
+ end
74
+ end