badline 0.10.0 → 0.11.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/CLAUDE.md +5 -3
  4. data/README.md +60 -5
  5. data/doc/library.md +15 -1
  6. data/doc/pinned-behaviour.md +75 -0
  7. data/doc/snapshots.md +19 -3
  8. data/lib/badline/address_bus/fittings.rb +38 -0
  9. data/lib/badline/address_bus/pla.rb +38 -21
  10. data/lib/badline/address_bus/roms.rb +12 -5
  11. data/lib/badline/address_bus.rb +20 -2
  12. data/lib/badline/c128/banks.rb +35 -0
  13. data/lib/badline/c128/bus.rb +347 -0
  14. data/lib/badline/c128/bus_state.rb +53 -0
  15. data/lib/badline/c128/color_lines.rb +39 -0
  16. data/lib/badline/c128/cpu.rb +10 -0
  17. data/lib/badline/c128/keys.rb +35 -0
  18. data/lib/badline/c128/mmu.rb +182 -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 +120 -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 +320 -0
  29. data/lib/badline/chrout_trap.rb +3 -0
  30. data/lib/badline/computer/attachments.rb +4 -13
  31. data/lib/badline/computer/kernal_traps.rb +29 -0
  32. data/lib/badline/computer.rb +13 -8
  33. data/lib/badline/frontend/app.rb +34 -21
  34. data/lib/badline/frontend/boot.rb +31 -1
  35. data/lib/badline/frontend/controls.rb +11 -0
  36. data/lib/badline/frontend/keys.rb +28 -1
  37. data/lib/badline/frontend/machine_window.rb +16 -13
  38. data/lib/badline/frontend/menu_pages.rb +6 -1
  39. data/lib/badline/frontend/menu_ports.rb +1 -1
  40. data/lib/badline/frontend/pause_menu.rb +1 -0
  41. data/lib/badline/frontend/screen.rb +33 -8
  42. data/lib/badline/frontend/screens.rb +90 -0
  43. data/lib/badline/frontend/timeline.rb +15 -4
  44. data/lib/badline/frontend.rb +1 -0
  45. data/lib/badline/kernal_trap/file.rb +1 -1
  46. data/lib/badline/kernal_trap/layout.rb +58 -8
  47. data/lib/badline/kernal_trap/load.rb +1 -1
  48. data/lib/badline/kernal_trap/save.rb +1 -1
  49. data/lib/badline/kernal_trap/serial.rb +5 -0
  50. data/lib/badline/keyboard.rb +12 -1
  51. data/lib/badline/machine.rb +16 -7
  52. data/lib/badline/media.rb +8 -6
  53. data/lib/badline/model.rb +29 -12
  54. data/lib/badline/options/event.rb +5 -1
  55. data/lib/badline/options/family.rb +52 -0
  56. data/lib/badline/options/help.rb +5 -1
  57. data/lib/badline/options/table.rb +8 -2
  58. data/lib/badline/options/validation.rb +14 -1
  59. data/lib/badline/options.rb +20 -19
  60. data/lib/badline/roms/README +51 -0
  61. data/lib/badline/roms/c128/basic.rom +0 -0
  62. data/lib/badline/roms/c128/character.rom +0 -0
  63. data/lib/badline/roms/c128/kernal.rom +0 -0
  64. data/lib/badline/roms/c64gs.rom +0 -0
  65. data/lib/badline/roms/kernal-pet64.rom +0 -0
  66. data/lib/badline/snapshot/container.rb +2 -0
  67. data/lib/badline/snapshot/image.rb +14 -0
  68. data/lib/badline/snapshot/setup.rb +15 -10
  69. data/lib/badline/snapshot/state.rb +5 -4
  70. data/lib/badline/snapshot/vice.rb +2 -1
  71. data/lib/badline/snapshot.rb +9 -2
  72. data/lib/badline/version.rb +1 -1
  73. data/lib/badline/vic/bank.rb +2 -1
  74. data/lib/badline/vic/display_state.rb +12 -0
  75. data/lib/badline/vic/registers.rb +3 -0
  76. data/lib/badline/vic.rb +124 -9
  77. data/lib/badline.rb +1 -0
  78. metadata +27 -1
data/lib/badline/model.rb CHANGED
@@ -8,39 +8,56 @@ module Badline
8
8
  # a datasette. The names are x64sc's.
9
9
  module Model
10
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)
11
+ Profile = Data.define(:name, :vic_model, :cia_model, :sid_model, :region, :kernal, :datasette, :board)
12
12
 
13
13
  # The breadbin PAL C64: the 6569 VIC-II, 6526 CIAs and the 6581 SID.
14
14
  C64 = Profile.new(name: "c64", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
15
- region: Region::PAL, kernal: :c64, datasette: true)
15
+ region: Region::PAL, kernal: :c64, datasette: true, board: :c64)
16
16
 
17
17
  # The PAL C64C: the HMOS 8565 VIC-II, 6526A CIAs and the 8580 SID.
18
18
  C64C = Profile.new(name: "c64c", vic_model: :mos8565, cia_model: :mos6526a, sid_model: :mos8580,
19
- region: Region::PAL, kernal: :c64, datasette: true)
19
+ region: Region::PAL, kernal: :c64, datasette: true, board: :c64)
20
20
 
21
21
  # The NTSC C64 with the 6567R8 VIC-II.
22
22
  NTSC = Profile.new(name: "ntsc", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
23
- region: Region::NTSC, kernal: :c64, datasette: true)
23
+ region: Region::NTSC, kernal: :c64, datasette: true, board: :c64)
24
24
 
25
25
  # The NTSC C64C: the 8562, the 6567R8's HMOS successor, with the
26
26
  # C64C's CIAs and SID.
27
27
  NEW_NTSC = Profile.new(name: "newntsc", vic_model: :mos8565, cia_model: :mos6526a, sid_model: :mos8580,
28
- region: Region::NTSC, kernal: :c64, datasette: true)
28
+ region: Region::NTSC, kernal: :c64, datasette: true, board: :c64)
29
29
 
30
30
  # The first NTSC C64s, with the 6567R56A VIC-II.
31
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)
32
+ region: Region::NTSC_OLD, kernal: :c64, datasette: true, board: :c64)
33
33
 
34
34
  # The Drean C64 of Argentina, on PAL-N with the 6572 VIC-II.
35
35
  DREAN = Profile.new(name: "drean", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
36
- region: Region::DREAN, kernal: :c64, datasette: true)
36
+ region: Region::DREAN, kernal: :c64, datasette: true, board: :c64)
37
37
 
38
38
  # The portable SX-64: a PAL C64's chips with its own KERNAL, a 1541
39
39
  # built in as device 8 and no cassette port.
40
40
  SX64 = Profile.new(name: "sx64", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
41
- region: Region::PAL, kernal: :sx64, datasette: false)
41
+ region: Region::PAL, kernal: :sx64, datasette: false, board: :c64)
42
42
 
43
- ALL = [C64, C64C, NTSC, NEW_NTSC, OLD_NTSC, DREAN, SX64].freeze
43
+ # The PET 64, or Educator 64: a PAL C64's board in a PET's case, with
44
+ # its own KERNAL and a green monochrome monitor built in.
45
+ PET64 = Profile.new(name: "pet64", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
46
+ region: Region::PAL, kernal: :pet64, datasette: true, board: :pet64)
47
+
48
+ # The C64GS games console: a PAL C64C's board with its own BASIC and
49
+ # KERNAL, which show an animation asking for a cartridge when it has
50
+ # none, in a case with no keyboard and no cassette port.
51
+ C64GS = Profile.new(name: "c64gs", vic_model: :mos8565, cia_model: :mos6526a, sid_model: :mos8580,
52
+ region: Region::PAL, kernal: :gs, datasette: false, board: :gs)
53
+
54
+ # The MAX Machine, or Ultimax, of Japan: the 6566, an NTSC VIC-II
55
+ # for static RAM, which runs here as the first NTSC C64s' 6567R56A,
56
+ # one CIA, 2K of RAM and no ROMs, so it runs only a cartridge's.
57
+ ULTIMAX = Profile.new(name: "ultimax", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
58
+ region: Region::NTSC_OLD, kernal: :c64, datasette: true, board: :max)
59
+
60
+ ALL = [C64, C64C, NTSC, NEW_NTSC, OLD_NTSC, DREAN, SX64, PET64, C64GS, ULTIMAX].freeze
44
61
 
45
62
  def self.named(name)
46
63
  model = ALL.find { |candidate| candidate.name == name }
@@ -49,13 +66,13 @@ module Badline
49
66
  model
50
67
  end
51
68
 
52
- # The model whose chips, region, KERNAL and datasette `machine` has, or
53
- # nil for none.
69
+ # The model whose chips, region, KERNAL, datasette and board `machine`
70
+ # has, or nil for none.
54
71
  def self.of(machine)
55
72
  ALL.find do |model|
56
73
  model.vic_model == machine.vic_model && model.cia_model == machine.cia_model &&
57
74
  model.sid_model == machine.sid_model && model.region == machine.region &&
58
- model.kernal == machine.kernal && model.datasette == machine.datasette
75
+ model.kernal == machine.kernal && model.datasette == machine.datasette && model.board == machine.board
59
76
  end
60
77
  end
61
78
  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
@@ -89,7 +91,9 @@ module Badline
89
91
  the frame as a .bmp, with the frame's number in place of %d. menu
90
92
  opens the pause menu, and menu=PAGE opens it on snapshots, drive,
91
93
  datasette, expansion, ports, sound or power. Its frames still count,
92
- and resume closes it. reset, freeze and quit take no argument.
94
+ and resume closes it. display=vdc or display=vic shows the C128's
95
+ 80 or 40 column screen, as F8 switches them. reset, freeze and quit
96
+ take no argument.
93
97
  BANNER
94
98
  end
95
99
 
@@ -5,12 +5,15 @@ module Badline
5
5
  DEFAULT_RATE = 44_100
6
6
 
7
7
  # The names Badline::Model builds, in the order the help lists them.
8
- MODELS = %w[c64 c64c ntsc newntsc oldntsc drean sx64].freeze
8
+ MODELS = %w[c64 c64c ntsc newntsc oldntsc drean sx64 pet64 c64gs ultimax].freeze
9
9
 
10
10
  # The VIC-20's models, as xvic names its video standards. Only PAL is
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
@@ -36,9 +39,12 @@ module Badline
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
@@ -27,6 +27,30 @@ Downloaded from
27
27
  https://www.zimmers.net/anonftp/pub/cbm/firmware/computers/c64/kernal.sx.251104-04.bin
28
28
  SHA-1 aa136e91ecf3c5ac64f696b3dbcbfc5ba0871c98.
29
29
 
30
+ kernal-pet64.rom
31
+ ----------------
32
+ The KERNAL of the PET 64, 4064 and Educator 64, 901246-01. It boots
33
+ with the "**** COMMODORE 4064 BASIC V2.0 ****" banner and a white
34
+ cursor, and its interrupt routine keeps the border, background and
35
+ sprite colours black unless bit 7 of $0286 is set. Computer.new(kernal:
36
+ :pet64) fits it, as --model pet64 does.
37
+
38
+ Downloaded from
39
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/computers/c64/kernal.4064.901246-01.bin
40
+ MD5 da92801e3a03b005b746a4dd0b639c7c.
41
+
42
+ c64gs.rom
43
+ ---------
44
+ The C64GS's BASIC and KERNAL, 390852-01, 16 KB: BASIC for $A000-$BFFF
45
+ followed by the KERNAL for $E000-$FFFF. Its BASIC is 901226-01's but
46
+ for 30 unused bytes. Its KERNAL is 901227-03's with most changes in
47
+ $F72C-$FC92, the cassette routines' place, and many jumps redirected to
48
+ I/O error 5. Computer.new(kernal: :gs) fits both, as --model c64gs does.
49
+
50
+ Downloaded from
51
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/computers/c64/64gs.390852-01.bin
52
+ MD5 049e657afafbdd802e8c6a4f88f3d730.
53
+
30
54
  dos1541.rom
31
55
  -----------
32
56
  The 1541 disk drive's DOS 2.6, 16 KB for $C000-$FFFF: 325302-01
@@ -63,3 +87,30 @@ vic20/character.rom
63
87
  -------------------
64
88
  The character generator ROM, 901460-03, for $8000-$8FFF.
65
89
  SHA-1 4fd85ab6647ee2ac7ba40f729323f2472d35b9b4.
90
+
91
+ Commodore 128 ROMs
92
+ ==================
93
+
94
+ Downloaded from
95
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/computers/c128/
96
+ C128::Bus maps them in C128 mode. C64 mode runs basic.rom, kernal.rom
97
+ and character.rom above.
98
+
99
+ c128/basic.rom
100
+ --------------
101
+ BASIC 7.0, 318022-02 (basic.318022-02.bin), 32 KB: the low half for
102
+ $4000-$7FFF followed by the high half for $8000-$BFFF.
103
+ SHA-1 953dcdf5784a6b39ef84dd6fd968c7a03d8d6816.
104
+
105
+ c128/kernal.rom
106
+ ---------------
107
+ 318020-05 (kernal.318020-05.bin), 16 KB: the screen editor for
108
+ $C000-$CFFF, the Z80 BIOS for $D000-$DFFF and the KERNAL for
109
+ $E000-$FFFF.
110
+ SHA-1 ceb6e1a1bf7e08eb9cbc651afa29e26adccf38ab.
111
+
112
+ c128/character.rom
113
+ ------------------
114
+ The character generator ROM, 390059-01 (characters.390059-01.bin),
115
+ 8 KB: the C64's set, the same as character.rom, followed by the C128's.
116
+ SHA-1 29ed066d513f2d5c09ff26d9166ba23c2afb2b3f.
Binary file
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
@@ -3,32 +3,36 @@
3
3
  module Badline
4
4
  module Snapshot
5
5
  # How a machine was built: its VIC, CIA and SID models, its region, its
6
- # RAM expansion, the size of its REU, its KERNAL and whether it has a
7
- # datasette, the arguments Computer.new takes. A State starts with them, so a machine to restore it into can
8
- # be built first.
9
- Setup = Data.define(:vic_model, :cia_model, :sid_model, :region, :ram_expansion, :reu, :kernal, :datasette)
6
+ # RAM expansion, the size of its REU, its KERNAL, whether it has a
7
+ # datasette and its board, the arguments Computer.new takes. A State
8
+ # starts with them, so a machine to restore it into can be built first.
9
+ Setup = Data.define(:vic_model, :cia_model, :sid_model, :region, :ram_expansion, :reu, :kernal, :datasette,
10
+ :board)
10
11
 
11
12
  class Setup
12
13
  SID_MODELS = %i[mos6581 mos8580].freeze
13
14
  REGIONS = [Region::PAL, Region::NTSC, Region::NTSC_OLD, Region::DREAN].freeze
14
15
  RAM_EXPANSIONS = [:none, *RAMExpansion::TYPES.keys].freeze
15
16
  KERNALS = AddressBus::ROMs::KERNALS.keys.freeze
17
+ BOARDS = AddressBus::Fittings::BOARDS
16
18
 
17
19
  def self.of(address_bus)
18
20
  new(vic_model: address_bus.vic.model, cia_model: address_bus.cia1.model, sid_model: address_bus.sid.model,
19
21
  region: address_bus.region, ram_expansion: address_bus.ram_expansion.type, reu: address_bus.reu&.size_kb,
20
- kernal: address_bus.kernal, datasette: address_bus.datasette.connected?)
22
+ kernal: address_bus.kernal, datasette: address_bus.datasette.connected?, board: address_bus.board)
21
23
  end
22
24
 
23
- # Layout 3 has no KERNAL or datasette, and stands for the C64's.
25
+ # Layout 3 has no KERNAL or datasette, and stands for the C64's, and
26
+ # layouts before 6 have no board, and stand for the C64's.
24
27
  def self.read(input)
25
28
  new(vic_model: VIC::MODELS.fetch(input.int), cia_model: CIA::MODELS.fetch(input.int),
26
29
  sid_model: SID_MODELS.fetch(input.int), region: REGIONS.fetch(input.int),
27
30
  ram_expansion: ram_expansion(RAM_EXPANSIONS.fetch(input.int)), reu: reu(input.int),
28
31
  kernal: input.schema > 3 ? KERNALS.fetch(input.int) : :c64,
29
- datasette: input.schema > 3 ? input.boolean? : true)
32
+ datasette: input.schema > 3 ? input.boolean? : true,
33
+ board: input.schema > 5 ? BOARDS.fetch(input.int) : :c64)
30
34
  rescue IndexError
31
- raise FormatError, "the state names a chip model, region, RAM expansion or KERNAL badline doesn't know"
35
+ raise FormatError, "the state names a chip model, region, RAM expansion, KERNAL or board badline doesn't know"
32
36
  end
33
37
 
34
38
  def self.ram_expansion(name) = name == :none ? nil : name
@@ -45,12 +49,12 @@ module Badline
45
49
  out.int(VIC::MODELS.index(vic_model)).int(CIA::MODELS.index(cia_model))
46
50
  out.int(SID_MODELS.index(sid_model)).int(REGIONS.index(region))
47
51
  out.int(RAM_EXPANSIONS.index(ram_expansion || :none)).int(reu || 0)
48
- out.int(KERNALS.index(kernal)).boolean(datasette)
52
+ out.int(KERNALS.index(kernal)).boolean(datasette).int(BOARDS.index(board))
49
53
  end
50
54
 
51
55
  # A machine built this way, at power-on.
52
56
  def build
53
- Computer.new(vic_model:, cia_model:, sid_model:, region:, ram_expansion:, reu:, kernal:, datasette:)
57
+ Computer.new(vic_model:, cia_model:, sid_model:, region:, ram_expansion:, reu:, kernal:, datasette:, board:)
54
58
  end
55
59
 
56
60
  # The chips, led by the model's name when they make one of
@@ -63,6 +67,7 @@ module Badline
63
67
  parts << "a #{reu}K REU" if reu
64
68
  parts << "the #{kernal} KERNAL" unless kernal == :c64
65
69
  parts << "no datasette" unless datasette
70
+ parts << "the #{board} board" unless board == :c64
66
71
  parts.join(", ")
67
72
  end
68
73
  end
@@ -14,10 +14,11 @@ module Badline
14
14
  # The layout the chips write their fields in. It goes up whenever a
15
15
  # save_state writes something else, so a State in another layout
16
16
  # fails before anything is read into a machine. Layout 3, without the
17
- # KERNAL and datasette in the setup, and layout 4, without the VIA
18
- # shift register's clock delay, still read.
19
- SCHEMA = 5
20
- READABLE = [3, 4, SCHEMA].freeze
17
+ # KERNAL and datasette in the setup, layout 4, without the VIA shift
18
+ # register's clock delay, and layout 5, without the board in the
19
+ # setup, still read.
20
+ SCHEMA = 6
21
+ READABLE = [3, 4, 5, SCHEMA].freeze
21
22
 
22
23
  attr_reader :values, :strings
23
24
 
@@ -69,7 +69,8 @@ module Badline
69
69
  reu = nil unless reu && REU1764.reads?(reu)
70
70
  Setup.new(vic_model: model, cia_model: model == :mos8565 ? :mos6526a : :mos6526, sid_model: sid,
71
71
  region: vic ? VICII.region(vic) : Region::PAL, ram_expansion: nil,
72
- reu: reu ? REU1764.size_kb(reu) : nil, kernal: :c64, datasette: true)
72
+ reu: reu ? REU1764.size_kb(reu) : nil, kernal: :c64, datasette: true,
73
+ board: :c64)
73
74
  end
74
75
  end
75
76
  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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.10.0"
4
+ VERSION = "0.11.0"
5
5
  end
@@ -110,7 +110,8 @@ module Badline
110
110
  private
111
111
 
112
112
  # In Ultimax mode the cartridge ROMH replaces the character ROM
113
- # shadow, visible at $3000-$3FFF of the window.
113
+ # shadow, visible at $3000-$3FFF of the window. The MAX is in
114
+ # Ultimax mode with no cartridge too.
114
115
  def ultimax_peek(offset)
115
116
  romh = @romh
116
117
  if romh && offset.allbits?(0x3000)
@@ -68,6 +68,12 @@ module Badline
68
68
  # True once AEC has followed BA down and the VIC owns the bus.
69
69
  def bus_taken?(column) = @ba ? column >= @ba + BA_DELAY : false
70
70
 
71
+ # The CPU kept the bus through the cycle before +column+, so on a bad
72
+ # line AEC falls BA_DELAY cycles after it at the earliest.
73
+ def keep_bus(column)
74
+ @ba &&= column
75
+ end
76
+
71
77
  def fetching?(column)
72
78
  @matched && column >= FETCH_FIRST && column <= DMA_LAST
73
79
  end
@@ -98,6 +104,12 @@ module Badline
98
104
 
99
105
  def new_line(line)
100
106
  @ba = nil
107
+ raster_step(line)
108
+ end
109
+
110
+ # The raster counter moved on to +line+, at the line's start or, under
111
+ # the VIC-IIe's TEST bit, within it.
112
+ def raster_step(line)
101
113
  @line_bits = line & 0b111
102
114
  @den_line = line == FIRST_LINE
103
115
  @in_window = line.between?(FIRST_LINE, LAST_LINE)
@@ -150,6 +150,9 @@ module Badline
150
150
  def fast? = @iie && @bytes[0x30].anybits?(0x01)
151
151
  def test? = @iie && @bytes[0x30].anybits?(0x02)
152
152
 
153
+ # FAST and TEST as bits 0 and 1.
154
+ def clock_bits = @iie ? @bytes[0x30] & 0x03 : 0
155
+
153
156
  def raster_target = uint16(@bytes[0x12], (@bytes[0x11] & 0x80) >> 7)
154
157
 
155
158
  def latch_irq!(bit)