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
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module PET
7
+ # PETMEM 1.3: the board, the RAM below $8000 in K and the 8096's
8
+ # control register at $FFF0, then the RAM, the screen RAM, 1K or 2K
9
+ # as the board has, or on the 8296 its 32K from $8000, an 8096's or
10
+ # 8296's 64K board, and 24 bytes of xpet's own settings, which name
11
+ # the 2001.
12
+ #
13
+ # The board is 0 for the discrete video, 1 for a 40-column CRTC, 2
14
+ # for an 80-column one, 3 for the SuperPET, 4 for the 8096 and 5 for
15
+ # the 8296. xpet's ROMs aren't in the snapshot.
16
+ module PETMem
17
+ NAME = "PETMEM"
18
+ MAJOR = 1
19
+ MINOR = 3
20
+ DISCRETE = 0
21
+ CRTC40 = 1
22
+ CRTC80 = 2
23
+ SUPERPET = 3
24
+ CBM8096 = 4
25
+ CBM8296 = 5
26
+ # The byte after the header's control register, as xpet writes it.
27
+ HEADER_END = 0x05
28
+ # xpet's settings at the end, but for the second byte, which is
29
+ # SETTINGS_2001 on a 2001.
30
+ SETTINGS = [0x00, 0x00, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00, 0x40, 0x00, 0x00, 0x00,
31
+ 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00].freeze
32
+ SETTINGS_2001 = 0x03
33
+ EXPANSION = 0x10000
34
+
35
+ module_function
36
+
37
+ def reads?(section) = section.major == MAJOR && section.minor == MINOR
38
+
39
+ def board(model)
40
+ return CBM8296 if model.cbm8296?
41
+ return CBM8096 if model.expansion
42
+
43
+ case model.video
44
+ when :crtc40 then CRTC40
45
+ when :crtc80 then CRTC80
46
+ else DISCRETE
47
+ end
48
+ end
49
+
50
+ def export(machine)
51
+ bus = machine.bus
52
+ expansion = bus.expansion
53
+ fields = FieldWriter.new.byte(board(machine.model)).byte(0).byte(machine.ram_kb)
54
+ fields.byte(expansion ? expansion.control : 0).byte(HEADER_END)
55
+ fields.bytes(machine.ram.snapshot).bytes(bus.screen.cells)
56
+ fields.bytes(expansion.cells) if expansion
57
+ settings = SETTINGS.dup
58
+ settings[1] = SETTINGS_2001 if machine.model.basic1?
59
+ fields.bytes(settings).section(NAME, MAJOR, MINOR)
60
+ end
61
+
62
+ # The board's number in the header.
63
+ def board_of(section) = FieldReader.new(section).byte
64
+
65
+ def ram_kb(section) = FieldReader.new(section).skip(2).byte
66
+
67
+ # Whether xpet's settings at the end name a 2001.
68
+ def pet2001?(section)
69
+ data = section.data
70
+ data.bytesize >= SETTINGS.length && data.getbyte(data.bytesize - SETTINGS.length + 1).to_i.anybits?(0x01)
71
+ end
72
+
73
+ # The RAM, the screen RAM and an 8096's or 8296's board and control
74
+ # register
75
+ # by their BL-RAM keys, in a keyed module's writer, for a machine
76
+ # with `screen_bytes` of screen RAM.
77
+ def import(section, out, screen_bytes)
78
+ fields = FieldReader.new(section)
79
+ board = fields.byte
80
+ fields.skip(1)
81
+ ram_kb = fields.byte
82
+ control = fields.byte
83
+ fields.skip(1)
84
+ out.bytes("ram", fields.string(ram_kb * 1024)).bytes("screen", fields.string(screen_bytes))
85
+ return unless [CBM8096, CBM8296].include?(board)
86
+
87
+ out.int("expansion.control", control).bytes("expansion.ram", fields.string(EXPANSION))
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module PET
7
+ # PIA1 and PIA2 1.0: port A's output and direction registers and
8
+ # its control register as read, with the CA1 and CA2 flags in bits 7
9
+ # and 6, the same for port B, and the levels on CA2 and CB2 in bits
10
+ # 7 and 6 of the last byte, high while a line is an input.
11
+ #
12
+ # Read: each side's flags come from its control register, and a
13
+ # handshake under way from the level its C2 output is at. The levels
14
+ # outside on CA1 and CB1 aren't saved: they stay as the machine has
15
+ # them, with PIA 1's CB1 on the vertical drive line.
16
+ module PIAs
17
+ NAMES = %w[PIA1 PIA2].freeze
18
+ MAJOR = 1
19
+ MINOR = 0
20
+ CA2_HIGH = 0x80
21
+ CB2_HIGH = 0x40
22
+ HANDSHAKE = 0x20
23
+
24
+ module_function
25
+
26
+ def reads?(section) = section.major == MAJOR && section.minor == MINOR
27
+
28
+ # The PIA's module, from its BL-PIA1 or BL-PIA2 fields in `input`.
29
+ def export(name, pia, input)
30
+ fields = FieldWriter.new.byte(input.int("ora", 0)).byte(input.int("ddra", 0)).byte(pia.cra)
31
+ fields.byte(input.int("orb", 0)).byte(input.int("ddrb", 0)).byte(pia.crb)
32
+ fields.byte((pia.ca2_output ? CA2_HIGH : 0) | (pia.cb2_output ? CB2_HIGH : 0))
33
+ fields.section(name, MAJOR, MINOR)
34
+ end
35
+
36
+ # The registers and the control lines by their BL-PIA keys, in a
37
+ # keyed module's writer.
38
+ def import(section, out)
39
+ ora, ddra, cra, orb, ddrb, crb, lines = FieldReader.new(section).bytes(7)
40
+ out.int("ora", ora).int("ddra", ddra).int("orb", orb).int("ddrb", ddrb)
41
+ control_lines(out, "ca.", cra, lines.anybits?(CA2_HIGH))
42
+ control_lines(out, "cb.", crb, lines.anybits?(CB2_HIGH))
43
+ end
44
+
45
+ def control_lines(out, prefix, register, c2_high)
46
+ control = register & 0x3f
47
+ out.int("#{prefix}control", control).bool("#{prefix}c1_flag", register.anybits?(0x80))
48
+ out.bool("#{prefix}c2_flag", register.anybits?(0x40)).int("#{prefix}pulse", 0)
49
+ out.bool("#{prefix}handshake_low", control & 0x38 == HANDSHAKE && !c2_high)
50
+ out.bool("#{prefix}c2_high", c2_high) if control.nobits?(0x20)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module PET
7
+ # Restores a PET from a snapshot xpet wrote. The modules badline
8
+ # reads become the PET's own keyed modules (Keyed), which it loads
9
+ # as it loads its own snapshot. Whatever they don't set stays as the
10
+ # machine has it at power-on, and the datasettes' tapes and the
11
+ # disk device 8 serves stay as they are.
12
+ module Restore
13
+ # MAINCPU and PETMEM hold the machine itself, so a version badline
14
+ # doesn't read fails the restore. The others are left out and
15
+ # reported.
16
+ READ = [MainCPU::NAME, PETMem::NAME, CRTC::NAME, "PIA1", "PIA2", VIA::NAME].freeze
17
+ # The host's media, which a restore keeps.
18
+ KEPT = %w[BL-TAPE1 BL-TAPE2 BL-TRAPDOS].freeze
19
+
20
+ module_function
21
+
22
+ # Applies the modules badline reads to `computer`, and returns a
23
+ # Report. The snapshot goes into a new machine first, so one that
24
+ # fails leaves `computer` as it was, and one of a machine built
25
+ # another way fails too.
26
+ def import(container, computer)
27
+ unless computer.family == :pet
28
+ raise FormatError, "the snapshot is of a PET, not a #{Image::FAMILIES.fetch(computer.family.to_s)}"
29
+ end
30
+
31
+ setup = PET.setup(container)
32
+ ours = PETSetup.of(computer)
33
+ raise FormatError, "the snapshot is of a #{setup}, not a #{ours}" unless setup == ours
34
+
35
+ apply(container, setup.build)
36
+ computer.power_cycle!
37
+ report = apply(container, computer)
38
+ kept = computer.mounted? ? ["the disk mounted in device 8 stays mounted"] : []
39
+ Report.new(applied: report.applied, ignored: report.ignored + kept)
40
+ end
41
+
42
+ # Applies the modules badline reads to `computer`, a machine built
43
+ # as the snapshot's was, and returns the Report.
44
+ def apply(container, computer)
45
+ applied = []
46
+ ignored = []
47
+ sections = []
48
+ READ.each do |name|
49
+ section = container[name]
50
+ next if section.nil?
51
+
52
+ if reads?(section)
53
+ applied << name
54
+ else
55
+ raise FormatError, "#{section}: badline doesn't read this version" if required?(name)
56
+
57
+ ignored << "#{section}: badline doesn't read this version, left out"
58
+ end
59
+ end
60
+ computer.save_modules.each { |section| sections << section if KEPT.include?(section.name) }
61
+ computer.load_modules(Keyed::Modules.new(modules(container, computer, applied) + sections))
62
+ Report.new(applied:, ignored: ignored + left_out(container))
63
+ end
64
+
65
+ def required?(name) = [MainCPU::NAME, PETMem::NAME].include?(name)
66
+
67
+ def reads?(section)
68
+ case section.name
69
+ when MainCPU::NAME then MainCPU.reads?(section)
70
+ when PETMem::NAME then PETMem.reads?(section)
71
+ when CRTC::NAME then CRTC.reads?(section)
72
+ when VIA::NAME then VIA.reads?(section)
73
+ else PIAs.reads?(section)
74
+ end
75
+ end
76
+
77
+ # The PET's keyed modules from the ones `applied` names.
78
+ def modules(container, computer, applied)
79
+ model = computer.model
80
+ crtc = applied.include?(CRTC::NAME) ? container[CRTC::NAME] : nil
81
+ video = video(crtc, computer)
82
+ drive = driving?(crtc, video, model)
83
+ sections = [Keyed.section("BL-MACHINE") { |out| machine(out, container, computer, drive) },
84
+ Keyed.section("BL-RAM") do |out|
85
+ PETMem.import(container[PETMem::NAME], out, model.screen_bytes)
86
+ end]
87
+ sections << Keyed.section("BL-CPU") { |out| MainCPU.import(container[MainCPU::NAME], out) }
88
+ %w[PIA1 PIA2].each do |name|
89
+ next unless applied.include?(name)
90
+
91
+ sections << Keyed.section("BL-#{name}") do |out|
92
+ PIAs.import(container[name], out)
93
+ out.bool("cb.c1_high", drive) if name == "PIA1"
94
+ end
95
+ end
96
+ if applied.include?(VIA::NAME)
97
+ sections << Keyed.section("BL-VIA") { |out| VIA.import(container[VIA::NAME], out) }
98
+ end
99
+ sections + video
100
+ end
101
+
102
+ def machine(out, container, computer, drive)
103
+ out.text("family", "pet")
104
+ PETSetup.of(computer).write_fields(out)
105
+ out.int("cycles", FieldReader.new(container[MainCPU::NAME]).qword).bool("vertical_drive", drive)
106
+ end
107
+
108
+ # The vertical drive line, low in the CRTC's vertical sync or past
109
+ # the discrete video's rows, as the video's module, `video`, has it.
110
+ def driving?(crtc, video, model)
111
+ return true if crtc.nil?
112
+
113
+ fields = Keyed::Reader.parse(video.first)
114
+ if model.video == :discrete
115
+ fields.int("line", 0) < Badline::PET::DiscreteVideo::DISPLAYED_LINES
116
+ else
117
+ fields.int("vsync_lines", 0).zero?
118
+ end
119
+ end
120
+
121
+ # The discrete video's module, or the CRTC's, run on from xpet's
122
+ # counters to where badline's drops the vertical drive line on the
123
+ # cycle xpet's does (CRTC::CRTC_LEAD). The CRTC runs on from the
124
+ # start of the beam's line, or for a beam in the first cycles of
125
+ # its line, a frame less those cycles on, which lands on the same
126
+ # counters while the registers stood through the frame as they
127
+ # stand.
128
+ def video(crtc, computer)
129
+ return [] if crtc.nil?
130
+
131
+ model = computer.model
132
+ return [Keyed.section("BL-VIDEO") { |out| CRTC.import_discrete(crtc, out) }] if model.video == :discrete
133
+
134
+ counters = Keyed.section("BL-CRTC") do |out|
135
+ CRTC.import(crtc, out, CRTC.columns(model), CRTC.attributes(model))
136
+ end
137
+ cycles = (CRTC::CRTC_LEAD + CRTC.column_of(crtc)) % CRTC.frame_cycles(CRTC.registers(crtc))
138
+ fields = CRTC.run_on(computer, Keyed::Reader.parse(counters), cycles)
139
+ [Keyed.section("BL-CRTC") { |out| CRTC.copy(fields, out) }]
140
+ end
141
+
142
+ def left_out(container)
143
+ container.sections.reject { |section| READ.include?(section.name) }
144
+ .map { |section| "#{section}: badline doesn't read this module, left out" }
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module PET
7
+ # VIA 2.2, the 6522 at $E840: port A's and port B's output and
8
+ # direction registers, timer 1's latch and counter, timer 2's low
9
+ # and high latch, the value it was last loaded with and its counter,
10
+ # which timers will set their interrupt flag (timer 1 in bit 7,
11
+ # timer 2 in bit 6 while it counts cycles), the shift register, the
12
+ # ACR, the PCR, the interrupt flags and enable mask, timer 1's level
13
+ # on PB7 in bit 7, the shift register's clock ticks into its byte,
14
+ # CA2's level in bit 7, and whether timer 2's next underflow sets
15
+ # its flag. xpet saves each counter as a read on the next cycle
16
+ # finds it, where badline's holds it before that cycle counts.
17
+ #
18
+ # Written: timer 2's high latch and the value it was loaded with,
19
+ # which badline doesn't keep, as xpet has them before a program
20
+ # loads it, or while the shift register clocks off it, as its low
21
+ # byte reloads. Free-running, the shift register's ticks are only
22
+ # whether CB1 is low.
23
+ #
24
+ # Read: the registers and both timers with their latches, as they
25
+ # count from the next cycle, and the shift register where its clock
26
+ # ticks are. A load or reload under way, a shift clock tick due off
27
+ # timer 2 and the latched ports start afresh, and CB2's level
28
+ # follows from the PCR.
29
+ module VIA
30
+ NAME = "VIA"
31
+ MAJOR = 2
32
+ MINOR = 2
33
+ T1_ARMED = 0x80
34
+ T2_TIMED = 0x40
35
+ LEVELS = 0x60
36
+ # The shift register's clock ticks a byte takes, which an idle
37
+ # register's count stands at.
38
+ TICKS = 16
39
+ SIZE = 28
40
+ # The shift register's modes that clock off timer 2.
41
+ T2_SHIFTS = [1, 4, 5].freeze
42
+
43
+ module_function
44
+
45
+ def reads?(section) = section.major == MAJOR && section.minor == MINOR
46
+
47
+ # The VIA's module, from its BL-VIA fields in `input`.
48
+ def export(input)
49
+ fields = FieldWriter.new
50
+ %w[ora ddra orb ddrb].each { |key| fields.byte(input.int(key, 0)) }
51
+ fields.word(input.int("t1.latch", 0xffff)).word(timer1(input))
52
+ t2 = timer2(input)
53
+ latch_low = input.int("t2.latch_low", 0xff)
54
+ fields.byte(latch_low).byte(0xff).word(shifting?(input) ? (t2 & 0xff00) | latch_low : 0xffff).word(t2)
55
+ fields.byte(armed(input)).byte(input.int("sr.data", 0))
56
+ fields.byte(input.int("acr", 0)).byte(input.int("pcr", 0))
57
+ fields.byte(input.int("ifr.flags", 0)).byte(input.int("ifr.enable", 0))
58
+ fields.byte(input.bool("t1.pb7", true) ? 0x80 : 0)
59
+ fields.byte(ticks(input))
60
+ fields.byte(LEVELS | (input.bool("ca.c2_output", true) ? 0x80 : 0)).zeros(2)
61
+ fields.byte(input.bool("t2.armed", false) ? 1 : 0).zeros(2).section(NAME, MAJOR, MINOR)
62
+ end
63
+
64
+ # Timer 1's counter as a read on the next cycle finds it: as it
65
+ # stands while a load holds it, the latch while it reloads, and one
66
+ # less otherwise.
67
+ def timer1(input)
68
+ counter = input.int("t1.counter", 0xffff)
69
+ return counter if input.bool("t1.hold", false)
70
+ return input.int("t1.latch", 0xffff) if input.bool("t1.reload", false)
71
+
72
+ (counter - 1) & 0xffff
73
+ end
74
+
75
+ # Timer 2's counter as a read on the next cycle finds it: as it
76
+ # stands while it counts pulses or a load holds it, and one less
77
+ # otherwise.
78
+ def timer2(input)
79
+ counter = input.int("t2.counter", 0xffff)
80
+ return counter if input.bool("t2.hold", false) || input.bool("t2.counting_pulses", false)
81
+
82
+ (counter - 1) & 0xffff
83
+ end
84
+
85
+ # The shift register's clock ticks into its byte: two a bit, the
86
+ # first one dropping CB1, TICKS when idle. Free-running, its bits
87
+ # never run out, and only whether CB1 is low counts.
88
+ def ticks(input)
89
+ bits = input.int("sr.bits", 0)
90
+ return TICKS if bits.zero?
91
+
92
+ low = input.bool("sr.clock", true) ? 0 : 1
93
+ return low if input.int("sr.mode", 0) == 4
94
+
95
+ ((8 - bits) * 2) + low
96
+ end
97
+
98
+ # The shift register from its mode, data and the ticks into its
99
+ # byte (#ticks), with the last bit put out on CB2.
100
+ def shift_register(out, mode, data, ticks)
101
+ ticks = [ticks, TICKS].min
102
+ bits = ticks == TICKS ? 0 : 8 - (ticks / 2)
103
+ bits = 8 if mode == 4 && ticks < TICKS
104
+ out.int("sr.mode", mode).int("sr.data", data).int("sr.bits", bits).int("sr.delay", 0)
105
+ out.bool("sr.clock", ticks.even?).bool("sr.cb2", ticks.zero? || data.anybits?(0x01))
106
+ end
107
+
108
+ # Whether the shift register clocks off timer 2.
109
+ def shifting?(input) = T2_SHIFTS.include?((input.int("acr", 0) >> 2) & 0x07)
110
+
111
+ # Timer 2 sets its flag, or clocks the shift register, while it
112
+ # counts cycles.
113
+ def armed(input)
114
+ pulses = input.bool("t2.count_pulses", false)
115
+ timed = !pulses && (input.bool("t2.armed", false) || shifting?(input))
116
+ (input.bool("t1.armed", false) ? T1_ARMED : 0) | (timed ? T2_TIMED : 0)
117
+ end
118
+
119
+ # The registers, the timers and the control lines by their BL-VIA
120
+ # keys, in a keyed module's writer.
121
+ def import(section, out)
122
+ bytes = FieldReader.new(section).bytes(SIZE)
123
+ acr = bytes[0x10]
124
+ pcr = bytes[0x11]
125
+ out.int("ora", bytes[0]).int("ddra", bytes[1]).int("orb", bytes[2]).int("ddrb", bytes[3])
126
+ out.int("acr", acr).int("pcr", pcr)
127
+ timers(out, bytes, acr)
128
+ shift_register(out, (acr >> 2) & 0x07, bytes[0x0f], bytes[0x15])
129
+ out.int("ifr.flags", bytes[0x12] & 0x7f).int("ifr.enable", bytes[0x13] & 0x7f)
130
+ control_lines(out, "ca.", pcr & 0x0f, bytes[0x16].anybits?(0x80))
131
+ control_lines(out, "cb.", pcr >> 4, true)
132
+ end
133
+
134
+ def timers(out, bytes, acr)
135
+ out.int("t1.latch", bytes[4] | (bytes[5] << 8)).int("t1.counter",
136
+ ((bytes[6] | (bytes[7] << 8)) + 1) & 0xffff)
137
+ out.bool("t1.hold", false)
138
+ out.bool("t1.reload", false).bool("t1.armed", bytes[0x0e].anybits?(T1_ARMED))
139
+ out.bool("t1.pb7", bytes[0x14].anybits?(0x80))
140
+ pulses = acr.anybits?(0x20)
141
+ t2 = bytes[0x0c] | (bytes[0x0d] << 8)
142
+ out.int("t2.latch_low", bytes[8]).int("t2.counter", pulses ? t2 : (t2 + 1) & 0xffff).bool("t2.hold", false)
143
+ out.bool("t2.low_reload", false).bool("t2.low_underflowed", false)
144
+ out.bool("t2.armed", bytes[0x19].anybits?(0x01))
145
+ out.bool("t2.count_pulses", pulses).bool("t2.counting_pulses", pulses)
146
+ end
147
+
148
+ # A handshake under way holds C2 low.
149
+ def control_lines(out, prefix, control, c2_high)
150
+ mode = control >> 1
151
+ handshake_low = mode == 4 && !c2_high
152
+ output = case mode
153
+ when 4 then !handshake_low
154
+ when 6 then false
155
+ else true
156
+ end
157
+ out.int("#{prefix}control", control).bool("#{prefix}handshake_low", handshake_low)
158
+ out.int("#{prefix}pulse", 0).bool("#{prefix}c2_output", output)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -13,6 +13,7 @@ require "badline/snapshot/vice/peripherals"
13
13
  require "badline/snapshot/vice/vicii"
14
14
  require "badline/snapshot/vice/vicii_import"
15
15
  require "badline/snapshot/vice/restore"
16
+ require "badline/snapshot/vice/pet"
16
17
 
17
18
  module Badline
18
19
  module Snapshot
@@ -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
@@ -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