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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/CLAUDE.md +8 -7
- data/README.md +128 -9
- data/doc/media.md +9 -0
- data/doc/pinned-behaviour.md +67 -0
- data/doc/snapshot-format.md +215 -18
- data/doc/snapshots.md +55 -0
- data/lib/badline/c64/common.rb +1 -1
- data/lib/badline/chips/crtc/keyed_state.rb +46 -0
- data/lib/badline/chips/crtc.rb +163 -0
- data/lib/badline/chips/pia/control_lines.rb +138 -0
- data/lib/badline/chips/pia/keyed_parts.rb +56 -0
- data/lib/badline/chips/pia.rb +138 -0
- data/lib/badline/chips/vic/register_log.rb +1 -4
- data/lib/badline/core.rb +3 -0
- data/lib/badline/drives/ieee_bus.rb +74 -0
- data/lib/badline/drives/model1551/cable.rb +5 -2
- data/lib/badline/drives/model1551/tia.rb +13 -34
- data/lib/badline/drives/model1551.rb +26 -4
- data/lib/badline/drives/model2031/ieee_port.rb +133 -0
- data/lib/badline/drives/model2031/keyed_state.rb +66 -0
- data/lib/badline/drives/model2031/parts.rb +25 -0
- data/lib/badline/drives/model2031.rb +92 -0
- data/lib/badline/drives.rb +1 -0
- data/lib/badline/frontend/app.rb +1 -1
- data/lib/badline/frontend/controls.rb +23 -3
- data/lib/badline/frontend/gamepads.rb +10 -3
- data/lib/badline/frontend/keys.rb +88 -0
- data/lib/badline/frontend/media_slots.rb +1 -0
- data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
- data/lib/badline/frontend/menu/menu_media.rb +27 -28
- data/lib/badline/frontend/menu/menu_pages.rb +24 -2
- data/lib/badline/frontend/menu/menu_ports.rb +22 -1
- data/lib/badline/frontend/menu/pause_menu.rb +1 -0
- data/lib/badline/frontend/pet_keyboard.rb +124 -0
- data/lib/badline/frontend/screens.rb +2 -1
- data/lib/badline/frontend/timeline.rb +16 -9
- data/lib/badline/frontend.rb +1 -0
- data/lib/badline/kernal_trap/dos/writes.rb +3 -1
- data/lib/badline/kernal_trap/dos.rb +6 -1
- data/lib/badline/kernal_trap/frames.rb +117 -0
- data/lib/badline/kernal_trap/ieee.rb +151 -0
- data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
- data/lib/badline/kernal_trap/keyed_state.rb +4 -4
- data/lib/badline/kernal_trap/routine.rb +3 -53
- data/lib/badline/kernal_trap/serial.rb +4 -102
- data/lib/badline/kernal_trap/steps.rb +64 -0
- data/lib/badline/kernal_trap.rb +4 -0
- data/lib/badline/machine/keyboard_buffer.rb +8 -0
- data/lib/badline/machine/settling.rb +1 -1
- data/lib/badline/machine.rb +29 -6
- data/lib/badline/media/extensions.rb +5 -2
- data/lib/badline/media/pet_media.rb +40 -0
- data/lib/badline/media/program.rb +14 -2
- data/lib/badline/media/true_drive.rb +19 -6
- data/lib/badline/media/vic20_media.rb +2 -2
- data/lib/badline/media.rb +41 -17
- data/lib/badline/options/family.rb +17 -6
- data/lib/badline/options/help.rb +23 -3
- data/lib/badline/options/table.rb +22 -4
- data/lib/badline/options/validation.rb +84 -5
- data/lib/badline/options.rb +47 -16
- data/lib/badline/pet/bus/expansion.rb +200 -0
- data/lib/badline/pet/bus.rb +222 -0
- data/lib/badline/pet/canvas.rb +97 -0
- data/lib/badline/pet/cassettes.rb +63 -0
- data/lib/badline/pet/characters.rb +32 -0
- data/lib/badline/pet/cpu.rb +10 -0
- data/lib/badline/pet/crtc_video.rb +155 -0
- data/lib/badline/pet/discrete_video.rb +83 -0
- data/lib/badline/pet/ieee_drive.rb +76 -0
- data/lib/badline/pet/ieee_ports.rb +77 -0
- data/lib/badline/pet/ieee_traps.rb +80 -0
- data/lib/badline/pet/keyboard_ports.rb +74 -0
- data/lib/badline/pet/keyed_modules.rb +88 -0
- data/lib/badline/pet/keyed_parts.rb +127 -0
- data/lib/badline/pet/keyed_state.rb +42 -0
- data/lib/badline/pet/model.rb +163 -0
- data/lib/badline/pet/rom_sets.rb +89 -0
- data/lib/badline/pet/sound.rb +128 -0
- data/lib/badline/pet/via.rb +10 -0
- data/lib/badline/pet/via_ports.rb +19 -0
- data/lib/badline/pet.rb +244 -0
- data/lib/badline/plus4/keyed_state.rb +5 -3
- data/lib/badline/plus4/tcbm_drive.rb +14 -0
- data/lib/badline/plus4.rb +1 -1
- data/lib/badline/roms/README +169 -0
- data/lib/badline/roms/dos2031.rom +0 -0
- data/lib/badline/roms/pet/basic1.rom +0 -0
- data/lib/badline/roms/pet/basic2.rom +0 -0
- data/lib/badline/roms/pet/basic4.rom +0 -0
- data/lib/badline/roms/pet/characters-din.rom +0 -0
- data/lib/badline/roms/pet/characters1.rom +0 -0
- data/lib/badline/roms/pet/characters2.rom +0 -0
- data/lib/badline/roms/pet/editor1.rom +0 -0
- data/lib/badline/roms/pet/editor2-b.rom +0 -0
- data/lib/badline/roms/pet/editor2-n.rom +0 -0
- data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-b.rom +0 -0
- data/lib/badline/roms/pet/editor4-n.rom +0 -0
- data/lib/badline/roms/pet/kernal1.rom +0 -0
- data/lib/badline/roms/pet/kernal2.rom +0 -0
- data/lib/badline/roms/pet/kernal4.rom +0 -0
- data/lib/badline/snapshot/container.rb +6 -1
- data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
- data/lib/badline/snapshot/field_doc.rb +17 -3
- data/lib/badline/snapshot/image.rb +28 -10
- data/lib/badline/snapshot/pet_setup.rb +88 -0
- data/lib/badline/snapshot/storage_setup.rb +7 -1
- data/lib/badline/snapshot/vice/pet.rb +117 -0
- data/lib/badline/snapshot/vice/pet_cpu.rb +67 -0
- data/lib/badline/snapshot/vice/pet_crtc.rb +246 -0
- data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
- data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
- data/lib/badline/snapshot/vice/pet_restore.rb +150 -0
- data/lib/badline/snapshot/vice/pet_via.rb +164 -0
- data/lib/badline/snapshot/vice.rb +1 -0
- data/lib/badline/snapshot.rb +9 -5
- data/lib/badline/storage/d80_image.rb +48 -0
- data/lib/badline/storage/d82_image.rb +21 -0
- data/lib/badline/storage/tap.rb +16 -3
- data/lib/badline/storage.rb +2 -0
- data/lib/badline/support/pet_checkpoint.rb +38 -0
- data/lib/badline/timing.rb +5 -4
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic20/attachments.rb +7 -2
- data/lib/badline/vic20/bus/keyed_state.rb +5 -4
- data/lib/badline/vic20/bus.rb +5 -0
- data/lib/badline/vic20.rb +1 -1
- 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,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
|