badline 0.8.0 → 0.10.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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +60 -19
  5. data/doc/library.md +10 -3
  6. data/doc/media.md +11 -2
  7. data/doc/pinned-behaviour.md +199 -9
  8. data/doc/sid-player.md +8 -4
  9. data/doc/snapshots.md +79 -33
  10. data/lib/badline/address_bus/pla.rb +130 -0
  11. data/lib/badline/address_bus/roms.rb +32 -0
  12. data/lib/badline/address_bus.rb +13 -92
  13. data/lib/badline/audio/bare_player.rb +24 -0
  14. data/lib/badline/audio/checkpoints.rb +42 -0
  15. data/lib/badline/audio/jukebox.rb +11 -4
  16. data/lib/badline/audio/machine_player.rb +16 -0
  17. data/lib/badline/audio/renderer.rb +80 -9
  18. data/lib/badline/audio/stereo.rb +3 -5
  19. data/lib/badline/audio.rb +1 -0
  20. data/lib/badline/chrout_trap.rb +4 -3
  21. data/lib/badline/computer/attachments.rb +52 -12
  22. data/lib/badline/computer/saved_state.rb +1 -0
  23. data/lib/badline/computer.rb +50 -33
  24. data/lib/badline/control_ports.rb +10 -3
  25. data/lib/badline/core.rb +51 -0
  26. data/lib/badline/data_path.rb +37 -0
  27. data/lib/badline/datasette.rb +63 -10
  28. data/lib/badline/drive1541/disk.rb +3 -3
  29. data/lib/badline/drive1541/disk_state.rb +14 -5
  30. data/lib/badline/drive1541/serial_port.rb +5 -8
  31. data/lib/badline/drive1541/sleep.rb +5 -6
  32. data/lib/badline/drive1541.rb +3 -3
  33. data/lib/badline/frontend/app.rb +63 -64
  34. data/lib/badline/frontend/boot.rb +34 -4
  35. data/lib/badline/frontend/confirmation.rb +17 -11
  36. data/lib/badline/frontend/controls.rb +12 -6
  37. data/lib/badline/frontend/drive_led.rb +9 -10
  38. data/lib/badline/frontend/frame_rate.rb +4 -4
  39. data/lib/badline/frontend/machine_window.rb +87 -0
  40. data/lib/badline/frontend/menu_dialogs.rb +52 -14
  41. data/lib/badline/frontend/menu_media.rb +70 -16
  42. data/lib/badline/frontend/menu_pages.rb +47 -42
  43. data/lib/badline/frontend/menu_ports.rb +69 -0
  44. data/lib/badline/frontend/name_field.rb +88 -0
  45. data/lib/badline/frontend/pacer.rb +6 -5
  46. data/lib/badline/frontend/pause_menu.rb +52 -37
  47. data/lib/badline/frontend/player_screen.rb +1 -1
  48. data/lib/badline/frontend/player_window.rb +4 -4
  49. data/lib/badline/frontend/screen.rb +39 -37
  50. data/lib/badline/frontend/sid_view.rb +1 -1
  51. data/lib/badline/frontend/snapshots.rb +145 -17
  52. data/lib/badline/frontend/sound.rb +17 -9
  53. data/lib/badline/frontend/timeline.rb +36 -5
  54. data/lib/badline/frontend.rb +3 -0
  55. data/lib/badline/iec_bus.rb +16 -28
  56. data/lib/badline/kernal_trap/drive.rb +3 -0
  57. data/lib/badline/kernal_trap/file.rb +1 -1
  58. data/lib/badline/kernal_trap/layout.rb +112 -0
  59. data/lib/badline/kernal_trap/load.rb +24 -25
  60. data/lib/badline/kernal_trap/routine.rb +17 -5
  61. data/lib/badline/kernal_trap/save.rb +13 -23
  62. data/lib/badline/kernal_trap/serial.rb +38 -24
  63. data/lib/badline/kernal_trap.rb +1 -0
  64. data/lib/badline/keyboard.rb +17 -14
  65. data/lib/badline/machine.rb +60 -0
  66. data/lib/badline/media/disk_list.rb +75 -0
  67. data/lib/badline/media/disk_set.rb +13 -3
  68. data/lib/badline/media/vic20_basic.rb +54 -0
  69. data/lib/badline/media/vic20_cartridge.rb +74 -0
  70. data/lib/badline/media/vic20_media.rb +132 -0
  71. data/lib/badline/media.rb +39 -20
  72. data/lib/badline/model.rb +62 -0
  73. data/lib/badline/options/event.rb +10 -6
  74. data/lib/badline/options/help.rb +25 -13
  75. data/lib/badline/options/table.rb +15 -1
  76. data/lib/badline/options/validation.rb +63 -3
  77. data/lib/badline/options.rb +37 -34
  78. data/lib/badline/region.rb +16 -4
  79. data/lib/badline/reu/ram.rb +27 -0
  80. data/lib/badline/reu/saved_state.rb +27 -0
  81. data/lib/badline/roms/README +37 -0
  82. data/lib/badline/roms/kernal-sx64.rom +0 -0
  83. data/lib/badline/roms/vic20/basic.rom +0 -0
  84. data/lib/badline/roms/vic20/character.rom +0 -0
  85. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  86. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  87. data/lib/badline/sid/decimator.rb +13 -0
  88. data/lib/badline/sid/filter.rb +6 -3
  89. data/lib/badline/sid.rb +19 -0
  90. data/lib/badline/snapshot/container.rb +2 -0
  91. data/lib/badline/snapshot/fields.rb +1 -0
  92. data/lib/badline/snapshot/image.rb +15 -0
  93. data/lib/badline/snapshot/setup.rb +20 -8
  94. data/lib/badline/snapshot/state.rb +15 -7
  95. data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
  96. data/lib/badline/snapshot/vice/peripherals.rb +12 -2
  97. data/lib/badline/snapshot/vice/restore.rb +3 -1
  98. data/lib/badline/snapshot/vice/reu1764.rb +55 -0
  99. data/lib/badline/snapshot/vice/vicii.rb +59 -13
  100. data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
  101. data/lib/badline/snapshot/vice.rb +25 -23
  102. data/lib/badline/snapshot.rb +10 -0
  103. data/lib/badline/storage/crt_file.rb +16 -5
  104. data/lib/badline/storage/disk_image.rb +3 -0
  105. data/lib/badline/storage/host_directory.rb +3 -0
  106. data/lib/badline/storage/t64.rb +10 -4
  107. data/lib/badline/storage/tap.rb +44 -8
  108. data/lib/badline/storage/unavailable.rb +3 -1
  109. data/lib/badline/storage.rb +5 -5
  110. data/lib/badline/timing.rb +12 -0
  111. data/lib/badline/version.rb +1 -1
  112. data/lib/badline/via/shift_register.rb +43 -16
  113. data/lib/badline/vic/bank.rb +53 -16
  114. data/lib/badline/vic/registers.rb +20 -4
  115. data/lib/badline/vic/sprite/timing.rb +5 -9
  116. data/lib/badline/vic/sprite.rb +4 -3
  117. data/lib/badline/vic.rb +34 -15
  118. data/lib/badline/vic20/attachments.rb +103 -0
  119. data/lib/badline/vic20/bus.rb +308 -0
  120. data/lib/badline/vic20/cpu.rb +10 -0
  121. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  122. data/lib/badline/vic20/port_wiring.rb +50 -0
  123. data/lib/badline/vic20/saved_state.rb +152 -0
  124. data/lib/badline/vic20/sound/output.rb +83 -0
  125. data/lib/badline/vic20/sound.rb +233 -0
  126. data/lib/badline/vic20/user_via_ports.rb +47 -0
  127. data/lib/badline/vic20/vic/painter.rb +239 -0
  128. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  129. data/lib/badline/vic20/vic/text_window.rb +71 -0
  130. data/lib/badline/vic20/vic.rb +267 -0
  131. data/lib/badline/vic20.rb +220 -0
  132. data/lib/badline.rb +2 -38
  133. metadata +37 -2
  134. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
data/lib/badline/media.rb CHANGED
@@ -3,6 +3,10 @@
3
3
  require "badline/media/true_drive"
4
4
  require "badline/media/queue"
5
5
  require "badline/media/disk_set"
6
+ require "badline/media/disk_list"
7
+ require "badline/media/vic20_basic"
8
+ require "badline/media/vic20_cartridge"
9
+ require "badline/media/vic20_media"
6
10
 
7
11
  module Badline
8
12
  module Media
@@ -27,23 +31,15 @@ module Badline
27
31
  # A .g64 plugs in a true drive as device 8 (TrueDrive), and with one
28
32
  # there, a .d64 goes into it instead of the KERNAL traps too. The
29
33
  # autostart then loads through it.
30
- def attach(computer, path, autostart: true, subtune: nil, **options)
31
- if TrueDrive.takes?(computer, path)
32
- attach_true_drive(computer, path, options.fetch(:disk, {}), autostart:)
33
- elsif File.directory?(path)
34
- computer.mount(Storage::HostDirectory.new(path))
35
- "Mounted #{path} as device 8"
36
- elsif File.extname(path).downcase == ".crt"
37
- attach_cartridge(computer, path, options.fetch(:cartridge, {}))
38
- elsif File.extname(path).downcase == ".sid"
39
- attach_sid(computer, path, autostart:, subtune:)
40
- elsif File.extname(path).downcase == ".tap"
41
- attach_tape(computer, path, autostart:)
42
- elsif MOUNT_TYPES.key?(File.extname(path).downcase)
43
- attach_storage(computer, path, options.fetch(:disk, {}), autostart:)
44
- else
45
- attach_prg(computer, path, autostart:)
46
- end
34
+ #
35
+ # An .m3u or .vfl list of disks attaches the first disk it lists
36
+ # (DiskList). A machine other than the C64 takes its own media
37
+ # (Vic20Media).
38
+ def attach(computer, path, autostart: true, subtune: nil, **)
39
+ path = DiskList.disk(path)
40
+ return Vic20Media.attach(computer, path, autostart:, **) unless computer.is_a?(Computer)
41
+
42
+ attach_c64(computer, path, autostart:, subtune:, **)
47
43
  end
48
44
 
49
45
  # Swaps the disk in device 8 for a disk image or a host directory,
@@ -64,16 +60,39 @@ module Badline
64
60
  "Inserted #{path} in device 8"
65
61
  end
66
62
 
63
+ # The RAM expansion a VIC-20 for `path` should be built with
64
+ # (Vic20Media.ram_for), a key of Vic20::Bus::RAM_CONFIGURATIONS.
65
+ def vic20_ram_for(path) = Vic20Media.ram_for(path)
66
+
67
67
  # The SID a machine for `path` should be built with. A .sid tune names
68
- # its own; everything else gets the 6581.
69
- def sid_model(path)
70
- return :mos6581 unless path && File.extname(path).downcase == ".sid"
68
+ # its own; everything else gets `otherwise`.
69
+ def sid_model(path, otherwise: :mos6581)
70
+ return otherwise unless path && File.extname(path).downcase == ".sid"
71
71
 
72
72
  Storage::SIDFile.new(path).sid_model
73
73
  end
74
74
 
75
75
  private
76
76
 
77
+ def attach_c64(computer, path, autostart:, subtune:, **options)
78
+ if TrueDrive.takes?(computer, path)
79
+ attach_true_drive(computer, path, options.fetch(:disk, {}), autostart:)
80
+ elsif File.directory?(path)
81
+ computer.mount(Storage::HostDirectory.new(path))
82
+ "Mounted #{path} as device 8"
83
+ elsif File.extname(path).downcase == ".crt"
84
+ attach_cartridge(computer, path, options.fetch(:cartridge, {}))
85
+ elsif File.extname(path).downcase == ".sid"
86
+ attach_sid(computer, path, autostart:, subtune:)
87
+ elsif File.extname(path).downcase == ".tap"
88
+ attach_tape(computer, path, autostart:)
89
+ elsif MOUNT_TYPES.key?(File.extname(path).downcase)
90
+ attach_storage(computer, path, options.fetch(:disk, {}), autostart:)
91
+ else
92
+ attach_prg(computer, path, autostart:)
93
+ end
94
+ end
95
+
77
96
  def disk?(path)
78
97
  storage = MOUNT_TYPES[File.extname(path).downcase]
79
98
  File.directory?(path) || (!storage.nil? && storage < Storage::DiskImage)
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ # = Model
5
+ #
6
+ # The C64s badline builds by name, as `--model` selects them: the chips
7
+ # each was sold with, its video standard, its KERNAL and whether it has
8
+ # a datasette. The names are x64sc's.
9
+ module Model
10
+ # Each field but the name is a keyword Computer.new takes.
11
+ Profile = Data.define(:name, :vic_model, :cia_model, :sid_model, :region, :kernal, :datasette)
12
+
13
+ # The breadbin PAL C64: the 6569 VIC-II, 6526 CIAs and the 6581 SID.
14
+ C64 = Profile.new(name: "c64", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
15
+ region: Region::PAL, kernal: :c64, datasette: true)
16
+
17
+ # The PAL C64C: the HMOS 8565 VIC-II, 6526A CIAs and the 8580 SID.
18
+ C64C = Profile.new(name: "c64c", vic_model: :mos8565, cia_model: :mos6526a, sid_model: :mos8580,
19
+ region: Region::PAL, kernal: :c64, datasette: true)
20
+
21
+ # The NTSC C64 with the 6567R8 VIC-II.
22
+ NTSC = Profile.new(name: "ntsc", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
23
+ region: Region::NTSC, kernal: :c64, datasette: true)
24
+
25
+ # The NTSC C64C: the 8562, the 6567R8's HMOS successor, with the
26
+ # C64C's CIAs and SID.
27
+ NEW_NTSC = Profile.new(name: "newntsc", vic_model: :mos8565, cia_model: :mos6526a, sid_model: :mos8580,
28
+ region: Region::NTSC, kernal: :c64, datasette: true)
29
+
30
+ # The first NTSC C64s, with the 6567R56A VIC-II.
31
+ OLD_NTSC = Profile.new(name: "oldntsc", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
32
+ region: Region::NTSC_OLD, kernal: :c64, datasette: true)
33
+
34
+ # The Drean C64 of Argentina, on PAL-N with the 6572 VIC-II.
35
+ DREAN = Profile.new(name: "drean", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
36
+ region: Region::DREAN, kernal: :c64, datasette: true)
37
+
38
+ # The portable SX-64: a PAL C64's chips with its own KERNAL, a 1541
39
+ # built in as device 8 and no cassette port.
40
+ SX64 = Profile.new(name: "sx64", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
41
+ region: Region::PAL, kernal: :sx64, datasette: false)
42
+
43
+ ALL = [C64, C64C, NTSC, NEW_NTSC, OLD_NTSC, DREAN, SX64].freeze
44
+
45
+ def self.named(name)
46
+ model = ALL.find { |candidate| candidate.name == name }
47
+ raise ArgumentError, "no C64 model named #{name}" if model.nil?
48
+
49
+ model
50
+ end
51
+
52
+ # The model whose chips, region, KERNAL and datasette `machine` has, or
53
+ # nil for none.
54
+ def self.of(machine)
55
+ ALL.find do |model|
56
+ model.vic_model == machine.vic_model && model.cia_model == machine.cia_model &&
57
+ model.sid_model == machine.sid_model && model.region == machine.region &&
58
+ model.kernal == machine.kernal && model.datasette == machine.datasette
59
+ end
60
+ end
61
+ end
62
+ end
@@ -6,10 +6,11 @@ module Badline
6
6
  # `frame` frames have run. An event given at several frames becomes
7
7
  # one Event per frame.
8
8
  class Event
9
- # The actions, and whether each takes an argument.
9
+ # The actions, and whether each takes an argument. menu's page may be
10
+ # left out.
10
11
  ACTIONS = {
11
- "key" => true, "type" => true, "insert" => true, "eject" => true, "screenshot" => true,
12
- "reset" => false, "freeze" => false, "quit" => false
12
+ "key" => true, "type" => true, "insert" => true, "eject" => true, "screenshot" => true, "menu" => true,
13
+ "reset" => false, "freeze" => false, "resume" => false, "quit" => false
13
14
  }.freeze
14
15
 
15
16
  # The frames a key or the freeze button is held for.
@@ -17,8 +18,8 @@ module Badline
17
18
 
18
19
  EJECTABLE = %w[disk tape cartridge].freeze
19
20
 
20
- # What insert takes: disk images, tapes and cartridges, or a
21
- # directory to mount as device 8.
21
+ # What insert takes: disk images, tapes and cartridges, or an .m3u
22
+ # or .vfl list of disks or a directory to mount as device 8.
22
23
  INSERTABLE = %w[.d64 .d71 .d81 .g64 .t64 .tap .crt].freeze
23
24
 
24
25
  attr_reader :frame, :action, :argument
@@ -79,6 +80,7 @@ module Badline
79
80
 
80
81
  def self.valid?(action, argument)
81
82
  return false unless ACTIONS.key?(action)
83
+ return true if action == "menu"
82
84
  return argument.empty? unless ACTIONS[action]
83
85
  return EJECTABLE.include?(argument) if action == "eject"
84
86
  return insertable?(argument) if action == "insert"
@@ -87,7 +89,9 @@ module Badline
87
89
  end
88
90
 
89
91
  def self.insertable?(path)
90
- File.directory?(path) || (File.exist?(path) && INSERTABLE.include?(File.extname(path).downcase))
92
+ return true if File.directory?(path)
93
+
94
+ File.exist?(path) && (INSERTABLE.include?(File.extname(path).downcase) || Media::DiskList.list?(path))
91
95
  end
92
96
  end
93
97
  end
@@ -37,20 +37,30 @@ module Badline
37
37
 
38
38
  def banner
39
39
  <<~BANNER
40
- Usage: #{@program} [options] [media]
40
+ Usage: #{@program} [c64] [options] [media]
41
+ #{@program} vic20 [options] [media]
41
42
  #{@program} --headless [options] tune.sid
42
43
  #{@program} [options] tune.sid --audio-out FILE
43
44
  #{@program} sid [options] FILE|DIR...
44
45
 
46
+ Commands:
47
+ c64 Run a C64, the one --model names (the default)
48
+ vic20 Run a PAL VIC-20, with the RAM --ram names
49
+ sid Play .sid tunes and directories of them
50
+
45
51
  Media can be a .prg/.p00 program, a .d64/.d71/.d81 disk image, a
46
- .g64 disk image for the true 1541, a .t64 tape archive, a .tap
47
- tape, a .crt cartridge, a .sid tune, a .vsf snapshot, or a directory
48
- to mount as device 8. It opens in the emulator window, where F11
49
- saves a snapshot and F12 restores it. Device 8 answers through
50
- traps on the KERNAL's disk routines, unless --true-drive puts an
51
- emulated 1541 there, which runs its own DOS and reads .d64 and .g64
52
- images only. A .sid tune opens in the SID player, as `sid` plays it,
53
- unless an option of the emulator's window asks for the machine.
52
+ .g64 disk image for the true 1541, an .m3u or .vfl list of disk
53
+ images, a .t64 tape archive, a .tap tape, a .crt cartridge, a .sid
54
+ tune, a .vsf snapshot, or a directory to mount as device 8. It
55
+ opens in the emulator window. There F11 quicksaves the machine to
56
+ the next of five slots in badline's data folder, replacing the
57
+ oldest, and F12 restores the newest quicksave or named save. The
58
+ window also autosaves every two minutes of play, which only the
59
+ pause menu loads. Device 8 answers through traps on the KERNAL's
60
+ disk routines, unless --true-drive puts an emulated 1541 there,
61
+ which runs its own DOS and reads .d64 and .g64 images only. A .sid
62
+ tune opens in the SID player, as `sid` plays it, unless an option
63
+ of the emulator's window asks for the machine.
54
64
 
55
65
  --headless plays a .sid tune on the host's audio device without the
56
66
  window, and --audio-out renders it to 16-bit PCM instead. The
@@ -74,10 +84,12 @@ module Badline
74
84
  key=NAME presses a C64 key (space, return, a, f1, run_stop, restore)
75
85
  or a joystick's direction or fire (joy1-up, joy2-fire) for #{Event::HOLD}
76
86
  frames. type=TEXT types it, with \\n for RETURN. insert=FILE swaps
77
- in a disk, tape or cartridge, and eject=disk, tape or cartridge
78
- takes one out. screenshot=FILE saves the frame as a .bmp, with the
79
- frame's number in place of %d. reset, freeze and quit take no
80
- argument.
87
+ in a disk, a list's first disk, a tape or a cartridge, and
88
+ eject=disk, tape or cartridge takes one out. screenshot=FILE saves
89
+ the frame as a .bmp, with the frame's number in place of %d. menu
90
+ opens the pause menu, and menu=PAGE opens it on snapshots, drive,
91
+ datasette, expansion, ports, sound or power. Its frames still count,
92
+ and resume closes it. reset, freeze and quit take no argument.
81
93
  BANNER
82
94
  end
83
95
 
@@ -4,6 +4,17 @@ module Badline
4
4
  class Options
5
5
  DEFAULT_RATE = 44_100
6
6
 
7
+ # The names Badline::Model builds, in the order the help lists them.
8
+ MODELS = %w[c64 c64c ntsc newntsc oldntsc drean sx64].freeze
9
+
10
+ # The VIC-20's models, as xvic names its video standards. Only PAL is
11
+ # emulated.
12
+ VIC20_MODELS = %w[pal ntsc].freeze
13
+
14
+ # The VIC-20 RAM expansions --ram takes, in the order the help lists
15
+ # them.
16
+ RAM_NAMES = %w[unexpanded 3k 8k 16k 24k 32k all].freeze
17
+
7
18
  SECTIONS = ["Options", "Window options", "Options without the window", "Testing options"].freeze
8
19
 
9
20
  # Every option of both builds, in the order the help lists them.
@@ -25,7 +36,10 @@ module Badline
25
36
  section: "Window options", needs: :window),
26
37
  Option.new("--reu SIZE", "Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384",
27
38
  section: "Window options", needs: :window),
28
- Option.new("--ntsc", "Run an NTSC C64, with the 6567R8 VIC-II, instead of a PAL one",
39
+ Option.new("--model NAME", "C64 to run: #{MODELS.join(', ')} (default: #{MODELS.first}); VIC-20: pal",
40
+ section: "Window options", needs: :window),
41
+ Option.new("--ntsc", "Run an NTSC C64, as --model ntsc does", section: "Window options", needs: :window),
42
+ Option.new("--ram SIZE", "VIC-20 RAM expansion: #{RAM_NAMES.join(', ')} (default: unexpanded)",
29
43
  section: "Window options", needs: :window),
30
44
  Option.new("--no-vsync", "Pace frames by the timer or the sound instead of the display",
31
45
  section: "Window options", needs: :window),
@@ -2,14 +2,47 @@
2
2
 
3
3
  module Badline
4
4
  class Options
5
- # The checks Options#parse makes once every argument has been read:
6
- # the options fit the mode, the numbers are positive and the files
7
- # exist.
5
+ # The checks Options#parse makes: that each value reads as its option
6
+ # takes it, and once every argument has been read, that the options
7
+ # fit the mode, the numbers are positive and the files exist.
8
8
  module Validation
9
9
  private
10
10
 
11
+ def sid_model_for(value)
12
+ return if value == "auto"
13
+ raise Error, "invalid argument: --sid #{value}" unless SID_MODELS.key?(value)
14
+
15
+ SID_MODELS[value]
16
+ end
17
+
18
+ def ram_configuration(value)
19
+ raise Error, "invalid argument: --ram #{value}" unless RAM_CONFIGURATIONS.key?(value)
20
+
21
+ RAM_CONFIGURATIONS[value]
22
+ end
23
+
24
+ def reu_size(value)
25
+ raise Error, "invalid argument: --reu #{value}" unless REU_SIZES.include?(value)
26
+
27
+ value.to_i
28
+ end
29
+
30
+ def number(flag, value)
31
+ raise Error, "invalid argument: #{flag} #{value}" unless value.match?(/\A\d+\z/)
32
+
33
+ value.to_i
34
+ end
35
+
36
+ def decimal(flag, value)
37
+ raise Error, "invalid argument: #{flag} #{value}" unless value.match?(DECIMAL)
38
+
39
+ value.to_f
40
+ end
41
+
11
42
  def validate
43
+ validate_family
12
44
  validate_mode
45
+ validate_models
13
46
  validate_numbers
14
47
  validate_paths
15
48
  end
@@ -25,6 +58,24 @@ module Badline
25
58
  end
26
59
  end
27
60
 
61
+ def validate_family
62
+ if @family == :vic20
63
+ validate_vic20
64
+ elsif !@ram.nil?
65
+ raise Error, "--ram needs vic20"
66
+ end
67
+ end
68
+
69
+ # The VIC-20 is a PAL one, in the window, without the C64's SID and
70
+ # REU.
71
+ def validate_vic20
72
+ raise Error, "the NTSC VIC-20 isn't emulated yet" if @models.include?("ntsc")
73
+
74
+ raise Error, "--sid needs the C64" unless @sid_model.nil?
75
+ raise Error, "--reu needs the C64" unless @reu.nil?
76
+ raise Error, "the VIC-20 needs the window" if headless?
77
+ end
78
+
28
79
  # `sid` takes directories as well as tunes.
29
80
  def validate_tune(path)
30
81
  return if !File.exist?(path) || File.extname(path).casecmp?(".sid")
@@ -33,6 +84,15 @@ module Badline
33
84
  raise Error, "not a .sid tune: #{path}"
34
85
  end
35
86
 
87
+ # --model and --ntsc may be given more than once, but only for one
88
+ # model.
89
+ def validate_models
90
+ names = @family == :vic20 ? VIC20_MODELS : MODELS
91
+ unknown = @models.find { |name| !names.include?(name) }
92
+ raise Error, "invalid argument: --model #{unknown}" unless unknown.nil?
93
+ raise Error, "conflicting models: #{@models.uniq.join(' and ')}" unless @models.uniq.size <= 1
94
+ end
95
+
36
96
  def validate_numbers
37
97
  positive("--subtune", @subtune)
38
98
  positive("--seconds", @seconds)
@@ -10,7 +10,9 @@ module Badline
10
10
  # The command line of both builds, badline-ruby and the native badline.
11
11
  # Either opens the window for any media, or with --headless or
12
12
  # --audio-out plays or renders a .sid tune without one. `sid`, given
13
- # first, plays .sid tunes and directories of them in the terminal. The
13
+ # first, plays .sid tunes and directories of them in the terminal.
14
+ # `c64` or `vic20`, given first, names the machine family, and without
15
+ # either the family is the C64. The
14
16
  # native build adds --no-sound and --version, and badline-ruby adds
15
17
  # --disable-jit.
16
18
  #
@@ -37,6 +39,13 @@ module Badline
37
39
 
38
40
  REU_SIZES = %w[128 256 512 1024 2048 4096 8192 16384].freeze
39
41
 
42
+ # The VIC-20's RAM expansions as --ram names them (RAM_NAMES), and the
43
+ # names Vic20::Bus::RAM_CONFIGURATIONS gives them.
44
+ RAM_CONFIGURATIONS = {
45
+ "unexpanded" => :unexpanded, "3k" => :"3k", "8k" => :"8k", "16k" => :"16k", "24k" => :"24k",
46
+ "32k" => :"32k", "all" => :all
47
+ }.freeze
48
+
40
49
  # A Float as OptionParser took one: digits with an optional sign,
41
50
  # fraction and exponent.
42
51
  DECIMAL = /\A[-+]?(\d+(\.\d+)?|\.\d+)([eE][-+]?\d+)?\z/
@@ -46,6 +55,13 @@ module Badline
46
55
  attr_reader :program, :media_path, :tune_paths, :subtune, :sid_model, :reu, :frames, :screenshot, :save_snapshot,
47
56
  :audio_out, :seconds, :songlengths, :filter_chunk, :timeline
48
57
 
58
+ # The machine family to build (Badline::Machine), :c64 or :vic20.
59
+ attr_reader :family
60
+
61
+ # The VIC-20's RAM expansion --ram names, a key of
62
+ # Vic20::Bus::RAM_CONFIGURATIONS, or nil without --ram.
63
+ attr_reader :ram
64
+
49
65
  def self.parse(argv, native: false) = new(native:).parse(argv)
50
66
 
51
67
  # The native build plays the SID in the window unless --no-sound says
@@ -55,11 +71,14 @@ module Badline
55
71
  @native = native
56
72
  @program = native ? "badline" : "badline-ruby"
57
73
  @media_path = nil
74
+ @family = :c64
58
75
  @sid_command = false
59
76
  @tune_paths = []
60
77
  @subtune = nil
61
78
  @sid_model = nil
79
+ @models = []
62
80
  @reu = nil
81
+ @ram = nil
63
82
  @autostart = true
64
83
  @writable = false
65
84
  @sound = native
@@ -68,7 +87,6 @@ module Badline
68
87
  @vsync = true
69
88
  @verbose = false
70
89
  @true_drive = false
71
- @ntsc = false
72
90
  @screenshot = ""
73
91
  @save_snapshot = ""
74
92
  @timeline = []
@@ -90,7 +108,7 @@ module Badline
90
108
 
91
109
  def parse(argv)
92
110
  args = argv.dup
93
- sid_command(args) if args.first == "sid"
111
+ subcommand(args)
94
112
  argument(args.shift, args) until args.empty?
95
113
  play_lone_tune
96
114
  validate unless help? || version?
@@ -147,10 +165,12 @@ module Badline
147
165
 
148
166
  def true_drive? = @true_drive
149
167
 
150
- def ntsc? = @ntsc
151
-
152
168
  def jit? = @jit
153
169
 
170
+ # The name of the model to build: a C64 Badline::Model names, or a
171
+ # VIC-20 of VIC20_MODELS. --ntsc is short for --model ntsc.
172
+ def model = @models.first || (@family == :vic20 ? VIC20_MODELS.first : MODELS.first)
173
+
154
174
  def help? = @help
155
175
 
156
176
  def version? = @version
@@ -163,15 +183,21 @@ module Badline
163
183
  [:both, @native ? :native : :ruby].include?(option.build)
164
184
  end
165
185
 
166
- def sid_command(args)
186
+ # Takes `sid`, `c64` or `vic20` when given first.
187
+ def subcommand(args)
188
+ case args.first
189
+ when "sid" then @sid_command = true
190
+ when "c64" then @family = :c64
191
+ when "vic20" then @family = :vic20
192
+ else return
193
+ end
167
194
  args.shift
168
- @sid_command = true
169
195
  end
170
196
 
171
197
  # A .sid on its own plays in the SID player, as `sid` would, unless an
172
198
  # option of the emulator's window asks for the machine.
173
199
  def play_lone_tune
174
- return if @sid_command || headless? || !@window_only.empty? || @media_path.nil?
200
+ return if @sid_command || @family == :vic20 || headless? || !@window_only.empty? || @media_path.nil?
175
201
 
176
202
  @sid_command = File.extname(@media_path).casecmp?(".sid")
177
203
  end
@@ -222,7 +248,9 @@ module Badline
222
248
  case name
223
249
  when "--subtune" then @subtune = number(flag, value)
224
250
  when "--sid" then @sid_model = sid_model_for(value)
251
+ when "--model" then @models << value
225
252
  when "--reu" then @reu = reu_size(value)
253
+ when "--ram" then @ram = ram_configuration(value)
226
254
  when "--frames" then @frames = number(flag, value)
227
255
  when "--screenshot" then @screenshot = value
228
256
  when "--save-snapshot" then @save_snapshot = value
@@ -244,7 +272,7 @@ module Badline
244
272
  when "--no-vsync" then @vsync = false
245
273
  when "--verbose" then @verbose = true
246
274
  when "--true-drive" then @true_drive = true
247
- when "--ntsc" then @ntsc = true
275
+ when "--ntsc" then @models << "ntsc"
248
276
  when "--unpaced" then @paced = false
249
277
  when "--disable-jit" then @jit = false
250
278
  when "--help" then @help = true
@@ -256,31 +284,6 @@ module Badline
256
284
  end
257
285
  end
258
286
 
259
- def sid_model_for(value)
260
- return if value == "auto"
261
- raise Error, "invalid argument: --sid #{value}" unless SID_MODELS.key?(value)
262
-
263
- SID_MODELS[value]
264
- end
265
-
266
- def reu_size(value)
267
- raise Error, "invalid argument: --reu #{value}" unless REU_SIZES.include?(value)
268
-
269
- value.to_i
270
- end
271
-
272
- def number(flag, value)
273
- raise Error, "invalid argument: #{flag} #{value}" unless value.match?(/\A\d+\z/)
274
-
275
- value.to_i
276
- end
277
-
278
- def decimal(flag, value)
279
- raise Error, "invalid argument: #{flag} #{value}" unless value.match?(DECIMAL)
280
-
281
- value.to_f
282
- end
283
-
284
287
  def media_argument(arg)
285
288
  raise Error, "unexpected argument: #{arg}" unless @sid_command || @media_path.nil?
286
289
 
@@ -21,8 +21,8 @@ module Badline
21
21
  # `sprite_cycle` is Bauer's cycle for sprite 0's p-access, and each
22
22
  # sprite after it takes two more. `sprite_display_cycle` is the cycle
23
23
  # that turns a sprite's display on or off. `x_hold` is how many pixels
24
- # the X counter spends past its run, repeating $184-$187, and a raster
25
- # of more than 512 pixels needs them.
24
+ # the X counter spends past its run, going over $180-$187 a second
25
+ # time, and a raster of more than 512 pixels needs them.
26
26
  Profile = Data.define(:name, :clock_hz, :cycles_per_line, :lines_per_frame, :hblank, :vblank,
27
27
  :display_x_bounds, :crop, :mains_hz, :sprite_cycle, :sprite_display_cycle,
28
28
  :x_hold)
@@ -40,8 +40,8 @@ module Badline
40
40
  # The 6567R8 of an NTSC C64: 65 cycles by 263 lines at 1,022,727 Hz,
41
41
  # with 60 Hz mains. The sprite fetches and the compares ahead of them
42
42
  # run a cycle later than on the 6569 (the spritesteal, spritex and
43
- # phi1timing testprogs), and the X counter pauses near $184, as Bauer's
44
- # 6567R8 diagram shows, reading $184-$187 for 8 pixels more. The
43
+ # phi1timing testprogs), and the X counter runs over $180-$187 twice,
44
+ # 8 pixels more, as the 6567R8's lightpen dump shows. The
45
45
  # blanked lines are the ones VICE's NTSC view leaves out, 12-27. It shows lines 0-11 below
46
46
  # line 262, and the crop leaves those out.
47
47
  NTSC = Profile.new(
@@ -65,6 +65,18 @@ module Badline
65
65
  sprite_cycle: 59, sprite_display_cycle: 58, x_hold: 0
66
66
  )
67
67
 
68
+ # The 6572 of the Drean C64, sold in Argentina on PAL-N: 65 cycles by
69
+ # 312 lines at 1,023,440 Hz, with 50 Hz mains. It lays out the line
70
+ # as the 6567R8 does, sprite fetches and X counter hold included
71
+ # (spritescan_drean), and blanks and crops lines as PAL does.
72
+ DREAN = Profile.new(
73
+ name: :drean, clock_hz: 1_023_440, cycles_per_line: 65, lines_per_frame: 312,
74
+ hblank: [63, 9].freeze, vblank: [300, 15].freeze,
75
+ display_x_bounds: [[135, 438].freeze, [128, 447].freeze].freeze,
76
+ crop: [96, 20, 384, 272].freeze, mains_hz: 50,
77
+ sprite_cycle: 59, sprite_display_cycle: 59, x_hold: 8
78
+ )
79
+
68
80
  # Whether a column or line lies in a [first, last] blanking span.
69
81
  def self.blanked?(position, span)
70
82
  first, last = span
@@ -52,8 +52,35 @@ module Badline
52
52
  @banks.each_index { |number| @banks[number] = input.boolean? ? input.blob : [] }
53
53
  end
54
54
 
55
+ # Every byte, as one string, with a bank the REC hasn't touched at
56
+ # its power-on contents.
57
+ def contents
58
+ power_on = power_on_strings
59
+ Array.new(@banks.length) do |number|
60
+ bank = @banks[number]
61
+ bank.empty? ? power_on[(number >> 1) & 1] : bank.pack("C*")
62
+ end.join
63
+ end
64
+
65
+ # Takes every byte from a string as long as the RAM. A bank that
66
+ # holds its power-on contents stays untouched.
67
+ def restore(bytes)
68
+ power_on = power_on_strings
69
+ @banks.each_index do |number|
70
+ slice = bytes.byteslice(number * BANK_SIZE, BANK_SIZE).to_s
71
+ @banks[number] = slice == power_on[(number >> 1) & 1] ? [] : slice.bytes
72
+ end
73
+ end
74
+
55
75
  private
56
76
 
77
+ def power_on_strings
78
+ [0, 1].map do |half|
79
+ @power_on[half] = power_on_bank(half == 1) if @power_on[half].empty?
80
+ @power_on[half].pack("C*")
81
+ end
82
+ end
83
+
57
84
  # A bank takes its power-on contents the first time it is touched,
58
85
  # so an REU costs only the memory a program uses.
59
86
  def bank(number)
@@ -32,6 +32,33 @@ module Badline
32
32
  @ram.load_state(input)
33
33
  end
34
34
 
35
+ # The sixteen bytes the REC's registers read from $DF00, without
36
+ # what reading the status does, as VICE's REU1764 module holds them.
37
+ def register_file = Array.new(16) { |register| register_value(register) }
38
+
39
+ # Sets the registers from a register file, as writing them would but
40
+ # without starting a transfer: each address and the length set both
41
+ # the counter and the value it starts from. A command with the
42
+ # execute bit set and the $FF00 trigger on arms the REC.
43
+ def restore_registers(values)
44
+ @status = (values[STATUS] & (INTERRUPT_PENDING | END_OF_BLOCK | FAULT)) | @chips
45
+ @command = values[COMMAND]
46
+ @c64 = @c64_start = values[C64_LOW] | (values[C64_HIGH] << 8)
47
+ @expansion = @expansion_start = values[EXPANSION_LOW] | (values[EXPANSION_HIGH] << 8)
48
+ @bank = @bank_start = values[EXPANSION_BANK] & @bank_bits
49
+ @length = @length_start = values[LENGTH_LOW] | (values[LENGTH_HIGH] << 8)
50
+ @interrupt_mask = values[INTERRUPT_MASK] | 0x1f
51
+ @address_control = values[ADDRESS_CONTROL] | 0x3f
52
+ @armed = @command.anybits?(EXECUTE) && @command.nobits?(NO_FF00_TRIGGER)
53
+ @requested = @running = @ba_was_low = false
54
+ interrupt(@status.anybits?(INTERRUPT_PENDING))
55
+ end
56
+
57
+ # The RAM, every byte of it, as one string.
58
+ def ram_contents = @ram.contents
59
+
60
+ def restore_ram(bytes) = @ram.restore(bytes)
61
+
35
62
  private
36
63
 
37
64
  def load_registers(input)