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,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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ # The PET's 6522 at $E840, a class of its own so its peripheral keeps
6
+ # one type (PET::VIAPorts), apart from the VIC-20's and the drives'.
7
+ class VIA < Chips::VIA
8
+ end
9
+ end
10
+ 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