badline 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/CLAUDE.md +25 -6
  4. data/CONTRIBUTING.md +8 -3
  5. data/README.md +63 -4
  6. data/doc/pinned-behaviour.md +75 -0
  7. data/doc/snapshots.md +15 -0
  8. data/lib/badline/address_bus/pla.rb +3 -16
  9. data/lib/badline/address_bus.rb +14 -0
  10. data/lib/badline/c128/banks.rb +35 -0
  11. data/lib/badline/c128/bus.rb +347 -0
  12. data/lib/badline/c128/bus_state.rb +53 -0
  13. data/lib/badline/c128/color_lines.rb +39 -0
  14. data/lib/badline/c128/cpu.rb +10 -0
  15. data/lib/badline/c128/drives.rb +95 -0
  16. data/lib/badline/c128/fast_serial.rb +62 -0
  17. data/lib/badline/c128/keys.rb +35 -0
  18. data/lib/badline/c128/mmu.rb +197 -0
  19. data/lib/badline/c128/mmu_pages.rb +105 -0
  20. data/lib/badline/c128/model.rb +39 -0
  21. data/lib/badline/c128/modes.rb +48 -0
  22. data/lib/badline/c128/saved_state.rb +121 -0
  23. data/lib/badline/c128/vdc/memory.rb +54 -0
  24. data/lib/badline/c128/vdc/painter.rb +143 -0
  25. data/lib/badline/c128/vdc/raster.rb +97 -0
  26. data/lib/badline/c128/vdc/window.rb +215 -0
  27. data/lib/badline/c128/vdc.rb +268 -0
  28. data/lib/badline/c128.rb +322 -0
  29. data/lib/badline/chrout_trap.rb +3 -0
  30. data/lib/badline/cia/fast_forward.rb +42 -0
  31. data/lib/badline/cia/serial.rb +12 -0
  32. data/lib/badline/cia/timer.rb +39 -0
  33. data/lib/badline/cia.rb +2 -0
  34. data/lib/badline/computer/attachments.rb +10 -13
  35. data/lib/badline/computer/kernal_traps.rb +29 -0
  36. data/lib/badline/computer.rb +5 -0
  37. data/lib/badline/drive/core.rb +172 -0
  38. data/lib/badline/{drive1541 → drive}/idle.rb +18 -19
  39. data/lib/badline/drive/idle_cpu.rb +28 -0
  40. data/lib/badline/{drive1541 → drive}/orbit.rb +2 -1
  41. data/lib/badline/drive/serial_lines.rb +73 -0
  42. data/lib/badline/{drive1541 → drive}/sleep.rb +4 -3
  43. data/lib/badline/drive/watch.rb +150 -0
  44. data/lib/badline/drive.rb +9 -0
  45. data/lib/badline/drive1541/bus.rb +5 -129
  46. data/lib/badline/drive1541/byte_ready.rb +48 -0
  47. data/lib/badline/drive1541/cpu.rb +1 -19
  48. data/lib/badline/drive1541/disk.rb +77 -20
  49. data/lib/badline/drive1541/disk_state.rb +7 -2
  50. data/lib/badline/drive1541/disk_via.rb +3 -0
  51. data/lib/badline/drive1541/mechanism.rb +48 -14
  52. data/lib/badline/drive1541/mechanism_state.rb +5 -4
  53. data/lib/badline/drive1541/rotation.rb +5 -5
  54. data/lib/badline/drive1541/serial_port.rb +4 -60
  55. data/lib/badline/drive1541.rb +13 -172
  56. data/lib/badline/drive1571/bus.rb +111 -0
  57. data/lib/badline/drive1571/cpu.rb +11 -0
  58. data/lib/badline/drive1571/serial_port.rb +37 -0
  59. data/lib/badline/drive1571/serial_via.rb +25 -0
  60. data/lib/badline/drive1571/wd1770.rb +34 -0
  61. data/lib/badline/drive1571.rb +225 -0
  62. data/lib/badline/frontend/app.rb +35 -22
  63. data/lib/badline/frontend/boot.rb +31 -1
  64. data/lib/badline/frontend/controls.rb +11 -0
  65. data/lib/badline/frontend/drive_led.rb +1 -1
  66. data/lib/badline/frontend/keys.rb +28 -1
  67. data/lib/badline/frontend/machine_window.rb +16 -13
  68. data/lib/badline/frontend/menu_media.rb +3 -3
  69. data/lib/badline/frontend/menu_pages.rb +8 -2
  70. data/lib/badline/frontend/menu_ports.rb +1 -1
  71. data/lib/badline/frontend/pause_menu.rb +1 -0
  72. data/lib/badline/frontend/screen.rb +33 -8
  73. data/lib/badline/frontend/screens.rb +90 -0
  74. data/lib/badline/frontend/timeline.rb +15 -4
  75. data/lib/badline/frontend.rb +1 -0
  76. data/lib/badline/iec_bus.rb +54 -4
  77. data/lib/badline/kernal_trap/file.rb +1 -1
  78. data/lib/badline/kernal_trap/layout.rb +58 -8
  79. data/lib/badline/kernal_trap/load.rb +1 -1
  80. data/lib/badline/kernal_trap/save.rb +1 -1
  81. data/lib/badline/kernal_trap/serial.rb +5 -0
  82. data/lib/badline/machine.rb +20 -9
  83. data/lib/badline/media/true_drive.rb +26 -23
  84. data/lib/badline/media.rb +8 -6
  85. data/lib/badline/options/event.rb +5 -1
  86. data/lib/badline/options/family.rb +52 -0
  87. data/lib/badline/options/help.rb +7 -2
  88. data/lib/badline/options/table.rb +8 -2
  89. data/lib/badline/options/validation.rb +14 -1
  90. data/lib/badline/options.rb +20 -19
  91. data/lib/badline/roms/README +36 -0
  92. data/lib/badline/roms/c128/basic.rom +0 -0
  93. data/lib/badline/roms/c128/character.rom +0 -0
  94. data/lib/badline/roms/c128/kernal.rom +0 -0
  95. data/lib/badline/roms/dos1571.rom +0 -0
  96. data/lib/badline/snapshot/container.rb +2 -0
  97. data/lib/badline/snapshot/image.rb +14 -0
  98. data/lib/badline/snapshot.rb +9 -2
  99. data/lib/badline/storage/d64_image.rb +3 -0
  100. data/lib/badline/storage/d71_image.rb +9 -4
  101. data/lib/badline/storage/g64_image.rb +7 -2
  102. data/lib/badline/time_of_day.rb +9 -0
  103. data/lib/badline/version.rb +1 -1
  104. data/lib/badline/via/fast_forward.rb +3 -3
  105. data/lib/badline/vic/display_state.rb +12 -0
  106. data/lib/badline/vic/registers.rb +3 -0
  107. data/lib/badline/vic.rb +108 -7
  108. data/lib/badline/vic20/attachments.rb +6 -0
  109. data/lib/badline/z80/arithmetic.rb +117 -0
  110. data/lib/badline/z80/bit_page.rb +55 -0
  111. data/lib/badline/z80/block_io.rb +58 -0
  112. data/lib/badline/z80/block_transfers.rb +59 -0
  113. data/lib/badline/z80/bus_cycles.rb +111 -0
  114. data/lib/badline/z80/core.rb +111 -0
  115. data/lib/badline/z80/exchanges.rb +50 -0
  116. data/lib/badline/z80/extended_page.rb +89 -0
  117. data/lib/badline/z80/flags.rb +37 -0
  118. data/lib/badline/z80/interrupt_acceptance.rb +46 -0
  119. data/lib/badline/z80/jumps.rb +82 -0
  120. data/lib/badline/z80/loads.rb +98 -0
  121. data/lib/badline/z80/main_page.rb +134 -0
  122. data/lib/badline/z80/registers.rb +120 -0
  123. data/lib/badline/z80/shifts.rb +64 -0
  124. data/lib/badline/z80.rb +11 -0
  125. data/lib/badline.rb +1 -0
  126. metadata +59 -4
@@ -2,51 +2,54 @@
2
2
 
3
3
  module Badline
4
4
  module Media
5
- # Device 8 as a true 1541 instead of the KERNAL traps. Once a Drive1541
6
- # is plugged in as device 8, disk media go into it rather than into the
7
- # traps, so a LOAD runs the DOS and every byte crosses the serial bus.
8
- # A .g64 holds the disk's raw GCR, which only a true drive reads, so it
9
- # plugs one in when there's none yet. The 1541 reads only .d64 and .g64
10
- # images, so the traps' other disk media, a .d71, .d81 or .t64 or a host
11
- # directory, raise Error.
5
+ # Device 8 as a true drive instead of the KERNAL traps: a 1541, or on
6
+ # the C128 its 1571. Once a true drive is plugged in as device 8, disk
7
+ # media go into it rather than into the traps, so a LOAD runs the DOS
8
+ # and every byte crosses the serial bus. A .g64 holds the disk's raw
9
+ # GCR, which only a true drive reads, so it plugs one in when there's
10
+ # none yet. The 1541 reads only .d64 and .g64 images, and the 1571
11
+ # .d71 and .g71 as well, so the traps' other disk media, a .d81 or
12
+ # .t64 or a host directory, raise Error.
12
13
  module TrueDrive
13
14
  class Error < ArgumentError; end
14
15
 
15
16
  class << self
16
- # Plugs a Drive1541 in as device 8, unless one is there already, and
17
- # returns it.
17
+ # Plugs the machine's true drive in as device 8, unless one is there
18
+ # already, and returns it.
18
19
  def plug(computer)
19
- drive(computer) || Drive1541.new.tap { |drive| computer.attach_drive1541(drive) }
20
+ drive(computer) || computer.plug_true_drive
20
21
  end
21
22
 
22
- # The Drive1541 on device 8, or nil.
23
+ # The true drive on device 8, or nil.
23
24
  def drive(computer)
24
- drive = computer.drive1541
25
+ drive = computer.true_drive
25
26
  drive if drive && drive.device == KernalTrap::Routine::DEVICE
26
27
  end
27
28
 
28
- # Whether the path goes in a true drive: a .g64 always, and other
29
- # disk media, which the traps would mount, when a true drive is
30
- # device 8.
29
+ # Whether the path goes in a true drive: a .g64 or .g71 always, and
30
+ # other disk media, which the traps would mount, when a true drive
31
+ # is device 8.
31
32
  def takes?(computer, path)
32
- return true if File.extname(path).casecmp?(".g64")
33
+ return true if %w[.g64 .g71].include?(File.extname(path).downcase)
33
34
 
34
35
  !drive(computer).nil? && (File.directory?(path) || MOUNT_TYPES.key?(File.extname(path).downcase))
35
36
  end
36
37
 
37
- # Puts the .d64 or .g64 in the drive on device 8, plugging one in
38
- # when there's none, and takes out what was mounted through the
39
- # traps, so LOAD and SAVE reach the drive too. `read_only` puts it
40
- # in write-protected.
38
+ # Puts the image in the drive on device 8, plugging one in when
39
+ # there's none, and takes out what was mounted through the traps,
40
+ # so LOAD and SAVE reach the drive too. `read_only` puts it in
41
+ # write-protected.
41
42
  def insert(computer, path, read_only: false)
42
- unless %w[.d64 .g64].include?(File.extname(path).downcase)
43
- raise Error, "#{path} is not a .d64 or .g64 image, which the 1541 reads"
43
+ name = drive(computer)&.model_name || (computer.family == :c128 ? "1571" : "1541")
44
+ types = name == "1571" ? %w[.d64 .g64 .d71 .g71] : %w[.d64 .g64]
45
+ unless types.include?(File.extname(path).downcase)
46
+ raise Error, "#{path} is not a #{types[0..-2].join(', ')} or #{types.last} image, which the #{name} reads"
44
47
  end
45
48
 
46
49
  disk = Drive1541::Disk.open(path, read_only:)
47
50
  computer.unmount
48
51
  plug(computer).insert(disk)
49
- "Inserted #{path} in the 1541 as device 8"
52
+ "Inserted #{path} in the #{name} as device 8"
50
53
  end
51
54
  end
52
55
  end
data/lib/badline/media.rb CHANGED
@@ -33,11 +33,11 @@ module Badline
33
33
  # autostart then loads through it.
34
34
  #
35
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).
36
+ # (DiskList). The VIC-20 takes its own media (Vic20Media), and the
37
+ # C128 in C64 mode a C64's.
38
38
  def attach(computer, path, autostart: true, subtune: nil, **)
39
39
  path = DiskList.disk(path)
40
- return Vic20Media.attach(computer, path, autostart:, **) unless computer.is_a?(Computer)
40
+ return Vic20Media.attach(computer, path, autostart:, **) if computer.family == :vic20
41
41
 
42
42
  attach_c64(computer, path, autostart:, subtune:, **)
43
43
  end
@@ -160,13 +160,15 @@ module Badline
160
160
  def start_prg(computer, data, autostart:)
161
161
  load_addr = computer.load_prg(data)
162
162
  # Run only makes sense for programs at BASIC start, or a byte ahead
163
- # of it, where BASIC keeps the zero before its first line.
164
- return unless autostart && (load_addr == BASIC_START || load_addr == BASIC_START - 1)
163
+ # of it, where BASIC keeps the zero before its first line. BASIC 7.0
164
+ # starts at $1C01 and keeps the end of its text at $1210.
165
+ start, text_end = computer.family == :c128 && computer.mode == :c128 ? [0x1c01, 0x1210] : [BASIC_START, 0x2d]
166
+ return unless autostart && (load_addr == start || load_addr == start - 1)
165
167
 
166
168
  # The end of the program, where BASIC's variables start, and where
167
169
  # the KERNAL's LOAD leaves its end address.
168
170
  end_addr = load_addr + data.length - 2
169
- [0x2d, 0xae].each { |pointer| computer.ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
171
+ [text_end, 0xae].each { |pointer| computer.ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
170
172
  computer.type_text("run\r")
171
173
  end
172
174
  end
@@ -10,7 +10,7 @@ module Badline
10
10
  # left out.
11
11
  ACTIONS = {
12
12
  "key" => true, "type" => true, "insert" => true, "eject" => true, "screenshot" => true, "menu" => true,
13
- "reset" => false, "freeze" => false, "resume" => false, "quit" => false
13
+ "display" => true, "reset" => false, "freeze" => false, "resume" => false, "quit" => false
14
14
  }.freeze
15
15
 
16
16
  # The frames a key or the freeze button is held for.
@@ -18,6 +18,9 @@ module Badline
18
18
 
19
19
  EJECTABLE = %w[disk tape cartridge].freeze
20
20
 
21
+ # The C128's screens display shows: the VIC-IIe's or the VDC's.
22
+ DISPLAYS = %w[vic vdc].freeze
23
+
21
24
  # What insert takes: disk images, tapes and cartridges, or an .m3u
22
25
  # or .vfl list of disks or a directory to mount as device 8.
23
26
  INSERTABLE = %w[.d64 .d71 .d81 .g64 .t64 .tap .crt].freeze
@@ -83,6 +86,7 @@ module Badline
83
86
  return true if action == "menu"
84
87
  return argument.empty? unless ACTIONS[action]
85
88
  return EJECTABLE.include?(argument) if action == "eject"
89
+ return DISPLAYS.include?(argument) if action == "display"
86
90
  return insertable?(argument) if action == "insert"
87
91
 
88
92
  !argument.empty?
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Options
5
+ # The machine family: the subcommand that names it, or else the media,
6
+ # and the models of each.
7
+ module Family
8
+ # Where C128 BASIC starts, which a program for the C128 loads at.
9
+ C128_BASIC_START = 0x1c01
10
+
11
+ private
12
+
13
+ # Takes `sid`, `c64`, `vic20` or `c128` when given first.
14
+ def subcommand(args)
15
+ case args.first
16
+ when "sid" then @sid_command = true
17
+ when "c64" then @family = :c64
18
+ when "vic20" then @family = :vic20
19
+ when "c128" then @family = :c128
20
+ else return
21
+ end
22
+ @family_named = !@sid_command
23
+ args.shift
24
+ end
25
+
26
+ def family_models
27
+ case @family
28
+ when :vic20 then VIC20_MODELS
29
+ when :c128 then C128_MODELS
30
+ else MODELS
31
+ end
32
+ end
33
+
34
+ # Without a family named, a program that loads at C128_BASIC_START is
35
+ # the C128's.
36
+ def pick_family
37
+ return if @family_named || @sid_command || @media_path.nil?
38
+ return unless %w[.prg .p00].include?(File.extname(@media_path).downcase) && File.file?(@media_path)
39
+
40
+ @family = :c128 if load_address(@media_path) == C128_BASIC_START
41
+ end
42
+
43
+ # The load address of a .prg, or of the program a .p00 wraps after its
44
+ # 26-byte header.
45
+ def load_address(path)
46
+ offset = File.extname(path).casecmp?(".p00") ? 26 : 0
47
+ bytes = File.binread(path).bytes
48
+ bytes.size >= offset + 2 ? bytes[offset] | (bytes[offset + 1] << 8) : -1
49
+ end
50
+ end
51
+ end
52
+ end
@@ -39,6 +39,7 @@ module Badline
39
39
  <<~BANNER
40
40
  Usage: #{@program} [c64] [options] [media]
41
41
  #{@program} vic20 [options] [media]
42
+ #{@program} c128 [options] [media]
42
43
  #{@program} --headless [options] tune.sid
43
44
  #{@program} [options] tune.sid --audio-out FILE
44
45
  #{@program} sid [options] FILE|DIR...
@@ -46,6 +47,7 @@ module Badline
46
47
  Commands:
47
48
  c64 Run a C64, the one --model names (the default)
48
49
  vic20 Run a PAL VIC-20, with the RAM --ram names
50
+ c128 Run a C128 in C128 mode, the one --model names
49
51
  sid Play .sid tunes and directories of them
50
52
 
51
53
  Media can be a .prg/.p00 program, a .d64/.d71/.d81 disk image, a
@@ -58,7 +60,8 @@ module Badline
58
60
  window also autosaves every two minutes of play, which only the
59
61
  pause menu loads. Device 8 answers through traps on the KERNAL's
60
62
  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
63
+ which runs its own DOS and reads .d64 and .g64 images only, or on
64
+ the C128 the C128D's 1571, which reads .d71 and .g71 too. A .sid
62
65
  tune opens in the SID player, as `sid` plays it, unless an option
63
66
  of the emulator's window asks for the machine.
64
67
 
@@ -89,7 +92,9 @@ module Badline
89
92
  the frame as a .bmp, with the frame's number in place of %d. menu
90
93
  opens the pause menu, and menu=PAGE opens it on snapshots, drive,
91
94
  datasette, expansion, ports, sound or power. Its frames still count,
92
- and resume closes it. reset, freeze and quit take no argument.
95
+ and resume closes it. display=vdc or display=vic shows the C128's
96
+ 80 or 40 column screen, as F8 switches them. reset, freeze and quit
97
+ take no argument.
93
98
  BANNER
94
99
  end
95
100
 
@@ -11,6 +11,9 @@ module Badline
11
11
  # emulated.
12
12
  VIC20_MODELS = %w[pal ntsc].freeze
13
13
 
14
+ # The C128's models, as C128::Model names them.
15
+ C128_MODELS = %w[c128 c128ntsc c128dcr c128dcrntsc].freeze
16
+
14
17
  # The VIC-20 RAM expansions --ram takes, in the order the help lists
15
18
  # them.
16
19
  RAM_NAMES = %w[unexpanded 3k 8k 16k 24k 32k all].freeze
@@ -32,13 +35,16 @@ module Badline
32
35
  Option.new("--sound", "Play the SID through the host's audio device (F10 mutes)",
33
36
  section: "Window options", needs: :window),
34
37
  Option.new("--no-sound", "Don't play the SID", section: "Window options", needs: :window, build: :native),
35
- Option.new("--true-drive", "Put a true 1541 on device 8 instead of the KERNAL traps",
38
+ Option.new("--true-drive", "Put a true 1541 on device 8 instead of the KERNAL traps, a 1571 on the C128",
36
39
  section: "Window options", needs: :window),
37
40
  Option.new("--reu SIZE", "Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384",
38
41
  section: "Window options", needs: :window),
39
- Option.new("--model NAME", "C64 to run: #{MODELS.join(', ')} (default: #{MODELS.first}); VIC-20: pal",
42
+ Option.new("--model NAME", "C64 to run: #{MODELS.join(', ')} (default: #{MODELS.first}); VIC-20: pal; " \
43
+ "C128: #{C128_MODELS.join(', ')}",
40
44
  section: "Window options", needs: :window),
41
45
  Option.new("--ntsc", "Run an NTSC C64, as --model ntsc does", section: "Window options", needs: :window),
46
+ Option.new("--c64", "Start the C128 in C64 mode, holding C= at power-on",
47
+ section: "Window options", needs: :window),
42
48
  Option.new("--ram SIZE", "VIC-20 RAM expansion: #{RAM_NAMES.join(', ')} (default: unexpanded)",
43
49
  section: "Window options", needs: :window),
44
50
  Option.new("--no-vsync", "Pace frames by the timer or the sound instead of the display",
@@ -64,6 +64,19 @@ module Badline
64
64
  elsif !@ram.nil?
65
65
  raise Error, "--ram needs vic20"
66
66
  end
67
+ if @family == :c128
68
+ validate_c128
69
+ elsif @c64_mode
70
+ raise Error, "--c64 needs c128"
71
+ end
72
+ end
73
+
74
+ # The C128 runs in the window, without an REU, and its NTSC models
75
+ # have names of their own.
76
+ def validate_c128
77
+ raise Error, "--ntsc needs the C64; the NTSC C128 is --model c128ntsc" if @models.include?("ntsc")
78
+ raise Error, "--reu needs the C64" unless @reu.nil?
79
+ raise Error, "the C128 needs the window" if headless?
67
80
  end
68
81
 
69
82
  # The VIC-20 is a PAL one, in the window, without the C64's SID and
@@ -87,7 +100,7 @@ module Badline
87
100
  # --model and --ntsc may be given more than once, but only for one
88
101
  # model.
89
102
  def validate_models
90
- names = @family == :vic20 ? VIC20_MODELS : MODELS
103
+ names = family_models
91
104
  unknown = @models.find { |name| !names.include?(name) }
92
105
  raise Error, "invalid argument: --model #{unknown}" unless unknown.nil?
93
106
  raise Error, "conflicting models: #{@models.uniq.join(' and ')}" unless @models.uniq.size <= 1
@@ -5,16 +5,17 @@ require "badline/options/event"
5
5
  require "badline/options/table"
6
6
  require "badline/options/help"
7
7
  require "badline/options/validation"
8
+ require "badline/options/family"
8
9
 
9
10
  module Badline
10
11
  # The command line of both builds, badline-ruby and the native badline.
11
12
  # Either opens the window for any media, or with --headless or
12
13
  # --audio-out plays or renders a .sid tune without one. `sid`, given
13
14
  # 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
16
- # native build adds --no-sound and --version, and badline-ruby adds
17
- # --disable-jit.
15
+ # `c64`, `vic20` or `c128`, given first, names the machine family, and
16
+ # without one the family is the C64, or the C128 for a program that loads
17
+ # where C128 BASIC starts. The native build adds --no-sound and
18
+ # --version, and badline-ruby adds --disable-jit.
18
19
  #
19
20
  # TABLE lists the options, and both the parser and the help read it. It
20
21
  # parses by hand, inside the subset of Ruby Spinel compiles. Values come
@@ -23,6 +24,7 @@ module Badline
23
24
  # arguments before paying for the emulator.
24
25
  class Options
25
26
  include Validation
27
+ include Family
26
28
 
27
29
  class Error < StandardError; end
28
30
 
@@ -55,7 +57,8 @@ module Badline
55
57
  attr_reader :program, :media_path, :tune_paths, :subtune, :sid_model, :reu, :frames, :screenshot, :save_snapshot,
56
58
  :audio_out, :seconds, :songlengths, :filter_chunk, :timeline
57
59
 
58
- # The machine family to build (Badline::Machine), :c64 or :vic20.
60
+ # The machine family to build (Badline::Machine), :c64, :vic20 or
61
+ # :c128.
59
62
  attr_reader :family
60
63
 
61
64
  # The VIC-20's RAM expansion --ram names, a key of
@@ -72,6 +75,8 @@ module Badline
72
75
  @program = native ? "badline" : "badline-ruby"
73
76
  @media_path = nil
74
77
  @family = :c64
78
+ @family_named = false
79
+ @c64_mode = false
75
80
  @sid_command = false
76
81
  @tune_paths = []
77
82
  @subtune = nil
@@ -110,6 +115,7 @@ module Badline
110
115
  args = argv.dup
111
116
  subcommand(args)
112
117
  argument(args.shift, args) until args.empty?
118
+ pick_family
113
119
  play_lone_tune
114
120
  validate unless help? || version?
115
121
  self
@@ -167,9 +173,14 @@ module Badline
167
173
 
168
174
  def jit? = @jit
169
175
 
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)
176
+ # The name of the model to build: a C64 Badline::Model names, a VIC-20
177
+ # of VIC20_MODELS or a C128 of C128_MODELS. --ntsc is short for --model
178
+ # ntsc.
179
+ def model = @models.first || family_models.first
180
+
181
+ # Whether --c64 asks the C128 to start in C64 mode, holding C= at
182
+ # power-on.
183
+ def c64_mode? = @c64_mode
173
184
 
174
185
  def help? = @help
175
186
 
@@ -183,17 +194,6 @@ module Badline
183
194
  [:both, @native ? :native : :ruby].include?(option.build)
184
195
  end
185
196
 
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
194
- args.shift
195
- end
196
-
197
197
  # A .sid on its own plays in the SID player, as `sid` would, unless an
198
198
  # option of the emulator's window asks for the machine.
199
199
  def play_lone_tune
@@ -273,6 +273,7 @@ module Badline
273
273
  when "--verbose" then @verbose = true
274
274
  when "--true-drive" then @true_drive = true
275
275
  when "--ntsc" then @models << "ntsc"
276
+ when "--c64" then @c64_mode = true
276
277
  when "--unpaced" then @paced = false
277
278
  when "--disable-jit" then @jit = false
278
279
  when "--help" then @help = true
@@ -61,6 +61,15 @@ https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1541/
61
61
  (1541-c000.325302-01.bin and 1541-e000.901229-05.bin) and concatenated.
62
62
  SHA-1 ab16f56989b27d89babe5f89c5a8cb3da71a82f0.
63
63
 
64
+ dos1571.rom
65
+ -----------
66
+ The 1571 disk drive's DOS 3.0, 310654-05, 32 KB for $8000-$FFFF.
67
+ Drive1571 loads it.
68
+
69
+ Downloaded from
70
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1571/1571-rom.310654-05.bin
71
+ SHA-1 f1be619c106641a685f6609e4d43d6fc9eac1e70.
72
+
64
73
  Commodore VIC-20 ROMs
65
74
  =====================
66
75
 
@@ -87,3 +96,30 @@ vic20/character.rom
87
96
  -------------------
88
97
  The character generator ROM, 901460-03, for $8000-$8FFF.
89
98
  SHA-1 4fd85ab6647ee2ac7ba40f729323f2472d35b9b4.
99
+
100
+ Commodore 128 ROMs
101
+ ==================
102
+
103
+ Downloaded from
104
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/computers/c128/
105
+ C128::Bus maps them in C128 mode. C64 mode runs basic.rom, kernal.rom
106
+ and character.rom above.
107
+
108
+ c128/basic.rom
109
+ --------------
110
+ BASIC 7.0, 318022-02 (basic.318022-02.bin), 32 KB: the low half for
111
+ $4000-$7FFF followed by the high half for $8000-$BFFF.
112
+ SHA-1 953dcdf5784a6b39ef84dd6fd968c7a03d8d6816.
113
+
114
+ c128/kernal.rom
115
+ ---------------
116
+ 318020-05 (kernal.318020-05.bin), 16 KB: the screen editor for
117
+ $C000-$CFFF, the Z80 BIOS for $D000-$DFFF and the KERNAL for
118
+ $E000-$FFFF.
119
+ SHA-1 ceb6e1a1bf7e08eb9cbc651afa29e26adccf38ab.
120
+
121
+ c128/character.rom
122
+ ------------------
123
+ The character generator ROM, 390059-01 (characters.390059-01.bin),
124
+ 8 KB: the C64's set, the same as character.rom, followed by the C128's.
125
+ SHA-1 29ed066d513f2d5c09ff26d9166ba23c2afb2b3f.
Binary file
Binary file
Binary file
Binary file
@@ -28,6 +28,8 @@ module Badline
28
28
  MACHINE = "C64SC"
29
29
  # xvic names its machine VIC20.
30
30
  VIC20 = "VIC20"
31
+ # x128 names its machine C128.
32
+ C128 = "C128"
31
33
  GZIP_MAGIC = "\x1f\x8b".b
32
34
 
33
35
  # The VICE version block, when VICE wrote the file: four version
@@ -29,6 +29,7 @@ module Badline
29
29
  # warns it without a block.
30
30
  def load(&)
31
31
  return load_vic20(&) if vic20?
32
+ return load_c128(&) if c128?
32
33
 
33
34
  computer = badline? ? opening { Computer.restored(state) } : Vice.setup(@container).build
34
35
  tell(badline? ? state_report(computer) : Vice::Restore.apply(@container, computer), &)
@@ -38,6 +39,9 @@ module Badline
38
39
  # Whether the machine is a VIC-20, as the container names it.
39
40
  def vic20? = @container.machine == Container::VIC20
40
41
 
42
+ # Whether the machine is a C128, as the container names it.
43
+ def c128? = @container.machine == Container::C128
44
+
41
45
  # Restores `computer` from the snapshot, and returns the Report. A
42
46
  # snapshot that fails leaves the machine as it was.
43
47
  def restore(computer, &)
@@ -60,6 +64,16 @@ module Badline
60
64
  machine
61
65
  end
62
66
 
67
+ # A C128 restores from its BADLINE module alone. x128's own modules
68
+ # aren't read.
69
+ def load_c128(&)
70
+ raise FormatError, "badline reads only its own C128 snapshots, not x128's" unless badline?
71
+
72
+ machine = C128.restored(state)
73
+ tell(state_report(machine), &)
74
+ machine
75
+ end
76
+
63
77
  def tell(report)
64
78
  report.ignored.each { |line| block_given? ? yield(line) : warn("badline: #{line}") }
65
79
  end
@@ -27,11 +27,12 @@ module Badline
27
27
  path
28
28
  end
29
29
 
30
- # A VIC-20's snapshot holds the BADLINE module alone, in a container
31
- # named as xvic names its machine.
30
+ # A VIC-20's or a C128's snapshot holds the BADLINE module alone, in a
31
+ # container named as xvic or x128 names its machine.
32
32
  def dump(computer)
33
33
  state = computer.snapshot
34
34
  return Container.new([MachineState.section(state)], machine: Container::VIC20).to_s if computer.family == :vic20
35
+ return Container.new([MachineState.section(state)], machine: Container::C128).to_s if computer.family == :c128
35
36
 
36
37
  Container.new(Vice.export(state) + [MachineState.section(state)]).to_s
37
38
  end
@@ -59,4 +60,10 @@ module Badline
59
60
 
60
61
  def restore_snapshot(path, &) = Snapshot.restore(self, path, &)
61
62
  end
63
+
64
+ class C128
65
+ def save_snapshot(path) = Snapshot.save(self, path)
66
+
67
+ def restore_snapshot(path, &) = Snapshot.restore(self, path, &)
68
+ end
62
69
  end
@@ -17,6 +17,9 @@ module Badline
17
17
 
18
18
  def storage_kind = D64
19
19
 
20
+ # The sides of a disk the image's tracks fill.
21
+ def sides = 1
22
+
20
23
  def sectors_in(track)
21
24
  case track
22
25
  when 1..17 then 21
@@ -7,6 +7,15 @@ module Badline
7
7
 
8
8
  def storage_kind = D71
9
9
 
10
+ # Tracks 1-35 on the first side and 36-70 on the second.
11
+ def track_count = 70
12
+
13
+ def sides = 2
14
+
15
+ def sectors_in(track)
16
+ track > 35 ? super(track - 35) : super
17
+ end
18
+
10
19
  private
11
20
 
12
21
  def error_tables = ERROR_TABLES
@@ -22,10 +31,6 @@ module Badline
22
31
  def bam_bitmap(track) = track > 35 ? [53, 0, 3 * (track - 36)] : super
23
32
 
24
33
  def double_sided? = @bytes[sector_offset(18, 0) + 3].anybits?(0x80)
25
-
26
- def sectors_in(track)
27
- track > 35 ? super(track - 35) : super
28
- end
29
34
  end
30
35
  end
31
36
  end
@@ -5,7 +5,9 @@ module Badline
5
5
  # A .g64 image: a 1541 disk's flux as raw GCR, track by track, for the
6
6
  # true drive to read the way it reads a real disk, copy protection and
7
7
  # all. It has no file system of its own, so only the true drive
8
- # (Drive1541::Disk.from_g64) can use it.
8
+ # (Drive1541::Disk.from_g64) can use it. A .g71 holds a 1571 disk the
9
+ # same way, with "GCR-1571" for its signature and 168 half tracks,
10
+ # the first side's 84 and then the second's.
9
11
  #
10
12
  # The header is the signature "GCR-1541", a version byte ($00), the
11
13
  # number of half tracks the tables cover (84, tracks 1 to 42.5) and
@@ -36,6 +38,7 @@ module Badline
36
38
  class FormatError < StandardError; end
37
39
 
38
40
  SIGNATURE = "GCR-1541"
41
+ SIGNATURE_1571 = "GCR-1571"
39
42
  HEADER_SIZE = 12
40
43
  HALF_TRACKS = 84
41
44
  MAX_TRACK_SIZE = 7928
@@ -124,7 +127,9 @@ module Badline
124
127
  private
125
128
 
126
129
  def parse
127
- raise FormatError, "Missing G64 signature" unless @data.start_with?(SIGNATURE)
130
+ unless @data.start_with?(SIGNATURE) || @data.start_with?(SIGNATURE_1571)
131
+ raise FormatError, "Missing G64 signature"
132
+ end
128
133
  raise FormatError, "G64 header is cut short" if @data.bytesize < HEADER_SIZE
129
134
 
130
135
  version, @half_tracks, @max_track_size = @data.byteslice(8, 4).unpack("CCv")
@@ -52,6 +52,15 @@ module Badline
52
52
  @latch = input.boolean? ? FIELDS.to_h { |field| [field, input.int] } : nil
53
53
  end
54
54
 
55
+ # Whether the clock is stopped with no alarm to raise, so its cycles
56
+ # move only the divider's phase.
57
+ def quiet? = @stopped && !@alarm_pending
58
+
59
+ # Runs +cycles+ quiet cycles at once.
60
+ def fast_forward(cycles)
61
+ @accumulator = (@accumulator + (cycles * @mains_hz)) % @cycles_per_pulse
62
+ end
63
+
55
64
  def cycle!
56
65
  @accumulator += @mains_hz
57
66
  pulse! if @accumulator >= @cycles_per_pulse
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.10.1"
4
+ VERSION = "0.11.1"
5
5
  end
@@ -3,14 +3,14 @@
3
3
  module Badline
4
4
  class VIA
5
5
  # Running a VIA a stretch of cycles at once while nothing but its
6
- # counters would change, for Drive1541::Idle.
6
+ # counters would change, for Drive::Idle.
7
7
  module FastForward
8
8
  # quiet_cycles for a VIA that will never set a flag of its own.
9
9
  QUIET = 1 << 40
10
10
 
11
11
  # How many cycles the VIA can run with nothing but its counters
12
12
  # changing: no flag set, no pulse on a control line and no bit
13
- # shifted. See Drive1541::Idle.
13
+ # shifted. See Drive::Idle.
14
14
  def quiet_cycles
15
15
  return 0 if @sr_uses_t2 || @shift_register.clocking? || @ca.pulsing? || @cb.pulsing?
16
16
 
@@ -26,7 +26,7 @@ module Badline
26
26
  @t2.fast_forward(cycles)
27
27
  end
28
28
 
29
- # The counters an orbit (see Drive1541::Idle) has to bring back: those
29
+ # The counters an orbit (see Drive::Idle) has to bring back: those
30
30
  # of the armed timers, and of the timers +touched+ names (bit 0 for
31
31
  # timer 1, bit 1 for timer 2). The others only count.
32
32
  def counter_state(touched)