badline 0.15.1 → 0.16.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/CLAUDE.md +8 -7
  4. data/README.md +102 -6
  5. data/doc/pinned-behaviour.md +67 -0
  6. data/doc/snapshot-format.md +213 -18
  7. data/doc/snapshots.md +14 -0
  8. data/lib/badline/c64/common.rb +1 -1
  9. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  10. data/lib/badline/chips/crtc.rb +163 -0
  11. data/lib/badline/chips/pia/control_lines.rb +138 -0
  12. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  13. data/lib/badline/chips/pia.rb +138 -0
  14. data/lib/badline/chips/vic/register_log.rb +1 -4
  15. data/lib/badline/core.rb +3 -0
  16. data/lib/badline/drives/ieee_bus.rb +28 -0
  17. data/lib/badline/drives/model1551/cable.rb +5 -2
  18. data/lib/badline/drives/model1551/tia.rb +13 -34
  19. data/lib/badline/drives/model1551.rb +26 -4
  20. data/lib/badline/drives.rb +1 -0
  21. data/lib/badline/frontend/app.rb +1 -1
  22. data/lib/badline/frontend/controls.rb +23 -3
  23. data/lib/badline/frontend/gamepads.rb +10 -3
  24. data/lib/badline/frontend/keys.rb +88 -0
  25. data/lib/badline/frontend/media_slots.rb +1 -0
  26. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  27. data/lib/badline/frontend/menu/menu_media.rb +27 -28
  28. data/lib/badline/frontend/menu/menu_pages.rb +15 -2
  29. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  30. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  31. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  32. data/lib/badline/frontend/screens.rb +2 -1
  33. data/lib/badline/frontend/timeline.rb +16 -9
  34. data/lib/badline/frontend.rb +1 -0
  35. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  36. data/lib/badline/kernal_trap/dos.rb +6 -1
  37. data/lib/badline/kernal_trap/frames.rb +117 -0
  38. data/lib/badline/kernal_trap/ieee.rb +148 -0
  39. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  40. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  41. data/lib/badline/kernal_trap/routine.rb +3 -53
  42. data/lib/badline/kernal_trap/serial.rb +4 -102
  43. data/lib/badline/kernal_trap/steps.rb +64 -0
  44. data/lib/badline/kernal_trap.rb +4 -0
  45. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  46. data/lib/badline/machine/settling.rb +1 -1
  47. data/lib/badline/machine.rb +28 -6
  48. data/lib/badline/media/extensions.rb +5 -2
  49. data/lib/badline/media/pet_media.rb +40 -0
  50. data/lib/badline/media/program.rb +14 -2
  51. data/lib/badline/media/vic20_media.rb +2 -2
  52. data/lib/badline/media.rb +36 -16
  53. data/lib/badline/options/family.rb +17 -6
  54. data/lib/badline/options/help.rb +19 -3
  55. data/lib/badline/options/table.rb +14 -2
  56. data/lib/badline/options/validation.rb +68 -5
  57. data/lib/badline/options.rb +31 -19
  58. data/lib/badline/pet/bus/expansion.rb +106 -0
  59. data/lib/badline/pet/bus.rb +204 -0
  60. data/lib/badline/pet/canvas.rb +97 -0
  61. data/lib/badline/pet/cassettes.rb +63 -0
  62. data/lib/badline/pet/characters.rb +26 -0
  63. data/lib/badline/pet/cpu.rb +10 -0
  64. data/lib/badline/pet/crtc_video.rb +142 -0
  65. data/lib/badline/pet/discrete_video.rb +83 -0
  66. data/lib/badline/pet/ieee_ports.rb +66 -0
  67. data/lib/badline/pet/ieee_traps.rb +75 -0
  68. data/lib/badline/pet/keyboard_ports.rb +74 -0
  69. data/lib/badline/pet/keyed_modules.rb +85 -0
  70. data/lib/badline/pet/keyed_parts.rb +123 -0
  71. data/lib/badline/pet/keyed_state.rb +42 -0
  72. data/lib/badline/pet/model.rb +113 -0
  73. data/lib/badline/pet/rom_sets.rb +65 -0
  74. data/lib/badline/pet/sound.rb +128 -0
  75. data/lib/badline/pet/via.rb +10 -0
  76. data/lib/badline/pet/via_ports.rb +19 -0
  77. data/lib/badline/pet.rb +226 -0
  78. data/lib/badline/plus4/keyed_state.rb +5 -3
  79. data/lib/badline/plus4/tcbm_drive.rb +14 -0
  80. data/lib/badline/plus4.rb +1 -1
  81. data/lib/badline/roms/README +137 -0
  82. data/lib/badline/roms/pet/basic1.rom +0 -0
  83. data/lib/badline/roms/pet/basic2.rom +0 -0
  84. data/lib/badline/roms/pet/basic4.rom +0 -0
  85. data/lib/badline/roms/pet/characters1.rom +0 -0
  86. data/lib/badline/roms/pet/characters2.rom +0 -0
  87. data/lib/badline/roms/pet/editor1.rom +0 -0
  88. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  89. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  90. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  91. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  92. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  93. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  94. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  95. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  96. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  97. data/lib/badline/roms/pet/kernal1.rom +0 -0
  98. data/lib/badline/roms/pet/kernal2.rom +0 -0
  99. data/lib/badline/roms/pet/kernal4.rom +0 -0
  100. data/lib/badline/snapshot/container.rb +2 -0
  101. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  102. data/lib/badline/snapshot/field_doc.rb +16 -3
  103. data/lib/badline/snapshot/image.rb +14 -7
  104. data/lib/badline/snapshot/pet_setup.rb +71 -0
  105. data/lib/badline/snapshot/storage_setup.rb +7 -1
  106. data/lib/badline/snapshot.rb +9 -5
  107. data/lib/badline/storage/d80_image.rb +48 -0
  108. data/lib/badline/storage/d82_image.rb +21 -0
  109. data/lib/badline/storage/tap.rb +16 -3
  110. data/lib/badline/storage.rb +2 -0
  111. data/lib/badline/support/pet_checkpoint.rb +38 -0
  112. data/lib/badline/timing.rb +5 -4
  113. data/lib/badline/version.rb +1 -1
  114. data/lib/badline/vic20/attachments.rb +7 -2
  115. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  116. data/lib/badline/vic20/bus.rb +5 -0
  117. data/lib/badline/vic20.rb +1 -1
  118. metadata +55 -1
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ # The picture a front end shows, a palette index a dot: 0 for black and
6
+ # 1 for the lit phosphor. The video paints it eight dots at a time, one
7
+ # character ROM row, and leaves alone the dots that would not change:
8
+ # each four dots remember the row last painted from them, and a
9
+ # painting that overlaps theirs from elsewhere makes them forget it.
10
+ #
11
+ # Each line keeps the span its last painting covered. When a line's
12
+ # span changes, the dots it covered before and doesn't now go black.
13
+ class Canvas
14
+ # Black and the green of a P1 phosphor, as the VICE testbench's
15
+ # references draw them.
16
+ PALETTE = [0x000000, 0x41ff00].freeze
17
+
18
+ # The eight dots of each ROM row, the leftmost in bit 7.
19
+ DOTS = Array.new(256) { |bits| Array.new(8) { |dot| (bits >> (7 - dot)) & 1 } }.freeze
20
+
21
+ attr_reader :display, :width, :height, :dirty_lines
22
+
23
+ # Nothing paints within +margin+ dots of either side.
24
+ def initialize(width, height, margin: 0)
25
+ @first = margin
26
+ @last = width - 8 - margin
27
+ @width = width
28
+ @height = height
29
+ @display = Array.new(width * height, 0)
30
+ @dirty_lines = Array.new(height, true)
31
+ @slot_width = width >> 2
32
+ @painted = Array.new(@slot_width * height, 0)
33
+ @span_from = Array.new(height, 0)
34
+ @span_to = Array.new(height, 0)
35
+ end
36
+
37
+ def palette = PALETTE
38
+
39
+ def clear_dirty_lines!
40
+ @dirty_lines.fill(false)
41
+ end
42
+
43
+ # Paints `bits` on `line` from dot `column` on, if they fit.
44
+ def paint(column, line, bits)
45
+ return if column < @first || column > @last
46
+
47
+ slot = (line * @slot_width) + (column >> 2)
48
+ return if @painted[slot] == bits
49
+
50
+ @painted[slot] = bits
51
+ @painted[slot - 1] = -1 if slot.positive?
52
+ @painted[slot + 1] = -1 if slot + 1 < @painted.length
53
+ @display[(line * @width) + column, 8] = DOTS[bits]
54
+ @dirty_lines[line] = true
55
+ end
56
+
57
+ # Sets `line`'s span to the dots from `from` up to `to`, none if `to`
58
+ # isn't past `from`, and blacks out what its last one covered beyond
59
+ # it.
60
+ def span(line, from, to)
61
+ width = @width
62
+ from = 0 if from.negative?
63
+ to = width if to > width
64
+ from = to = 0 if to <= from
65
+ old_from = @span_from[line]
66
+ old_to = @span_to[line]
67
+ return if from == old_from && to == old_to
68
+
69
+ blank(line, old_from, [from, old_to].min) if old_from < from
70
+ blank(line, [to, old_from].max, old_to) if old_to > to
71
+ @span_from[line] = from
72
+ @span_to[line] = to
73
+ end
74
+
75
+ # Blacks out every dot, as at power on.
76
+ def clear!
77
+ @display.fill(0)
78
+ @painted.fill(0)
79
+ @span_from.fill(0)
80
+ @span_to.fill(0)
81
+ @dirty_lines.fill(true)
82
+ end
83
+
84
+ private
85
+
86
+ def blank(line, from, to)
87
+ return if to <= from
88
+
89
+ @display.fill(0, (line * @width) + from, to - from)
90
+ first = (line * @slot_width) + (from >> 2)
91
+ @painted.fill(0, first, ((to + 3) >> 2) - (from >> 2))
92
+ @painted[first - 1] = -1 if from >= 4
93
+ @dirty_lines[line] = true
94
+ end
95
+ end
96
+ end
97
+ end
@@ -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,26 @@
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.
10
+ class Characters
11
+ TEXT = 0x800
12
+
13
+ attr_reader :rows
14
+
15
+ def initialize(file)
16
+ rom = ROM.read(file)
17
+ @rows = Array.new(0x1000) do |index|
18
+ set = index >> 11
19
+ code = (index >> 3) & 0xff
20
+ bits = rom[(set << 10) | ((code & 0x7f) << 3) | (index & 0x07)]
21
+ code >= 0x80 ? bits ^ 0xff : bits
22
+ end
23
+ end
24
+ end
25
+ end
26
+ 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,142 @@
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 high blanks the line.
11
+ #
12
+ # The picture is a Canvas 273 lines high, 704 dots wide on 80 columns
13
+ # and 384 on 40, framed as xpet frames it. Across, R1's characters sit
14
+ # in the middle as R1, R0 and R2 stand at the start of the frame, and
15
+ # each character shows as far right as it comes after its line's
16
+ # horizontal sync, so a line whose sync moves moves with it. Down, the
17
+ # frame's first line sits where the lines the last frame showed until
18
+ # the display stopped at R6 would sit in the middle (a frame where it
19
+ # never stopped leaves that alone), and each horizontal sync starts the
20
+ # next line of the picture. Nothing shows within MARGIN dots of either
21
+ # side.
22
+ class CRTCVideo
23
+ HEIGHT = 273
24
+
25
+ # The dots at each side that xpet leaves black even where the display
26
+ # runs on through horizontal sync.
27
+ MARGIN = 8
28
+
29
+ attr_reader :canvas
30
+
31
+ # The screen RAM's bytes.
32
+ attr_writer :cells
33
+
34
+ def initialize(characters, via, crtc_registers, columns80:)
35
+ @cells = []
36
+ @rows = characters.rows
37
+ @via = via
38
+ @registers = crtc_registers
39
+ @wide = columns80
40
+ @char_shift = columns80 ? 4 : 3
41
+ @canvas = Canvas.new(columns80 ? 704 : 384, HEIGHT, margin: MARGIN)
42
+ @displayed = 200
43
+ @frame_displayed = 0
44
+ @line_shown = false
45
+ @y = 0
46
+ @from_y = 0
47
+ @to_y = 0
48
+ @shown_from = 0
49
+ @shown_to = HEIGHT
50
+ @on_canvas = false
51
+ @origin = 0
52
+ @from = 0
53
+ @to = 0
54
+ frame_start
55
+ end
56
+
57
+ def character(address, raster, since_hsync)
58
+ @line_shown = true
59
+ return unless @on_canvas
60
+
61
+ x = @origin + (since_hsync << @char_shift)
62
+ @from = x if x < @from
63
+ @to = x if x > @to
64
+ bits = raster.anybits?(0x08) ? 0 : row_bits(address, raster)
65
+ if @wide
66
+ @canvas.paint(x, @y, bits >> 8)
67
+ @canvas.paint(x + 8, @y, bits & 0xff)
68
+ else
69
+ @canvas.paint(x, @y, bits)
70
+ end
71
+ end
72
+
73
+ # Horizontal sync takes the beam to the next line of the picture.
74
+ def hsync
75
+ end_canvas_line
76
+ @y += 1
77
+ @on_canvas = @y >= 0 && @y < HEIGHT
78
+ end
79
+
80
+ def line_end
81
+ @frame_displayed += 1 if @line_shown
82
+ @line_shown = false
83
+ end
84
+
85
+ # The display stops for the rest of the frame after the lines it has
86
+ # shown, which frame the next one.
87
+ def rows_end
88
+ @displayed = @frame_displayed
89
+ end
90
+
91
+ def frame_start
92
+ end_canvas_line
93
+ sweep(@shown_from, [@from_y, @shown_to].min)
94
+ sweep([@to_y, @shown_from].max, @shown_to)
95
+ @shown_from = @from_y
96
+ @shown_to = @to_y
97
+ @frame_displayed = 0
98
+ regs = @registers
99
+ @origin = ((@canvas.width - (regs[1] << @char_shift)) >> 1) - ((regs[0] + 1 - regs[2]) << @char_shift)
100
+ @y = (HEIGHT - (@displayed + 1)) / 2
101
+ @on_canvas = @y >= 0 && @y < HEIGHT
102
+ @from_y = @y.clamp(0, HEIGHT)
103
+ @to_y = @from_y
104
+ end
105
+
106
+ private
107
+
108
+ # The row's dots, both bytes' on 80 columns, high byte left.
109
+ def row_bits(address, raster)
110
+ set = @via.ca2_output ? Characters::TEXT : 0
111
+ invert = address.anybits?(0x1000) ? 0 : 0xffff
112
+ line = set | (raster & 0x07)
113
+ cell = address & 0x3ff
114
+ if @wide
115
+ left = @rows[line | (@cells[cell << 1] << 3)]
116
+ right = @rows[line | (@cells[(cell << 1) | 1] << 3)]
117
+ ((left << 8) | right) ^ invert
118
+ else
119
+ (@rows[line | (@cells[cell] << 3)] ^ invert) & 0xff
120
+ end
121
+ end
122
+
123
+ # Sets the line's span to the characters painted on it.
124
+ def end_canvas_line
125
+ if @on_canvas
126
+ @canvas.span(@y, @from, @to + (1 << @char_shift))
127
+ @to_y = @y + 1
128
+ end
129
+ @from = @canvas.width
130
+ @to = -@canvas.width
131
+ end
132
+
133
+ def sweep(from, to)
134
+ y = from
135
+ while y < to
136
+ @canvas.span(y, 0, 0)
137
+ y += 1
138
+ end
139
+ end
140
+ end
141
+ end
142
+ 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,66 @@
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.
19
+ class IEEEPorts
20
+ def initialize(bus)
21
+ @bus = bus
22
+ @pia1 = @pia2 = @via = nil
23
+ end
24
+
25
+ def connect(pia1, pia2, via)
26
+ @pia1 = pia1
27
+ @pia2 = pia2
28
+ @via = via
29
+ end
30
+
31
+ # The data lines.
32
+ def read_a(_lines) = ~low_lines & 0xff
33
+
34
+ def read_b(_lines) = 0xff
35
+
36
+ # PIA 1's PA6 reads EOI.
37
+ def eoi_input = low_lines.anybits?(Drives::IEEEBus::EOI) ? 0xbf : 0xff
38
+
39
+ # The VIA's PB0, PB6 and PB7 read NDAC, NRFD and DAV.
40
+ def via_inputs
41
+ low = low_lines
42
+ value = 0xff
43
+ value &= 0xfe if low.anybits?(Drives::IEEEBus::NDAC)
44
+ value &= 0xbf if low.anybits?(Drives::IEEEBus::NRFD)
45
+ value &= 0x7f if low.anybits?(Drives::IEEEBus::DAV)
46
+ value
47
+ end
48
+
49
+ private
50
+
51
+ def low_lines = @bus.low_lines(controller_lines)
52
+
53
+ # The lines the PET pulls low.
54
+ def controller_lines
55
+ low = ~@pia2.port_b_output & Drives::IEEEBus::DIO
56
+ low |= Drives::IEEEBus::EOI unless @pia1.ca2_output
57
+ low |= Drives::IEEEBus::NDAC unless @pia2.ca2_output
58
+ low |= Drives::IEEEBus::DAV unless @pia2.cb2_output
59
+ port_b = @via.port_b_output
60
+ low |= Drives::IEEEBus::NRFD if port_b.nobits?(0x02)
61
+ low |= Drives::IEEEBus::ATN if port_b.nobits?(0x04)
62
+ low
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,75 @@
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.
12
+ def mount(storage)
13
+ return @dos.insert(storage) if @dos
14
+
15
+ @dos = dos = KernalTrap::DOS.new(storage, channel_saves: true)
16
+ @ieee_trap = KernalTrap::IEEE.new(cpu:, bus:, layout: trap_layout, dos:).install
17
+ end
18
+
19
+ # Takes device 8's storage out, and the traps with it, so the KERNAL
20
+ # finds no device 8 on the bus.
21
+ def unmount
22
+ trap = @ieee_trap
23
+ return unless trap
24
+
25
+ trap.uninstall
26
+ @ieee_trap = nil
27
+ @dos = nil
28
+ end
29
+
30
+ # Whether device 8 serves a disk or directory through the traps.
31
+ def mounted? = !@dos.nil?
32
+
33
+ # The path of the disk or directory device 8 serves, or an empty one.
34
+ def mounted_path = @dos.nil? ? "" : @dos.path
35
+
36
+ # The true drive on the bus, which a PET has none of yet: every disk
37
+ # goes in device 8 through the traps.
38
+ def true_drive = nil
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
+ private
70
+
71
+ # The IEEELayout of the KERNAL the model runs.
72
+ def trap_layout = @model.trap_layout
73
+ end
74
+ end
75
+ 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