badline 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -0
  3. data/CLAUDE.md +15 -7
  4. data/README.md +142 -16
  5. data/doc/media.md +19 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +274 -31
  8. data/doc/snapshots.md +14 -0
  9. data/exe/badline-ruby +1 -1
  10. data/lib/badline/audio/machine_player.rb +2 -14
  11. data/lib/badline/c64/common.rb +1 -1
  12. data/lib/badline/c64/true_drives.rb +13 -1
  13. data/lib/badline/c64.rb +6 -0
  14. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  15. data/lib/badline/chips/crtc.rb +163 -0
  16. data/lib/badline/chips/pia/control_lines.rb +138 -0
  17. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  18. data/lib/badline/chips/pia.rb +138 -0
  19. data/lib/badline/chips/tia.rb +16 -0
  20. data/lib/badline/core.rb +3 -0
  21. data/lib/badline/drives/datasette/keyed_state.rb +8 -3
  22. data/lib/badline/drives/engine/core.rb +6 -14
  23. data/lib/badline/drives/engine/idle.rb +42 -4
  24. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  25. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  26. data/lib/badline/drives/engine/sleep.rb +1 -0
  27. data/lib/badline/drives/engine.rb +1 -0
  28. data/lib/badline/drives/ieee_bus.rb +28 -0
  29. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  30. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  31. data/lib/badline/drives/model1541.rb +1 -0
  32. data/lib/badline/drives/model1551/bus.rb +138 -0
  33. data/lib/badline/drives/model1551/cable.rb +107 -0
  34. data/lib/badline/drives/model1551/cpu.rb +14 -0
  35. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  36. data/lib/badline/drives/model1551/port.rb +74 -0
  37. data/lib/badline/drives/model1551/tia.rb +71 -0
  38. data/lib/badline/drives/model1551.rb +192 -0
  39. data/lib/badline/drives/model1571.rb +1 -0
  40. data/lib/badline/drives/model1581.rb +1 -0
  41. data/lib/badline/drives.rb +1 -0
  42. data/lib/badline/frontend/app.rb +1 -1
  43. data/lib/badline/frontend/boot.rb +20 -1
  44. data/lib/badline/frontend/controls.rb +23 -3
  45. data/lib/badline/frontend/gamepads.rb +10 -3
  46. data/lib/badline/frontend/keys.rb +88 -0
  47. data/lib/badline/frontend/media_slots.rb +1 -0
  48. data/lib/badline/frontend/menu/menu_dialogs.rb +15 -6
  49. data/lib/badline/frontend/menu/menu_media.rb +96 -19
  50. data/lib/badline/frontend/menu/menu_pages.rb +25 -7
  51. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  52. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  53. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  54. data/lib/badline/frontend/player/player_controller.rb +11 -4
  55. data/lib/badline/frontend/screens.rb +2 -1
  56. data/lib/badline/frontend/timeline.rb +16 -9
  57. data/lib/badline/frontend.rb +1 -0
  58. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  59. data/lib/badline/kernal_trap/dos.rb +6 -1
  60. data/lib/badline/kernal_trap/frames.rb +117 -0
  61. data/lib/badline/kernal_trap/ieee.rb +148 -0
  62. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  63. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  64. data/lib/badline/kernal_trap/routine.rb +3 -53
  65. data/lib/badline/kernal_trap/serial.rb +4 -102
  66. data/lib/badline/kernal_trap/steps.rb +64 -0
  67. data/lib/badline/kernal_trap.rb +4 -0
  68. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  69. data/lib/badline/machine/settling.rb +1 -1
  70. data/lib/badline/machine.rb +37 -11
  71. data/lib/badline/media/extensions.rb +5 -2
  72. data/lib/badline/media/pet_media.rb +40 -0
  73. data/lib/badline/media/program.rb +14 -2
  74. data/lib/badline/media/true_drive.rb +7 -5
  75. data/lib/badline/media/vic20_media.rb +2 -2
  76. data/lib/badline/media.rb +36 -16
  77. data/lib/badline/options/family.rb +17 -6
  78. data/lib/badline/options/help.rb +23 -5
  79. data/lib/badline/options/option.rb +2 -1
  80. data/lib/badline/options/table.rb +28 -4
  81. data/lib/badline/options/validation.rb +117 -14
  82. data/lib/badline/options.rb +65 -21
  83. data/lib/badline/pet/bus/expansion.rb +106 -0
  84. data/lib/badline/pet/bus.rb +204 -0
  85. data/lib/badline/pet/canvas.rb +97 -0
  86. data/lib/badline/pet/cassettes.rb +63 -0
  87. data/lib/badline/pet/characters.rb +26 -0
  88. data/lib/badline/pet/cpu.rb +10 -0
  89. data/lib/badline/pet/crtc_video.rb +142 -0
  90. data/lib/badline/pet/discrete_video.rb +83 -0
  91. data/lib/badline/pet/ieee_ports.rb +66 -0
  92. data/lib/badline/pet/ieee_traps.rb +75 -0
  93. data/lib/badline/pet/keyboard_ports.rb +74 -0
  94. data/lib/badline/pet/keyed_modules.rb +85 -0
  95. data/lib/badline/pet/keyed_parts.rb +123 -0
  96. data/lib/badline/pet/keyed_state.rb +42 -0
  97. data/lib/badline/pet/model.rb +113 -0
  98. data/lib/badline/pet/rom_sets.rb +65 -0
  99. data/lib/badline/pet/sound.rb +128 -0
  100. data/lib/badline/pet/via.rb +10 -0
  101. data/lib/badline/pet/via_ports.rb +19 -0
  102. data/lib/badline/pet.rb +226 -0
  103. data/lib/badline/plus4/acia/frames.rb +55 -0
  104. data/lib/badline/plus4/acia.rb +276 -16
  105. data/lib/badline/plus4/bus.rb +181 -45
  106. data/lib/badline/plus4/keyed_modules.rb +22 -5
  107. data/lib/badline/plus4/keyed_parts.rb +33 -4
  108. data/lib/badline/plus4/keyed_state.rb +10 -5
  109. data/lib/badline/plus4/ram_expansion.rb +145 -0
  110. data/lib/badline/plus4/sid_card.rb +95 -0
  111. data/lib/badline/plus4/sound_mix.rb +51 -0
  112. data/lib/badline/plus4/tcbm_drive.rb +134 -0
  113. data/lib/badline/plus4/user_port.rb +11 -2
  114. data/lib/badline/plus4.rb +104 -22
  115. data/lib/badline/roms/README +146 -0
  116. data/lib/badline/roms/dos1551.rom +0 -0
  117. data/lib/badline/roms/pet/basic1.rom +0 -0
  118. data/lib/badline/roms/pet/basic2.rom +0 -0
  119. data/lib/badline/roms/pet/basic4.rom +0 -0
  120. data/lib/badline/roms/pet/characters1.rom +0 -0
  121. data/lib/badline/roms/pet/characters2.rom +0 -0
  122. data/lib/badline/roms/pet/editor1.rom +0 -0
  123. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  124. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  125. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  126. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  127. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  128. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  129. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  130. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  131. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  132. data/lib/badline/roms/pet/kernal1.rom +0 -0
  133. data/lib/badline/roms/pet/kernal2.rom +0 -0
  134. data/lib/badline/roms/pet/kernal4.rom +0 -0
  135. data/lib/badline/snapshot/container.rb +2 -0
  136. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  137. data/lib/badline/snapshot/field_doc.rb +21 -6
  138. data/lib/badline/snapshot/image.rb +14 -7
  139. data/lib/badline/snapshot/pet_setup.rb +71 -0
  140. data/lib/badline/snapshot/plus4_setup.rb +52 -10
  141. data/lib/badline/snapshot/storage_setup.rb +7 -1
  142. data/lib/badline/snapshot.rb +9 -5
  143. data/lib/badline/storage/d80_image.rb +48 -0
  144. data/lib/badline/storage/d82_image.rb +21 -0
  145. data/lib/badline/storage/g64_image.rb +1 -1
  146. data/lib/badline/storage/tap.rb +16 -3
  147. data/lib/badline/storage.rb +2 -0
  148. data/lib/badline/support/pet_checkpoint.rb +38 -0
  149. data/lib/badline/timing.rb +5 -4
  150. data/lib/badline/version.rb +1 -1
  151. data/lib/badline/vic20/attachments.rb +7 -2
  152. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  153. data/lib/badline/vic20/bus.rb +5 -0
  154. data/lib/badline/vic20.rb +1 -1
  155. metadata +69 -1
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ # How a PET was built: its model's name, one of PET::Model::ALL, its
6
+ # RAM below $8000 in K and its editor's frame rate, the arguments
7
+ # PET.new takes, and the ROMs it runs by file: BASIC, the editor, the
8
+ # KERNAL and the characters.
9
+ PETSetup = Data.define(:model, :ram_kb, :refresh_hz, :basic, :editor, :kernal, :characters)
10
+
11
+ class PETSetup
12
+ MODELS = PET::Model::ALL.map(&:name).freeze
13
+
14
+ REFRESH_RATES = [50, 60].freeze
15
+
16
+ # The setup of `machine`.
17
+ def self.of(machine)
18
+ model = machine.model
19
+ refresh_hz = machine.refresh_hz
20
+ new(model: model.name, ram_kb: machine.ram.length >> 10, refresh_hz:, basic: model.basic,
21
+ editor: model.editor(refresh_hz), kernal: model.kernal, characters: model.characters)
22
+ end
23
+
24
+ # The model by name, the RAM, the frame rate and the ROMs, in a keyed
25
+ # snapshot module.
26
+ def write_fields(out)
27
+ out.text("model", model).int("ram_kb", ram_kb).int("refresh_hz", refresh_hz)
28
+ out.text("rom.basic", basic).text("rom.editor", editor).text("rom.kernal", kernal)
29
+ out.text("rom.characters", characters)
30
+ end
31
+
32
+ # A ROM the snapshot doesn't name is the model's own.
33
+ def self.read_fields(input)
34
+ profile = PET::Model.named(input.choice("model", MODELS, PET::Model::DEFAULT))
35
+ ram_kb = input.int("ram_kb", profile.ram_kb)
36
+ unless PET::Model::RAM_SIZES.include?(ram_kb)
37
+ raise FormatError, "the snapshot is of a PET with #{ram_kb}K of RAM, which badline doesn't build"
38
+ end
39
+
40
+ refresh_hz = input.int("refresh_hz", profile.crtc? ? 50 : 60)
41
+ unless REFRESH_RATES.include?(refresh_hz)
42
+ raise FormatError, "the snapshot is of a PET at #{refresh_hz} Hz, which badline doesn't build"
43
+ end
44
+
45
+ refresh_hz = 60 unless profile.crtc?
46
+ new(model: profile.name, ram_kb:, refresh_hz:, basic: input.text("rom.basic", profile.basic),
47
+ editor: input.text("rom.editor", profile.editor(refresh_hz)),
48
+ kernal: input.text("rom.kernal", profile.kernal),
49
+ characters: input.text("rom.characters", profile.characters))
50
+ end
51
+
52
+ # A machine built this way, at power-on, with the BASIC and editor
53
+ # PET::ROMSets names for its ROMs.
54
+ def build
55
+ version = PET::ROMSets::BASICS.find { |_, roms| roms.first == basic }&.first
56
+ PET.new(model:, ram_kb:, refresh_hz:, basic: version, editor: PET::ROMSets::EDITORS.key(editor))
57
+ rescue ArgumentError => e
58
+ raise FormatError, "the snapshot is of a #{self}, which badline doesn't build: #{e.message}"
59
+ end
60
+
61
+ # The ROMs are named where they aren't the model's own.
62
+ def to_s
63
+ text = "#{model} with #{ram_kb}K at #{refresh_hz} Hz"
64
+ roms = [basic, editor, kernal, characters]
65
+ profile = PET::Model.named(model)
66
+ stock = [profile.basic, profile.editor(refresh_hz), profile.kernal, profile.characters]
67
+ roms == stock ? text : "#{text} running #{roms.join(', ')}"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -4,20 +4,33 @@ module Badline
4
4
  module Snapshot
5
5
  # How a 264 series machine was built: its model's name, one of
6
6
  # Plus4::Model::ALL, which also says whether the Plus/4's function ROMs
7
- # are fitted, its RAM in K, and whether a 1551's TCBM interface is
8
- # plugged in, the arguments Plus4.new takes.
9
- Plus4Setup = Data.define(:model, :ram_kb, :tcbm)
7
+ # are fitted, its RAM in K, whether a 1551's TCBM interface is plugged
8
+ # in, the SID card's SID model, or nil without one, and its address,
9
+ # and the RAM expansion's kind, or nil without one, the arguments
10
+ # Plus4.new takes.
11
+ Plus4Setup = Data.define(:model, :ram_kb, :tcbm, :sid_card, :sid_card_address, :ram_expansion)
10
12
 
11
13
  class Plus4Setup
12
14
  MODELS = Plus4::Model::ALL.map(&:name).freeze
13
15
 
16
+ SID_CARDS = %w[none mos6581 mos8580].freeze
17
+
18
+ RAM_EXPANSIONS = ["none", *Plus4::RAMExpansion::KINDS.keys.map(&:to_s)].freeze
19
+
14
20
  # The setup of `machine`.
15
- def self.of(machine) = new(model: machine.model.name, ram_kb: machine.ram_kb, tcbm: !machine.tias.empty?)
21
+ def self.of(machine)
22
+ card = machine.sid_card
23
+ new(model: machine.model.name, ram_kb: machine.ram_kb, tcbm: !machine.tias.empty?, sid_card: card&.model,
24
+ sid_card_address: card ? card.address : Plus4::SIDCard::ADDRESSES.first,
25
+ ram_expansion: machine.ram_expansion&.kind)
26
+ end
16
27
 
17
- # The model by name, the RAM and the TCBM interface, in a keyed
18
- # snapshot module.
28
+ # The model by name, the RAM, the TCBM interface, the SID card's
29
+ # model and address and the RAM expansion's kind, "none" without
30
+ # them, in a keyed snapshot module.
19
31
  def write_fields(out)
20
- out.text("model", model).int("ram_kb", ram_kb).bool("tcbm", tcbm)
32
+ out.text("model", model).int("ram_kb", ram_kb).bool("tcbm", tcbm).text("sid_card", (sid_card || :none).to_s)
33
+ out.int("sid_card_address", sid_card_address).text("ram_expansion", (ram_expansion || :none).to_s)
21
34
  end
22
35
 
23
36
  def self.read_fields(input)
@@ -27,13 +40,42 @@ module Badline
27
40
  raise FormatError, "the snapshot is of a Plus/4 with #{ram_kb}K of RAM, which badline doesn't build"
28
41
  end
29
42
 
30
- new(model:, ram_kb:, tcbm: input.bool("tcbm", false))
43
+ sid_card = named(input.choice("sid_card", SID_CARDS, "none"))
44
+ ram_expansion = named(input.choice("ram_expansion", RAM_EXPANSIONS, "none"))
45
+ new(model:, ram_kb:, tcbm: input.bool("tcbm", false), sid_card:, sid_card_address: sid_card_address(input),
46
+ ram_expansion:)
31
47
  end
32
48
 
49
+ # A choice's name as a Symbol, or nil for "none".
50
+ def self.named(name) = name == "none" ? nil : name.to_sym
51
+
52
+ def self.sid_card_address(input)
53
+ address = input.int("sid_card_address", Plus4::SIDCard::ADDRESSES.first)
54
+ return address if Plus4::SIDCard::ADDRESSES.include?(address)
55
+
56
+ raise FormatError, format("the snapshot is of a Plus/4 with a SID card at $%<address>04X, " \
57
+ "which badline doesn't build", address:)
58
+ end
59
+ private_class_method :named, :sid_card_address
60
+
33
61
  # A machine built this way, at power-on.
34
- def build = Plus4.new(model:, ram: ram_kb, tcbm:)
62
+ def build = Plus4.new(model:, ram: ram_kb, tcbm:, sid_card:, sid_card_address:, ram_expansion:)
63
+
64
+ def to_s
65
+ parts = ["#{ram_kb}K"]
66
+ parts << "a TCBM interface" if tcbm
67
+ parts << sid_card_part if sid_card
68
+ parts << "a #{ram_expansion} RAM expansion" if ram_expansion
69
+ "#{model} with #{parts.join(' and ')}"
70
+ end
35
71
 
36
- def to_s = "#{model} with #{ram_kb}K#{' and a TCBM interface' if tcbm}"
72
+ private
73
+
74
+ def sid_card_part
75
+ format("%<article>s %<model>s SID card at $%<address>04X", article: sid_card == :mos8580 ? "an" : "a",
76
+ model: sid_card.to_s.delete_prefix("mos"),
77
+ address: sid_card_address)
78
+ end
37
79
  end
38
80
  end
39
81
  end
@@ -11,6 +11,8 @@ module Badline
11
11
  D71 = 2
12
12
  D81 = 3
13
13
  T64_ARCHIVE = 4
14
+ D80 = 5
15
+ D82 = 6
14
16
 
15
17
  module_function
16
18
 
@@ -18,6 +20,8 @@ module Badline
18
20
  return D71 if image.is_a?(Storage::D71Image)
19
21
  return D64 if image.is_a?(Storage::D64Image)
20
22
  return D81 if image.is_a?(Storage::D81Image)
23
+ return D82 if image.is_a?(Storage::D82Image)
24
+ return D80 if image.is_a?(Storage::D80Image)
21
25
 
22
26
  raise ArgumentError, "#{image.class} can't go in a snapshot"
23
27
  end
@@ -37,7 +41,7 @@ module Badline
37
41
  end
38
42
  end
39
43
 
40
- KIND_NAMES = %w[directory d64 d71 d81 t64].freeze
44
+ KIND_NAMES = %w[directory d64 d71 d81 t64 d80 d82].freeze
41
45
 
42
46
  # Opens the storage write_fields wrote: the same directory by its
43
47
  # path, an archive with the bytes it held, or a disk image with the
@@ -55,6 +59,8 @@ module Badline
55
59
  case kind
56
60
  when D64 then Storage::D64Image.new(path, read_only:, bytes:)
57
61
  when D71 then Storage::D71Image.new(path, read_only:, bytes:)
62
+ when D80 then Storage::D80Image.new(path, read_only:, bytes:)
63
+ when D82 then Storage::D82Image.new(path, read_only:, bytes:)
58
64
  else Storage::D81Image.new(path, read_only:, bytes:)
59
65
  end
60
66
  end
@@ -35,9 +35,9 @@ module Badline
35
35
  # The machine's snapshot file. Saving first runs the machine on to the
36
36
  # end of the instruction its CPU is in, and of an REU transfer under
37
37
  # way (Machine::Settling), so a save advances it by a few cycles, and a
38
- # second save straight after runs none. A VIC-20's, a C128's or a
39
- # Plus/4's snapshot is in a container named as xvic, x128 or xplus4
40
- # names its machine.
38
+ # second save straight after runs none. A VIC-20's, a C128's, a
39
+ # Plus/4's or a PET's snapshot is in a container named as xvic, x128,
40
+ # xplus4 or xpet names its machine.
41
41
  def dump(computer) = Image.of(computer).to_s
42
42
 
43
43
  def read(path) = Image.new(Container.parse(File.binread(path)), name: File.basename(path))
@@ -51,8 +51,8 @@ module Badline
51
51
  # snapshot that fails leaves the machine as it was.
52
52
  def restore(computer, path, &) = read(path).restore(computer, &)
53
53
 
54
- # A machine's snapshots, as .vsf files and in memory. C64, Vic20, C128
55
- # and Plus4 each include it.
54
+ # A machine's snapshots, as .vsf files and in memory. C64, Vic20, C128,
55
+ # Plus4 and PET each include it.
56
56
  module Files
57
57
  def save_snapshot(path) = Snapshot.save(self, path)
58
58
 
@@ -95,4 +95,8 @@ module Badline
95
95
  class Plus4
96
96
  include Snapshot::Files
97
97
  end
98
+
99
+ class PET
100
+ include Snapshot::Files
101
+ end
98
102
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ # An 8050 disk: 77 tracks on one side, in four speed zones.
6
+ class D80Image < DiskImage
7
+ ERROR_TABLES = { 535_331 => 2083 }.freeze
8
+
9
+ # The last track of each zone but the innermost, from the outermost
10
+ # in: tracks 1-39, 40-53 and 54-64, and the last zone from 65 on.
11
+ ZONE_LAST_TRACKS = [39, 53, 64].freeze
12
+
13
+ # The sectors a track holds in each zone, from the outermost in.
14
+ ZONE_SECTORS = [29, 27, 25, 23].freeze
15
+
16
+ # The tracks a side holds.
17
+ SIDE_TRACKS = 77
18
+
19
+ def sectors_in(track)
20
+ zone = ZONE_LAST_TRACKS.count { |last| track > last }
21
+ ZONE_SECTORS[zone]
22
+ end
23
+
24
+ private
25
+
26
+ def error_tables = ERROR_TABLES
27
+
28
+ # The header block holds the disk name at $06 and the ID at $18,
29
+ # after the link to the BAM's first block and the format, "C".
30
+ def header_name = 0x06
31
+ def directory_track = 39
32
+ def directory_sector = 1
33
+ def directory_interleave = 3
34
+ def interleave = 6
35
+
36
+ # Only the directory track is kept from files. The BAM's blocks on
37
+ # track 38 are marked in use, and the rest of that track holds files.
38
+ def reserved_tracks = [39]
39
+
40
+ # The BAM takes blocks 0, 3, 6 and 9 of track 38 as it needs them,
41
+ # each covering 50 tracks with a free count and a four-byte bitmap
42
+ # each from offset 6.
43
+ def bam_tracks = 1..SIDE_TRACKS
44
+ def bam_count(track) = [38, 3 * ((track - 1) / 50), 6 + (5 * ((track - 1) % 50))]
45
+ def bam_bitmap(track) = [38, 3 * ((track - 1) / 50), 7 + (5 * ((track - 1) % 50))]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ # An 8250 disk: an 8050's 77 tracks on each side, tracks 78-154 on the
6
+ # second, one directory and one BAM for both.
7
+ class D82Image < D80Image
8
+ ERROR_TABLES = { 1_070_662 => 4166 }.freeze
9
+
10
+ def sectors_in(track)
11
+ track > SIDE_TRACKS ? super(track - SIDE_TRACKS) : super
12
+ end
13
+
14
+ private
15
+
16
+ def error_tables = ERROR_TABLES
17
+
18
+ def bam_tracks = 1..(2 * SIDE_TRACKS)
19
+ end
20
+ end
21
+ end
@@ -47,7 +47,7 @@ module Badline
47
47
  # track 1 and 1 for track 1.5, laid out as VICE lays one out: every
48
48
  # block as long as the longest track.
49
49
  def self.create(path, tracks, half_tracks: HALF_TRACKS)
50
- max = tracks.values.map { |bytes, _| bytes.length }.push(MAX_TRACK_SIZE).max
50
+ max = [MAX_TRACK_SIZE, *tracks.values.map { |bytes, _| bytes.length }].max
51
51
  offsets = Array.new(half_tracks, 0)
52
52
  zones = Array.new(half_tracks, 0)
53
53
  blocks = +"".b
@@ -3,7 +3,7 @@
3
3
  module Badline
4
4
  module Storage
5
5
  # Raw datasette pulse stream. The header holds the signature, the
6
- # version at $0c, the platform at $0d (0 C64, 1 VIC-20, 2 C16) and the
6
+ # version at $0c, the platform at $0d (0 C64, 1 VIC-20, 2 C16, 3 PET) and the
7
7
  # data size at $10; the data runs from $14, every byte the time to the
8
8
  # next falling edge on the tape read line, in units of 8 cycles. A
9
9
  # version 2 tape, the C16's and the Plus/4's, times half waves instead,
@@ -16,18 +16,31 @@ module Badline
16
16
  HEADER_SIZE = 0x14
17
17
  OVERFLOW = 0x100 * 8
18
18
 
19
+ # The platform byte of a PET tape, a version 1 tape timed in PET
20
+ # cycles.
21
+ PET_PLATFORM = 3
22
+
19
23
  # The file it was read from, expanded, and the file's bytes.
20
24
  attr_reader :version, :platform, :path, :bytes
21
25
 
22
26
  # A blank tape for `platform` at `path`, written to the host file: a
23
- # version 1 tape for the C64 (0) or the VIC-20 (1), and a version 2
24
- # C16 tape for the 264 series (2).
27
+ # version 1 tape for the C64 (0), the VIC-20 (1) or the PET (3), and
28
+ # a version 2 C16 tape for the 264 series (2).
25
29
  def self.create(path, platform:)
26
30
  header = platform == 2 ? C16_SIGNATURE + [2] : SIGNATURE + [1]
27
31
  File.binwrite(path, (header + [platform, 0, 0, 0, 0, 0, 0]).pack("C*"))
28
32
  new(path)
29
33
  end
30
34
 
35
+ # The platform byte of the tape at `path`, or nil when it isn't a
36
+ # TAP file, read from its header alone.
37
+ def self.platform_of(path)
38
+ header = File.binread(path, HEADER_SIZE)&.bytes || []
39
+ return unless [SIGNATURE, C16_SIGNATURE].include?(header[0, SIGNATURE.length]) && header.length > 0x0d
40
+
41
+ header[0x0d]
42
+ end
43
+
31
44
  # `bytes` stands in for the host file's contents, as a snapshot
32
45
  # restores the tape it holds.
33
46
  def initialize(path, bytes: nil)
@@ -7,6 +7,8 @@ require "badline/storage/disk_image"
7
7
  require "badline/storage/d64_image"
8
8
  require "badline/storage/d71_image"
9
9
  require "badline/storage/d81_image"
10
+ require "badline/storage/d80_image"
11
+ require "badline/storage/d82_image"
10
12
  require "badline/storage/g64_image"
11
13
  require "badline/storage/t64"
12
14
  require "badline/storage/tap"
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Support
5
+ # A Checkpoint of a PET: its CPU, the RAM below $8000, the screen RAM,
6
+ # the picture the video paints, both PIAs, the VIA, the CRTC's
7
+ # registers and counters, and the 8096's control register and 64K
8
+ # board, or nothing on another model.
9
+ module PETCheckpoint
10
+ COMPONENTS = %w[cpu ram screen display pia1 pia2 via crtc expansion].freeze
11
+
12
+ def self.take(machine)
13
+ ram = machine.ram
14
+ Checkpoint.new(machine.cycles, [
15
+ Checkpoint.fnv1a(Checkpoint.cpu_state(machine.cpu)),
16
+ Checkpoint.fnv1a(Checkpoint.ram(ram, ram.length)),
17
+ Checkpoint.fnv1a(machine.bus.screen.cells),
18
+ Checkpoint.fnv1a(machine.video.display),
19
+ Checkpoint.fnv1a(pia_state(machine.pia1)),
20
+ Checkpoint.fnv1a(pia_state(machine.pia2)),
21
+ Checkpoint.fnv1a(Vic20Checkpoint.via_state(machine.via)),
22
+ Checkpoint.fnv1a(crtc_state(machine.crtc)),
23
+ Checkpoint.fnv1a(expansion_state(machine.bus.expansion))
24
+ ], COMPONENTS)
25
+ end
26
+
27
+ def self.parse(line) = Checkpoint.parse(line, COMPONENTS)
28
+
29
+ def self.pia_state(pia) = [pia.port_a_output, pia.port_b_output, pia.cra, pia.crb]
30
+
31
+ def self.crtc_state(crtc) = crtc.registers + [crtc.column, crtc.line, crtc.row]
32
+
33
+ def self.expansion_state(expansion)
34
+ expansion ? [expansion.control, *expansion.cells] : []
35
+ end
36
+ end
37
+ end
38
+ end
@@ -5,8 +5,9 @@ module Badline
5
5
  #
6
6
  # What a front end needs to run a machine's frames and show them: the CPU
7
7
  # clock, the raster's cycles per line and lines per frame, and the
8
- # [left, top, width, height] of the raster it shows, and how many of the
9
- # window's square pixels wide each of its pixels shows. Each machine
10
- # derives it from its region.
11
- Timing = Data.define(:clock_hz, :cycles_per_line, :lines_per_frame, :crop, :pixel_width)
8
+ # [left, top, width, height] of the raster it shows, how many of the
9
+ # window's square pixels wide each of its pixels shows, and how many of
10
+ # its pixels share one window pixel, as an 80-column screen's dots do.
11
+ # Each machine derives it from its region.
12
+ Timing = Data.define(:clock_hz, :cycles_per_line, :lines_per_frame, :crop, :pixel_width, :dots)
12
13
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.15.0"
4
+ VERSION = "0.16.0"
5
5
  end
@@ -23,12 +23,17 @@ module Badline
23
23
  # Puts a cartridge's ROM chips, each a CRTFile::Chip with its address
24
24
  # and bytes, into their blocks, and switches the machine off and on,
25
25
  # as a cartridge goes in with the power off. The KERNAL then starts
26
- # the cartridge in BLK5 that signs itself with A0CBM.
27
- def attach_cartridge(chips)
26
+ # the cartridge in BLK5 that signs itself with A0CBM. +name+ is the
27
+ # name it goes by (#cartridge_name).
28
+ def attach_cartridge(chips, name = "")
28
29
  chips.each { |chip| @bus.map_rom(chip.address, chip.data) }
30
+ @bus.cartridge_name = name
29
31
  power_cycle!
30
32
  end
31
33
 
34
+ # The name of the cartridge in the expansion blocks, or an empty one.
35
+ def cartridge_name = @bus.cartridge_name
36
+
32
37
  private
33
38
 
34
39
  # The VIC-20's KERNAL, which the traps stand in for.
@@ -11,9 +11,9 @@ module Badline
11
11
  end
12
12
 
13
13
  # The bytes each side's data lines hold, under `prefix`, and a
14
- # cartridge's ROM chips as rom<N>.start and rom<N>.bytes, in a keyed
15
- # snapshot module (Snapshot::Keyed). The pages are mapped again once
16
- # they are loaded.
14
+ # cartridge's name and its ROM chips as rom<N>.start and rom<N>.bytes,
15
+ # in a keyed snapshot module (Snapshot::Keyed). The pages are mapped
16
+ # again once they are loaded.
17
17
  module KeyedState
18
18
  def save_fields(out, prefix)
19
19
  out.int("#{prefix}data", @data).int("#{prefix}video_data", @video_data)
@@ -28,7 +28,7 @@ module Badline
28
28
  def cartridge? = !@roms.empty?
29
29
 
30
30
  def save_rom_fields(out)
31
- out.int("roms", @roms.length)
31
+ out.text("name", @cartridge_name).int("roms", @roms.length)
32
32
  @roms.each_with_index do |rom, number|
33
33
  out.int("rom#{number}.start", rom.start)
34
34
  rom.save_fields(out, "rom#{number}.bytes")
@@ -37,6 +37,7 @@ module Badline
37
37
 
38
38
  def load_rom_fields(input)
39
39
  @roms = []
40
+ @cartridge_name = input.text("name", "")
40
41
  input.int("roms", 0).times do |number|
41
42
  bytes = input.bytes("rom#{number}.bytes", "".b).bytes
42
43
  @roms << ROM.new(bytes, length: bytes.length, start: input.word("rom#{number}.start", 0))
@@ -160,6 +160,10 @@ module Badline
160
160
  # The last byte on the V-bus.
161
161
  attr_reader :video_data
162
162
 
163
+ # The name of the cartridge whose ROM chips are in the expansion
164
+ # blocks, or an empty one.
165
+ attr_accessor :cartridge_name
166
+
163
167
  # +vic+, +via1+ and +via2+ are the chips in I/O 0, each answering
164
168
  # peek and poke with the CPU's address. +blocks+ names the expansion
165
169
  # blocks that hold RAM (BLOCKS), and +kernal+ the KERNAL ROM's file.
@@ -177,6 +181,7 @@ module Badline
177
181
  @read_pages = Array.new(256, @open_bus)
178
182
  @write_pages = Array.new(256, @open_bus)
179
183
  @roms = []
184
+ @cartridge_name = ""
180
185
  @debug_register = nil
181
186
  @via_write_handler = nil
182
187
  self.blocks = blocks
data/lib/badline/vic20.rb CHANGED
@@ -120,7 +120,7 @@ module Badline
120
120
  blanked = profile.blanked_lines
121
121
  Timing.new(clock_hz: profile.clock_hz, cycles_per_line: profile.cycles_per_line,
122
122
  lines_per_frame: profile.lines_per_frame,
123
- crop: [0, blanked, width, profile.lines_per_frame - blanked], pixel_width: 2)
123
+ crop: [0, blanked, width, profile.lines_per_frame - blanked], pixel_width: 2, dots: 1)
124
124
  end
125
125
 
126
126
  # The RESTORE key pulls VIA 1's CA1 low while it is held. The KERNAL