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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/CLAUDE.md +8 -7
- data/README.md +102 -6
- data/doc/pinned-behaviour.md +67 -0
- data/doc/snapshot-format.md +213 -18
- data/doc/snapshots.md +14 -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 +28 -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.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 +15 -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 +148 -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 +28 -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/vic20_media.rb +2 -2
- data/lib/badline/media.rb +36 -16
- data/lib/badline/options/family.rb +17 -6
- data/lib/badline/options/help.rb +19 -3
- data/lib/badline/options/table.rb +14 -2
- data/lib/badline/options/validation.rb +68 -5
- data/lib/badline/options.rb +31 -19
- data/lib/badline/pet/bus/expansion.rb +106 -0
- data/lib/badline/pet/bus.rb +204 -0
- data/lib/badline/pet/canvas.rb +97 -0
- data/lib/badline/pet/cassettes.rb +63 -0
- data/lib/badline/pet/characters.rb +26 -0
- data/lib/badline/pet/cpu.rb +10 -0
- data/lib/badline/pet/crtc_video.rb +142 -0
- data/lib/badline/pet/discrete_video.rb +83 -0
- data/lib/badline/pet/ieee_ports.rb +66 -0
- data/lib/badline/pet/ieee_traps.rb +75 -0
- data/lib/badline/pet/keyboard_ports.rb +74 -0
- data/lib/badline/pet/keyed_modules.rb +85 -0
- data/lib/badline/pet/keyed_parts.rb +123 -0
- data/lib/badline/pet/keyed_state.rb +42 -0
- data/lib/badline/pet/model.rb +113 -0
- data/lib/badline/pet/rom_sets.rb +65 -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 +226 -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 +137 -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/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.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 +2 -0
- data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
- data/lib/badline/snapshot/field_doc.rb +16 -3
- data/lib/badline/snapshot/image.rb +14 -7
- data/lib/badline/snapshot/pet_setup.rb +71 -0
- data/lib/badline/snapshot/storage_setup.rb +7 -1
- 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 +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,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
|