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.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/CLAUDE.md +8 -7
  4. data/README.md +128 -9
  5. data/doc/media.md +9 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +215 -18
  8. data/doc/snapshots.md +55 -0
  9. data/lib/badline/c64/common.rb +1 -1
  10. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  11. data/lib/badline/chips/crtc.rb +163 -0
  12. data/lib/badline/chips/pia/control_lines.rb +138 -0
  13. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  14. data/lib/badline/chips/pia.rb +138 -0
  15. data/lib/badline/chips/vic/register_log.rb +1 -4
  16. data/lib/badline/core.rb +3 -0
  17. data/lib/badline/drives/ieee_bus.rb +74 -0
  18. data/lib/badline/drives/model1551/cable.rb +5 -2
  19. data/lib/badline/drives/model1551/tia.rb +13 -34
  20. data/lib/badline/drives/model1551.rb +26 -4
  21. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  22. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  23. data/lib/badline/drives/model2031/parts.rb +25 -0
  24. data/lib/badline/drives/model2031.rb +92 -0
  25. data/lib/badline/drives.rb +1 -0
  26. data/lib/badline/frontend/app.rb +1 -1
  27. data/lib/badline/frontend/controls.rb +23 -3
  28. data/lib/badline/frontend/gamepads.rb +10 -3
  29. data/lib/badline/frontend/keys.rb +88 -0
  30. data/lib/badline/frontend/media_slots.rb +1 -0
  31. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  32. data/lib/badline/frontend/menu/menu_media.rb +27 -28
  33. data/lib/badline/frontend/menu/menu_pages.rb +24 -2
  34. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  35. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  36. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  37. data/lib/badline/frontend/screens.rb +2 -1
  38. data/lib/badline/frontend/timeline.rb +16 -9
  39. data/lib/badline/frontend.rb +1 -0
  40. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  41. data/lib/badline/kernal_trap/dos.rb +6 -1
  42. data/lib/badline/kernal_trap/frames.rb +117 -0
  43. data/lib/badline/kernal_trap/ieee.rb +151 -0
  44. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  45. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  46. data/lib/badline/kernal_trap/routine.rb +3 -53
  47. data/lib/badline/kernal_trap/serial.rb +4 -102
  48. data/lib/badline/kernal_trap/steps.rb +64 -0
  49. data/lib/badline/kernal_trap.rb +4 -0
  50. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  51. data/lib/badline/machine/settling.rb +1 -1
  52. data/lib/badline/machine.rb +29 -6
  53. data/lib/badline/media/extensions.rb +5 -2
  54. data/lib/badline/media/pet_media.rb +40 -0
  55. data/lib/badline/media/program.rb +14 -2
  56. data/lib/badline/media/true_drive.rb +19 -6
  57. data/lib/badline/media/vic20_media.rb +2 -2
  58. data/lib/badline/media.rb +41 -17
  59. data/lib/badline/options/family.rb +17 -6
  60. data/lib/badline/options/help.rb +23 -3
  61. data/lib/badline/options/table.rb +22 -4
  62. data/lib/badline/options/validation.rb +84 -5
  63. data/lib/badline/options.rb +47 -16
  64. data/lib/badline/pet/bus/expansion.rb +200 -0
  65. data/lib/badline/pet/bus.rb +222 -0
  66. data/lib/badline/pet/canvas.rb +97 -0
  67. data/lib/badline/pet/cassettes.rb +63 -0
  68. data/lib/badline/pet/characters.rb +32 -0
  69. data/lib/badline/pet/cpu.rb +10 -0
  70. data/lib/badline/pet/crtc_video.rb +155 -0
  71. data/lib/badline/pet/discrete_video.rb +83 -0
  72. data/lib/badline/pet/ieee_drive.rb +76 -0
  73. data/lib/badline/pet/ieee_ports.rb +77 -0
  74. data/lib/badline/pet/ieee_traps.rb +80 -0
  75. data/lib/badline/pet/keyboard_ports.rb +74 -0
  76. data/lib/badline/pet/keyed_modules.rb +88 -0
  77. data/lib/badline/pet/keyed_parts.rb +127 -0
  78. data/lib/badline/pet/keyed_state.rb +42 -0
  79. data/lib/badline/pet/model.rb +163 -0
  80. data/lib/badline/pet/rom_sets.rb +89 -0
  81. data/lib/badline/pet/sound.rb +128 -0
  82. data/lib/badline/pet/via.rb +10 -0
  83. data/lib/badline/pet/via_ports.rb +19 -0
  84. data/lib/badline/pet.rb +244 -0
  85. data/lib/badline/plus4/keyed_state.rb +5 -3
  86. data/lib/badline/plus4/tcbm_drive.rb +14 -0
  87. data/lib/badline/plus4.rb +1 -1
  88. data/lib/badline/roms/README +169 -0
  89. data/lib/badline/roms/dos2031.rom +0 -0
  90. data/lib/badline/roms/pet/basic1.rom +0 -0
  91. data/lib/badline/roms/pet/basic2.rom +0 -0
  92. data/lib/badline/roms/pet/basic4.rom +0 -0
  93. data/lib/badline/roms/pet/characters-din.rom +0 -0
  94. data/lib/badline/roms/pet/characters1.rom +0 -0
  95. data/lib/badline/roms/pet/characters2.rom +0 -0
  96. data/lib/badline/roms/pet/editor1.rom +0 -0
  97. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  98. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  99. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  100. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  101. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  102. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  103. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  104. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  105. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  106. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  107. data/lib/badline/roms/pet/kernal1.rom +0 -0
  108. data/lib/badline/roms/pet/kernal2.rom +0 -0
  109. data/lib/badline/roms/pet/kernal4.rom +0 -0
  110. data/lib/badline/snapshot/container.rb +6 -1
  111. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  112. data/lib/badline/snapshot/field_doc.rb +17 -3
  113. data/lib/badline/snapshot/image.rb +28 -10
  114. data/lib/badline/snapshot/pet_setup.rb +88 -0
  115. data/lib/badline/snapshot/storage_setup.rb +7 -1
  116. data/lib/badline/snapshot/vice/pet.rb +117 -0
  117. data/lib/badline/snapshot/vice/pet_cpu.rb +67 -0
  118. data/lib/badline/snapshot/vice/pet_crtc.rb +246 -0
  119. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  120. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  121. data/lib/badline/snapshot/vice/pet_restore.rb +150 -0
  122. data/lib/badline/snapshot/vice/pet_via.rb +164 -0
  123. data/lib/badline/snapshot/vice.rb +1 -0
  124. data/lib/badline/snapshot.rb +9 -5
  125. data/lib/badline/storage/d80_image.rb +48 -0
  126. data/lib/badline/storage/d82_image.rb +21 -0
  127. data/lib/badline/storage/tap.rb +16 -3
  128. data/lib/badline/storage.rb +2 -0
  129. data/lib/badline/support/pet_checkpoint.rb +38 -0
  130. data/lib/badline/timing.rb +5 -4
  131. data/lib/badline/version.rb +1 -1
  132. data/lib/badline/vic20/attachments.rb +7 -2
  133. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  134. data/lib/badline/vic20/bus.rb +5 -0
  135. data/lib/badline/vic20.rb +1 -1
  136. metadata +70 -1
@@ -0,0 +1,88 @@
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, a 2031 on the IEEE-488 bus (IEEEDrive), and device 8 through the
11
+ # traps.
12
+ #
13
+ # A load lays the pages out before the chips load, and works out what
14
+ # each unit derives from its fields once every unit is loaded.
15
+ module KeyedModules
16
+ def save_modules
17
+ bus = @bus
18
+ sections = [Snapshot::Keyed.section("BL-MACHINE") { |out| save_machine_fields(out) }]
19
+ sections << Snapshot::Keyed.section("BL-CPU") { |out| @cpu.save_fields(out) }
20
+ sections << Snapshot::Keyed.section("BL-RAM") { |out| bus.save_fields(out) }
21
+ sections << Snapshot::Keyed.section("BL-PIA1") { |out| @pia1.save_fields(out) }
22
+ sections << Snapshot::Keyed.section("BL-PIA2") { |out| @pia2.save_fields(out) }
23
+ sections << Snapshot::Keyed.section("BL-VIA") { |out| @via.save_fields(out) }
24
+ save_video_modules(sections)
25
+ cassettes = @cassettes
26
+ sections << Snapshot::Keyed.section("BL-TAPE1") { |out| cassettes.datasette1.save_fields(out) }
27
+ sections << Snapshot::Keyed.section("BL-TAPE2") { |out| cassettes.datasette2.save_fields(out) }
28
+ save_drive_modules(sections)
29
+ sections << Snapshot::Keyed.section("BL-TRAPDOS") { |out| save_trap_dos_fields(out) } if mounted?
30
+ sections
31
+ end
32
+
33
+ # Raises Snapshot::FormatError for the modules of a machine built
34
+ # another way.
35
+ def load_modules(modules)
36
+ machine = modules.machine
37
+ check_snapshot_setup(Snapshot::PETSetup.read_fields(machine))
38
+ load_machine_fields(machine)
39
+ @bus.load_fields(modules.required("BL-RAM"))
40
+ @bus.restored!
41
+ @pia1.load_fields(modules.required("BL-PIA1"))
42
+ @pia2.load_fields(modules.required("BL-PIA2"))
43
+ @via.load_fields(modules.required("BL-VIA"))
44
+ load_video_modules(modules)
45
+ @cpu.load_fields(modules.required("BL-CPU"))
46
+ @cassettes.datasette1.load_fields(modules.required("BL-TAPE1"))
47
+ @cassettes.datasette2.load_fields(modules.required("BL-TAPE2"))
48
+ load_drive_modules(modules)
49
+ trap_dos = modules.optional("BL-TRAPDOS")
50
+ trap_dos ? load_trap_dos_fields(trap_dos, detached: modules.detached?) : unmount
51
+ @cpu.restored!
52
+ @via.restored!
53
+ self
54
+ end
55
+
56
+ private
57
+
58
+ # The CRTC and what the CRTC boards paint from it, or what the
59
+ # discrete boards paint.
60
+ def save_video_modules(sections)
61
+ if @crtc_video
62
+ sections << Snapshot::Keyed.section("BL-CRTC") { |out| @crtc.save_fields(out) }
63
+ sections << Snapshot::Keyed.section("BL-VIDEO") { |out| @crtc_output.save_fields(out) }
64
+ else
65
+ sections << Snapshot::Keyed.section("BL-VIDEO") { |out| @discrete.save_fields(out) }
66
+ end
67
+ end
68
+
69
+ def load_video_modules(modules)
70
+ if @crtc_video
71
+ @crtc.load_fields(modules.required("BL-CRTC"))
72
+ @crtc_output.load_fields(modules.required("BL-VIDEO"))
73
+ else
74
+ @discrete.load_fields(modules.required("BL-VIDEO"))
75
+ end
76
+ end
77
+
78
+ def check_snapshot_setup(setup)
79
+ ours = Snapshot::PETSetup.of(self)
80
+ return if setup == ours
81
+
82
+ raise Snapshot::FormatError, "the snapshot is of a #{setup}, not a #{ours}"
83
+ end
84
+ end
85
+
86
+ include KeyedModules
87
+ end
88
+ end
@@ -0,0 +1,127 @@
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`, and
84
+ # on the 8296 the user port lines its RAM last followed. The banks
85
+ # are laid over the pages again once the bus is loaded.
86
+ module KeyedState
87
+ def save_fields(out, prefix)
88
+ out.int("#{prefix}control", @control).bytes("#{prefix}ram", @cells)
89
+ out.int("#{prefix}user_port", @lines) if @board8296
90
+ end
91
+
92
+ def load_fields(input, prefix)
93
+ @control = input.byte("#{prefix}control", @control)
94
+ input.bytes_into("#{prefix}ram", @cells)
95
+ @lines = input.int("#{prefix}user_port", USER_PORT_LINES) & USER_PORT_LINES
96
+ end
97
+ end
98
+
99
+ include KeyedState
100
+ end
101
+
102
+ # The RAM below $8000, the screen RAM, all 32K from $8000 on an 8296,
103
+ # the last byte on the bus under bus.* and an 8096's or 8296's board
104
+ # under expansion.*, in a keyed snapshot module (BL-RAM). The pages
105
+ # are laid out again once they are loaded (#restored!). The ROMs come
106
+ # with the model.
107
+ module KeyedState
108
+ def save_fields(out)
109
+ @ram.save_fields(out, "ram")
110
+ out.bytes("screen", @screen.cells).int("bus.data", @data)
111
+ @expansion&.save_fields(out, "expansion.")
112
+ end
113
+
114
+ def load_fields(input)
115
+ @ram.load_fields(input, "ram")
116
+ @data = input.byte("bus.data", @data)
117
+ input.bytes_into("screen", @screen.cells)
118
+ @expansion&.load_fields(input, "expansion.")
119
+ end
120
+
121
+ def restored! = map_pages
122
+ end
123
+
124
+ include KeyedState
125
+ end
126
+ end
127
+ 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,163 @@
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 a 64K
7
+ # board banked through $FFF0 is fitted, the 8096's or the 8296's own.
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. The 8296 has 128K of
18
+ # dynamic RAM, the 32K below $8000, the 32K above it, of which the
19
+ # screen is the start and the rest lies under the ROMs, and the 64K
20
+ # board (Bus::Expansion).
21
+ module Model
22
+ Profile = Data.define(:name, :ram_kb, :basic, :editor_50hz, :editor_60hz, :kernal, :characters, :keyboard,
23
+ :video, :expansion) do
24
+ def crtc? = video != :discrete
25
+
26
+ def editor(refresh_hz) = refresh_hz == 50 ? editor_50hz : editor_60hz
27
+
28
+ # The 8296, whose board decodes its memory in a PLA.
29
+ def cbm8296? = name == "8296"
30
+
31
+ # The screen RAM's bytes, and on the 8296 all 32K of RAM from $8000.
32
+ def screen_bytes
33
+ return 0x8000 if cbm8296?
34
+
35
+ video == :crtc80 ? 0x800 : 0x400
36
+ end
37
+
38
+ # How many pages from $8000 on the screen RAM answers in.
39
+ def screen_pages = name == "2001" || cbm8296? ? 0x10 : 0x08
40
+
41
+ # The BASIC 1 KERNAL keeps its keyboard buffer and jiffy clock in
42
+ # page 2; BASIC 2 and 4 keep them where each other does.
43
+ def basic1? = basic == "pet/basic1.rom"
44
+
45
+ # The BASIC's version, "1", "2" or "4" (ROMSets::BASICS).
46
+ def basic_version = ROMSets::BASICS.find { |_, roms| roms.first == basic }.first
47
+
48
+ # Where the KERNAL keeps its IEEE-488 primitives
49
+ # (KernalTrap::IEEELayout).
50
+ def trap_layout
51
+ case kernal
52
+ when "pet/kernal1.rom" then KernalTrap::PET_BASIC1_LAYOUT
53
+ when "pet/kernal2.rom" then KernalTrap::PET_BASIC2_LAYOUT
54
+ else KernalTrap::PET_BASIC4_LAYOUT
55
+ end
56
+ end
57
+ end
58
+
59
+ PET2001 = Profile.new(name: "2001", ram_kb: 8, basic: "pet/basic1.rom", editor_50hz: "pet/editor1.rom",
60
+ editor_60hz: "pet/editor1.rom", kernal: "pet/kernal1.rom",
61
+ characters: "pet/characters1.rom", keyboard: :graphics, video: :discrete,
62
+ expansion: false)
63
+ PET3032 = Profile.new(name: "3032", ram_kb: 32, basic: "pet/basic2.rom", editor_50hz: "pet/editor2-n.rom",
64
+ editor_60hz: "pet/editor2-n.rom", kernal: "pet/kernal2.rom",
65
+ characters: "pet/characters2.rom", keyboard: :graphics, video: :discrete,
66
+ expansion: false)
67
+ PET3032B = Profile.new(name: "3032b", ram_kb: 32, basic: "pet/basic2.rom", editor_50hz: "pet/editor2-b.rom",
68
+ editor_60hz: "pet/editor2-b.rom", kernal: "pet/kernal2.rom",
69
+ characters: "pet/characters2.rom", keyboard: :business, video: :discrete,
70
+ expansion: false)
71
+ PET4032 = Profile.new(name: "4032", ram_kb: 32, basic: "pet/basic4.rom",
72
+ editor_50hz: "pet/editor4-40-n-50hz.rom", editor_60hz: "pet/editor4-40-n-60hz.rom",
73
+ kernal: "pet/kernal4.rom", characters: "pet/characters2.rom", keyboard: :graphics,
74
+ video: :crtc40, expansion: false)
75
+ CBM8032 = Profile.new(name: "8032", ram_kb: 32, basic: "pet/basic4.rom",
76
+ editor_50hz: "pet/editor4-80-b-50hz.rom", editor_60hz: "pet/editor4-80-b-60hz.rom",
77
+ kernal: "pet/kernal4.rom", characters: "pet/characters2.rom", keyboard: :business,
78
+ video: :crtc80, expansion: false)
79
+ CBM8096 = Profile.new(name: "8096", ram_kb: 32, basic: "pet/basic4.rom",
80
+ editor_50hz: "pet/editor4-80-b-50hz.rom", editor_60hz: "pet/editor4-80-b-60hz.rom",
81
+ kernal: "pet/kernal4.rom", characters: "pet/characters2.rom", keyboard: :business,
82
+ video: :crtc80, expansion: true)
83
+
84
+ CBM8296 = Profile.new(name: "8296", ram_kb: 32, basic: "pet/basic4.rom",
85
+ editor_50hz: "pet/editor4-80-b-50hz-8296.rom", editor_60hz: "pet/editor4-80-b-60hz.rom",
86
+ kernal: "pet/kernal4.rom", characters: "pet/characters2.rom", keyboard: :business,
87
+ video: :crtc80, expansion: true)
88
+
89
+ ALL = [PET2001, PET3032, PET3032B, PET4032, CBM8032, CBM8096, CBM8296].freeze
90
+
91
+ DEFAULT = "8032"
92
+
93
+ # The RAM a board can have, in K, below $8000: the 3008, 3016 and
94
+ # 4016 are 3032s and 4032s with less of it.
95
+ RAM_SIZES = [4, 8, 16, 32].freeze
96
+
97
+ def self.named(name)
98
+ model = ALL.find { |candidate| candidate.name == name }
99
+ raise ArgumentError, "no PET model named #{name}" if model.nil?
100
+
101
+ model
102
+ end
103
+
104
+ # Whether the +profile+'s board takes +ram_kb+ below $8000: the 8296
105
+ # has its 32K soldered on.
106
+ def self.ram_fits?(profile, ram_kb)
107
+ profile.cbm8296? ? ram_kb == 32 : RAM_SIZES.include?(ram_kb)
108
+ end
109
+
110
+ # The +jumpers+ the +profile+'s board has fitted, which only the
111
+ # 8296's takes (ROMSets::JUMPERS), in that order. Raises
112
+ # ArgumentError for any it doesn't have.
113
+ def self.checked_jumpers(profile, jumpers)
114
+ known = ROMSets.jumpers(profile.name)
115
+ unknown = jumpers.reject { |jumper| known.include?(jumper) }
116
+ raise ArgumentError, "the #{profile.name} has no jumper #{unknown.first}" unless unknown.empty?
117
+
118
+ known.select { |jumper| jumpers.include?(jumper) }
119
+ end
120
+
121
+ # The +profile+ with the character ROM ROMSets::CHARACTERS names in
122
+ # place of its own, nil for its own. Raises ArgumentError for one the
123
+ # model doesn't take.
124
+ def self.fitted(profile, characters)
125
+ return profile if characters.nil?
126
+
127
+ name = profile.name
128
+ unless ROMSets.characters(name).include?(characters)
129
+ raise ArgumentError, "the #{name} takes no character ROM #{characters}"
130
+ end
131
+
132
+ Profile.new(name:, ram_kb: profile.ram_kb, basic: profile.basic, editor_50hz: profile.editor_50hz,
133
+ editor_60hz: profile.editor_60hz, kernal: profile.kernal,
134
+ characters: ROMSets::CHARACTERS.fetch(characters), keyboard: profile.keyboard,
135
+ video: profile.video, expansion: profile.expansion)
136
+ end
137
+
138
+ # The +profile+ with the BASIC and the editor ROMSets names in place
139
+ # of its own: +basic+ a key of ROMSets::BASICS and +editor+ one of
140
+ # ROMSets::EDITORS, each nil for the one that fits. An editor named
141
+ # serves at either frame rate, so the machine's has to be its own.
142
+ # Raises ArgumentError for ROMs the model doesn't take.
143
+ def self.upgraded(profile, basic, editor)
144
+ return profile if basic.nil? && editor.nil?
145
+
146
+ name = profile.name
147
+ basic ||= ROMSets.stock_basic(name)
148
+ fitting = ROMSets.editors(name, basic)
149
+ raise ArgumentError, "the #{name} takes no BASIC #{basic}" if fitting.empty?
150
+ unless editor.nil? || fitting.include?(editor)
151
+ raise ArgumentError, "the #{name} with BASIC #{basic} takes no editor #{editor}"
152
+ end
153
+
154
+ basic_rom, kernal = ROMSets::BASICS.fetch(basic)
155
+ Profile.new(name:, ram_kb: profile.ram_kb, basic: basic_rom,
156
+ editor_50hz: ROMSets::EDITORS.fetch(editor || fitting.first),
157
+ editor_60hz: ROMSets::EDITORS.fetch(editor || fitting.last), kernal:,
158
+ characters: profile.characters, keyboard: profile.keyboard, video: profile.video,
159
+ expansion: profile.expansion)
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,89 @@
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, and the 8296's character ROMs and board jumpers. Options
8
+ # reads it to check --basic, --editor, --characters and --jumpers, so
9
+ # it loads nothing else.
10
+ #
11
+ # An editor ROM goes with one BASIC, scans one keyboard and drives one
12
+ # board's video, so a model and a BASIC fit one editor, and on a CRTC
13
+ # board its 50 Hz and its 60 Hz versions.
14
+ module ROMSets
15
+ # BASIC's ROM and its KERNAL's, by version.
16
+ BASICS = {
17
+ "1" => %w[pet/basic1.rom pet/kernal1.rom], "2" => %w[pet/basic2.rom pet/kernal2.rom],
18
+ "4" => %w[pet/basic4.rom pet/kernal4.rom]
19
+ }.freeze
20
+
21
+ # The editors by the names --editor takes: the BASIC, the keyboard,
22
+ # N or B, and on a CRTC board the columns and the frame rate.
23
+ EDITORS = {
24
+ "1" => "pet/editor1.rom", "2-n" => "pet/editor2-n.rom", "2-b" => "pet/editor2-b.rom",
25
+ "4-n" => "pet/editor4-n.rom", "4-b" => "pet/editor4-b.rom", "4-40-n-50hz" => "pet/editor4-40-n-50hz.rom",
26
+ "4-40-n-60hz" => "pet/editor4-40-n-60hz.rom", "4-80-b-50hz" => "pet/editor4-80-b-50hz.rom",
27
+ "4-80-b-50hz-0283" => "pet/editor4-80-b-50hz-0283.rom", "4-80-b-60hz" => "pet/editor4-80-b-60hz.rom",
28
+ "4-80-b-50hz-8296" => "pet/editor4-80-b-50hz-8296.rom"
29
+ }.freeze
30
+
31
+ # The BASICs each model takes, its own first, and the editors that
32
+ # fit each, the 50 Hz one first and the 60 Hz one last on a CRTC
33
+ # board. The 8032's other 50 Hz editor, 901474-04 dated 0283, sits
34
+ # between them, and the 8296 takes the 8032's as well as its own.
35
+ FITS = {
36
+ "2001" => { "1" => %w[1], "2" => %w[2-n] },
37
+ "3032" => { "2" => %w[2-n], "4" => %w[4-n] },
38
+ "3032b" => { "2" => %w[2-b], "4" => %w[4-b] },
39
+ "4032" => { "4" => %w[4-40-n-50hz 4-40-n-60hz] },
40
+ "8032" => { "4" => %w[4-80-b-50hz 4-80-b-50hz-0283 4-80-b-60hz] },
41
+ "8096" => { "4" => %w[4-80-b-50hz 4-80-b-50hz-0283 4-80-b-60hz] },
42
+ "8296" => { "4" => %w[4-80-b-50hz-8296 4-80-b-50hz 4-80-b-50hz-0283 4-80-b-60hz] }
43
+ }.freeze
44
+
45
+ # The character ROMs by the names --characters takes, and the ones
46
+ # each model's board takes, its own first: the 8296's socket takes
47
+ # 901447-10, its ASCII one, or the 4K 324242-01, which holds a DIN set
48
+ # in its first half and 901447-10 in its second.
49
+ CHARACTERS = { "ascii" => "pet/characters2.rom", "din" => "pet/characters-din.rom" }.freeze
50
+
51
+ CHARACTER_FITS = { "8296" => %w[ascii din] }.freeze
52
+
53
+ # The jumpers each model's board takes, by the names --jumpers takes
54
+ # (Bus::Expansion), none on a board not named.
55
+ JUMPERS = { "8296" => %w[ram9 rama userport] }.freeze
56
+
57
+ # The cycles a line and the lines a frame the CRTC editors set up
58
+ # where they differ from 50-cycle lines, 400 a frame at 50 Hz and 333
59
+ # at 60 Hz, by the editor's ROM.
60
+ CRTC_FRAMES = {
61
+ "pet/editor4-80-b-50hz-0283.rom" => [64, 313], "pet/editor4-80-b-50hz-8296.rom" => [59, 339]
62
+ }.freeze
63
+
64
+ # The cycles a line and the lines a frame the CRTC editor in the ROM
65
+ # named sets up at +refresh_hz+.
66
+ def self.crtc_frame(editor, refresh_hz) = CRTC_FRAMES[editor] || [50, refresh_hz == 50 ? 400 : 333]
67
+
68
+ # The BASIC the model has, without --basic.
69
+ def self.stock_basic(model) = FITS.fetch(model).keys.first
70
+
71
+ # The editors that fit the model with the BASIC, or an empty list for
72
+ # a BASIC it doesn't take.
73
+ def self.editors(model, basic) = FITS.fetch(model).fetch(basic, [])
74
+
75
+ # The character ROMs the model takes, by name, none for one that takes
76
+ # only its own.
77
+ def self.characters(model) = CHARACTER_FITS.fetch(model, [])
78
+
79
+ # The jumpers the model's board takes.
80
+ def self.jumpers(model) = JUMPERS.fetch(model, [])
81
+
82
+ # The editor that fits the model with the BASIC at +refresh_hz+.
83
+ def self.editor(model, basic, refresh_hz)
84
+ fitting = editors(model, basic)
85
+ refresh_hz == 60 ? fitting.last : fitting.first
86
+ end
87
+ end
88
+ end
89
+ 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