badline 0.15.1 → 0.16.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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/CLAUDE.md +8 -7
  4. data/README.md +128 -9
  5. data/doc/media.md +9 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +215 -18
  8. data/doc/snapshots.md +55 -0
  9. data/lib/badline/c64/common.rb +1 -1
  10. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  11. data/lib/badline/chips/crtc.rb +163 -0
  12. data/lib/badline/chips/pia/control_lines.rb +138 -0
  13. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  14. data/lib/badline/chips/pia.rb +138 -0
  15. data/lib/badline/chips/vic/register_log.rb +1 -4
  16. data/lib/badline/core.rb +3 -0
  17. data/lib/badline/drives/ieee_bus.rb +74 -0
  18. data/lib/badline/drives/model1551/cable.rb +5 -2
  19. data/lib/badline/drives/model1551/tia.rb +13 -34
  20. data/lib/badline/drives/model1551.rb +26 -4
  21. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  22. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  23. data/lib/badline/drives/model2031/parts.rb +25 -0
  24. data/lib/badline/drives/model2031.rb +92 -0
  25. data/lib/badline/drives.rb +1 -0
  26. data/lib/badline/frontend/app.rb +1 -1
  27. data/lib/badline/frontend/controls.rb +23 -3
  28. data/lib/badline/frontend/gamepads.rb +10 -3
  29. data/lib/badline/frontend/keys.rb +88 -0
  30. data/lib/badline/frontend/media_slots.rb +1 -0
  31. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  32. data/lib/badline/frontend/menu/menu_media.rb +27 -28
  33. data/lib/badline/frontend/menu/menu_pages.rb +24 -2
  34. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  35. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  36. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  37. data/lib/badline/frontend/screens.rb +2 -1
  38. data/lib/badline/frontend/timeline.rb +16 -9
  39. data/lib/badline/frontend.rb +1 -0
  40. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  41. data/lib/badline/kernal_trap/dos.rb +6 -1
  42. data/lib/badline/kernal_trap/frames.rb +117 -0
  43. data/lib/badline/kernal_trap/ieee.rb +151 -0
  44. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  45. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  46. data/lib/badline/kernal_trap/routine.rb +3 -53
  47. data/lib/badline/kernal_trap/serial.rb +4 -102
  48. data/lib/badline/kernal_trap/steps.rb +64 -0
  49. data/lib/badline/kernal_trap.rb +4 -0
  50. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  51. data/lib/badline/machine/settling.rb +1 -1
  52. data/lib/badline/machine.rb +29 -6
  53. data/lib/badline/media/extensions.rb +5 -2
  54. data/lib/badline/media/pet_media.rb +40 -0
  55. data/lib/badline/media/program.rb +14 -2
  56. data/lib/badline/media/true_drive.rb +19 -6
  57. data/lib/badline/media/vic20_media.rb +2 -2
  58. data/lib/badline/media.rb +41 -17
  59. data/lib/badline/options/family.rb +17 -6
  60. data/lib/badline/options/help.rb +23 -3
  61. data/lib/badline/options/table.rb +22 -4
  62. data/lib/badline/options/validation.rb +84 -5
  63. data/lib/badline/options.rb +47 -16
  64. data/lib/badline/pet/bus/expansion.rb +200 -0
  65. data/lib/badline/pet/bus.rb +222 -0
  66. data/lib/badline/pet/canvas.rb +97 -0
  67. data/lib/badline/pet/cassettes.rb +63 -0
  68. data/lib/badline/pet/characters.rb +32 -0
  69. data/lib/badline/pet/cpu.rb +10 -0
  70. data/lib/badline/pet/crtc_video.rb +155 -0
  71. data/lib/badline/pet/discrete_video.rb +83 -0
  72. data/lib/badline/pet/ieee_drive.rb +76 -0
  73. data/lib/badline/pet/ieee_ports.rb +77 -0
  74. data/lib/badline/pet/ieee_traps.rb +80 -0
  75. data/lib/badline/pet/keyboard_ports.rb +74 -0
  76. data/lib/badline/pet/keyed_modules.rb +88 -0
  77. data/lib/badline/pet/keyed_parts.rb +127 -0
  78. data/lib/badline/pet/keyed_state.rb +42 -0
  79. data/lib/badline/pet/model.rb +163 -0
  80. data/lib/badline/pet/rom_sets.rb +89 -0
  81. data/lib/badline/pet/sound.rb +128 -0
  82. data/lib/badline/pet/via.rb +10 -0
  83. data/lib/badline/pet/via_ports.rb +19 -0
  84. data/lib/badline/pet.rb +244 -0
  85. data/lib/badline/plus4/keyed_state.rb +5 -3
  86. data/lib/badline/plus4/tcbm_drive.rb +14 -0
  87. data/lib/badline/plus4.rb +1 -1
  88. data/lib/badline/roms/README +169 -0
  89. data/lib/badline/roms/dos2031.rom +0 -0
  90. data/lib/badline/roms/pet/basic1.rom +0 -0
  91. data/lib/badline/roms/pet/basic2.rom +0 -0
  92. data/lib/badline/roms/pet/basic4.rom +0 -0
  93. data/lib/badline/roms/pet/characters-din.rom +0 -0
  94. data/lib/badline/roms/pet/characters1.rom +0 -0
  95. data/lib/badline/roms/pet/characters2.rom +0 -0
  96. data/lib/badline/roms/pet/editor1.rom +0 -0
  97. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  98. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  99. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  100. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  101. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  102. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  103. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  104. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  105. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  106. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  107. data/lib/badline/roms/pet/kernal1.rom +0 -0
  108. data/lib/badline/roms/pet/kernal2.rom +0 -0
  109. data/lib/badline/roms/pet/kernal4.rom +0 -0
  110. data/lib/badline/snapshot/container.rb +6 -1
  111. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  112. data/lib/badline/snapshot/field_doc.rb +17 -3
  113. data/lib/badline/snapshot/image.rb +28 -10
  114. data/lib/badline/snapshot/pet_setup.rb +88 -0
  115. data/lib/badline/snapshot/storage_setup.rb +7 -1
  116. data/lib/badline/snapshot/vice/pet.rb +117 -0
  117. data/lib/badline/snapshot/vice/pet_cpu.rb +67 -0
  118. data/lib/badline/snapshot/vice/pet_crtc.rb +246 -0
  119. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  120. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  121. data/lib/badline/snapshot/vice/pet_restore.rb +150 -0
  122. data/lib/badline/snapshot/vice/pet_via.rb +164 -0
  123. data/lib/badline/snapshot/vice.rb +1 -0
  124. data/lib/badline/snapshot.rb +9 -5
  125. data/lib/badline/storage/d80_image.rb +48 -0
  126. data/lib/badline/storage/d82_image.rb +21 -0
  127. data/lib/badline/storage/tap.rb +16 -3
  128. data/lib/badline/storage.rb +2 -0
  129. data/lib/badline/support/pet_checkpoint.rb +38 -0
  130. data/lib/badline/timing.rb +5 -4
  131. data/lib/badline/version.rb +1 -1
  132. data/lib/badline/vic20/attachments.rb +7 -2
  133. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  134. data/lib/badline/vic20/bus.rb +5 -0
  135. data/lib/badline/vic20.rb +1 -1
  136. metadata +70 -1
@@ -34,7 +34,7 @@ module Badline
34
34
  # of a machine in memory (.of).
35
35
  class Image
36
36
  MACHINE = "BL-MACHINE"
37
- FAMILIES = { "c64" => "C64", "vic20" => "VIC-20", "c128" => "C128", "plus4" => "Plus/4" }.freeze
37
+ FAMILIES = { "c64" => "C64", "vic20" => "VIC-20", "c128" => "C128", "plus4" => "Plus/4", "pet" => "PET" }.freeze
38
38
 
39
39
  attr_reader :container
40
40
 
@@ -53,15 +53,18 @@ module Badline
53
53
  new(Container.new(computer.save_modules, machine: machine_name(computer)))
54
54
  end
55
55
 
56
- # The snapshot with VICE's modules ahead of the BL-* ones for a C64,
57
- # so x64sc opens it too. Raises FormatError for a Plus/4, whose
58
- # snapshots badline doesn't write for xplus4.
56
+ # The snapshot with VICE's modules ahead of the BL-* ones for a C64
57
+ # or a PET, so x64sc or xpet opens it too. Raises FormatError for a
58
+ # Plus/4, whose snapshots badline doesn't write for xplus4.
59
59
  def self.exported(computer)
60
60
  raise FormatError, "badline doesn't export a Plus/4's snapshot for VICE" if computer.family == :plus4
61
61
 
62
62
  computer.settle_for_snapshot!
63
63
  sections = computer.save_modules
64
- sections = Vice.export(computer) + sections if computer.family == :c64
64
+ case computer.family
65
+ when :c64 then sections = Vice.export(computer) + sections
66
+ when :pet then sections = Vice::PET.export(computer, sections) + sections
67
+ end
65
68
  new(Container.new(sections, machine: machine_name(computer)))
66
69
  end
67
70
 
@@ -71,6 +74,7 @@ module Badline
71
74
  when :vic20 then Container::VIC20
72
75
  when :c128 then Container::C128
73
76
  when :plus4 then Container::PLUS4
77
+ when :pet then Container::PET
74
78
  else Container::MACHINE
75
79
  end
76
80
  end
@@ -82,6 +86,7 @@ module Badline
82
86
  when "vic20" then Vic20Setup.read_fields(machine)
83
87
  when "c128" then C128Setup.read_fields(machine)
84
88
  when "plus4" then Plus4Setup.read_fields(machine)
89
+ when "pet" then PETSetup.read_fields(machine)
85
90
  else C64Setup.read_fields(machine)
86
91
  end
87
92
  end
@@ -97,15 +102,16 @@ module Badline
97
102
 
98
103
  # A new machine, built as the snapshot's was and restored from it. A
99
104
  # badline snapshot restores from its BL-* modules alone, and a VICE
100
- # one through the modules badline reads. xvic's, x128's and xplus4's own
101
- # modules aren't read. Yields a line for each thing left out, or
102
- # warns it without a block. `detached` restores it without the host's
103
- # files (Keyed::Modules).
105
+ # one through the modules badline reads, x64sc's or xpet's. xvic's,
106
+ # x128's and xplus4's own modules aren't read. Yields a line for
107
+ # each thing left out, or warns it without a block. `detached`
108
+ # restores it without the host's files (Keyed::Modules).
104
109
  def load(detached: false, &)
105
110
  refuse_old_layout
106
111
  raise FormatError, "badline reads only its own VIC-20 snapshots, not xvic's" if vic20? && !badline?
107
112
  raise FormatError, "badline reads only its own C128 snapshots, not x128's" if c128? && !badline?
108
113
  raise FormatError, "badline reads only its own Plus/4 snapshots, not xplus4's" if plus4? && !badline?
114
+ return load_xpet(&) if pet? && !badline?
109
115
  return load_vice(&) unless badline?
110
116
 
111
117
  modules = Keyed::Modules.new(sections, detached:)
@@ -123,12 +129,18 @@ module Badline
123
129
  # Whether the machine is of the 264 series, as the container names it.
124
130
  def plus4? = @container.machine == Container::PLUS4
125
131
 
132
+ # Whether the machine is a PET, as the container names it.
133
+ def pet? = @container.machine == Container::PET
134
+
126
135
  # Restores `computer` from the snapshot, and returns the Report. The
127
136
  # snapshot goes into a new machine first, so one that fails, or is of
128
137
  # a machine built another way, leaves `computer` as it was.
129
138
  def restore(computer, &)
130
139
  refuse_old_layout
131
- report = badline? ? restore_modules(computer) : Vice::Restore.import(@container, computer)
140
+ report = if badline? then restore_modules(computer)
141
+ elsif pet? then Vice::PET::Restore.import(@container, computer)
142
+ else Vice::Restore.import(@container, computer)
143
+ end
132
144
  tell(report, &)
133
145
  report
134
146
  end
@@ -141,6 +153,12 @@ module Badline
141
153
  computer
142
154
  end
143
155
 
156
+ def load_xpet(&)
157
+ computer = Vice::PET.setup(@container).build
158
+ tell(Vice::PET::Restore.apply(@container, computer), &)
159
+ computer
160
+ end
161
+
144
162
  # The snapshot goes into a new machine first, so one that fails
145
163
  # leaves `computer` as it was.
146
164
  def restore_modules(computer)
@@ -0,0 +1,88 @@
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, the ROMs it runs by file: BASIC, the editor, the
8
+ # KERNAL and the characters, and the 8296 board's jumpers, by name and
9
+ # comma-separated.
10
+ PETSetup = Data.define(:model, :ram_kb, :refresh_hz, :basic, :editor, :kernal, :characters, :jumpers)
11
+
12
+ class PETSetup
13
+ MODELS = PET::Model::ALL.map(&:name).freeze
14
+
15
+ REFRESH_RATES = [50, 60].freeze
16
+
17
+ # The setup of `machine`.
18
+ def self.of(machine)
19
+ model = machine.model
20
+ refresh_hz = machine.refresh_hz
21
+ new(model: model.name, ram_kb: machine.ram.length >> 10, refresh_hz:, basic: model.basic,
22
+ editor: model.editor(refresh_hz), kernal: model.kernal, characters: model.characters,
23
+ jumpers: machine.jumpers.join(","))
24
+ end
25
+
26
+ # The model by name, the RAM, the frame rate, the ROMs and the
27
+ # jumpers where a board has any, in a keyed snapshot module.
28
+ def write_fields(out)
29
+ out.text("model", model).int("ram_kb", ram_kb).int("refresh_hz", refresh_hz)
30
+ out.text("rom.basic", basic).text("rom.editor", editor).text("rom.kernal", kernal)
31
+ out.text("rom.characters", characters)
32
+ out.text("jumpers", jumpers) unless jumpers.empty?
33
+ end
34
+
35
+ # A ROM the snapshot doesn't name is the model's own, and a board
36
+ # without jumpers named has none fitted.
37
+ def self.read_fields(input)
38
+ profile = PET::Model.named(input.choice("model", MODELS, PET::Model::DEFAULT))
39
+ ram_kb = input.int("ram_kb", profile.ram_kb)
40
+ unless PET::Model.ram_fits?(profile, ram_kb)
41
+ raise FormatError, "the snapshot is of a PET with #{ram_kb}K of RAM, which badline doesn't build"
42
+ end
43
+
44
+ refresh_hz = input.int("refresh_hz", profile.crtc? ? 50 : 60)
45
+ unless REFRESH_RATES.include?(refresh_hz)
46
+ raise FormatError, "the snapshot is of a PET at #{refresh_hz} Hz, which badline doesn't build"
47
+ end
48
+
49
+ refresh_hz = 60 unless profile.crtc?
50
+ new(model: profile.name, ram_kb:, refresh_hz:, basic: input.text("rom.basic", profile.basic),
51
+ editor: input.text("rom.editor", profile.editor(refresh_hz)),
52
+ kernal: input.text("rom.kernal", profile.kernal),
53
+ characters: input.text("rom.characters", profile.characters), jumpers: input.text("jumpers", ""))
54
+ end
55
+
56
+ # A machine built this way, at power-on, with the BASIC, editor and
57
+ # character ROMs PET::ROMSets names for its ROMs.
58
+ def build
59
+ version = PET::ROMSets::BASICS.find { |_, roms| roms.first == basic }&.first
60
+ PET.new(model:, ram_kb:, refresh_hz:, basic: version, editor: PET::ROMSets::EDITORS.key(editor),
61
+ characters: fitted_characters, jumpers: jumpers.split(","))
62
+ rescue ArgumentError => e
63
+ raise FormatError, "the snapshot is of a #{self}, which badline doesn't build: #{e.message}"
64
+ end
65
+
66
+ # The ROMs are named where they aren't the model's own, and the
67
+ # jumpers where any are fitted.
68
+ def to_s
69
+ text = "#{model} with #{ram_kb}K at #{refresh_hz} Hz"
70
+ text = "#{text} jumpered #{jumpers}" unless jumpers.empty?
71
+ roms = [basic, editor, kernal, characters]
72
+ profile = PET::Model.named(model)
73
+ stock = [profile.basic, profile.editor(refresh_hz), profile.kernal, profile.characters]
74
+ roms == stock ? text : "#{text} running #{roms.join(', ')}"
75
+ end
76
+
77
+ private
78
+
79
+ # The name of a character ROM other than the model's own, nil for its
80
+ # own.
81
+ def fitted_characters
82
+ return nil if characters == PET::Model.named(model).characters
83
+
84
+ PET::ROMSets::CHARACTERS.key(characters) || characters
85
+ end
86
+ end
87
+ end
88
+ 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
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/snapshot/vice/pet_cpu"
4
+ require "badline/snapshot/vice/pet_mem"
5
+ require "badline/snapshot/vice/pet_pias"
6
+ require "badline/snapshot/vice/pet_via"
7
+ require "badline/snapshot/vice/pet_crtc"
8
+ require "badline/snapshot/vice/pet_restore"
9
+
10
+ module Badline
11
+ module Snapshot
12
+ module Vice
13
+ # xpet's modules for the PET: the CPU, the RAM, the CRTC, both PIAs
14
+ # and the VIA, written from badline's machine and read into it. Each
15
+ # module's file says what maps and what is approximated. They go
16
+ # through the PET's own keyed fields (Keyed), which a save writes and a
17
+ # load reads, so they read no chip's insides.
18
+ module PET
19
+ # The models badline builds by the board PETMEM names (PETMem): the
20
+ # discrete board is a 2001's where xpet's settings say so, and a
21
+ # 3032 otherwise.
22
+ MODELS = { PETMem::CRTC40 => "4032", PETMem::CRTC80 => "8032", PETMem::CBM8096 => "8096",
23
+ PETMem::CBM8296 => "8296" }.freeze
24
+ # The modules xpet resets the machine without, for the parts badline
25
+ # doesn't model in VICE's terms, each as xpet leaves them with
26
+ # nothing attached: no file system drive open, both cassette ports
27
+ # on with a datasette in #1, and nothing in the user port.
28
+ PERIPHERALS = [
29
+ ["FSDRIVE", 0, 0, Array.new(258, 0)], ["TAPEPORT", 1, 0, [0x01, 0x01, 0x01]], ["USERPORT", 1, 0, [0x01, 0x00]]
30
+ ].freeze
31
+ # The cycles a frame below which a CRTC editor runs at 60 Hz.
32
+ SIXTY_HZ_FRAME = 18_333
33
+ # The 8032's 50 Hz editors by the cycles a line they set up: xpet's
34
+ # own, 901474-04 dated 0283, sets 64.
35
+ EDITOR_0283_LINE = 64
36
+
37
+ module_function
38
+
39
+ # The modules xpet 3.10 needs, in its order, describing `machine`,
40
+ # which Machine::Settling has brought between instructions, from
41
+ # its keyed modules, `sections`.
42
+ def export(machine, sections)
43
+ readers = {}
44
+ sections.each { |section| readers[section.name] = Keyed::Reader.parse(section) }
45
+ [MainCPU.export(machine), CPU6809.export(machine), PETMem.export(machine),
46
+ CRTC.export(machine, reader(readers, "BL-CRTC"), reader(readers, "BL-VIDEO")),
47
+ PIAs.export("PIA1", machine.pia1, reader(readers, "BL-PIA1")),
48
+ PIAs.export("PIA2", machine.pia2, reader(readers, "BL-PIA2")), VIA.export(reader(readers, "BL-VIA"))] +
49
+ PERIPHERALS.map { |name, major, minor, bytes| FieldWriter.new.bytes(bytes).section(name, major, minor) }
50
+ end
51
+
52
+ def reader(readers, name) = readers[name] || Keyed::Reader.missing(name)
53
+
54
+ # How the machine xpet saved was built: the model from PETMEM's
55
+ # board, the RAM, and on a CRTC board the editor's frame rate, and
56
+ # for an 8032's 50 Hz editor which one, from the CRTC's registers.
57
+ # xpet doesn't save the keyboard, the ROMs or an 8296's jumpers, so
58
+ # a discrete board is a 3032 with the graphics keyboard unless it is
59
+ # a 2001, each model has its own ROMs, and an 8296 none of its
60
+ # jumpers, as xpet's own has.
61
+ def setup(container)
62
+ memory = memory(container)
63
+ ram_kb = PETMem.ram_kb(memory)
64
+ name = model_name(memory)
65
+ profile = Badline::PET::Model.named(name)
66
+ unless Badline::PET::Model.ram_fits?(profile, ram_kb)
67
+ raise FormatError, "#{memory}: a #{name} with #{ram_kb}K of RAM, which badline doesn't build"
68
+ end
69
+
70
+ crtc = container[CRTC::NAME]
71
+ crtc = nil unless crtc.nil? || CRTC.reads?(crtc)
72
+ refresh_hz = refresh_hz(crtc, profile)
73
+ profile = Badline::PET::Model.upgraded(profile, nil, editor(crtc, profile, refresh_hz))
74
+ PETSetup.new(model: name, ram_kb:, refresh_hz:, basic: profile.basic, editor: profile.editor(refresh_hz),
75
+ kernal: profile.kernal, characters: profile.characters, jumpers: "")
76
+ end
77
+
78
+ def memory(container)
79
+ section = container[PETMem::NAME]
80
+ raise FormatError, "the snapshot has no #{PETMem::NAME} module" if section.nil?
81
+ raise FormatError, "#{section}: badline doesn't read this version" unless PETMem.reads?(section)
82
+
83
+ section
84
+ end
85
+
86
+ def model_name(memory)
87
+ board = PETMem.board_of(memory)
88
+ return PETMem.pet2001?(memory) ? "2001" : "3032" if board == PETMem::DISCRETE
89
+
90
+ name = MODELS[board]
91
+ return name unless name.nil?
92
+
93
+ what = board == PETMem::SUPERPET ? "a SuperPET" : "board #{board}"
94
+ raise FormatError, "#{memory}: #{what}, which badline doesn't build"
95
+ end
96
+
97
+ # The editor's frame rate, from the cycles the CRTC's registers set
98
+ # up a frame.
99
+ def refresh_hz(crtc, profile)
100
+ return 60 unless profile.crtc?
101
+ return 50 if crtc.nil?
102
+
103
+ registers = CRTC.registers(crtc)
104
+ (registers[0] + 1) * CRTC.frame_lines(registers) > SIXTY_HZ_FRAME ? 50 : 60
105
+ end
106
+
107
+ # The editor by ROMSets' name where the model's own doesn't set up
108
+ # the CRTC's frame, or nil.
109
+ def editor(crtc, profile, refresh_hz)
110
+ return nil if crtc.nil? || refresh_hz != 50 || profile.video != :crtc80
111
+
112
+ CRTC.registers(crtc)[0] + 1 == EDITOR_0283_LINE ? "4-80-b-50hz-0283" : nil
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module PET
7
+ # The 6502 as xpet 3.10 saves it, MAINCPU 1.3: the clock, the
8
+ # registers, the last opcode and two words after it, then the IRQ
9
+ # and NMI bookkeeping: five clocks, the IRQ and NMI source counts and
10
+ # the pending interrupts. xpet's MAINCPU has no BA line to save.
11
+ #
12
+ # Written: the clock and the registers, which between instructions
13
+ # are the whole CPU, with the IRQ sources that pull the line and a
14
+ # pending IRQ. VICE's interrupt clocks are written as long past.
15
+ #
16
+ # Read: the clock and the registers. The CPU starts on an
17
+ # instruction boundary, as xpet's snapshot is taken on one.
18
+ module MainCPU
19
+ NAME = "MAINCPU"
20
+ MAJOR = 1
21
+ MINOR = 3
22
+ IK_IRQ = 0x02
23
+
24
+ module_function
25
+
26
+ def reads?(section) = section.major == MAJOR && section.minor == MINOR
27
+
28
+ def export(machine)
29
+ cpu = machine.cpu
30
+ fields = FieldWriter.new.qword(machine.cycles)
31
+ [cpu.a, cpu.x, cpu.y, cpu.stack_pointer].each { |register| fields.byte(register) }
32
+ fields.word(cpu.program_counter).byte(cpu.p)
33
+ fields.dword(0).dword(0).dword(0)
34
+ 5.times { fields.qword(0) }
35
+ irqs = [machine.pia1.irq?, machine.pia2.irq?, machine.via.irq?].count(true)
36
+ fields.dword(irqs).dword(0).dword(irqs.positive? ? IK_IRQ : 0)
37
+ fields.section(NAME, MAJOR, MINOR)
38
+ end
39
+
40
+ # The clock, the registers by their BL-CPU keys, in a keyed
41
+ # module's writer.
42
+ def import(section, out)
43
+ fields = FieldReader.new(section)
44
+ clock = fields.qword
45
+ out.int("a", fields.byte).int("x", fields.byte).int("y", fields.byte).int("sp", fields.byte)
46
+ out.int("pc", fields.word).int("p", fields.byte).int("cycles", clock)
47
+ clock
48
+ end
49
+ end
50
+
51
+ # The SuperPET's 6809, CPU6809 1.1, which xpet saves on every model
52
+ # and won't load a snapshot without: the clock and the registers as
53
+ # an xpet with no SuperPET board leaves them. badline doesn't read
54
+ # it.
55
+ module CPU6809
56
+ NAME = "CPU6809"
57
+
58
+ module_function
59
+
60
+ def export(machine)
61
+ FieldWriter.new.qword(machine.cycles).zeros(11).byte(0x04).zeros(7).section(NAME, 1, 1)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module PET
7
+ # CRTC 1.2, which xpet saves on every model, the discrete boards'
8
+ # video too, as a 6545 with the registers fixed: the board's screen
9
+ # settings, the registers, the one selected, the beam's character,
10
+ # row and line in the row, whether the frame is in its vertical
11
+ # adjust, the row's start in screen RAM, the lines of vertical sync
12
+ # left, whether the rows show, the picture's size, the lines in the
13
+ # frame and the line in it, then xpet's picture.
14
+ #
15
+ # Written: the registers and the beam as badline's CRTC, or its
16
+ # discrete video, has them. The picture is left blank, as xpet
17
+ # paints it again.
18
+ #
19
+ # Read: the registers and the beam, with what follows from them: the
20
+ # memory address, the characters since horizontal sync and whether
21
+ # the character shows. The picture starts again with the next
22
+ # frame.
23
+ module CRTC
24
+ NAME = "CRTC"
25
+ MAJOR = 1
26
+ MINOR = 2
27
+ # The registers xpet saves, two more than badline's CRTC has.
28
+ REGISTERS = 20
29
+ # Where the selected register is in the module, and the beam after
30
+ # it.
31
+ SELECTED = 0x23
32
+ # The board's screen settings xpet saves ahead of the registers:
33
+ # the screen RAM's masks and the bits of the frame's start address
34
+ # it latches (BOARD_ATTRIBUTES), then the bytes a character reads
35
+ # and whether the board is a 2001's.
36
+ SETTINGS = [0xff, 0x03, 0x00, 0x20, 0x00, 0x20, 0x00, 0x10, 0xff, 0x1f].freeze
37
+ # The 8296's, whose screen RAM spans 4K.
38
+ SETTINGS_8296 = [0xff, 0x0f, 0x00, 0x20, 0x00, 0x20, 0xff, 0x0f, 0xff, 0x1f].freeze
39
+ # The 2001's and 3000 series' video as xpet's CRTC runs it: 64
40
+ # cycles a line, 32 rows of 8 lines and 4 more, the first 25 rows
41
+ # shown.
42
+ DISCRETE_REGISTERS = [0x3f, 0x28, 0x32, 0x08, 0x1f, 0x04, 0x19, 0x1d, 0x00, 0x07, 0x00, 0x00, 0x10, 0x00,
43
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00].freeze
44
+ DISCRETE_HEIGHT = 216
45
+ DISCRETE_LINE = 64
46
+ DISCRETE_FRAME = DISCRETE_LINE * 260
47
+ # How many cycles badline's beam runs ahead of xpet's, where both
48
+ # drop the vertical drive line on the same cycle.
49
+ DISCRETE_LEAD = 20
50
+ CRTC_LEAD = -3
51
+ HEIGHT = 272
52
+ # A byte xpet writes after the picture's size.
53
+ AFTER_SIZE = 0x20
54
+ # The memory address's bits that pick a byte of screen RAM.
55
+ SCREEN_BITS = 0x0fff
56
+ # The bits above them, which the boards decode as attributes.
57
+ HIGH_BITS = 0x3000
58
+
59
+ # Where the beam is, in xpet's terms: the character, the row and
60
+ # its line, the lines of vertical sync left, whether the rows show,
61
+ # the row's start address and the line in the frame.
62
+ Beam = Data.define(:column, :row, :raster, :vsync, :shown, :start, :frame_line)
63
+
64
+ module_function
65
+
66
+ def reads?(section) = section.major == MAJOR && section.minor == MINOR
67
+
68
+ def width(model) = model.video == :crtc80 ? 704 : 384
69
+
70
+ def height(model) = model.video == :discrete ? DISCRETE_HEIGHT : HEIGHT
71
+
72
+ def columns(model) = model.video == :crtc80 ? 2 : 1
73
+
74
+ # The frame's start address bits xpet latches on the board: MA12,
75
+ # which inverts the 80-column boards' picture.
76
+ def attributes(model) = model.video == :crtc80 ? 0x1000 : 0
77
+
78
+ def export(machine, crtc_input, video_input)
79
+ model = machine.model
80
+ discrete = model.video == :discrete
81
+ registers = discrete ? DISCRETE_REGISTERS : crtc_registers(crtc_input)
82
+ beam = if discrete
83
+ discrete_beam(video_input)
84
+ else
85
+ crtc_beam(run_on(machine, crtc_input, -CRTC_LEAD % frame_cycles(registers)), registers)
86
+ end
87
+ fields = FieldWriter.new.bytes(model.cbm8296? ? SETTINGS_8296 : SETTINGS).word(attributes(model))
88
+ fields.byte(0).byte(columns(model))
89
+ fields.byte(model.basic1? ? 1 : 0).bytes(registers).byte(discrete ? 0 : crtc_input.int("selected", 0))
90
+ beam_fields(fields, beam, registers, model)
91
+ picture(fields, model)
92
+ fields.section(NAME, MAJOR, MINOR)
93
+ end
94
+
95
+ def crtc_registers(input)
96
+ registers = input.bytes("registers", "".b).bytes
97
+ Array.new(REGISTERS) { |index| registers[index] || 0 }
98
+ end
99
+
100
+ def beam_fields(fields, beam, registers, model)
101
+ fields.byte(beam.column).byte(beam.row).byte(beam.raster).zeros(2).flag(beam.raster.zero?)
102
+ fields.word(beam.start & attributes(model)).word((beam.start & SCREEN_BITS) * columns(model))
103
+ fields.byte(beam.vsync).byte(0).flag(beam.shown)
104
+ fields.word(width(model)).word(height(model)).byte(AFTER_SIZE).zeros(3)
105
+ shown = registers[6] * (registers[9] + 1)
106
+ fields.byte([(height(model) - shown) / 2, 0].max).byte(0)
107
+ fields.word(frame_lines(registers)).word(beam.frame_line).byte(0)
108
+ end
109
+
110
+ # Blank, as xpet paints it again: the line it draws, its size twice
111
+ # over, and four lines more than it shows.
112
+ def picture(fields, model)
113
+ width = width(model)
114
+ height = height(model)
115
+ fields.dword(0).dword(width).dword(height).dword(width).zeros(width * (height + 4))
116
+ end
117
+
118
+ def frame_lines(registers) = ((registers[4] + 1) * (registers[9] + 1)) + registers[5]
119
+
120
+ def frame_cycles(registers) = (registers[0] + 1) * frame_lines(registers)
121
+
122
+ # The BL-CRTC fields of a CRTC loaded with those in `input` and run
123
+ # `cycles` cycles on its own, in a machine built as `machine` was.
124
+ def run_on(machine, input, cycles)
125
+ crtc = PETSetup.of(machine).build.crtc
126
+ crtc.load_fields(input)
127
+ cycles.times { crtc.cycle! }
128
+ Keyed::Reader.parse(Keyed.section("BL-CRTC") { |out| crtc.save_fields(out) })
129
+ end
130
+
131
+ # Writes the BL-CRTC fields `input` holds to `out`.
132
+ def copy(input, out)
133
+ out.bytes("registers", input.bytes("registers", "".b)).int("selected", input.int("selected", 0))
134
+ %w[column since_hsync line row vsync_lines row_start address].each { |key| out.int(key, input.int(key, 0)) }
135
+ %w[adjusting row_display column_display].each { |key| out.bool(key, input.bool(key, false)) }
136
+ end
137
+
138
+ # Both move a row's start on at R1 of its last line, and badline's
139
+ # moves it again where the vertical adjust runs past R9 lines,
140
+ # which xpet's doesn't.
141
+ def crtc_beam(input, registers)
142
+ column = input.int("column", 0)
143
+ row = input.int("row", 0)
144
+ line = input.int("line", 0)
145
+ adjusting = input.bool("adjusting", false)
146
+ start = input.int("row_start", 0)
147
+ start = (start - registers[1]) & 0x3fff if adjusting && moved?(line, column, registers)
148
+ rows = registers[4] + 1
149
+ lines = registers[9] + 1
150
+ Beam.new(column:, row: adjusting ? rows : row, raster: adjusting ? 0 : line,
151
+ vsync: input.int("vsync_lines", 0), shown: input.bool("row_display", true), start:,
152
+ frame_line: adjusting ? (rows * lines) + line : (row * lines) + line)
153
+ end
154
+
155
+ # Whether the row's start has moved on by `column` of `line`.
156
+ def moved?(line, column, registers)
157
+ line > registers[9] || (line == registers[9] && column > registers[1])
158
+ end
159
+
160
+ # The discrete video's line in the frame, in xpet's fixed rows: the
161
+ # rows show until line 200, vertical sync runs from row 29 for 16
162
+ # lines, and the 4 lines past row 31 are the vertical adjust.
163
+ def discrete_beam(input)
164
+ cycle = ((input.int("line", 0) * DISCRETE_LINE) + input.int("column", 0) - DISCRETE_LEAD) % DISCRETE_FRAME
165
+ line = cycle / DISCRETE_LINE
166
+ row = [line >> 3, 32].min
167
+ adjusting = row == 32
168
+ raster = adjusting ? 0 : line & 0x07
169
+ column = cycle % DISCRETE_LINE
170
+ sync = line - (DISCRETE_REGISTERS[7] * 8)
171
+ moved = !adjusting && moved?(raster, column, DISCRETE_REGISTERS)
172
+ Beam.new(column:, row:, raster:, vsync: sync.between?(0, 15) ? 16 - sync : 0, shown: line < 200,
173
+ start: 0x1000 + ((moved ? row + 1 : row) * 40), frame_line: line)
174
+ end
175
+
176
+ # The beam the module holds, with the row's start as the module
177
+ # has it: the attribute bits latched in the high word and the
178
+ # offset into screen RAM, in bytes, in the low.
179
+ def beam(section)
180
+ fields = FieldReader.new(section).skip(SELECTED + 1)
181
+ column, row, raster = fields.bytes(3)
182
+ fields.skip(3)
183
+ latched = fields.word
184
+ offset = fields.word
185
+ vsync = fields.byte
186
+ fields.skip(1)
187
+ shown = fields.flag?
188
+ fields.skip(12)
189
+ Beam.new(column:, row:, raster:, vsync:, shown:, start: (latched << 16) | offset,
190
+ frame_line: fields.word)
191
+ end
192
+
193
+ def registers(section) = FieldReader.new(section).skip(SETTINGS.length + 5).bytes(REGISTERS)
194
+
195
+ def selected(section) = FieldReader.new(section).skip(SELECTED).byte
196
+
197
+ # How many cycles into its line xpet's beam is. A beam whose line
198
+ # has moved on a cycle before it starts holds -1.
199
+ def column_of(section)
200
+ column = beam(section).column
201
+ column < 0x80 ? column : column - 0x100
202
+ end
203
+
204
+ # The CRTC's fields by their BL-CRTC keys at the start of the line
205
+ # the module's beam is in (#column_of), in a keyed module's writer,
206
+ # for a board that reads `columns` bytes a character with
207
+ # `attributes` latched from the frame's start.
208
+ def import(section, out, columns, attributes)
209
+ registers = registers(section)
210
+ beam = beam(section)
211
+ rows = registers[4] + 1
212
+ adjusting = beam.row >= rows
213
+ line = adjusting ? beam.frame_line - (rows * (registers[9] + 1)) : beam.raster
214
+ start = start_address(beam.start, registers, columns, attributes)
215
+ if adjusting
216
+ start += registers[1] if line > registers[9]
217
+ elsif moved?(line, column_of(section), registers)
218
+ start -= registers[1]
219
+ end
220
+ start &= 0x3fff
221
+ out.bytes("registers", registers.first(Chips::CRTC::MASKS.length)).int("selected", selected(section) & 0x1f)
222
+ out.int("column", 0).int("line", [line, 0].max).int("row", adjusting ? rows - 1 : beam.row)
223
+ out.int("since_hsync", registers[0] + 1 - registers[2]).int("vsync_lines", beam.vsync)
224
+ out.bool("adjusting", adjusting).bool("row_display", beam.shown).bool("column_display", true)
225
+ out.int("row_start", start).int("address", start)
226
+ end
227
+
228
+ # The row's start address from the module's (#beam): the bits
229
+ # that pick screen RAM from its offset, the attribute bits the
230
+ # board latches from its high word, and those above from R12.
231
+ def start_address(start, registers, columns, attributes)
232
+ high = (registers[12] << 8) & HIGH_BITS & ~attributes
233
+ high | ((start >> 16) & attributes) | (((start & 0xffff) / columns) & SCREEN_BITS)
234
+ end
235
+
236
+ # The discrete video's fields by their BL-VIDEO keys, in a keyed
237
+ # module's writer.
238
+ def import_discrete(section, out)
239
+ cycle = ((beam(section).frame_line * DISCRETE_LINE) + column_of(section) + DISCRETE_LEAD) % DISCRETE_FRAME
240
+ out.int("column", cycle % DISCRETE_LINE).int("line", cycle / DISCRETE_LINE)
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end