badline 0.16.1 → 0.17.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +102 -12
  5. data/doc/media.md +5 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +376 -69
  8. data/doc/snapshots.md +148 -14
  9. data/lib/badline/cbm2/acia.rb +10 -0
  10. data/lib/badline/cbm2/bus.rb +263 -0
  11. data/lib/badline/cbm2/cpu.rb +58 -0
  12. data/lib/badline/cbm2/crtc_video.rb +133 -0
  13. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  14. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  15. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  16. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  17. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  18. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  19. data/lib/badline/cbm2/keyed_state.rb +91 -0
  20. data/lib/badline/cbm2/model.rb +44 -0
  21. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  22. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  23. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  24. data/lib/badline/cbm2/p500/model.rb +39 -0
  25. data/lib/badline/cbm2/p500/pages.rb +57 -0
  26. data/lib/badline/cbm2/p500.rb +262 -0
  27. data/lib/badline/cbm2/sid_page.rb +19 -0
  28. data/lib/badline/cbm2.rb +267 -0
  29. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  30. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  31. data/lib/badline/chips/acia.rb +307 -0
  32. data/lib/badline/chips/crtc.rb +3 -2
  33. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  34. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  35. data/lib/badline/chips/riot/timer.rb +80 -0
  36. data/lib/badline/chips/riot.rb +162 -0
  37. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  38. data/lib/badline/chips/rriot.rb +132 -0
  39. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  40. data/lib/badline/chips/tpi.rb +233 -0
  41. data/lib/badline/chips/vic/bank.rb +60 -5
  42. data/lib/badline/chips/vic/sprite.rb +4 -3
  43. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  44. data/lib/badline/drives/datasette.rb +27 -3
  45. data/lib/badline/drives/model8050/disk.rb +163 -0
  46. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  47. data/lib/badline/drives/model8050/idle.rb +177 -0
  48. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  49. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  50. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  51. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  52. data/lib/badline/drives/model8050/parts.rb +37 -0
  53. data/lib/badline/drives/model8050.rb +243 -0
  54. data/lib/badline/frontend/app.rb +10 -3
  55. data/lib/badline/frontend/boot.rb +1 -1
  56. data/lib/badline/frontend/controls.rb +51 -15
  57. data/lib/badline/frontend/gamepads.rb +3 -3
  58. data/lib/badline/frontend/keys.rb +61 -2
  59. data/lib/badline/frontend/media_slots.rb +1 -1
  60. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  61. data/lib/badline/frontend/menu/menu_pages.rb +18 -4
  62. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  63. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  64. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  65. data/lib/badline/frontend/timeline.rb +19 -10
  66. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  67. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  68. data/lib/badline/kernal_trap/dos.rb +18 -5
  69. data/lib/badline/kernal_trap/ieee.rb +4 -2
  70. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  71. data/lib/badline/machine/ieee_drives.rb +194 -0
  72. data/lib/badline/machine/settling.rb +4 -0
  73. data/lib/badline/machine.rb +23 -6
  74. data/lib/badline/media/cbm2_media.rb +29 -0
  75. data/lib/badline/media/program.rb +6 -3
  76. data/lib/badline/media/true_drive.rb +38 -9
  77. data/lib/badline/media.rb +15 -6
  78. data/lib/badline/options/family.rb +15 -3
  79. data/lib/badline/options/help.rb +16 -2
  80. data/lib/badline/options/table.rb +18 -4
  81. data/lib/badline/options/validation.rb +27 -3
  82. data/lib/badline/options.rb +21 -7
  83. data/lib/badline/pet/cassettes.rb +22 -1
  84. data/lib/badline/pet/ieee_drive.rb +5 -64
  85. data/lib/badline/pet/ieee_ports.rb +7 -0
  86. data/lib/badline/pet.rb +15 -4
  87. data/lib/badline/plus4/acia.rb +4 -298
  88. data/lib/badline/plus4/keyed_parts.rb +0 -44
  89. data/lib/badline/plus4.rb +1 -0
  90. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  91. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  92. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  93. data/lib/badline/roms/README +111 -0
  94. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  95. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  96. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  97. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  98. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  99. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  100. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  101. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  102. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  103. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  104. data/lib/badline/snapshot/container.rb +7 -2
  105. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  106. data/lib/badline/snapshot/field_doc.rb +38 -3
  107. data/lib/badline/snapshot/image.rb +62 -9
  108. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  109. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  110. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  111. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  112. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  113. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  114. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  115. data/lib/badline/snapshot/vice/cias.rb +13 -9
  116. data/lib/badline/snapshot/vice/p500.rb +96 -0
  117. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  118. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  119. data/lib/badline/snapshot/vice/pet.rb +7 -3
  120. data/lib/badline/snapshot/vice/pet_cpu.rb +5 -2
  121. data/lib/badline/snapshot/vice/pet_crtc.rb +10 -15
  122. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  123. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  124. data/lib/badline/snapshot/vice/pet_restore.rb +70 -6
  125. data/lib/badline/snapshot/vice/pet_via.rb +6 -5
  126. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  127. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  128. data/lib/badline/snapshot/vice.rb +3 -0
  129. data/lib/badline/snapshot.rb +11 -3
  130. data/lib/badline/storage/crt_file.rb +7 -1
  131. data/lib/badline/storage/d64_image.rb +7 -0
  132. data/lib/badline/storage/d80_image.rb +17 -1
  133. data/lib/badline/storage/d81_image.rb +14 -0
  134. data/lib/badline/storage/disk_image/bam.rb +18 -3
  135. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  136. data/lib/badline/storage/disk_image/writing.rb +35 -0
  137. data/lib/badline/storage/disk_image.rb +2 -0
  138. data/lib/badline/storage.rb +1 -0
  139. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  140. data/lib/badline/support/p500_checkpoint.rb +35 -0
  141. data/lib/badline/version.rb +1 -1
  142. data/lib/badline.rb +1 -0
  143. metadata +72 -2
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module P500
7
+ # VIC-II 1.3 as xcbm5x0 writes it, which runs VICE's line-based
8
+ # VIC-II rather than x64sc's, so the module is laid out otherwise
9
+ # than x64sc's under the same name: whether bad lines may happen,
10
+ # whether this line is one, the vertical border, the colour
11
+ # buffer, colour RAM, whether the sequencer is idle, the light
12
+ # pen's trigger and position, the video matrix buffer, the sprites
13
+ # whose DMA starts next line, a word xcbm5x0 keeps at $00100000,
14
+ # the beam's cycle and line, the registers, the sprite collisions
15
+ # pending, the sprites' DMA, the VIC's bank, VC, the matrix's
16
+ # width, VCBASE, then sprite and drawing fields with the bank
17
+ # again, the line its picture draws next and its size, and the
18
+ # picture. xcbm5x0's cycle in the line is badline's column.
19
+ #
20
+ # Written: the registers, colour RAM, the buffers, the beam with
21
+ # its counters and the sprites' DMA as badline's VIC has them, the
22
+ # bank as TPI2's PC6 and PC7 pick it, and the rest as xcbm5x0
23
+ # leaves it. The picture is left blank, as xcbm5x0 paints it again.
24
+ #
25
+ # Read: the registers, colour RAM, the buffers, the beam with VC,
26
+ # VCBASE and whether the sequencer is idle, the light pen and the
27
+ # sprites' DMA, through the x64sc module's reader (VICII::Import).
28
+ # The row counter follows from the line and YSCROLL, and the
29
+ # sprites' counters and the borders' flip-flops start afresh.
30
+ # xcbm5x0's VIC isn't x64sc's, so a program badline runs on from its
31
+ # snapshot reads the raster a cycle or so off now and then.
32
+ module VICII
33
+ NAME = Vice::VICII::NAME
34
+ MAJOR = 1
35
+ MINOR = 3
36
+ ALLOW_BAD_LINES = 0x000
37
+ BAD_LINE = 0x001
38
+ BLANK = 0x002
39
+ COLOR_BUFFER = 0x003
40
+ COLOR_RAM = 0x02b
41
+ IDLE = 0x42b
42
+ LIGHT_PEN = 0x42c
43
+ MATRIX_BUFFER = 0x42f
44
+ RAM_BASE = 0x458
45
+ CYCLE = 0x45c
46
+ LINE = 0x45d
47
+ REGISTERS = 0x45f
48
+ SPRITE_DMA = 0x4a0
49
+ BANK = 0x4a2
50
+ VC = 0x4a4
51
+ VC_ADD = 0x4a6
52
+ VC_BASE = 0x4a7
53
+ VIDEO_INT = 0x4a9
54
+ DRAW_FLAGS = 0x4cd
55
+ FETCH_BANK = 0x4cf
56
+ DRAWN_LINE = 0x4d1
57
+ PICTURE = 0x4d5
58
+ HEADER = 0x4e1
59
+ # The picture's size: PAL's and NTSC's.
60
+ SIZES = { 312 => [504, 312], 263 => [520, 275] }.freeze
61
+
62
+ module_function
63
+
64
+ def reads?(section) = section.major == MAJOR && section.minor == MINOR
65
+
66
+ # The lines xcbm5x0's picture has, which name its region.
67
+ def picture_lines(section) = FieldReader.new(section).skip(PICTURE + 4).dword
68
+
69
+ def export(machine)
70
+ vic = machine.vic
71
+ width, height = SIZES.fetch(vic.region.lines_per_frame)
72
+ bytes = Array.new(HEADER + (width * (height + 4)), 0)
73
+ sequencer(vic, bytes)
74
+ bytes[COLOR_RAM, 0x400] = Array.new(0x400) { |offset| machine.color_ram.nibble(0xd400 + offset) }
75
+ beam(vic, bytes)
76
+ bank = ((machine.tpi2.port_c_output >> 6) & 0x03) << 14
77
+ [BANK, FETCH_BANK].each { |at| word(bytes, at, bank) }
78
+ [RAM_BASE + 2, DRAW_FLAGS].each { |at| bytes[at] = 0x10 }
79
+ bytes[VIDEO_INT] = 1
80
+ [vic.rasterline, width, height, width].each_with_index do |value, n|
81
+ dword(bytes, DRAWN_LINE + (4 * n), value)
82
+ end
83
+ FieldWriter.new.bytes(bytes).section(NAME, MAJOR, MINOR)
84
+ end
85
+
86
+ # The sequencer's state and the buffers it reads.
87
+ def sequencer(vic, bytes)
88
+ state = vic.display_state
89
+ bytes[ALLOW_BAD_LINES] = state.bad_lines_enabled ? 1 : 0
90
+ bytes[BAD_LINE] = state.bad_line_condition? ? 1 : 0
91
+ bytes[BLANK] = vic.sequencer.borders.first ? 1 : 0
92
+ bytes[COLOR_BUFFER, 40] = vic.color_buffer.map { |value| value || 0 }
93
+ bytes[IDLE] = state.display? ? 0 : 1
94
+ bytes[MATRIX_BUFFER, 40] = vic.character_buffer.map { |value| value || 0 }
95
+ end
96
+
97
+ # The beam, the registers and the counters.
98
+ def beam(vic, bytes)
99
+ state = vic.display_state
100
+ bytes[CYCLE] = vic.column
101
+ word(bytes, LINE, vic.rasterline)
102
+ bytes[REGISTERS, 0x40] = vic.register_file
103
+ bytes[SPRITE_DMA] = 8.times.sum { |n| vic.sprites[n].displaying? ? 1 << n : 0 }
104
+ word(bytes, VC, state.vc)
105
+ bytes[VC_ADD] = 40
106
+ word(bytes, VC_BASE, state.vc_base)
107
+ end
108
+
109
+ def word(bytes, at, value) = bytes[at, 2] = [value & 0xffff].pack("v").bytes
110
+
111
+ def dword(bytes, at, value) = bytes[at, 4] = [value].pack("V").bytes
112
+
113
+ def import(section, vic, color_ram)
114
+ Import.new(section, vic.region).apply_to(vic, color_ram, 0xd400)
115
+ end
116
+
117
+ # Reads xcbm5x0's module into the fields x64sc's reader puts into
118
+ # badline's VIC.
119
+ class Import < Vice::VICII::Import
120
+ # x64sc's cycle is badline's column plus one, its cycle 0 the
121
+ # last column of the line before (VICII.vice_cycle).
122
+ def initialize(section, region)
123
+ super(nil)
124
+ bytes = section.data.bytes
125
+ @registers = bytes[REGISTERS, 0x40]
126
+ line = bytes[LINE] | (bytes[LINE + 1] << 8)
127
+ @cycle = (bytes[CYCLE] + 1) % region.cycles_per_line
128
+ @line = @cycle.zero? ? (line + 1) % region.lines_per_frame : line
129
+ @frame_start = false
130
+ @irq_status = @registers[0x19]
131
+ @raster_match = false
132
+ @character_buffer = bytes[MATRIX_BUFFER, 40]
133
+ @color_buffer = bytes[COLOR_BUFFER, 40]
134
+ read_old_counters(bytes)
135
+ @color_ram = bytes[COLOR_RAM, 0x400]
136
+ @sprite_mc = Array.new(8, 0)
137
+ @sprite_mcbase = Array.new(8, 0)
138
+ @sprite_exp_ff = 0
139
+ end
140
+
141
+ private
142
+
143
+ def read_old_counters(bytes)
144
+ @bad_lines_enabled = !bytes[ALLOW_BAD_LINES].zero?
145
+ @collisions = [@registers[0x1e], @registers[0x1f]]
146
+ @display = bytes[IDLE].zero?
147
+ @vc_base = bytes[VC_BASE] | (bytes[VC_BASE + 1] << 8)
148
+ @vc = bytes[VC] | (bytes[VC + 1] << 8)
149
+ @rc = @display ? (@line - @registers[0x11]) & 0x07 : 7
150
+ @vmli = 0
151
+ @lp_low = false
152
+ @lp_triggered = !bytes[LIGHT_PEN].zero?
153
+ @fetch_d011 = @registers[0x11]
154
+ @dma_bits = bytes[SPRITE_DMA]
155
+ @display_bits = @dma_bits
156
+ blank = !bytes[BLANK].zero?
157
+ @vertical_border = blank
158
+ @vertical_armed = false
159
+ @main_border = blank
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -5,13 +5,16 @@ require "badline/snapshot/vice/pet_mem"
5
5
  require "badline/snapshot/vice/pet_pias"
6
6
  require "badline/snapshot/vice/pet_via"
7
7
  require "badline/snapshot/vice/pet_crtc"
8
+ require "badline/snapshot/vice/pet_drive"
9
+ require "badline/snapshot/vice/pet_drive8050"
8
10
  require "badline/snapshot/vice/pet_restore"
9
11
 
10
12
  module Badline
11
13
  module Snapshot
12
14
  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
+ # xpet's modules for the PET: the CPU, the RAM, the CRTC, both PIAs,
16
+ # the VIA and a true 2031, written from badline's machine and read
17
+ # into it. Each
15
18
  # module's file says what maps and what is approximated. They go
16
19
  # through the PET's own keyed fields (Keyed), which a save writes and a
17
20
  # load reads, so they read no chip's insides.
@@ -45,7 +48,8 @@ module Badline
45
48
  [MainCPU.export(machine), CPU6809.export(machine), PETMem.export(machine),
46
49
  CRTC.export(machine, reader(readers, "BL-CRTC"), reader(readers, "BL-VIDEO")),
47
50
  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"))] +
51
+ PIAs.export("PIA2", machine.pia2, reader(readers, "BL-PIA2")), VIA.export(reader(readers, "BL-VIA")),
52
+ *Drive2031.export(machine, readers), *Drive8050.export(machine, readers)] +
49
53
  PERIPHERALS.map { |name, major, minor, bytes| FieldWriter.new.bytes(bytes).section(name, major, minor) }
50
54
  end
51
55
 
@@ -25,14 +25,17 @@ module Badline
25
25
 
26
26
  def reads?(section) = section.major == MAJOR && section.minor == MINOR
27
27
 
28
- def export(machine)
28
+ def export(machine) = write(machine, [machine.pia1.irq?, machine.pia2.irq?, machine.via.irq?].count(true))
29
+
30
+ # The module for `machine`, whose CPU's IRQ line `irqs` sources
31
+ # pull.
32
+ def write(machine, irqs)
29
33
  cpu = machine.cpu
30
34
  fields = FieldWriter.new.qword(machine.cycles)
31
35
  [cpu.a, cpu.x, cpu.y, cpu.stack_pointer].each { |register| fields.byte(register) }
32
36
  fields.word(cpu.program_counter).byte(cpu.p)
33
37
  fields.dword(0).dword(0).dword(0)
34
38
  5.times { fields.qword(0) }
35
- irqs = [machine.pia1.irq?, machine.pia2.irq?, machine.via.irq?].count(true)
36
39
  fields.dword(irqs).dword(0).dword(irqs.positive? ? IK_IRQ : 0)
37
40
  fields.section(NAME, MAJOR, MINOR)
38
41
  end
@@ -82,7 +82,8 @@ module Badline
82
82
  beam = if discrete
83
83
  discrete_beam(video_input)
84
84
  else
85
- crtc_beam(run_on(machine, crtc_input, -CRTC_LEAD % frame_cycles(registers)), registers)
85
+ crtc_beam(run_on(fresh_crtc(machine), crtc_input, -CRTC_LEAD % frame_cycles(registers)),
86
+ registers)
86
87
  end
87
88
  fields = FieldWriter.new.bytes(model.cbm8296? ? SETTINGS_8296 : SETTINGS).word(attributes(model))
88
89
  fields.byte(0).byte(columns(model))
@@ -119,10 +120,12 @@ module Badline
119
120
 
120
121
  def frame_cycles(registers) = (registers[0] + 1) * frame_lines(registers)
121
122
 
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
123
+ # The CRTC of a machine built as `machine` was, at power-on.
124
+ def fresh_crtc(machine) = PETSetup.of(machine).build.crtc
125
+
126
+ # The BL-CRTC fields of `crtc`, a CRTC of its own, loaded with those
127
+ # in `input` and run `cycles` cycles.
128
+ def run_on(crtc, input, cycles)
126
129
  crtc.load_fields(input)
127
130
  cycles.times { crtc.cycle! }
128
131
  Keyed::Reader.parse(Keyed.section("BL-CRTC") { |out| crtc.save_fields(out) })
@@ -135,16 +138,13 @@ module Badline
135
138
  %w[adjusting row_display column_display].each { |key| out.bool(key, input.bool(key, false)) }
136
139
  end
137
140
 
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
+ # Both move a row's start on at R1 of its last line.
141
142
  def crtc_beam(input, registers)
142
143
  column = input.int("column", 0)
143
144
  row = input.int("row", 0)
144
145
  line = input.int("line", 0)
145
146
  adjusting = input.bool("adjusting", false)
146
147
  start = input.int("row_start", 0)
147
- start = (start - registers[1]) & 0x3fff if adjusting && moved?(line, column, registers)
148
148
  rows = registers[4] + 1
149
149
  lines = registers[9] + 1
150
150
  Beam.new(column:, row: adjusting ? rows : row, raster: adjusting ? 0 : line,
@@ -212,12 +212,7 @@ module Badline
212
212
  adjusting = beam.row >= rows
213
213
  line = adjusting ? beam.frame_line - (rows * (registers[9] + 1)) : beam.raster
214
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
215
+ start = (start - registers[1]) & 0x3fff if !adjusting && moved?(line, column_of(section), registers)
221
216
  out.bytes("registers", registers.first(Chips::CRTC::MASKS.length)).int("selected", selected(section) & 0x1f)
222
217
  out.int("column", 0).int("line", [line, 0].max).int("row", adjusting ? rows - 1 : beam.row)
223
218
  out.int("since_hsync", registers[0] + 1 - registers[2]).int("vsync_lines", beam.vsync)
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module PET
7
+ # A true 2031 on device 8, as xpet 3.10, xcbm2 and xcbm5x0 save it with true drive
8
+ # emulation: DRIVE8 to DRIVE11 2.0, one a unit, DRIVECPU0 1.3, the
9
+ # drive's 6502 and its RAM, and its VIAs as VIA 2.2 modules,
10
+ # 2031VIA1D0 at $1800 on the IEEE-488 bus and VIA2D0 at $1C00 on
11
+ # the mechanism.
12
+ #
13
+ # DRIVECPU0 holds the clock, the registers, the last opcode, the
14
+ # main CPU's clock, more clocks of the drive's interrupt bookkeeping,
15
+ # then the 2K of RAM from $5C and twelve bytes after it. DRIVE8
16
+ # holds the head's half track at $10, with track 18 at 36 as badline
17
+ # numbers them, the speed zone its VIA set at $24, $47 and $4B, the
18
+ # drive's type as a word at $28, and from $2C the disk's place in
19
+ # its turn and the clock it was last moved on at. The rest of it,
20
+ # the read electronics among it, badline doesn't read. xpet's head
21
+ # rests on a half track two phases on from where badline's stepper
22
+ # puts it (SLIP).
23
+ #
24
+ # Written: the registers and the RAM, the VIAs as the PET's VIA is
25
+ # written with the stepper's phase moved to xpet's, and DRIVE8 as
26
+ # xpet saves an idle 2031 but for the head's half track, the zone,
27
+ # and the disk at the start of its turn as of the snapshot's clock.
28
+ # The interrupt clocks are written as long past. The snapshot is
29
+ # settled with the drive's CPU between instructions too
30
+ # (PET#settle_for_vice!). One taken while the drive is mid-transfer
31
+ # on the bus may stall there in xpet, as xpet's own snapshots taken
32
+ # while it searches for a file do.
33
+ #
34
+ # Read: the registers, the RAM, the VIAs as the PET's VIA is read,
35
+ # and the head's half track, with the motor, the LED and the zone
36
+ # from VIA 2's port B and the stepper's phase as xpet's (SLIP). The
37
+ # disk turns on from where badline's left it, and the disk in the
38
+ # drive stays.
39
+ module Drive2031
40
+ UNITS = %w[DRIVE8 DRIVE9 DRIVE10 DRIVE11].freeze
41
+ DRIVE_MAJOR = 2
42
+ DRIVE_MINOR = 0
43
+ CPU = "DRIVECPU0"
44
+ CPU_MAJOR = 1
45
+ CPU_MINOR = 3
46
+ VIA1 = "2031VIA1D0"
47
+ VIA2 = "VIA2D0"
48
+ NAMES = [*UNITS, CPU, VIA1, VIA2].freeze
49
+ # Where DRIVECPU0's RAM starts, and its length.
50
+ RAM_AT = 0x5c
51
+ RAM_SIZE = 0x800
52
+ TAIL = 12
53
+ HALF_TRACK = 0x10
54
+ ZONES = [0x24, 0x47, 0x4b].freeze
55
+ SIZE = 145
56
+ # The disk's place in its turn, and the clock it was last moved on at.
57
+ TURN = 0x2c
58
+ # DRIVE8 as xpet saves an idle 2031, and a unit with no drive, by
59
+ # the bytes that aren't 0.
60
+ IDLE = { 0x00 => 0x01, 0x01 => 0x01, 0x02 => 0x01, 0x0f => 0x01, 0x10 => 0x25, 0x1a => 0x01, 0x1b => 0xdf,
61
+ 0x1f => 0x54, 0x20 => 0x55, 0x24 => 0x03, 0x28 => 0xef, 0x29 => 0x07, 0x2c => 0x60, 0x2d => 0x41,
62
+ 0x2e => 0x0f, 0x30 => 0x17, 0x31 => 0x51, 0x32 => 0x0f, 0x40 => 0x54, 0x41 => 0x01, 0x42 => 0x54,
63
+ 0x47 => 0x03, 0x4b => 0x03, 0x6f => 0xcd, 0x70 => 0xab, 0x71 => 0x34, 0x72 => 0x12 }.freeze
64
+ NONE = { 0x00 => 0x01, 0x01 => 0x01, 0x02 => 0x01, 0x0e => 0x01, 0x0f => 0x01, 0x10 => 0x24, 0x1a => 0x01,
65
+ 0x20 => 0x55, 0x6f => 0xcd, 0x70 => 0xab, 0x71 => 0x34, 0x72 => 0x12, 0x8f => 0x01 }.freeze
66
+ SLIP = 2
67
+ MOTOR = 0x04
68
+ LED = 0x08
69
+
70
+ module_function
71
+
72
+ # Whether the snapshot has a true 2031.
73
+ def present?(container) = !container[CPU].nil? && !container[VIA1].nil?
74
+
75
+ def reads?(section)
76
+ case section.name
77
+ when CPU then section.major == CPU_MAJOR && section.minor == CPU_MINOR
78
+ when VIA1, VIA2 then VIA.reads?(section)
79
+ else section.major == DRIVE_MAJOR && section.minor == DRIVE_MINOR
80
+ end
81
+ end
82
+
83
+ # xpet's modules for the 2031 whose BL-D8 modules `readers` holds,
84
+ # and none without one or for an 8050 or 8250, which they leave
85
+ # out.
86
+ def export(machine, readers)
87
+ drive = readers["BL-D8"]
88
+ return [] if drive.nil? || drive.text("model", "2031") != "2031"
89
+
90
+ via2 = readers.fetch("BL-D8-VIA2")
91
+ mechanism = readers.fetch("BL-D8-MECH")
92
+ zone = ((via2.int("orb", 0) | ~via2.int("ddrb", 0)) >> 5) & 0x03
93
+ idle = filled(IDLE)
94
+ idle[HALF_TRACK] = mechanism.int("half_track", 36)
95
+ ZONES.each { |offset| idle[offset] = zone }
96
+ idle[TURN, 8] = [0, machine.cycles & 0xffff_ffff].pack("VV").bytes
97
+ [FieldWriter.new.bytes(idle).section(UNITS.first, DRIVE_MAJOR, DRIVE_MINOR),
98
+ *UNITS.drop(1).map { |name| unit(NONE, name) },
99
+ cpu(machine, readers.fetch("BL-D8-CPU"), readers.fetch("BL-D8-RAM")),
100
+ stepper(VIA.export(via2, VIA2), mechanism.int("slip", 0)),
101
+ VIA.export(readers.fetch("BL-D8-VIA1"), VIA1)]
102
+ end
103
+
104
+ # VIA 2's port B with the stepper's phase as xpet's head takes it
105
+ # where badline's head is: SLIP on from badline's.
106
+ def stepper(section, slip)
107
+ bytes = section.data.bytes
108
+ bytes[2] = (bytes[2] & 0xfc) | ((bytes[2] - slip + SLIP) & 0x03)
109
+ section.with(data: bytes.pack("C*"))
110
+ end
111
+
112
+ def unit(bytes, name) = FieldWriter.new.bytes(filled(bytes)).section(name, DRIVE_MAJOR, DRIVE_MINOR)
113
+
114
+ def filled(bytes) = Array.new(SIZE) { |offset| bytes.fetch(offset, 0) }
115
+
116
+ def cpu(machine, input, ram)
117
+ bytes = ram.bytes("ram", "".b).bytes
118
+ cpu_section(machine, input, Array.new(RAM_SIZE) { |address| bytes[address] || 0 })
119
+ end
120
+
121
+ # The drive's clock, at its 1 MHz, for the machine's at its own.
122
+ def drive_clock(machine) = machine.cycles * Drives::Model2031::CLOCK_HZ / machine.timing.clock_hz
123
+
124
+ # DRIVECPU0 for a drive CPU whose BL- fields `input` holds, with
125
+ # `ram` the drive's RAM as xpet lays it out: the drive's clock,
126
+ # the registers, the machine's clock, the drive's clock again, and
127
+ # the RAM.
128
+ def cpu_section(machine, input, ram)
129
+ clock = drive_clock(machine)
130
+ fields = FieldWriter.new.qword(clock)
131
+ %w[a x y sp].each { |key| fields.byte(input.int(key, 0)) }
132
+ fields.word(input.int("pc", 0)).byte(input.int("p", 0x20)).dword(0)
133
+ fields.qword(machine.cycles).zeros(16).qword(clock).zeros(1).qword(0).zeros(8).qword(0)
134
+ fields.zeros(16).bytes(ram).zeros(TAIL)
135
+ fields.section(CPU, CPU_MAJOR, CPU_MINOR)
136
+ end
137
+
138
+ # The 2031's keyed modules, named for device 8, from the
139
+ # snapshot's, with `disk` the drive's disk module badline keeps,
140
+ # or nil.
141
+ def import(container, disk)
142
+ via2 = container[VIA2]
143
+ sections = [Keyed.section("BL-D8") { |out| drive(container, out) },
144
+ Keyed.section("BL-D8-RAM") { |out| out.bytes("ram", ram(container[CPU])) },
145
+ Keyed.section("BL-D8-VIA1") { |out| VIA.import(container[VIA1], out) },
146
+ Keyed.section("BL-D8-VIA2") { |out| VIA.import(via2, out) },
147
+ Keyed.section("BL-D8-CPU") { |out| registers(container[CPU], out) },
148
+ Keyed.section("BL-D8-MECH") { |out| mechanism(container, via2, out) }]
149
+ disk ? sections + [disk] : sections
150
+ end
151
+
152
+ def drive(container, out)
153
+ out.text("model", "2031").int("phase", 0).int("cycles", FieldReader.new(container[CPU]).qword)
154
+ out.bool("so_pending", false)
155
+ end
156
+
157
+ def ram(section) = FieldReader.new(section).skip(RAM_AT).string(RAM_SIZE)
158
+
159
+ def registers(section, out)
160
+ fields = FieldReader.new(section)
161
+ clock = fields.qword
162
+ out.int("a", fields.byte).int("x", fields.byte).int("y", fields.byte).int("sp", fields.byte)
163
+ out.int("pc", fields.word).int("p", fields.byte).int("cycles", clock)
164
+ end
165
+
166
+ # The head where DRIVE8 has it, and the stepper's phase as VIA 2
167
+ # drives it, which the half track's low two bits stand for.
168
+ def mechanism(container, via2, out)
169
+ half_track = FieldReader.new(container[UNITS.first]).skip(HALF_TRACK).byte
170
+ bytes = FieldReader.new(via2).bytes(4)
171
+ port_b = bytes[2] | (~bytes[3] & 0xff)
172
+ out.int("half_track", half_track).int("slip", SLIP)
173
+ out.int("zone", (port_b >> 5) & 0x03)
174
+ out.bool("motor", port_b.anybits?(MOTOR)).bool("led", port_b.anybits?(LED))
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module PET
7
+ # A true 8050 or 8250 on device 8, as xpet 3.10, xcbm2 and xcbm5x0 save it with true
8
+ # drive emulation: DRIVE8 2.0 with both drives, DRIVE9 to DRIVE11
9
+ # with none, DRIVECPU0 1.3, the interface processor and its RAM,
10
+ # RIOT1D0 and RIOT2D0 0.0, its two 6532s, and FDC0 0.0. xpet runs
11
+ # the floppy controller at a high level, so its 6504, VIA and 6530
12
+ # aren't in the snapshot: FDC0 holds the controller's state, its
13
+ # last track in byte 6.
14
+ #
15
+ # DRIVE8 holds a block of 139 bytes for each drive, then 6 for the
16
+ # unit: each block has its head's half track, twice the track, at
17
+ # $10 and the drive's type as a word at $28 (8050 or 8250).
18
+ # DRIVECPU0 is laid out as the 2031's (Drive2031), its RAM the IP's
19
+ # page 0, the 6532s' 128 bytes each, then the 4K of common RAM.
20
+ # A RIOT module holds port A's output and direction registers, port
21
+ # B's, the edge control (bit 0 the PA7 interrupt, bit 1 a rising
22
+ # edge), the flags (bit 7 the timer's interrupt, which xpet raises
23
+ # IRQ for, bit 6 PA7's), the timer's
24
+ # counter, its divider as a word, two bytes xpet leaves 0, and
25
+ # whether the timer interrupts.
26
+ #
27
+ # Written, once the controller is in its idle loop between
28
+ # instructions (PET#settle_for_vice!): the IP's registers and RAM,
29
+ # the 6532s, the heads' tracks, and FDC0 as xpet leaves an idle
30
+ # controller with the track badline's controller last stepped to.
31
+ # A unit whose controller is still busy is left out of xpet's
32
+ # modules. Read: the IP's registers and RAM and the 6532s, with the
33
+ # controller as a unit switched on has it in its idle loop, its
34
+ # heads on track 1, from where its next job steps them. A job under
35
+ # way in xpet's controller starts again from there. The disks in
36
+ # the drives stay.
37
+ module Drive8050
38
+ UNIT = "DRIVE8"
39
+ CPU = Drive2031::CPU
40
+ RIOTS = %w[RIOT1D0 RIOT2D0].freeze
41
+ FDC = "FDC0"
42
+ NAMES = [UNIT, CPU, *RIOTS, FDC].freeze
43
+ TYPES = { 0x1f72 => "8050", 0x203a => "8250" }.freeze
44
+ # Where each drive's block starts in DRIVE8, and its fields.
45
+ DRIVES = [0x00, 0x8b].freeze
46
+ HALF_TRACK = 0x10
47
+ TYPE = 0x28
48
+ SIZE = 284
49
+ # DRIVE8 as xpet saves an idle 8050, by the bytes that aren't 0,
50
+ # but for the type.
51
+ IDLE = { 0x00 => 0x01, 0x01 => 0x02, 0x02 => 0x01, 0x0e => 0x01, 0x0f => 0x01, 0x10 => 0x4c, 0x1a => 0x01,
52
+ 0x20 => 0x55, 0x30 => 0x06, 0x6f => 0xcd, 0x70 => 0xab, 0x71 => 0x34, 0x72 => 0x12, 0x99 => 0x01,
53
+ 0x9a => 0x01, 0x9b => 0x24, 0xa5 => 0x01, 0xab => 0x55, 0x11a => 0x01 }.freeze
54
+ ZERO_PAGE = 0x80
55
+ COMMON = 0x1000
56
+ # FDC0 as xpet leaves an idle controller, and where the track goes.
57
+ FDC_IDLE = [0x04, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00].freeze
58
+ FDC_TRACK = 6
59
+ # Where badline's controller keeps drive 0's track, in the 6530's
60
+ # RAM.
61
+ TRACK_AT = 0x21
62
+ DIVIDERS = [1, 8, 64, 1024].freeze
63
+ IRQ_EDGE = 0x01
64
+ RISING = 0x02
65
+ TIMER_FLAG = 0x80
66
+ PA7_FLAG = 0x40
67
+
68
+ module_function
69
+
70
+ # Whether the snapshot has a true 8050 or 8250.
71
+ def present?(container) = !container[FDC].nil? && !container[CPU].nil?
72
+
73
+ def reads?(section)
74
+ case section.name
75
+ when CPU then Drive2031.reads?(section)
76
+ when UNIT then section.major == Drive2031::DRIVE_MAJOR && section.minor == Drive2031::DRIVE_MINOR
77
+ else section.major.zero? && section.minor.zero?
78
+ end
79
+ end
80
+
81
+ # The model DRIVE8 names, or nil.
82
+ def model(container)
83
+ unit = container[UNIT]
84
+ return nil if unit.nil? || unit.data.bytesize < SIZE
85
+
86
+ TYPES[FieldReader.new(unit).skip(TYPE).word]
87
+ end
88
+
89
+ # xpet's modules for the 8050 or 8250 whose BL-D8 modules `readers`
90
+ # holds, and none without one, or with its controller away from
91
+ # its idle loop.
92
+ def export(machine, readers)
93
+ drive = readers["BL-D8"]
94
+ model = drive&.text("model", "")
95
+ return [] unless TYPES.value?(model) && idle?(readers)
96
+
97
+ [unit(model, readers.fetch("BL-D8-MECH")), *Drive2031::UNITS.drop(1).map { |name| none(name) },
98
+ Drive2031.cpu_section(machine, readers.fetch("BL-D8-IPCPU"), ram(readers)),
99
+ riot(RIOTS[0], readers.fetch("BL-D8-RIOT1")), riot(RIOTS[1], readers.fetch("BL-D8-RIOT2")),
100
+ fdc(readers.fetch("BL-D8-RRIOT"))]
101
+ end
102
+
103
+ # Whether both processors are between instructions, the
104
+ # controller at the start of its idle loop.
105
+ def idle?(readers)
106
+ fdc = readers.fetch("BL-D8-FDCCPU")
107
+ !position?(fdc) && !position?(readers.fetch("BL-D8-IPCPU")) &&
108
+ fdc.int("pc", 0) == Drives::Model8050::Idle::FDC_LOOP
109
+ end
110
+
111
+ def position?(cpu) = !cpu.text("pos.seq", "").empty?
112
+
113
+ def unit(model, mechanism)
114
+ bytes = Array.new(SIZE) { |offset| IDLE.fetch(offset, 0) }
115
+ cylinders = mechanism.ints("cylinders", [0, 0])
116
+ DRIVES.each_with_index do |at, drive|
117
+ bytes[at + HALF_TRACK] = ((cylinders[drive] || 0) + 1) * 2
118
+ bytes[at + TYPE, 2] = [TYPES.key(model)].pack("v").bytes
119
+ end
120
+ FieldWriter.new.bytes(bytes).section(UNIT, Drive2031::DRIVE_MAJOR, Drive2031::DRIVE_MINOR)
121
+ end
122
+
123
+ def none(name) = Drive2031.unit(Drive2031::NONE, name)
124
+
125
+ # The IP's RAM as xpet lays it out: page 0 from both 6532s, then
126
+ # the common RAM.
127
+ def ram(readers)
128
+ [readers.fetch("BL-D8-RIOT1"), readers.fetch("BL-D8-RIOT2")].flat_map do |riot|
129
+ filled(riot.bytes("ram", "".b).bytes, ZERO_PAGE)
130
+ end + filled(readers.fetch("BL-D8-COMMON").bytes("ram", "".b).bytes, COMMON)
131
+ end
132
+
133
+ def filled(bytes, length) = Array.new(length) { |index| bytes[index] || 0 }
134
+
135
+ def riot(name, input)
136
+ edge = input.bool("edge_irq_enabled", false) ? IRQ_EDGE : 0
137
+ edge |= RISING if input.bool("rising_edge", false)
138
+ timer = input.bool("timer.flag", false) && input.bool("timer.irq_enabled", false)
139
+ flags = (timer ? TIMER_FLAG : 0) | (input.bool("pa7_flag", false) ? PA7_FLAG : 0)
140
+ divider = DIVIDERS.fetch(input.int("timer.interval", 0) & 0x03)
141
+ fields = FieldWriter.new.byte(input.int("ora", 0)).byte(input.int("ddra", 0)).byte(input.int("orb", 0))
142
+ fields.byte(input.int("ddrb", 0)).byte(edge).byte(flags).byte(input.int("timer.counter", 0)).word(divider)
143
+ fields.zeros(2).flag(input.bool("timer.irq_enabled", false)).section(name, 0, 0)
144
+ end
145
+
146
+ def fdc(rriot)
147
+ bytes = FDC_IDLE.dup
148
+ bytes[FDC_TRACK] = rriot.bytes("ram", "".b).getbyte(TRACK_AT).to_i
149
+ FieldWriter.new.bytes(bytes).section(FDC, 0, 0)
150
+ end
151
+
152
+ # The unit's keyed modules, named for device 8, from the
153
+ # snapshot's: the IP's side from xpet's, the controller's side
154
+ # from `reference`, a unit's in its idle loop, and `disks`, the
155
+ # drives' disk modules badline keeps.
156
+ def import(container, reference, disks)
157
+ cpu = container[CPU]
158
+ page = FieldReader.new(cpu).skip(Drive2031::RAM_AT).string(ZERO_PAGE * 2)
159
+ common = FieldReader.new(cpu).skip(Drive2031::RAM_AT + (ZERO_PAGE * 2)).string(COMMON)
160
+ sections = [Keyed.section("BL-D8") { |out| unit_fields(container, reference, out) },
161
+ Keyed.section("BL-D8-COMMON") { |out| out.bytes("ram", common) },
162
+ riot_section("BL-D8-RIOT1", container[RIOTS[0]], page.byteslice(0, ZERO_PAGE)),
163
+ riot_section("BL-D8-RIOT2", container[RIOTS[1]], page.byteslice(ZERO_PAGE, ZERO_PAGE)),
164
+ Keyed.section("BL-D8-IPCPU") { |out| Drive2031.registers(cpu, out) }]
165
+ sections + reference.select { |section| CONTROLLER.include?(section.name) } + disks
166
+ end
167
+
168
+ # The controller's side, which comes from a unit in its idle loop.
169
+ CONTROLLER = %w[BL-D8-FDCCPU BL-D8-VIA BL-D8-RRIOT BL-D8-MECH].freeze
170
+
171
+ def unit_fields(container, reference, out)
172
+ own = Keyed::Reader.parse(reference.find { |section| section.name == "BL-D8" })
173
+ out.text("model", own.text("model", "8050")).int("phase", 0)
174
+ out.int("cycles", FieldReader.new(container[CPU]).qword).int("fdc_data", own.int("fdc_data", 0))
175
+ end
176
+
177
+ def riot_section(name, section, ram)
178
+ fields = FieldReader.new(section)
179
+ ora, ddra, orb, ddrb, edge, flags, counter = fields.bytes(7)
180
+ divider = fields.word
181
+ fields.skip(2)
182
+ irq = fields.flag?
183
+ Keyed.section(name) do |out|
184
+ out.bytes("ram", ram).int("ora", ora).int("ddra", ddra).int("orb", orb).int("ddrb", ddrb)
185
+ out.bool("edge_irq_enabled", edge.anybits?(IRQ_EDGE)).bool("rising_edge", edge.anybits?(RISING))
186
+ out.bool("pa7_flag", flags.anybits?(PA7_FLAG)).int("timer.counter", counter)
187
+ out.int("timer.interval", DIVIDERS.index(divider) || 0).int("timer.prescale", [divider, 1].max)
188
+ out.bool("timer.flag", flags.anybits?(TIMER_FLAG)).bool("timer.expired", flags.anybits?(TIMER_FLAG))
189
+ out.bool("timer.irq_enabled", irq)
190
+ end
191
+ end
192
+
193
+ # A unit of `model` switched on and run into its controller's idle
194
+ # loop, both processors between instructions, as keyed modules
195
+ # named for device 8, run once a model.
196
+ def reference(model)
197
+ @references ||= {}
198
+ @references[model] ||= booted(model)
199
+ end
200
+
201
+ def booted(model)
202
+ unit = model == "8250" ? Drives::Model8250.new : Drives::Model8050.new
203
+ Drives::Model8050::BOOT_CYCLES.times { unit.host_cycle! }
204
+ unit.cycle! until idle_loop?(unit)
205
+ unit.save_modules([], "BL-D8")
206
+ end
207
+
208
+ def idle_loop?(unit)
209
+ cpu = unit.fdc_cpu
210
+ unit.ip_cpu.boundary? && cpu.boundary? && cpu.program_counter == Drives::Model8050::Idle::FDC_LOOP
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end