badline 0.15.0 → 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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -0
  3. data/CLAUDE.md +15 -7
  4. data/README.md +142 -16
  5. data/doc/media.md +19 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +274 -31
  8. data/doc/snapshots.md +14 -0
  9. data/exe/badline-ruby +1 -1
  10. data/lib/badline/audio/machine_player.rb +2 -14
  11. data/lib/badline/c64/common.rb +1 -1
  12. data/lib/badline/c64/true_drives.rb +13 -1
  13. data/lib/badline/c64.rb +6 -0
  14. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  15. data/lib/badline/chips/crtc.rb +163 -0
  16. data/lib/badline/chips/pia/control_lines.rb +138 -0
  17. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  18. data/lib/badline/chips/pia.rb +138 -0
  19. data/lib/badline/chips/tia.rb +16 -0
  20. data/lib/badline/core.rb +3 -0
  21. data/lib/badline/drives/datasette/keyed_state.rb +8 -3
  22. data/lib/badline/drives/engine/core.rb +6 -14
  23. data/lib/badline/drives/engine/idle.rb +42 -4
  24. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  25. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  26. data/lib/badline/drives/engine/sleep.rb +1 -0
  27. data/lib/badline/drives/engine.rb +1 -0
  28. data/lib/badline/drives/ieee_bus.rb +28 -0
  29. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  30. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  31. data/lib/badline/drives/model1541.rb +1 -0
  32. data/lib/badline/drives/model1551/bus.rb +138 -0
  33. data/lib/badline/drives/model1551/cable.rb +107 -0
  34. data/lib/badline/drives/model1551/cpu.rb +14 -0
  35. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  36. data/lib/badline/drives/model1551/port.rb +74 -0
  37. data/lib/badline/drives/model1551/tia.rb +71 -0
  38. data/lib/badline/drives/model1551.rb +192 -0
  39. data/lib/badline/drives/model1571.rb +1 -0
  40. data/lib/badline/drives/model1581.rb +1 -0
  41. data/lib/badline/drives.rb +1 -0
  42. data/lib/badline/frontend/app.rb +1 -1
  43. data/lib/badline/frontend/boot.rb +20 -1
  44. data/lib/badline/frontend/controls.rb +23 -3
  45. data/lib/badline/frontend/gamepads.rb +10 -3
  46. data/lib/badline/frontend/keys.rb +88 -0
  47. data/lib/badline/frontend/media_slots.rb +1 -0
  48. data/lib/badline/frontend/menu/menu_dialogs.rb +15 -6
  49. data/lib/badline/frontend/menu/menu_media.rb +96 -19
  50. data/lib/badline/frontend/menu/menu_pages.rb +25 -7
  51. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  52. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  53. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  54. data/lib/badline/frontend/player/player_controller.rb +11 -4
  55. data/lib/badline/frontend/screens.rb +2 -1
  56. data/lib/badline/frontend/timeline.rb +16 -9
  57. data/lib/badline/frontend.rb +1 -0
  58. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  59. data/lib/badline/kernal_trap/dos.rb +6 -1
  60. data/lib/badline/kernal_trap/frames.rb +117 -0
  61. data/lib/badline/kernal_trap/ieee.rb +148 -0
  62. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  63. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  64. data/lib/badline/kernal_trap/routine.rb +3 -53
  65. data/lib/badline/kernal_trap/serial.rb +4 -102
  66. data/lib/badline/kernal_trap/steps.rb +64 -0
  67. data/lib/badline/kernal_trap.rb +4 -0
  68. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  69. data/lib/badline/machine/settling.rb +1 -1
  70. data/lib/badline/machine.rb +37 -11
  71. data/lib/badline/media/extensions.rb +5 -2
  72. data/lib/badline/media/pet_media.rb +40 -0
  73. data/lib/badline/media/program.rb +14 -2
  74. data/lib/badline/media/true_drive.rb +7 -5
  75. data/lib/badline/media/vic20_media.rb +2 -2
  76. data/lib/badline/media.rb +36 -16
  77. data/lib/badline/options/family.rb +17 -6
  78. data/lib/badline/options/help.rb +23 -5
  79. data/lib/badline/options/option.rb +2 -1
  80. data/lib/badline/options/table.rb +28 -4
  81. data/lib/badline/options/validation.rb +117 -14
  82. data/lib/badline/options.rb +65 -21
  83. data/lib/badline/pet/bus/expansion.rb +106 -0
  84. data/lib/badline/pet/bus.rb +204 -0
  85. data/lib/badline/pet/canvas.rb +97 -0
  86. data/lib/badline/pet/cassettes.rb +63 -0
  87. data/lib/badline/pet/characters.rb +26 -0
  88. data/lib/badline/pet/cpu.rb +10 -0
  89. data/lib/badline/pet/crtc_video.rb +142 -0
  90. data/lib/badline/pet/discrete_video.rb +83 -0
  91. data/lib/badline/pet/ieee_ports.rb +66 -0
  92. data/lib/badline/pet/ieee_traps.rb +75 -0
  93. data/lib/badline/pet/keyboard_ports.rb +74 -0
  94. data/lib/badline/pet/keyed_modules.rb +85 -0
  95. data/lib/badline/pet/keyed_parts.rb +123 -0
  96. data/lib/badline/pet/keyed_state.rb +42 -0
  97. data/lib/badline/pet/model.rb +113 -0
  98. data/lib/badline/pet/rom_sets.rb +65 -0
  99. data/lib/badline/pet/sound.rb +128 -0
  100. data/lib/badline/pet/via.rb +10 -0
  101. data/lib/badline/pet/via_ports.rb +19 -0
  102. data/lib/badline/pet.rb +226 -0
  103. data/lib/badline/plus4/acia/frames.rb +55 -0
  104. data/lib/badline/plus4/acia.rb +276 -16
  105. data/lib/badline/plus4/bus.rb +181 -45
  106. data/lib/badline/plus4/keyed_modules.rb +22 -5
  107. data/lib/badline/plus4/keyed_parts.rb +33 -4
  108. data/lib/badline/plus4/keyed_state.rb +10 -5
  109. data/lib/badline/plus4/ram_expansion.rb +145 -0
  110. data/lib/badline/plus4/sid_card.rb +95 -0
  111. data/lib/badline/plus4/sound_mix.rb +51 -0
  112. data/lib/badline/plus4/tcbm_drive.rb +134 -0
  113. data/lib/badline/plus4/user_port.rb +11 -2
  114. data/lib/badline/plus4.rb +104 -22
  115. data/lib/badline/roms/README +146 -0
  116. data/lib/badline/roms/dos1551.rom +0 -0
  117. data/lib/badline/roms/pet/basic1.rom +0 -0
  118. data/lib/badline/roms/pet/basic2.rom +0 -0
  119. data/lib/badline/roms/pet/basic4.rom +0 -0
  120. data/lib/badline/roms/pet/characters1.rom +0 -0
  121. data/lib/badline/roms/pet/characters2.rom +0 -0
  122. data/lib/badline/roms/pet/editor1.rom +0 -0
  123. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  124. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  125. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  126. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  127. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  128. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  129. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  130. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  131. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  132. data/lib/badline/roms/pet/kernal1.rom +0 -0
  133. data/lib/badline/roms/pet/kernal2.rom +0 -0
  134. data/lib/badline/roms/pet/kernal4.rom +0 -0
  135. data/lib/badline/snapshot/container.rb +2 -0
  136. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  137. data/lib/badline/snapshot/field_doc.rb +21 -6
  138. data/lib/badline/snapshot/image.rb +14 -7
  139. data/lib/badline/snapshot/pet_setup.rb +71 -0
  140. data/lib/badline/snapshot/plus4_setup.rb +52 -10
  141. data/lib/badline/snapshot/storage_setup.rb +7 -1
  142. data/lib/badline/snapshot.rb +9 -5
  143. data/lib/badline/storage/d80_image.rb +48 -0
  144. data/lib/badline/storage/d82_image.rb +21 -0
  145. data/lib/badline/storage/g64_image.rb +1 -1
  146. data/lib/badline/storage/tap.rb +16 -3
  147. data/lib/badline/storage.rb +2 -0
  148. data/lib/badline/support/pet_checkpoint.rb +38 -0
  149. data/lib/badline/timing.rb +5 -4
  150. data/lib/badline/version.rb +1 -1
  151. data/lib/badline/vic20/attachments.rb +7 -2
  152. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  153. data/lib/badline/vic20/bus.rb +5 -0
  154. data/lib/badline/vic20.rb +1 -1
  155. metadata +69 -1
@@ -8,6 +8,18 @@ module Badline
8
8
  module Validation
9
9
  private
10
10
 
11
+ def sid_card_for(value)
12
+ raise Error, "invalid argument: --sid-card #{value}" unless SID_MODELS.key?(value)
13
+
14
+ SID_MODELS[value]
15
+ end
16
+
17
+ def sid_card_address_for(value)
18
+ raise Error, "invalid argument: --sid-card-address #{value}" unless SID_CARD_ADDRESSES.key?(value.downcase)
19
+
20
+ SID_CARD_ADDRESSES[value.downcase]
21
+ end
22
+
11
23
  def sid_model_for(value)
12
24
  return if value == "auto"
13
25
  raise Error, "invalid argument: --sid #{value}" unless SID_MODELS.key?(value)
@@ -27,6 +39,24 @@ module Badline
27
39
  PLUS4_RAM[value]
28
40
  end
29
41
 
42
+ def pet_ram(value)
43
+ raise Error, "invalid argument: --ram #{value}" unless PET_RAM.key?(value)
44
+
45
+ PET_RAM[value]
46
+ end
47
+
48
+ def plus4_ram_expansion(value)
49
+ raise Error, "invalid argument: --ram-expansion #{value}" unless PLUS4_RAM_EXPANSIONS.key?(value)
50
+
51
+ PLUS4_RAM_EXPANSIONS[value]
52
+ end
53
+
54
+ def drive_type_for(value)
55
+ raise Error, "invalid argument: --drive-type #{value}" unless PLUS4_DRIVE_TYPES.include?(value)
56
+
57
+ value
58
+ end
59
+
30
60
  def reu_size(value)
31
61
  raise Error, "invalid argument: --reu #{value}" unless REU_SIZES.include?(value)
32
62
 
@@ -47,6 +77,8 @@ module Badline
47
77
 
48
78
  def validate
49
79
  validate_family
80
+ raise Error, "--drive-type needs plus4" unless @drive_type.nil? || @family == :plus4
81
+
50
82
  validate_mode
51
83
  validate_models
52
84
  validate_numbers
@@ -56,6 +88,7 @@ module Badline
56
88
  def validate_mode
57
89
  if headless?
58
90
  raise Error, "#{@window_only.first} needs the window" unless @window_only.empty?
91
+ raise Error, "#{@any_window_only.first} needs the window" unless player_window? || @any_window_only.empty?
59
92
  raise Error, "no tune given" if tune_paths.empty? && !player_window?
60
93
 
61
94
  tune_paths.each { |path| validate_tune(path) }
@@ -65,16 +98,10 @@ module Badline
65
98
  end
66
99
 
67
100
  def validate_family
68
- if @family == :vic20
69
- validate_vic20
70
- elsif !@ram.nil?
71
- raise Error, "--ram needs vic20 or plus4"
72
- end
73
- if @family == :plus4
74
- validate_plus4
75
- elsif !@cartridges.empty?
76
- raise Error, "--#{@cartridges.keys.first} needs plus4"
77
- end
101
+ ram_option
102
+ validate_vic20 if @family == :vic20
103
+ @family == :plus4 ? validate_plus4 : refuse_plus4_options
104
+ @family == :pet ? validate_pet : refuse_pet_options
78
105
  if @family == :c128
79
106
  validate_c128
80
107
  elsif @c64_mode
@@ -82,6 +109,19 @@ module Badline
82
109
  end
83
110
  end
84
111
 
112
+ # --ram names the VIC-20's RAM expansion, or the Plus/4's or the PET's
113
+ # RAM.
114
+ def ram_option
115
+ return if @ram_name.nil?
116
+
117
+ case @family
118
+ when :vic20 then @ram = ram_configuration(@ram_name)
119
+ when :plus4 then @ram_kb = plus4_ram(@ram_name)
120
+ when :pet then @ram_kb = pet_ram(@ram_name)
121
+ else raise Error, "--ram needs vic20, plus4 or pet"
122
+ end
123
+ end
124
+
85
125
  # The C128 runs in the window, without an REU, and its NTSC models
86
126
  # have names of their own.
87
127
  def validate_c128
@@ -100,19 +140,82 @@ module Badline
100
140
  raise Error, "the VIC-20 needs the window" if headless?
101
141
  end
102
142
 
103
- # The Plus/4 runs in the window, without the C64's SID and REU, its
104
- # NTSC models have names of their own, and its cartridge slots take
105
- # raw ROM images.
143
+ # Only the Plus/4 has cartridge slots for raw ROM images and takes a
144
+ # SID card and a RAM expansion.
145
+ def refuse_plus4_options
146
+ raise Error, "--#{@cartridges.keys.first} needs plus4" unless @cartridges.empty?
147
+ raise Error, "--sid-card needs plus4" unless @sid_card.nil?
148
+ raise Error, "--sid-card-address needs plus4" unless @sid_card_address.nil?
149
+ raise Error, "--ram-expansion needs plus4" unless @ram_expansion.nil?
150
+ end
151
+
152
+ # The Plus/4 runs in the window, without the C64's SID and REU but
153
+ # with a SID card if asked, its NTSC models have names of their own,
154
+ # and its cartridge slots take raw ROM images.
106
155
  def validate_plus4
107
156
  raise Error, "--ntsc needs the C64; the NTSC Plus/4 is --model plus4ntsc" if @models.include?("ntsc")
108
- raise Error, "--sid needs the C64" unless @sid_model.nil?
157
+ raise Error, "--sid needs the C64; the Plus/4's is --sid-card" unless @sid_model.nil?
109
158
  raise Error, "--reu needs the C64" unless @reu.nil?
110
159
  raise Error, "the Plus/4 needs the window" if headless?
111
160
 
161
+ validate_plus4_expansions
112
162
  image = @cartridges.values.find { |path| !File.extname(path).casecmp?(".bin") }
113
163
  raise Error, "not a .bin ROM image: #{image}" unless image.nil?
114
164
  end
115
165
 
166
+ # A RAM expansion goes over 64K, and a SID card's address needs a
167
+ # card.
168
+ def validate_plus4_expansions
169
+ raise Error, "--ram-expansion needs 64k" unless @ram_expansion.nil? || [nil, 64].include?(@ram_kb)
170
+ raise Error, "--sid-card-address needs --sid-card" unless @sid_card_address.nil? || @sid_card
171
+ end
172
+
173
+ # Only the PET has a frame rate to pick and BASIC and editor ROMs to
174
+ # fit.
175
+ def refuse_pet_options
176
+ raise Error, "--60hz needs pet" unless @refresh_hz.nil?
177
+ raise Error, "--basic needs pet" unless @basic.nil?
178
+ raise Error, "--editor needs pet" unless @editor.nil?
179
+ end
180
+
181
+ # The PET runs in the window, without the C64's SID and REU or a true
182
+ # drive, at the frame rate its editor sets.
183
+ def validate_pet
184
+ raise Error, "--ntsc needs the C64; a CRTC PET's 60 Hz editor is --60hz" if @models.include?("ntsc")
185
+ raise Error, "--sid needs the C64" unless @sid_model.nil?
186
+ raise Error, "--reu needs the C64" unless @reu.nil?
187
+ raise Error, "the PET has no true drive yet" if @true_drive
188
+ raise Error, "the PET needs the window" if headless?
189
+
190
+ validate_pet_roms if PET_MODELS.include?(model)
191
+ end
192
+
193
+ # --basic and --editor name ROMs the model's board takes
194
+ # (PET::ROMSets), and an editor named sets the frame rate.
195
+ def validate_pet_roms
196
+ raise Error, "invalid argument: --basic #{@basic}" unless @basic.nil? || PET::ROMSets::BASICS.key?(@basic)
197
+ raise Error, "invalid argument: --editor #{@editor}" unless @editor.nil? || PET::ROMSets::EDITORS.key?(@editor)
198
+
199
+ basic = @basic || PET::ROMSets.stock_basic(model)
200
+ fitting = PET::ROMSets.editors(model, basic)
201
+ raise Error, "the #{model} takes no BASIC #{basic}" if fitting.empty?
202
+ return if @editor.nil?
203
+ raise Error, "the #{model} with BASIC #{basic} takes no editor #{@editor}" unless fitting.include?(@editor)
204
+
205
+ editor_rate(fitting)
206
+ end
207
+
208
+ # The frame rate of the editor named, on a CRTC board, which has a 50
209
+ # Hz and a 60 Hz one.
210
+ def editor_rate(fitting)
211
+ return if fitting.size == 1
212
+
213
+ editor_hz = @editor == fitting.last ? 60 : 50
214
+ raise Error, "--60hz takes a 60 Hz editor, not #{@editor}" if @refresh_hz == 60 && editor_hz == 50
215
+
216
+ @refresh_hz = editor_hz
217
+ end
218
+
116
219
  # `sid` takes directories as well as tunes.
117
220
  def validate_tune(path)
118
221
  return if !File.exist?(path) || File.extname(path).casecmp?(".sid")
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/pet/rom_sets"
3
4
  require "badline/options/option"
4
5
  require "badline/options/event"
5
6
  require "badline/options/table"
@@ -12,10 +13,12 @@ module Badline
12
13
  # Either opens the window for any media, or with --headless or
13
14
  # --audio-out plays or renders a .sid tune without one. `sid`, given
14
15
  # first, plays .sid tunes and directories of them in the terminal.
15
- # `c64`, `vic20`, `c128` or `plus4`, given first, names the machine
16
- # family, and without one the family is the C64, or the C128 for a program
17
- # that loads where C128 BASIC starts. The native build adds --no-sound and
18
- # --version, and badline-ruby adds --disable-jit.
16
+ # `c64`, `vic20`, `c128`, `plus4` or `pet`, given first, names the
17
+ # machine family, and without one the family is the C64, the C128 for a
18
+ # program that loads where C128 BASIC starts, or the PET for an 8050's
19
+ # or 8250's disk or a PET tape.
20
+ # The native build adds --no-sound and --version, and badline-ruby adds
21
+ # --disable-jit.
19
22
  #
20
23
  # TABLE lists the options, and both the parser and the help read it. It
21
24
  # parses by hand, inside the subset of Ruby Spinel compiles. Values come
@@ -51,6 +54,15 @@ module Badline
51
54
  # The Plus/4's RAM sizes as --ram names them, in K.
52
55
  PLUS4_RAM = { "16k" => 16, "32k" => 32, "64k" => 64 }.freeze
53
56
 
57
+ # The PET's, below $8000.
58
+ PET_RAM = { "4k" => 4, "8k" => 8, "16k" => 16, "32k" => 32 }.freeze
59
+
60
+ # Where --sid-card-address puts the Plus/4's SID card.
61
+ SID_CARD_ADDRESSES = { "fd40" => 0xfd40, "fe80" => 0xfe80 }.freeze
62
+
63
+ # The Plus/4's RAM expansions as --ram-expansion names them.
64
+ PLUS4_RAM_EXPANSIONS = { "hannes256" => :hannes256, "hannes1m" => :hannes1m, "csory256" => :csory256 }.freeze
65
+
54
66
  # The Plus/4's cartridge slots, by the options that fill them and
55
67
  # xplus4's names (Media::Plus4Media::SLOTS).
56
68
  CARTRIDGE_SLOTS = { "--c1lo" => "c1lo", "--c1hi" => "c1hi", "--c2lo" => "c2lo", "--c2hi" => "c2hi" }.freeze
@@ -64,22 +76,38 @@ module Badline
64
76
  attr_reader :program, :media_path, :tune_paths, :subtune, :sid_model, :reu, :frames, :screenshot, :save_snapshot,
65
77
  :audio_out, :seconds, :songlengths, :filter_chunk, :timeline
66
78
 
67
- # The machine family to build (Badline::Machine), :c64, :vic20, :c128
68
- # or :plus4.
79
+ # The machine family to build (Badline::Machine), :c64, :vic20, :c128,
80
+ # :plus4 or :pet.
69
81
  attr_reader :family
70
82
 
83
+ # The ROMs --basic and --editor fit a PET with, keys of
84
+ # PET::ROMSets::BASICS and PET::ROMSets::EDITORS, or nil for its own.
85
+ attr_reader :basic, :editor
86
+
71
87
  # The VIC-20's RAM expansion --ram names, a key of
72
88
  # Vic20::Bus::RAM_CONFIGURATIONS, or nil without --ram.
73
89
  attr_reader :ram
74
90
 
75
- # The Plus/4's RAM in K --ram names, one of PLUS4_RAM's, or nil without
76
- # --ram.
91
+ # The Plus/4's or the PET's RAM in K --ram names, one of PLUS4_RAM's or
92
+ # PET_RAM's, or nil without --ram.
77
93
  attr_reader :ram_kb
78
94
 
79
95
  # The Plus/4's cartridge ROM images --c1lo, --c1hi, --c2lo and --c2hi
80
96
  # name, by slot: { "c1lo" => path }.
81
97
  attr_reader :cartridges
82
98
 
99
+ # The SID model of the Plus/4's SID card --sid-card names, :mos6581 or
100
+ # :mos8580, or nil without one.
101
+ attr_reader :sid_card
102
+
103
+ # Where --sid-card-address puts the Plus/4's SID card, $FD40 without
104
+ # it.
105
+ attr_reader :sid_card_address
106
+
107
+ # The Plus/4's RAM expansion --ram-expansion names, a key of
108
+ # Plus4::RAMExpansion::KINDS, or nil without one.
109
+ attr_reader :ram_expansion
110
+
83
111
  def self.parse(argv, native: false) = new(native:).parse(argv)
84
112
 
85
113
  # The native build plays the SID in the window unless --no-sound says
@@ -96,10 +124,17 @@ module Badline
96
124
  @tune_paths = []
97
125
  @subtune = nil
98
126
  @sid_model = nil
127
+ @sid_card = nil
128
+ @sid_card_address = nil
129
+ @ram_expansion = nil
99
130
  @models = []
100
131
  @reu = nil
101
132
  @ram = nil
102
133
  @ram_kb = nil
134
+ @ram_name = nil
135
+ @refresh_hz = nil
136
+ @basic = nil
137
+ @editor = nil
103
138
  @cartridges = {}
104
139
  @autostart = true
105
140
  @writable = false
@@ -109,6 +144,7 @@ module Badline
109
144
  @vsync = true
110
145
  @verbose = false
111
146
  @true_drive = false
147
+ @drive_type = nil
112
148
  @screenshot = ""
113
149
  @save_snapshot = ""
114
150
  @vice = false
@@ -126,6 +162,7 @@ module Badline
126
162
  @tui = true
127
163
  @all_subtunes = false
128
164
  @window_only = []
165
+ @any_window_only = []
129
166
  @headless_only = []
130
167
  end
131
168
 
@@ -187,7 +224,10 @@ module Badline
187
224
 
188
225
  def verbose? = @verbose
189
226
 
190
- def true_drive? = @true_drive
227
+ def true_drive? = @true_drive || !@drive_type.nil?
228
+
229
+ # The Plus/4's true drive --drive-type names, "1551" without it.
230
+ def drive_type = @drive_type || "1551"
191
231
 
192
232
  # Whether --save-snapshot adds VICE's modules, so x64sc opens it.
193
233
  def vice? = @vice
@@ -203,6 +243,10 @@ module Badline
203
243
  # power-on.
204
244
  def c64_mode? = @c64_mode
205
245
 
246
+ # The frame rate of a CRTC PET's editor: 60 with --60hz or a 60 Hz
247
+ # --editor, else 50.
248
+ def refresh_hz = @refresh_hz || 50
249
+
206
250
  def help? = @help
207
251
 
208
252
  def version? = @version
@@ -218,7 +262,7 @@ module Badline
218
262
  # A .sid on its own plays in the SID player, as `sid` would, unless an
219
263
  # option of the emulator's window asks for the machine.
220
264
  def play_lone_tune
221
- return if @sid_command || %i[vic20 plus4].include?(@family) || headless? || !@window_only.empty?
265
+ return if @sid_command || %i[vic20 plus4 pet].include?(@family) || headless? || !@window_only.empty?
222
266
  return if @media_path.nil?
223
267
 
224
268
  @sid_command = File.extname(@media_path).casecmp?(".sid")
@@ -254,9 +298,11 @@ module Badline
254
298
  option
255
299
  end
256
300
 
257
- # Notes an option only the window or only the headless player takes.
301
+ # Notes an option only the window, only a window of either kind, or
302
+ # only the headless player takes.
258
303
  def restrict(option, flag)
259
304
  @window_only << flag if option.needs == :window
305
+ @any_window_only << flag if option.needs == :any_window
260
306
  @headless_only << flag if option.needs == :headless
261
307
  end
262
308
 
@@ -270,9 +316,15 @@ module Badline
270
316
  case name
271
317
  when "--subtune" then @subtune = number(flag, value)
272
318
  when "--sid" then @sid_model = sid_model_for(value)
319
+ when "--sid-card" then @sid_card = sid_card_for(value)
320
+ when "--sid-card-address" then @sid_card_address = sid_card_address_for(value)
321
+ when "--ram-expansion" then @ram_expansion = plus4_ram_expansion(value)
273
322
  when "--model" then @models << value
274
323
  when "--reu" then @reu = reu_size(value)
275
- when "--ram" then ram_option(value)
324
+ when "--ram" then @ram_name = value
325
+ when "--basic" then @basic = value
326
+ when "--editor" then @editor = value
327
+ when "--drive-type" then @drive_type = drive_type_for(value)
276
328
  when "--c1lo", "--c1hi", "--c2lo", "--c2hi" then @cartridges[CARTRIDGE_SLOTS.fetch(name)] = value
277
329
  when "--frames" then @frames = number(flag, value)
278
330
  when "--screenshot" then @screenshot = value
@@ -286,15 +338,6 @@ module Badline
286
338
  end
287
339
  end
288
340
 
289
- # --ram names the VIC-20's RAM expansion, or the Plus/4's RAM.
290
- def ram_option(value)
291
- if @family == :plus4
292
- @ram_kb = plus4_ram(value)
293
- else
294
- @ram = ram_configuration(value)
295
- end
296
- end
297
-
298
341
  def switch(name)
299
342
  case name
300
343
  when "--no-autostart" then @autostart = false
@@ -307,6 +350,7 @@ module Badline
307
350
  when "--vice" then @vice = true
308
351
  when "--ntsc" then @models << "ntsc"
309
352
  when "--c64" then @c64_mode = true
353
+ when "--60hz" then @refresh_hz = 60
310
354
  when "--unpaced" then @paced = false
311
355
  when "--disable-jit" then @jit = false
312
356
  when "--help" then @help = true
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ class Bus
6
+ # The 8096's 64K board: four 16K banks, which a write-only control
7
+ # register at $FFF0 lays over $8000-$FFFF, two at a time:
8
+ #
9
+ # bit 7 the board on; when clear, a plain 8032 and the rest ignored
10
+ # bit 6 I/O shows through at $E800-$EFFF
11
+ # bit 5 the screen shows through at $8000-$8FFF
12
+ # bit 3 bank 3 at $C000-$FFFF, else bank 1
13
+ # bit 2 bank 2 at $8000-$BFFF, else bank 0
14
+ # bit 1 writes to $C000-$FFFF go nowhere
15
+ # bit 0 writes to $8000-$BFFF go nowhere
16
+ #
17
+ # A write to $FFF0 lands where $FFF0 was before it, too. The RES line
18
+ # clears the register, so the 8096 starts as an 8032.
19
+ class Expansion
20
+ # A 16K bank of the board, seen from +start+.
21
+ class Window
22
+ attr_accessor :read_only
23
+
24
+ def initialize(cells, offset, start)
25
+ @cells = cells
26
+ @offset = offset - start
27
+ @read_only = false
28
+ end
29
+
30
+ def peek(addr) = @cells[addr + @offset]
31
+
32
+ def poke(addr, value)
33
+ @cells[addr + @offset] = value unless @read_only
34
+ end
35
+ end
36
+
37
+ # Page $FF's writes, which pass to what the page holds and set the
38
+ # control register at $FFF0.
39
+ class ControlPort
40
+ attr_accessor :under
41
+
42
+ def initialize(expansion, under)
43
+ @expansion = expansion
44
+ @under = under
45
+ end
46
+
47
+ def poke(addr, value)
48
+ @under.poke(addr, value)
49
+ @expansion.control = value if addr == 0xfff0
50
+ end
51
+ end
52
+
53
+ attr_reader :cells, :control
54
+
55
+ def initialize(bus)
56
+ @bus = bus
57
+ @cells = Array.new(0x10000, 0)
58
+ @banks = [0x8000, 0xc000, 0x8000, 0xc000].each_with_index.map do |start, bank|
59
+ Window.new(@cells, bank * 0x4000, start)
60
+ end
61
+ @port = ControlPort.new(self, @banks[1])
62
+ @control = 0
63
+ end
64
+
65
+ def control=(value)
66
+ @control = value
67
+ @bus.map_pages
68
+ end
69
+
70
+ def reset!
71
+ self.control = 0
72
+ end
73
+
74
+ def power_on!
75
+ @cells.fill(0)
76
+ end
77
+
78
+ # Lays the banks the register picks over the pages, and the
79
+ # control register over page $FF's writes.
80
+ def map(reads, writes)
81
+ map_banks(reads, writes) if @control.anybits?(0x80)
82
+ @port.under = writes[0xff]
83
+ writes[0xff] = @port
84
+ end
85
+
86
+ private
87
+
88
+ def map_banks(reads, writes)
89
+ low = @banks[@control.anybits?(0x04) ? 2 : 0]
90
+ high = @banks[@control.anybits?(0x08) ? 3 : 1]
91
+ low.read_only = @control.anybits?(0x01)
92
+ high.read_only = @control.anybits?(0x02)
93
+ first = @control.anybits?(0x20) ? 0x90 : 0x80
94
+ reads.fill(low, first, 0xc0 - first)
95
+ writes.fill(low, first, 0xc0 - first)
96
+ [[0xc0, 0x28], [0xe8, 0x08], [0xf0, 0x10]].each do |page, count|
97
+ next if page == 0xe8 && @control.anybits?(0x40)
98
+
99
+ reads.fill(high, page, count)
100
+ writes.fill(high, page, count)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/pet/bus/expansion"
4
+
5
+ module Badline
6
+ class PET
7
+ # The CPU's bus:
8
+ #
9
+ # $0000-$7FFF - RAM, 4K, 8K, 16K or 32K of it from $0000, then nothing
10
+ # $8000-$8FFF - screen RAM, repeated (Model), then nothing
11
+ # $9000-$AFFF - two empty ROM sockets
12
+ # $B000-$DFFF - BASIC 4, or BASIC 1 or 2 from $C000
13
+ # $E000-$E7FF - the screen editor ROM
14
+ # $E800-$E8FF - I/O (IO)
15
+ # $E900-$EFFF - nothing
16
+ # $F000-$FFFF - the KERNAL ROM
17
+ #
18
+ # The data lines hold the last byte they carried, so a read from an
19
+ # empty spot finds it, mostly the high byte of the address just
20
+ # fetched, as PET/unconnected shows on a real 3016. Writes to ROM and
21
+ # to empty spots go nowhere. An 8096 banks its 64K board over
22
+ # $8000-$FFFF (Expansion).
23
+ class Bus
24
+ include Addressable
25
+
26
+ # The last byte the CPU moved, which an empty spot reads back.
27
+ class OpenBus
28
+ def initialize(bus)
29
+ @bus = bus
30
+ end
31
+
32
+ def peek(_addr) = @bus.data
33
+ def poke(_addr, _value); end
34
+ end
35
+
36
+ # Screen RAM, repeated over the pages it answers in.
37
+ class ScreenRAM
38
+ attr_reader :cells
39
+
40
+ def initialize(size)
41
+ @cells = Array.new(size, 0)
42
+ @mask = size - 1
43
+ end
44
+
45
+ def peek(addr) = @cells[addr & @mask]
46
+
47
+ def poke(addr, value)
48
+ @cells[addr & @mask] = value
49
+ end
50
+ end
51
+
52
+ # $E800-$E8FF. Each chip answers on one address line, with its
53
+ # registers repeated: A4 selects PIA 1, A5 PIA 2, A6 the VIA and A7
54
+ # the CRTC, where there is one. Where more than one chip answers a
55
+ # read, each pulls the lines it drives low, so the CPU reads the AND
56
+ # of them, and a write reaches them all. Where none answers, as in
57
+ # $E800-$E80F, the CPU reads the last byte on the bus.
58
+ class IO
59
+ def initialize(bus, pia1:, pia2:, via:, crtc:, crtc_fitted:)
60
+ @bus = bus
61
+ @pia1 = pia1
62
+ @pia2 = pia2
63
+ @via = via
64
+ @crtc = crtc
65
+ @lines = crtc_fitted ? 0xf0 : 0x70
66
+ end
67
+
68
+ def peek(addr)
69
+ return @bus.data if addr.nobits?(@lines)
70
+
71
+ value = 0xff
72
+ value &= @pia1.peek(addr) if addr.anybits?(0x10)
73
+ value &= @pia2.peek(addr) if addr.anybits?(0x20)
74
+ value &= @via.peek(addr) if addr.anybits?(0x40)
75
+ value &= @crtc.peek(addr) if addr.anybits?(@lines & 0x80)
76
+ value
77
+ end
78
+
79
+ def poke(addr, value)
80
+ @pia1.poke(addr, value) if addr.anybits?(0x10)
81
+ @pia2.poke(addr, value) if addr.anybits?(0x20)
82
+ @via.poke(addr, value) if addr.anybits?(0x40)
83
+ @crtc.poke(addr, value) if addr.anybits?(@lines & 0x80)
84
+ @bus.io_written
85
+ end
86
+ end
87
+
88
+ # VICE's debug cartridge register at $8BFF, which a write calls the
89
+ # handler with, the exit code the VICE testprogs report. It answers
90
+ # on every model, ahead of the 2001's screen RAM there and the
91
+ # 8096's banks, and every other write to its page passes to what
92
+ # the page holds.
93
+ class DebugRegister
94
+ ADDRESS = 0x8bff
95
+
96
+ attr_accessor :under
97
+
98
+ def initialize(&handler)
99
+ @handler = handler
100
+ @under = nil
101
+ end
102
+
103
+ def poke(addr, value)
104
+ return @handler.call(value) if addr == ADDRESS
105
+
106
+ @under.poke(addr, value)
107
+ end
108
+ end
109
+
110
+ attr_reader :ram, :screen, :expansion
111
+
112
+ # The last byte the CPU read or wrote.
113
+ attr_reader :data
114
+
115
+ # +model+ is a Model::Profile, +ram_kb+ the RAM below $8000 and
116
+ # +editor+ the editor ROM's file. The CRTC answers only on a board
117
+ # the model fits one on.
118
+ def initialize(model:, ram_kb:, editor:, pia1:, pia2:, via:, crtc:)
119
+ addressable_at(0, length: 2**16)
120
+ @ram = Memory.new([], length: ram_kb * 1024, start: 0)
121
+ @screen = ScreenRAM.new(model.screen_bytes)
122
+ @screen_pages = model.screen_pages
123
+ basic = ROM.read(model.basic)
124
+ @basic_rom = ROM.new(basic, length: basic.length, start: 0xe000 - basic.length)
125
+ @editor_rom = ROM.load(editor, 0xe000)
126
+ @kernal_rom = ROM.load(model.kernal, 0xf000)
127
+ @io = IO.new(self, pia1:, pia2:, via:, crtc:, crtc_fitted: model.crtc?)
128
+ @open_bus = OpenBus.new(self)
129
+ @data = 0
130
+ @read_pages = Array.new(256, @open_bus)
131
+ @write_pages = Array.new(256, @open_bus)
132
+ @expansion = model.expansion ? Expansion.new(self) : nil
133
+ @debug_register = nil
134
+ @io_write_handler = nil
135
+ map_pages
136
+ end
137
+
138
+ # Calls the block after each write to the I/O chips, which may have
139
+ # moved the lines their ports drive.
140
+ def on_io_write(&handler)
141
+ @io_write_handler = handler
142
+ end
143
+
144
+ def io_written
145
+ @io_write_handler&.call
146
+ end
147
+
148
+ # The RES line reaches the 8096's control register.
149
+ def reset!
150
+ @expansion&.reset!
151
+ end
152
+
153
+ def power_on!
154
+ @ram.clear!
155
+ @screen.cells.fill(0)
156
+ @expansion&.power_on!
157
+ end
158
+
159
+ def peek(addr)
160
+ @data = @read_pages[addr >> 8].peek(addr)
161
+ end
162
+
163
+ def poke(addr, value)
164
+ @data = value
165
+ @write_pages[addr >> 8].poke(addr, value)
166
+ end
167
+
168
+ # Whether the CPU reads the KERNAL ROM at $F000-$FFFF, which an 8096's
169
+ # board covers while its control register turns it on.
170
+ def kernal_mapped? = @expansion.nil? || @expansion.control.nobits?(0x80)
171
+
172
+ # Calls the block with each byte written to $8BFF (DebugRegister).
173
+ def install_debug_register(&)
174
+ @debug_register = DebugRegister.new(&)
175
+ map_pages
176
+ end
177
+
178
+ # Lays the pages out again, for the 8096's control register.
179
+ def map_pages
180
+ map(@open_bus, 0x00, 0x100)
181
+ map(@ram, 0x00, @ram.length >> 8)
182
+ map(@screen, 0x80, @screen_pages)
183
+ @read_pages.fill(@basic_rom, @basic_rom.start >> 8, @basic_rom.length >> 8)
184
+ @read_pages.fill(@editor_rom, 0xe0, 0x08)
185
+ map(@io, 0xe8, 0x01)
186
+ @read_pages.fill(@kernal_rom, 0xf0, 0x10)
187
+ @expansion&.map(@read_pages, @write_pages)
188
+ map_debug_register if @debug_register
189
+ end
190
+
191
+ private
192
+
193
+ def map_debug_register
194
+ @debug_register.under = @write_pages[0x8b]
195
+ @write_pages[0x8b] = @debug_register
196
+ end
197
+
198
+ def map(chip, first, count)
199
+ @read_pages.fill(chip, first, count)
200
+ @write_pages.fill(chip, first, count)
201
+ end
202
+ end
203
+ end
204
+ end