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
@@ -49,7 +49,8 @@ module Badline
49
49
  # wrapped as the banner is.
50
50
  def media_kinds
51
51
  text = "Media can be #{listed(Media::Extensions::PROGRAMS)} program, #{listed(%i[disk])} disk image, " \
52
- "#{listed(%i[gcr])} disk image for a true drive, #{listed(%i[disk_list])} list of disk images, " \
52
+ "#{listed(%i[ieee_disk])} disk image for a PET, #{listed(%i[gcr])} disk image for a true drive, " \
53
+ "#{listed(%i[disk_list])} list of disk images, " \
53
54
  "#{listed(%i[archive])} tape archive, #{listed(%i[tape])} tape, #{listed(%i[cartridge])} " \
54
55
  "cartridge, #{listed(%i[tune])} tune, #{listed(%w[.vsf])} snapshot, or a directory to mount as device 8."
55
56
  wrap(text)
@@ -84,6 +85,7 @@ module Badline
84
85
  #{@program} vic20 [options] [media]
85
86
  #{@program} c128 [options] [media]
86
87
  #{@program} plus4 [options] [media]
88
+ #{@program} pet [options] [media]
87
89
  #{@program} --headless [options] tune.sid
88
90
  #{@program} [options] tune.sid --audio-out FILE
89
91
  #{@program} sid [options] FILE|DIR...
@@ -93,6 +95,7 @@ module Badline
93
95
  vic20 Run a PAL VIC-20, with the RAM --ram names
94
96
  c128 Run a C128 in C128 mode, the one --model names
95
97
  plus4 Run a Plus/4 or a C16, the one --model names
98
+ pet Run a PET or a CBM, the one --model names
96
99
  sid Play .sid tunes and directories of them
97
100
 
98
101
  #{media_kinds}
@@ -116,6 +119,18 @@ module Badline
116
119
  --drive-type 1541 a 1541 on the serial bus instead. There Escape is
117
120
  ESC, Page Down RUN/STOP and F4 HELP.
118
121
 
122
+ The PET and the CBM take programs, disks, .d80 and .d82 among them,
123
+ and tapes, and a .d80, a .d82 or a PET tape starts one without
124
+ `pet`. Their keys type what the host's keys type, Alt with a key
125
+ presses the PET's SHIFT and the key its unshifted character is on,
126
+ and the pause menu's KEYBOARD page says where the rest are. --basic
127
+ and --editor fit the upgrades Commodore sold, as in --model 2001
128
+ --basic 2. BASIC 1 can't LOAD from a disk drive, though a disk still
129
+ starts its first program, and BASIC 2 can. The 8032 and the 8096
130
+ have two 50 Hz editors: 4-80-b-50hz, the default, and
131
+ 4-80-b-50hz-0283, another chip's, with 64-cycle lines and 313 lines
132
+ a frame.
133
+
119
134
  --headless plays a .sid tune on the host's audio device without the
120
135
  window, and --audio-out renders it to 16-bit PCM instead. The
121
136
  container follows the file's extension, .wav or .aiff. Played on a
@@ -136,8 +151,9 @@ module Badline
136
151
  --at and --script run events once a number of frames has run, as
137
152
  in --at 3500:key=space or --at 250,500:screenshot=shot%05d.bmp.
138
153
  key=NAME presses a key (space, return, a, f1, run_stop, restore, and
139
- on the C128 esc, help, keypad1 and the rest, and on the Plus/4
140
- esc, help and f1 to f3) or a joystick's
154
+ on the C128 esc, help, keypad1 and the rest, on the Plus/4
155
+ esc, help and f1 to f3, and on the PET rvs, escape and kp1) or a
156
+ joystick's
141
157
  direction or fire (joy1-up, joy2-fire) for #{Event::HOLD} frames, and
142
158
  pushes the C128's caps_lock or forty_eighty, which lock. type=TEXT
143
159
  types it, with \\n for RETURN. insert=FILE swaps in a disk, a list's
@@ -18,6 +18,10 @@ module Badline
18
18
  # another name for the C16.
19
19
  PLUS4_MODELS = %w[plus4 plus4ntsc c16 c16ntsc c116].freeze
20
20
 
21
+ # The PETs and CBMs, as PET::Model names them, the 8032 first, the
22
+ # default.
23
+ PET_MODELS = %w[8032 8096 4032 3032 3032b 2001].freeze
24
+
21
25
  # The true drives --drive-type names for the Plus/4.
22
26
  PLUS4_DRIVE_TYPES = %w[1541 1551].freeze
23
27
 
@@ -50,13 +54,21 @@ module Badline
50
54
  Option.new("--reu SIZE", "Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384",
51
55
  section: "Window options", needs: :window),
52
56
  Option.new("--model NAME", "C64 to run: #{MODELS.join(', ')} (default: #{MODELS.first}); VIC-20: pal; " \
53
- "C128: #{C128_MODELS.join(', ')}; Plus/4: #{PLUS4_MODELS.join(', ')}",
57
+ "C128: #{C128_MODELS.join(', ')}; Plus/4: #{PLUS4_MODELS.join(', ')}; " \
58
+ "PET: #{PET_MODELS.join(', ')}",
54
59
  section: "Window options", needs: :window),
55
60
  Option.new("--ntsc", "Run an NTSC C64, as --model ntsc does", section: "Window options", needs: :window),
56
61
  Option.new("--c64", "Start the C128 in C64 mode, holding C= at power-on",
57
62
  section: "Window options", needs: :window),
58
63
  Option.new("--ram SIZE", "VIC-20 RAM expansion: #{RAM_NAMES.join(', ')} (default: unexpanded); " \
59
- "Plus/4 RAM: 16k, 32k or 64k (default: the model's own)",
64
+ "Plus/4 RAM: 16k, 32k or 64k; PET RAM: 4k, 8k, 16k or 32k (default: the model's own)",
65
+ section: "Window options", needs: :window),
66
+ Option.new("--60hz", "Run a CRTC PET's 60 Hz editor instead of its 50 Hz one",
67
+ section: "Window options", needs: :window),
68
+ Option.new("--basic VERSION", "Fit a PET with BASIC 1, 2 or 4, as its board takes (default: its own)",
69
+ section: "Window options", needs: :window),
70
+ Option.new("--editor NAME", "Fit a PET with an editor ROM its board and BASIC take: " \
71
+ "#{PET::ROMSets::EDITORS.keys.join(', ')}",
60
72
  section: "Window options", needs: :window),
61
73
  Option.new("--c1lo FILE", "Put a .bin ROM image in the Plus/4's cartridge 1, low slot ($8000)",
62
74
  section: "Window options", needs: :window),
@@ -39,6 +39,12 @@ module Badline
39
39
  PLUS4_RAM[value]
40
40
  end
41
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
+
42
48
  def plus4_ram_expansion(value)
43
49
  raise Error, "invalid argument: --ram-expansion #{value}" unless PLUS4_RAM_EXPANSIONS.key?(value)
44
50
 
@@ -92,12 +98,10 @@ module Badline
92
98
  end
93
99
 
94
100
  def validate_family
95
- if @family == :vic20
96
- validate_vic20
97
- elsif !@ram.nil?
98
- raise Error, "--ram needs vic20 or plus4"
99
- end
101
+ ram_option
102
+ validate_vic20 if @family == :vic20
100
103
  @family == :plus4 ? validate_plus4 : refuse_plus4_options
104
+ @family == :pet ? validate_pet : refuse_pet_options
101
105
  if @family == :c128
102
106
  validate_c128
103
107
  elsif @c64_mode
@@ -105,6 +109,19 @@ module Badline
105
109
  end
106
110
  end
107
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
+
108
125
  # The C128 runs in the window, without an REU, and its NTSC models
109
126
  # have names of their own.
110
127
  def validate_c128
@@ -153,6 +170,52 @@ module Badline
153
170
  raise Error, "--sid-card-address needs --sid-card" unless @sid_card_address.nil? || @sid_card
154
171
  end
155
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
+
156
219
  # `sid` takes directories as well as tunes.
157
220
  def validate_tune(path)
158
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,9 @@ 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
+
54
60
  # Where --sid-card-address puts the Plus/4's SID card.
55
61
  SID_CARD_ADDRESSES = { "fd40" => 0xfd40, "fe80" => 0xfe80 }.freeze
56
62
 
@@ -70,16 +76,20 @@ module Badline
70
76
  attr_reader :program, :media_path, :tune_paths, :subtune, :sid_model, :reu, :frames, :screenshot, :save_snapshot,
71
77
  :audio_out, :seconds, :songlengths, :filter_chunk, :timeline
72
78
 
73
- # The machine family to build (Badline::Machine), :c64, :vic20, :c128
74
- # or :plus4.
79
+ # The machine family to build (Badline::Machine), :c64, :vic20, :c128,
80
+ # :plus4 or :pet.
75
81
  attr_reader :family
76
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
+
77
87
  # The VIC-20's RAM expansion --ram names, a key of
78
88
  # Vic20::Bus::RAM_CONFIGURATIONS, or nil without --ram.
79
89
  attr_reader :ram
80
90
 
81
- # The Plus/4's RAM in K --ram names, one of PLUS4_RAM's, or nil without
82
- # --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.
83
93
  attr_reader :ram_kb
84
94
 
85
95
  # The Plus/4's cartridge ROM images --c1lo, --c1hi, --c2lo and --c2hi
@@ -121,6 +131,10 @@ module Badline
121
131
  @reu = nil
122
132
  @ram = nil
123
133
  @ram_kb = nil
134
+ @ram_name = nil
135
+ @refresh_hz = nil
136
+ @basic = nil
137
+ @editor = nil
124
138
  @cartridges = {}
125
139
  @autostart = true
126
140
  @writable = false
@@ -229,6 +243,10 @@ module Badline
229
243
  # power-on.
230
244
  def c64_mode? = @c64_mode
231
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
+
232
250
  def help? = @help
233
251
 
234
252
  def version? = @version
@@ -244,7 +262,7 @@ module Badline
244
262
  # A .sid on its own plays in the SID player, as `sid` would, unless an
245
263
  # option of the emulator's window asks for the machine.
246
264
  def play_lone_tune
247
- 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?
248
266
  return if @media_path.nil?
249
267
 
250
268
  @sid_command = File.extname(@media_path).casecmp?(".sid")
@@ -303,7 +321,9 @@ module Badline
303
321
  when "--ram-expansion" then @ram_expansion = plus4_ram_expansion(value)
304
322
  when "--model" then @models << value
305
323
  when "--reu" then @reu = reu_size(value)
306
- 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
307
327
  when "--drive-type" then @drive_type = drive_type_for(value)
308
328
  when "--c1lo", "--c1hi", "--c2lo", "--c2hi" then @cartridges[CARTRIDGE_SLOTS.fetch(name)] = value
309
329
  when "--frames" then @frames = number(flag, value)
@@ -318,15 +338,6 @@ module Badline
318
338
  end
319
339
  end
320
340
 
321
- # --ram names the VIC-20's RAM expansion, or the Plus/4's RAM.
322
- def ram_option(value)
323
- if @family == :plus4
324
- @ram_kb = plus4_ram(value)
325
- else
326
- @ram = ram_configuration(value)
327
- end
328
- end
329
-
330
341
  def switch(name)
331
342
  case name
332
343
  when "--no-autostart" then @autostart = false
@@ -339,6 +350,7 @@ module Badline
339
350
  when "--vice" then @vice = true
340
351
  when "--ntsc" then @models << "ntsc"
341
352
  when "--c64" then @c64_mode = true
353
+ when "--60hz" then @refresh_hz = 60
342
354
  when "--unpaced" then @paced = false
343
355
  when "--disable-jit" then @jit = false
344
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