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,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class PET
|
|
5
|
+
# The whole machine as keyed snapshot modules (Snapshot::Keyed), in the
|
|
6
|
+
# order a load applies them: BL-MACHINE with the setup, the RAM with
|
|
7
|
+
# the screen RAM, the last byte on the bus and an 8096's board, both
|
|
8
|
+
# PIAs, the VIA, the CRTC where the model has one, the picture with
|
|
9
|
+
# the video's beam, the CPU, the datasettes at cassette ports #1 and
|
|
10
|
+
# #2, and device 8 through the traps.
|
|
11
|
+
#
|
|
12
|
+
# A load lays the pages out before the chips load, and works out what
|
|
13
|
+
# each unit derives from its fields once every unit is loaded.
|
|
14
|
+
module KeyedModules
|
|
15
|
+
def save_modules
|
|
16
|
+
bus = @bus
|
|
17
|
+
sections = [Snapshot::Keyed.section("BL-MACHINE") { |out| save_machine_fields(out) }]
|
|
18
|
+
sections << Snapshot::Keyed.section("BL-CPU") { |out| @cpu.save_fields(out) }
|
|
19
|
+
sections << Snapshot::Keyed.section("BL-RAM") { |out| bus.save_fields(out) }
|
|
20
|
+
sections << Snapshot::Keyed.section("BL-PIA1") { |out| @pia1.save_fields(out) }
|
|
21
|
+
sections << Snapshot::Keyed.section("BL-PIA2") { |out| @pia2.save_fields(out) }
|
|
22
|
+
sections << Snapshot::Keyed.section("BL-VIA") { |out| @via.save_fields(out) }
|
|
23
|
+
save_video_modules(sections)
|
|
24
|
+
cassettes = @cassettes
|
|
25
|
+
sections << Snapshot::Keyed.section("BL-TAPE1") { |out| cassettes.datasette1.save_fields(out) }
|
|
26
|
+
sections << Snapshot::Keyed.section("BL-TAPE2") { |out| cassettes.datasette2.save_fields(out) }
|
|
27
|
+
sections << Snapshot::Keyed.section("BL-TRAPDOS") { |out| save_trap_dos_fields(out) } if mounted?
|
|
28
|
+
sections
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Raises Snapshot::FormatError for the modules of a machine built
|
|
32
|
+
# another way.
|
|
33
|
+
def load_modules(modules)
|
|
34
|
+
machine = modules.machine
|
|
35
|
+
check_snapshot_setup(Snapshot::PETSetup.read_fields(machine))
|
|
36
|
+
load_machine_fields(machine)
|
|
37
|
+
@bus.load_fields(modules.required("BL-RAM"))
|
|
38
|
+
@bus.restored!
|
|
39
|
+
@pia1.load_fields(modules.required("BL-PIA1"))
|
|
40
|
+
@pia2.load_fields(modules.required("BL-PIA2"))
|
|
41
|
+
@via.load_fields(modules.required("BL-VIA"))
|
|
42
|
+
load_video_modules(modules)
|
|
43
|
+
@cpu.load_fields(modules.required("BL-CPU"))
|
|
44
|
+
@cassettes.datasette1.load_fields(modules.required("BL-TAPE1"))
|
|
45
|
+
@cassettes.datasette2.load_fields(modules.required("BL-TAPE2"))
|
|
46
|
+
trap_dos = modules.optional("BL-TRAPDOS")
|
|
47
|
+
trap_dos ? load_trap_dos_fields(trap_dos, detached: modules.detached?) : unmount
|
|
48
|
+
@cpu.restored!
|
|
49
|
+
@via.restored!
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# The CRTC and what the CRTC boards paint from it, or what the
|
|
56
|
+
# discrete boards paint.
|
|
57
|
+
def save_video_modules(sections)
|
|
58
|
+
if @crtc_video
|
|
59
|
+
sections << Snapshot::Keyed.section("BL-CRTC") { |out| @crtc.save_fields(out) }
|
|
60
|
+
sections << Snapshot::Keyed.section("BL-VIDEO") { |out| @crtc_output.save_fields(out) }
|
|
61
|
+
else
|
|
62
|
+
sections << Snapshot::Keyed.section("BL-VIDEO") { |out| @discrete.save_fields(out) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def load_video_modules(modules)
|
|
67
|
+
if @crtc_video
|
|
68
|
+
@crtc.load_fields(modules.required("BL-CRTC"))
|
|
69
|
+
@crtc_output.load_fields(modules.required("BL-VIDEO"))
|
|
70
|
+
else
|
|
71
|
+
@discrete.load_fields(modules.required("BL-VIDEO"))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def check_snapshot_setup(setup)
|
|
76
|
+
ours = Snapshot::PETSetup.of(self)
|
|
77
|
+
return if setup == ours
|
|
78
|
+
|
|
79
|
+
raise Snapshot::FormatError, "the snapshot is of a #{setup}, not a #{ours}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
include KeyedModules
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class PET
|
|
5
|
+
class Canvas
|
|
6
|
+
# The picture in a keyed snapshot module, under `prefix`: each dot's
|
|
7
|
+
# palette index, the row each four dots were last painted from, and
|
|
8
|
+
# each line's span. Every line is drawn again once loaded.
|
|
9
|
+
module KeyedState
|
|
10
|
+
def save_fields(out, prefix)
|
|
11
|
+
out.bytes("#{prefix}dots", @display).ints("#{prefix}painted", @painted)
|
|
12
|
+
out.ints("#{prefix}span_from", @span_from).ints("#{prefix}span_to", @span_to)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def load_fields(input, prefix)
|
|
16
|
+
input.bytes_into("#{prefix}dots", @display)
|
|
17
|
+
input.ints_into("#{prefix}painted", @painted)
|
|
18
|
+
input.ints_into("#{prefix}span_from", @span_from)
|
|
19
|
+
input.ints_into("#{prefix}span_to", @span_to)
|
|
20
|
+
@dirty_lines.fill(true)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
include KeyedState
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class CRTCVideo
|
|
28
|
+
# Where the beam is on the picture and what the frames so far have
|
|
29
|
+
# framed it by, and the picture under canvas.*, in a keyed snapshot
|
|
30
|
+
# module (BL-VIDEO).
|
|
31
|
+
module KeyedState
|
|
32
|
+
def save_fields(out)
|
|
33
|
+
out.int("displayed", @displayed).int("frame_displayed", @frame_displayed).bool("line_shown", @line_shown)
|
|
34
|
+
out.int("y", @y).int("from_y", @from_y).int("to_y", @to_y).int("shown_from", @shown_from)
|
|
35
|
+
out.int("shown_to", @shown_to).bool("on_canvas", @on_canvas).int("origin", @origin)
|
|
36
|
+
out.int("from", @from).int("to", @to)
|
|
37
|
+
@canvas.save_fields(out, "canvas.")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def load_fields(input)
|
|
41
|
+
@displayed = input.int("displayed", @displayed)
|
|
42
|
+
@frame_displayed = input.int("frame_displayed", @frame_displayed)
|
|
43
|
+
@line_shown = input.bool("line_shown", @line_shown)
|
|
44
|
+
@y = input.int("y", @y)
|
|
45
|
+
@from_y = input.int("from_y", @from_y)
|
|
46
|
+
@to_y = input.int("to_y", @to_y)
|
|
47
|
+
@shown_from = input.int("shown_from", @shown_from)
|
|
48
|
+
@shown_to = input.int("shown_to", @shown_to)
|
|
49
|
+
@on_canvas = input.bool("on_canvas", @on_canvas)
|
|
50
|
+
@origin = input.int("origin", @origin)
|
|
51
|
+
@from = input.int("from", @from)
|
|
52
|
+
@to = input.int("to", @to)
|
|
53
|
+
@canvas.load_fields(input, "canvas.")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
include KeyedState
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class DiscreteVideo
|
|
61
|
+
# The cycle in the line and the line in the frame, and the picture
|
|
62
|
+
# under canvas.*, in a keyed snapshot module (BL-VIDEO). The row and
|
|
63
|
+
# the line of the picture follow from the line on load.
|
|
64
|
+
module KeyedState
|
|
65
|
+
def save_fields(out)
|
|
66
|
+
out.int("column", @column).int("line", @line)
|
|
67
|
+
@canvas.save_fields(out, "canvas.")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def load_fields(input)
|
|
71
|
+
@column = input.int("column", @column) % CYCLES_PER_LINE
|
|
72
|
+
@line = input.int("line", @line) % LINES_PER_FRAME
|
|
73
|
+
follow_line
|
|
74
|
+
@canvas.load_fields(input, "canvas.")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
include KeyedState
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class Bus
|
|
82
|
+
class Expansion
|
|
83
|
+
# The control register and the board's 64K, under `prefix`. The
|
|
84
|
+
# banks are laid over the pages again once the bus is loaded.
|
|
85
|
+
module KeyedState
|
|
86
|
+
def save_fields(out, prefix)
|
|
87
|
+
out.int("#{prefix}control", @control).bytes("#{prefix}ram", @cells)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def load_fields(input, prefix)
|
|
91
|
+
@control = input.byte("#{prefix}control", @control)
|
|
92
|
+
input.bytes_into("#{prefix}ram", @cells)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
include KeyedState
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The RAM below $8000, the screen RAM, the last byte on the bus under
|
|
100
|
+
# bus.* and an 8096's board under expansion.*, in a keyed snapshot
|
|
101
|
+
# module (BL-RAM). The pages are laid out again once they are loaded
|
|
102
|
+
# (#restored!). The ROMs come with the model.
|
|
103
|
+
module KeyedState
|
|
104
|
+
def save_fields(out)
|
|
105
|
+
@ram.save_fields(out, "ram")
|
|
106
|
+
out.bytes("screen", @screen.cells).int("bus.data", @data)
|
|
107
|
+
@expansion&.save_fields(out, "expansion.")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def load_fields(input)
|
|
111
|
+
@ram.load_fields(input, "ram")
|
|
112
|
+
@data = input.byte("bus.data", @data)
|
|
113
|
+
input.bytes_into("screen", @screen.cells)
|
|
114
|
+
@expansion&.load_fields(input, "expansion.")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def restored! = map_pages
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
include KeyedState
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class PET
|
|
5
|
+
# The machine's own fields in a keyed snapshot module (Snapshot::Keyed,
|
|
6
|
+
# BL-MACHINE): the family, its setup by name (Snapshot::PETSetup), the
|
|
7
|
+
# cycle count, the vertical drive line, and the host's typing still to
|
|
8
|
+
# feed and on_init handlers still to run under host.*. The badline
|
|
9
|
+
# version that wrote it is written for reports and never read.
|
|
10
|
+
module KeyedState
|
|
11
|
+
# How many more on_init handlers the machine a snapshot was taken of had
|
|
12
|
+
# yet to run than this one has. Nil until the machine is restored.
|
|
13
|
+
attr_reader :init_handlers_lost
|
|
14
|
+
|
|
15
|
+
# Whether a snapshot saves now (Machine::Settling): the CPU between
|
|
16
|
+
# instructions or jammed.
|
|
17
|
+
def settled_for_snapshot? = @cpu.boundary? || @cpu.jammed?
|
|
18
|
+
|
|
19
|
+
# Nothing defers its work past the cycle.
|
|
20
|
+
def settle_chips_for_snapshot = nil
|
|
21
|
+
|
|
22
|
+
def save_machine_fields(out)
|
|
23
|
+
out.text("family", "pet").text("written_by", VERSION)
|
|
24
|
+
Snapshot::PETSetup.of(self).write_fields(out)
|
|
25
|
+
out.int("cycles", @cycles).bool("vertical_drive", @vertical_drive)
|
|
26
|
+
pending = @pending_keys
|
|
27
|
+
out.ints("host.pending_keys", pending) if pending
|
|
28
|
+
out.int("host.init_pending", @cycles <= @init_threshold ? @init_handlers.length : 0)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def load_machine_fields(input)
|
|
32
|
+
@cycles = input.int("cycles", @cycles)
|
|
33
|
+
@vertical_drive = input.bool("vertical_drive", @vertical_drive)
|
|
34
|
+
pending = input.ints("host.pending_keys", [])
|
|
35
|
+
@pending_keys = pending.empty? ? nil : pending.dup
|
|
36
|
+
@init_handlers_lost = [input.int("host.init_pending", 0) - @init_handlers.length, 0].max
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
include KeyedState
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class PET
|
|
5
|
+
# The PETs and CBMs badline builds by name, after xpet's -model names:
|
|
6
|
+
# the board's RAM, its stock ROM set and video, and whether the 8096's
|
|
7
|
+
# 64K board is fitted.
|
|
8
|
+
#
|
|
9
|
+
# +video+ is :discrete, the fixed 40-column, 60 Hz generator of the
|
|
10
|
+
# 2001 and 3000 series, :crtc40 or :crtc80, a 6545 the editor ROM
|
|
11
|
+
# programs. The CRTC editors come in a 50 Hz and a 60 Hz version, and
|
|
12
|
+
# the discrete ones run at 60 Hz alone, so +editor_50hz+ and
|
|
13
|
+
# +editor_60hz+ name the same ROM there.
|
|
14
|
+
#
|
|
15
|
+
# The 2001's 1K of screen RAM repeats four times through $8000-$8FFF.
|
|
16
|
+
# Later boards decode $8000-$87FF alone, the 40-column ones with their
|
|
17
|
+
# 1K twice over and the 80-column ones with 2K.
|
|
18
|
+
module Model
|
|
19
|
+
Profile = Data.define(:name, :ram_kb, :basic, :editor_50hz, :editor_60hz, :kernal, :characters, :keyboard,
|
|
20
|
+
:video, :expansion) do
|
|
21
|
+
def crtc? = video != :discrete
|
|
22
|
+
|
|
23
|
+
def editor(refresh_hz) = refresh_hz == 50 ? editor_50hz : editor_60hz
|
|
24
|
+
|
|
25
|
+
def screen_bytes = video == :crtc80 ? 0x800 : 0x400
|
|
26
|
+
|
|
27
|
+
# How many pages from $8000 on the screen RAM answers in.
|
|
28
|
+
def screen_pages = name == "2001" ? 0x10 : 0x08
|
|
29
|
+
|
|
30
|
+
# The BASIC 1 KERNAL keeps its keyboard buffer and jiffy clock in
|
|
31
|
+
# page 2; BASIC 2 and 4 keep them where each other does.
|
|
32
|
+
def basic1? = basic == "pet/basic1.rom"
|
|
33
|
+
|
|
34
|
+
# The BASIC's version, "1", "2" or "4" (ROMSets::BASICS).
|
|
35
|
+
def basic_version = ROMSets::BASICS.find { |_, roms| roms.first == basic }.first
|
|
36
|
+
|
|
37
|
+
# Where the KERNAL keeps its IEEE-488 primitives
|
|
38
|
+
# (KernalTrap::IEEELayout).
|
|
39
|
+
def trap_layout
|
|
40
|
+
case kernal
|
|
41
|
+
when "pet/kernal1.rom" then KernalTrap::PET_BASIC1_LAYOUT
|
|
42
|
+
when "pet/kernal2.rom" then KernalTrap::PET_BASIC2_LAYOUT
|
|
43
|
+
else KernalTrap::PET_BASIC4_LAYOUT
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
PET2001 = Profile.new(name: "2001", ram_kb: 8, basic: "pet/basic1.rom", editor_50hz: "pet/editor1.rom",
|
|
49
|
+
editor_60hz: "pet/editor1.rom", kernal: "pet/kernal1.rom",
|
|
50
|
+
characters: "pet/characters1.rom", keyboard: :graphics, video: :discrete,
|
|
51
|
+
expansion: false)
|
|
52
|
+
PET3032 = Profile.new(name: "3032", ram_kb: 32, basic: "pet/basic2.rom", editor_50hz: "pet/editor2-n.rom",
|
|
53
|
+
editor_60hz: "pet/editor2-n.rom", kernal: "pet/kernal2.rom",
|
|
54
|
+
characters: "pet/characters2.rom", keyboard: :graphics, video: :discrete,
|
|
55
|
+
expansion: false)
|
|
56
|
+
PET3032B = Profile.new(name: "3032b", ram_kb: 32, basic: "pet/basic2.rom", editor_50hz: "pet/editor2-b.rom",
|
|
57
|
+
editor_60hz: "pet/editor2-b.rom", kernal: "pet/kernal2.rom",
|
|
58
|
+
characters: "pet/characters2.rom", keyboard: :business, video: :discrete,
|
|
59
|
+
expansion: false)
|
|
60
|
+
PET4032 = Profile.new(name: "4032", ram_kb: 32, basic: "pet/basic4.rom",
|
|
61
|
+
editor_50hz: "pet/editor4-40-n-50hz.rom", editor_60hz: "pet/editor4-40-n-60hz.rom",
|
|
62
|
+
kernal: "pet/kernal4.rom", characters: "pet/characters2.rom", keyboard: :graphics,
|
|
63
|
+
video: :crtc40, expansion: false)
|
|
64
|
+
CBM8032 = Profile.new(name: "8032", ram_kb: 32, basic: "pet/basic4.rom",
|
|
65
|
+
editor_50hz: "pet/editor4-80-b-50hz.rom", editor_60hz: "pet/editor4-80-b-60hz.rom",
|
|
66
|
+
kernal: "pet/kernal4.rom", characters: "pet/characters2.rom", keyboard: :business,
|
|
67
|
+
video: :crtc80, expansion: false)
|
|
68
|
+
CBM8096 = Profile.new(name: "8096", ram_kb: 32, basic: "pet/basic4.rom",
|
|
69
|
+
editor_50hz: "pet/editor4-80-b-50hz.rom", editor_60hz: "pet/editor4-80-b-60hz.rom",
|
|
70
|
+
kernal: "pet/kernal4.rom", characters: "pet/characters2.rom", keyboard: :business,
|
|
71
|
+
video: :crtc80, expansion: true)
|
|
72
|
+
|
|
73
|
+
ALL = [PET2001, PET3032, PET3032B, PET4032, CBM8032, CBM8096].freeze
|
|
74
|
+
|
|
75
|
+
DEFAULT = "8032"
|
|
76
|
+
|
|
77
|
+
# The RAM a board can have, in K, below $8000: the 3008, 3016 and
|
|
78
|
+
# 4016 are 3032s and 4032s with less of it.
|
|
79
|
+
RAM_SIZES = [4, 8, 16, 32].freeze
|
|
80
|
+
|
|
81
|
+
def self.named(name)
|
|
82
|
+
model = ALL.find { |candidate| candidate.name == name }
|
|
83
|
+
raise ArgumentError, "no PET model named #{name}" if model.nil?
|
|
84
|
+
|
|
85
|
+
model
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The +profile+ with the BASIC and the editor ROMSets names in place
|
|
89
|
+
# of its own: +basic+ a key of ROMSets::BASICS and +editor+ one of
|
|
90
|
+
# ROMSets::EDITORS, each nil for the one that fits. An editor named
|
|
91
|
+
# serves at either frame rate, so the machine's has to be its own.
|
|
92
|
+
# Raises ArgumentError for ROMs the model doesn't take.
|
|
93
|
+
def self.upgraded(profile, basic, editor)
|
|
94
|
+
return profile if basic.nil? && editor.nil?
|
|
95
|
+
|
|
96
|
+
name = profile.name
|
|
97
|
+
basic ||= ROMSets.stock_basic(name)
|
|
98
|
+
fitting = ROMSets.editors(name, basic)
|
|
99
|
+
raise ArgumentError, "the #{name} takes no BASIC #{basic}" if fitting.empty?
|
|
100
|
+
unless editor.nil? || fitting.include?(editor)
|
|
101
|
+
raise ArgumentError, "the #{name} with BASIC #{basic} takes no editor #{editor}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
basic_rom, kernal = ROMSets::BASICS.fetch(basic)
|
|
105
|
+
Profile.new(name:, ram_kb: profile.ram_kb, basic: basic_rom,
|
|
106
|
+
editor_50hz: ROMSets::EDITORS.fetch(editor || fitting.first),
|
|
107
|
+
editor_60hz: ROMSets::EDITORS.fetch(editor || fitting.last), kernal:,
|
|
108
|
+
characters: profile.characters, keyboard: profile.keyboard, video: profile.video,
|
|
109
|
+
expansion: profile.expansion)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class PET
|
|
5
|
+
# The BASIC and editor ROMs each model's board takes: its own, and the
|
|
6
|
+
# upgrades Commodore sold for it, BASIC 2 for the 2001 and BASIC 4 for
|
|
7
|
+
# the 3032s. Options reads it to check --basic and --editor, so it
|
|
8
|
+
# loads nothing else.
|
|
9
|
+
#
|
|
10
|
+
# An editor ROM goes with one BASIC, scans one keyboard and drives one
|
|
11
|
+
# board's video, so a model and a BASIC fit one editor, and on a CRTC
|
|
12
|
+
# board its 50 Hz and its 60 Hz versions.
|
|
13
|
+
module ROMSets
|
|
14
|
+
# BASIC's ROM and its KERNAL's, by version.
|
|
15
|
+
BASICS = {
|
|
16
|
+
"1" => %w[pet/basic1.rom pet/kernal1.rom], "2" => %w[pet/basic2.rom pet/kernal2.rom],
|
|
17
|
+
"4" => %w[pet/basic4.rom pet/kernal4.rom]
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# The editors by the names --editor takes: the BASIC, the keyboard,
|
|
21
|
+
# N or B, and on a CRTC board the columns and the frame rate.
|
|
22
|
+
EDITORS = {
|
|
23
|
+
"1" => "pet/editor1.rom", "2-n" => "pet/editor2-n.rom", "2-b" => "pet/editor2-b.rom",
|
|
24
|
+
"4-n" => "pet/editor4-n.rom", "4-b" => "pet/editor4-b.rom", "4-40-n-50hz" => "pet/editor4-40-n-50hz.rom",
|
|
25
|
+
"4-40-n-60hz" => "pet/editor4-40-n-60hz.rom", "4-80-b-50hz" => "pet/editor4-80-b-50hz.rom",
|
|
26
|
+
"4-80-b-50hz-0283" => "pet/editor4-80-b-50hz-0283.rom", "4-80-b-60hz" => "pet/editor4-80-b-60hz.rom"
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# The BASICs each model takes, its own first, and the editors that
|
|
30
|
+
# fit each, the 50 Hz one first and the 60 Hz one last on a CRTC
|
|
31
|
+
# board. The 8032's other 50 Hz editor, 901474-04 dated 0283, sits
|
|
32
|
+
# between them.
|
|
33
|
+
FITS = {
|
|
34
|
+
"2001" => { "1" => %w[1], "2" => %w[2-n] },
|
|
35
|
+
"3032" => { "2" => %w[2-n], "4" => %w[4-n] },
|
|
36
|
+
"3032b" => { "2" => %w[2-b], "4" => %w[4-b] },
|
|
37
|
+
"4032" => { "4" => %w[4-40-n-50hz 4-40-n-60hz] },
|
|
38
|
+
"8032" => { "4" => %w[4-80-b-50hz 4-80-b-50hz-0283 4-80-b-60hz] },
|
|
39
|
+
"8096" => { "4" => %w[4-80-b-50hz 4-80-b-50hz-0283 4-80-b-60hz] }
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
# The cycles a line and the lines a frame the CRTC editors set up
|
|
43
|
+
# where they differ from 50-cycle lines, 400 a frame at 50 Hz and 333
|
|
44
|
+
# at 60 Hz, by the editor's ROM.
|
|
45
|
+
CRTC_FRAMES = { "pet/editor4-80-b-50hz-0283.rom" => [64, 313] }.freeze
|
|
46
|
+
|
|
47
|
+
# The cycles a line and the lines a frame the CRTC editor in the ROM
|
|
48
|
+
# named sets up at +refresh_hz+.
|
|
49
|
+
def self.crtc_frame(editor, refresh_hz) = CRTC_FRAMES[editor] || [50, refresh_hz == 50 ? 400 : 333]
|
|
50
|
+
|
|
51
|
+
# The BASIC the model has, without --basic.
|
|
52
|
+
def self.stock_basic(model) = FITS.fetch(model).keys.first
|
|
53
|
+
|
|
54
|
+
# The editors that fit the model with the BASIC, or an empty list for
|
|
55
|
+
# a BASIC it doesn't take.
|
|
56
|
+
def self.editors(model, basic) = FITS.fetch(model).fetch(basic, [])
|
|
57
|
+
|
|
58
|
+
# The editor that fits the model with the BASIC at +refresh_hz+.
|
|
59
|
+
def self.editor(model, basic, refresh_hz)
|
|
60
|
+
fitting = editors(model, basic)
|
|
61
|
+
refresh_hz == 60 ? fitting.last : fitting.first
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class PET
|
|
5
|
+
# The PET's sound: the level the VIA drives on CB2, which the 4000 and
|
|
6
|
+
# 8000 series take to their internal speaker and every model to the
|
|
7
|
+
# user port's pin M, where the 2001 and 3000 series had their speaker
|
|
8
|
+
# added. CB2 carries the shift register's bits in its shift-out modes,
|
|
9
|
+
# mode 4's free-running square wave under timer 2 for the bell and most
|
|
10
|
+
# PET music, and the PCR's CB2 output otherwise, held high or low by
|
|
11
|
+
# hand or floating high as an input.
|
|
12
|
+
#
|
|
13
|
+
# The machine calls #cycle! once a cycle while #recording?, from #record
|
|
14
|
+
# on, so a sound nobody records costs only that check. Each sample is
|
|
15
|
+
# the share of its cycles CB2 spent high, through the output stage: a
|
|
16
|
+
# one-pole low-pass at 5 kHz and a one-pole high-pass at 100 Hz, so a
|
|
17
|
+
# line left high or low settles to silence.
|
|
18
|
+
class Sound
|
|
19
|
+
LOW_PASS_HZ = 5_000
|
|
20
|
+
HIGH_PASS_HZ = 100
|
|
21
|
+
|
|
22
|
+
# A swing from CB2 low to CB2 high, as a signed 16-bit sample.
|
|
23
|
+
SCALE = 24_000.0
|
|
24
|
+
|
|
25
|
+
def initialize(via)
|
|
26
|
+
@via = via
|
|
27
|
+
@recording = false
|
|
28
|
+
@high = true
|
|
29
|
+
@run = 0
|
|
30
|
+
@rate = 0
|
|
31
|
+
@clock_hz = 0
|
|
32
|
+
@remainder = 0
|
|
33
|
+
@window = 0
|
|
34
|
+
@left = 0
|
|
35
|
+
@sum = 0
|
|
36
|
+
@low = 0.0
|
|
37
|
+
@drift = 0.0
|
|
38
|
+
@low_pass = 0.0
|
|
39
|
+
@high_pass = 0.0
|
|
40
|
+
@samples = []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def model = :cb2
|
|
44
|
+
|
|
45
|
+
attr_reader :recording
|
|
46
|
+
alias recording? recording
|
|
47
|
+
|
|
48
|
+
# Starts collecting the output, averaged down to `rate` samples a
|
|
49
|
+
# second at `clock_hz` cycles a second, for #drain_samples. The
|
|
50
|
+
# output stage takes CB2's level now as settled.
|
|
51
|
+
def record(rate:, clock_hz: 1_000_000)
|
|
52
|
+
@rate = rate
|
|
53
|
+
@clock_hz = clock_hz
|
|
54
|
+
@remainder = 0
|
|
55
|
+
@window = next_window
|
|
56
|
+
@left = @window
|
|
57
|
+
@sum = 0
|
|
58
|
+
@high = @via.cb2_output
|
|
59
|
+
@run = 0
|
|
60
|
+
@low = @drift = @high ? 1.0 : 0.0
|
|
61
|
+
@low_pass = 1.0 - Math.exp(-2.0 * Math::PI * LOW_PASS_HZ / rate)
|
|
62
|
+
@high_pass = 1.0 - Math.exp(-2.0 * Math::PI * HIGH_PASS_HZ / rate)
|
|
63
|
+
@samples = []
|
|
64
|
+
@recording = true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Takes CB2's level for this cycle, while recording.
|
|
68
|
+
def cycle!
|
|
69
|
+
high = @via.cb2_output
|
|
70
|
+
if high == @high
|
|
71
|
+
@run += 1
|
|
72
|
+
else
|
|
73
|
+
hold(@high, @run)
|
|
74
|
+
@high = high
|
|
75
|
+
@run = 1
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The samples closed since the last drain.
|
|
80
|
+
def drain_samples
|
|
81
|
+
return [] unless @recording
|
|
82
|
+
|
|
83
|
+
hold(@high, @run)
|
|
84
|
+
@run = 0
|
|
85
|
+
samples = @samples
|
|
86
|
+
@samples = []
|
|
87
|
+
samples
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# Adds `cycles` cycles with CB2 high or low, closing every sample
|
|
93
|
+
# they finish.
|
|
94
|
+
def hold(high, cycles)
|
|
95
|
+
left = @left
|
|
96
|
+
while cycles >= left
|
|
97
|
+
@sum += left if high
|
|
98
|
+
cycles -= left
|
|
99
|
+
close
|
|
100
|
+
left = @left
|
|
101
|
+
end
|
|
102
|
+
@sum += cycles if high
|
|
103
|
+
@left = left - cycles
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def close
|
|
107
|
+
@low += @low_pass * (@sum.fdiv(@window) - @low)
|
|
108
|
+
@drift += @high_pass * (@low - @drift)
|
|
109
|
+
sample = ((@low - @drift) * SCALE).round
|
|
110
|
+
sample = 32_767 if sample > 32_767
|
|
111
|
+
sample = -32_768 if sample < -32_768
|
|
112
|
+
@samples << sample
|
|
113
|
+
@sum = 0
|
|
114
|
+
@window = next_window
|
|
115
|
+
@left = @window
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Each window is a whole number of cycles, and they add up to the
|
|
119
|
+
# clock's cycles over a second.
|
|
120
|
+
def next_window
|
|
121
|
+
@remainder += @clock_hz
|
|
122
|
+
cycles = @remainder / @rate
|
|
123
|
+
@remainder -= cycles * @rate
|
|
124
|
+
cycles
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class PET
|
|
5
|
+
# What the VIA's ports read from outside the chip. Port B's PB5 is the
|
|
6
|
+
# vertical drive line, low in the retrace, and PB0, PB6 and PB7 read
|
|
7
|
+
# the IEEE-488 bus's NDAC, NRFD and DAV (IEEEPorts). The user port on
|
|
8
|
+
# port A floats high.
|
|
9
|
+
class VIAPorts
|
|
10
|
+
def initialize(machine)
|
|
11
|
+
@machine = machine
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def read_a(_lines) = 0xff
|
|
15
|
+
|
|
16
|
+
def read_b(_lines) = @machine.ieee.via_inputs & (@machine.vertical_drive? ? 0xff : 0xdf)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|