badline 0.10.1 → 0.11.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/CLAUDE.md +25 -6
  4. data/CONTRIBUTING.md +8 -3
  5. data/README.md +63 -4
  6. data/doc/pinned-behaviour.md +75 -0
  7. data/doc/snapshots.md +15 -0
  8. data/lib/badline/address_bus/pla.rb +3 -16
  9. data/lib/badline/address_bus.rb +14 -0
  10. data/lib/badline/c128/banks.rb +35 -0
  11. data/lib/badline/c128/bus.rb +347 -0
  12. data/lib/badline/c128/bus_state.rb +53 -0
  13. data/lib/badline/c128/color_lines.rb +39 -0
  14. data/lib/badline/c128/cpu.rb +10 -0
  15. data/lib/badline/c128/drives.rb +95 -0
  16. data/lib/badline/c128/fast_serial.rb +62 -0
  17. data/lib/badline/c128/keys.rb +35 -0
  18. data/lib/badline/c128/mmu.rb +197 -0
  19. data/lib/badline/c128/mmu_pages.rb +105 -0
  20. data/lib/badline/c128/model.rb +39 -0
  21. data/lib/badline/c128/modes.rb +48 -0
  22. data/lib/badline/c128/saved_state.rb +121 -0
  23. data/lib/badline/c128/vdc/memory.rb +54 -0
  24. data/lib/badline/c128/vdc/painter.rb +143 -0
  25. data/lib/badline/c128/vdc/raster.rb +97 -0
  26. data/lib/badline/c128/vdc/window.rb +215 -0
  27. data/lib/badline/c128/vdc.rb +268 -0
  28. data/lib/badline/c128.rb +322 -0
  29. data/lib/badline/chrout_trap.rb +3 -0
  30. data/lib/badline/cia/fast_forward.rb +42 -0
  31. data/lib/badline/cia/serial.rb +12 -0
  32. data/lib/badline/cia/timer.rb +39 -0
  33. data/lib/badline/cia.rb +2 -0
  34. data/lib/badline/computer/attachments.rb +10 -13
  35. data/lib/badline/computer/kernal_traps.rb +29 -0
  36. data/lib/badline/computer.rb +5 -0
  37. data/lib/badline/drive/core.rb +172 -0
  38. data/lib/badline/{drive1541 → drive}/idle.rb +18 -19
  39. data/lib/badline/drive/idle_cpu.rb +28 -0
  40. data/lib/badline/{drive1541 → drive}/orbit.rb +2 -1
  41. data/lib/badline/drive/serial_lines.rb +73 -0
  42. data/lib/badline/{drive1541 → drive}/sleep.rb +4 -3
  43. data/lib/badline/drive/watch.rb +150 -0
  44. data/lib/badline/drive.rb +9 -0
  45. data/lib/badline/drive1541/bus.rb +5 -129
  46. data/lib/badline/drive1541/byte_ready.rb +48 -0
  47. data/lib/badline/drive1541/cpu.rb +1 -19
  48. data/lib/badline/drive1541/disk.rb +77 -20
  49. data/lib/badline/drive1541/disk_state.rb +7 -2
  50. data/lib/badline/drive1541/disk_via.rb +3 -0
  51. data/lib/badline/drive1541/mechanism.rb +48 -14
  52. data/lib/badline/drive1541/mechanism_state.rb +5 -4
  53. data/lib/badline/drive1541/rotation.rb +5 -5
  54. data/lib/badline/drive1541/serial_port.rb +4 -60
  55. data/lib/badline/drive1541.rb +13 -172
  56. data/lib/badline/drive1571/bus.rb +111 -0
  57. data/lib/badline/drive1571/cpu.rb +11 -0
  58. data/lib/badline/drive1571/serial_port.rb +37 -0
  59. data/lib/badline/drive1571/serial_via.rb +25 -0
  60. data/lib/badline/drive1571/wd1770.rb +34 -0
  61. data/lib/badline/drive1571.rb +225 -0
  62. data/lib/badline/frontend/app.rb +35 -22
  63. data/lib/badline/frontend/boot.rb +31 -1
  64. data/lib/badline/frontend/controls.rb +11 -0
  65. data/lib/badline/frontend/drive_led.rb +1 -1
  66. data/lib/badline/frontend/keys.rb +28 -1
  67. data/lib/badline/frontend/machine_window.rb +16 -13
  68. data/lib/badline/frontend/menu_media.rb +3 -3
  69. data/lib/badline/frontend/menu_pages.rb +8 -2
  70. data/lib/badline/frontend/menu_ports.rb +1 -1
  71. data/lib/badline/frontend/pause_menu.rb +1 -0
  72. data/lib/badline/frontend/screen.rb +33 -8
  73. data/lib/badline/frontend/screens.rb +90 -0
  74. data/lib/badline/frontend/timeline.rb +15 -4
  75. data/lib/badline/frontend.rb +1 -0
  76. data/lib/badline/iec_bus.rb +54 -4
  77. data/lib/badline/kernal_trap/file.rb +1 -1
  78. data/lib/badline/kernal_trap/layout.rb +58 -8
  79. data/lib/badline/kernal_trap/load.rb +1 -1
  80. data/lib/badline/kernal_trap/save.rb +1 -1
  81. data/lib/badline/kernal_trap/serial.rb +5 -0
  82. data/lib/badline/machine.rb +20 -9
  83. data/lib/badline/media/true_drive.rb +26 -23
  84. data/lib/badline/media.rb +8 -6
  85. data/lib/badline/options/event.rb +5 -1
  86. data/lib/badline/options/family.rb +52 -0
  87. data/lib/badline/options/help.rb +7 -2
  88. data/lib/badline/options/table.rb +8 -2
  89. data/lib/badline/options/validation.rb +14 -1
  90. data/lib/badline/options.rb +20 -19
  91. data/lib/badline/roms/README +36 -0
  92. data/lib/badline/roms/c128/basic.rom +0 -0
  93. data/lib/badline/roms/c128/character.rom +0 -0
  94. data/lib/badline/roms/c128/kernal.rom +0 -0
  95. data/lib/badline/roms/dos1571.rom +0 -0
  96. data/lib/badline/snapshot/container.rb +2 -0
  97. data/lib/badline/snapshot/image.rb +14 -0
  98. data/lib/badline/snapshot.rb +9 -2
  99. data/lib/badline/storage/d64_image.rb +3 -0
  100. data/lib/badline/storage/d71_image.rb +9 -4
  101. data/lib/badline/storage/g64_image.rb +7 -2
  102. data/lib/badline/time_of_day.rb +9 -0
  103. data/lib/badline/version.rb +1 -1
  104. data/lib/badline/via/fast_forward.rb +3 -3
  105. data/lib/badline/vic/display_state.rb +12 -0
  106. data/lib/badline/vic/registers.rb +3 -0
  107. data/lib/badline/vic.rb +108 -7
  108. data/lib/badline/vic20/attachments.rb +6 -0
  109. data/lib/badline/z80/arithmetic.rb +117 -0
  110. data/lib/badline/z80/bit_page.rb +55 -0
  111. data/lib/badline/z80/block_io.rb +58 -0
  112. data/lib/badline/z80/block_transfers.rb +59 -0
  113. data/lib/badline/z80/bus_cycles.rb +111 -0
  114. data/lib/badline/z80/core.rb +111 -0
  115. data/lib/badline/z80/exchanges.rb +50 -0
  116. data/lib/badline/z80/extended_page.rb +89 -0
  117. data/lib/badline/z80/flags.rb +37 -0
  118. data/lib/badline/z80/interrupt_acceptance.rb +46 -0
  119. data/lib/badline/z80/jumps.rb +82 -0
  120. data/lib/badline/z80/loads.rb +98 -0
  121. data/lib/badline/z80/main_page.rb +134 -0
  122. data/lib/badline/z80/registers.rb +120 -0
  123. data/lib/badline/z80/shifts.rb +64 -0
  124. data/lib/badline/z80.rb +11 -0
  125. data/lib/badline.rb +1 -0
  126. metadata +59 -4
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drive
5
+ # A drive bus's eye on what the CPU reaches, for Idle and Orbit. While
6
+ # Idle records a pass of the DOS's idle loop, the bus watches it (see
7
+ # watch!). While Idle looks for an orbit, the bus guards the drive
8
+ # (see guard!). The bus calls watch_read and watch_write with the
9
+ # address folded into $0000-$1FFF, RAM and the two VIAs at $1800 and
10
+ # $1C00, while @watching.
11
+ module Watch
12
+ # VIA registers, as bits by offset, whose access makes a watched
13
+ # stretch volatile: reads of either VIA's counters and shift register
14
+ # and of VIA 1's port B, and writes to VIA 2's timers, shift register
15
+ # and ACR. Any write to VIA 1 does too (watch_write).
16
+ VOLATILE_VIA1_READS = 0x0731
17
+ VOLATILE_VIA2_READS = 0x0730
18
+ VOLATILE_VIA2_WRITES = 0x0ff0
19
+
20
+ # The timer each VIA register reaches, by offset, as bits: 1 for
21
+ # timer 1 and 2 for timer 2.
22
+ TIMER_REGISTERS = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 0, 0, 0, 0, 0, 0].freeze
23
+
24
+ # Whether a watched stretch did something that makes it unrepeatable.
25
+ attr_reader :volatile
26
+
27
+ # The RAM a watched stretch read or wrote, as each address's value
28
+ # from before the first access.
29
+ attr_reader :touched
30
+
31
+ # Whether the drive did something since guard! that reached outside
32
+ # it, or took something in from outside.
33
+ attr_reader :tainted
34
+
35
+ # The timers the drive read or wrote since guard!, as bits: VIA 1's
36
+ # timer 1 and timer 2 in bits 0 and 1, and VIA 2's in bits 2 and 3.
37
+ attr_reader :touched_timers
38
+
39
+ # Starts watching: see volatile and touched. The bus stops watching
40
+ # once the stretch turns volatile.
41
+ def watch!
42
+ @recording = true
43
+ @volatile = false
44
+ @touched = {}
45
+ watching!
46
+ end
47
+
48
+ def unwatch!
49
+ @recording = false
50
+ watching!
51
+ end
52
+
53
+ # Starts guarding: see tainted and touched_timers. Reading VIA 1's
54
+ # port B, which reads the serial bus, taints the drive, and so do
55
+ # any write to VIA 1, whose port B drives the serial bus, a write to
56
+ # VIA 2's shift register or ACR, and VIA 2 turning the motor on or
57
+ # changing the LED. The bus stops guarding once the drive is tainted.
58
+ def guard!
59
+ @guarding = true
60
+ @tainted = false
61
+ @touched_timers = 0
62
+ @led = @via2.port_b_output & Drive1541::Mechanism::LED
63
+ watching!
64
+ end
65
+
66
+ private
67
+
68
+ def init_watch
69
+ @recording = @guarding = @watching = false
70
+ @volatile = @tainted = false
71
+ @touched = {}
72
+ @touched_timers = 0
73
+ @led = 0
74
+ end
75
+
76
+ def watching!
77
+ @watching = @recording || @guarding
78
+ end
79
+
80
+ # An access to a chip beyond the VIAs, as the 1571's CIA and floppy
81
+ # controller are: it makes the stretch volatile and taints the
82
+ # drive.
83
+ def watch_chip
84
+ volatile! if @recording
85
+ taint! if @guarding
86
+ end
87
+
88
+ def touch(addr)
89
+ @touched[addr] = @ram.peek(addr) unless @touched.key?(addr)
90
+ end
91
+
92
+ def watch_read(addr)
93
+ if addr < 0x0800
94
+ touch(addr) if @recording
95
+ elsif addr >= 0x1800
96
+ via1 = addr < 0x1c00
97
+ watch(via1 ? VOLATILE_VIA1_READS : VOLATILE_VIA2_READS, addr) if @recording
98
+ guard_read(via1, addr & 0x0f) if @guarding
99
+ end
100
+ end
101
+
102
+ def watch_write(addr)
103
+ if addr >= 0x1c00
104
+ watch_via2_write(addr) if @recording
105
+ guard_via2_write(addr & 0x0f) if @guarding
106
+ elsif addr >= 0x1800
107
+ volatile! if @recording
108
+ taint! if @guarding
109
+ end
110
+ end
111
+
112
+ # Any write to VIA 1 makes the stretch volatile (watch_write), and so
113
+ # does a write to VIA 2 that leaves the motor on.
114
+ def watch_via2_write(addr)
115
+ watch(VOLATILE_VIA2_WRITES, addr)
116
+ volatile! if @recording && @via2.port_b_output.anybits?(Drive1541::Mechanism::MOTOR)
117
+ end
118
+
119
+ def watch(volatile, addr)
120
+ volatile! if volatile.anybits?(1 << (addr & 0x0f))
121
+ end
122
+
123
+ def volatile!
124
+ @volatile = true
125
+ @recording = false
126
+ watching!
127
+ end
128
+
129
+ def guard_read(via1, offset)
130
+ return taint! if via1 && offset.zero?
131
+
132
+ @touched_timers |= via1 ? TIMER_REGISTERS[offset] : TIMER_REGISTERS[offset] << 2
133
+ end
134
+
135
+ def guard_via2_write(offset)
136
+ return taint! if offset.between?(0x0a, 0x0b)
137
+
138
+ @touched_timers |= TIMER_REGISTERS[offset] << 2
139
+ lines = @via2.port_b_output
140
+ taint! if lines.anybits?(Drive1541::Mechanism::MOTOR) || (lines & Drive1541::Mechanism::LED) != @led
141
+ end
142
+
143
+ def taint!
144
+ @tainted = true
145
+ @guarding = false
146
+ watching!
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drive/sleep"
4
+ require "badline/drive/idle"
5
+ require "badline/drive/orbit"
6
+ require "badline/drive/core"
7
+ require "badline/drive/idle_cpu"
8
+ require "badline/drive/watch"
9
+ require "badline/drive/serial_lines"
@@ -18,41 +18,15 @@ module Badline
18
18
  # page is what its dummy read found. VICE-testprogs drive/openbus pins
19
19
  # both on a real 1541.
20
20
  #
21
- # While Idle records a pass of the DOS's idle loop, the bus watches it
22
- # (see watch!). While Idle looks for an orbit, the bus guards the drive
23
- # (see guard!).
21
+ # While Idle records a pass of the DOS's idle loop, the bus watches it,
22
+ # and while Idle looks for an orbit, it guards the drive (see
23
+ # Drive::Watch).
24
24
  class Bus
25
25
  include Addressable
26
-
27
- # VIA registers, as bits by offset, whose access makes a watched
28
- # stretch volatile: reads of either VIA's counters and shift register
29
- # and of VIA 1's port B, and writes to VIA 2's timers, shift register
30
- # and ACR. Any write to VIA 1 does too (watch_write).
31
- VOLATILE_VIA1_READS = 0x0731
32
- VOLATILE_VIA2_READS = 0x0730
33
- VOLATILE_VIA2_WRITES = 0x0ff0
34
-
35
- # The timer each VIA register reaches, by offset, as bits: 1 for
36
- # timer 1 and 2 for timer 2.
37
- TIMER_REGISTERS = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 0, 0, 0, 0, 0, 0].freeze
26
+ include Drive::Watch
38
27
 
39
28
  attr_reader :ram, :rom, :via1, :via2, :data
40
29
 
41
- # Whether a watched stretch did something that makes it unrepeatable.
42
- attr_reader :volatile
43
-
44
- # The RAM a watched stretch read or wrote, as each address's value
45
- # from before the first access.
46
- attr_reader :touched
47
-
48
- # Whether the drive did something since guard! that reached outside
49
- # it, or took something in from outside.
50
- attr_reader :tainted
51
-
52
- # The timers the drive read or wrote since guard!, as bits: VIA 1's
53
- # timer 1 and timer 2 in bits 0 and 1, and VIA 2's in bits 2 and 3.
54
- attr_reader :touched_timers
55
-
56
30
  def initialize(rom:, via1:, via2:)
57
31
  addressable_at(0, length: 2**16)
58
32
  @ram = Memory.new([], length: 0x0800, start: 0)
@@ -60,38 +34,7 @@ module Badline
60
34
  @via1 = via1
61
35
  @via2 = via2
62
36
  @data = 0
63
- @recording = @guarding = @watching = false
64
- @volatile = @tainted = false
65
- @touched = {}
66
- @touched_timers = 0
67
- @led = 0
68
- end
69
-
70
- # Starts watching: see volatile and touched. The bus stops watching
71
- # once the stretch turns volatile.
72
- def watch!
73
- @recording = true
74
- @volatile = false
75
- @touched = {}
76
- watching!
77
- end
78
-
79
- def unwatch!
80
- @recording = false
81
- watching!
82
- end
83
-
84
- # Starts guarding: see tainted and touched_timers. Reading VIA 1's
85
- # port B, which reads the serial bus, taints the drive, and so do
86
- # any write to VIA 1, whose port B drives the serial bus, a write to
87
- # VIA 2's shift register or ACR, and VIA 2 turning the motor on or
88
- # changing the LED. The bus stops guarding once the drive is tainted.
89
- def guard!
90
- @guarding = true
91
- @tainted = false
92
- @touched_timers = 0
93
- @led = @via2.port_b_output & Mechanism::LED
94
- watching!
37
+ init_watch
95
38
  end
96
39
 
97
40
  # The last byte on the data bus and the RAM. What a watched stretch
@@ -138,73 +81,6 @@ module Badline
138
81
  end
139
82
  watch_write(addr) if @watching
140
83
  end
141
-
142
- private
143
-
144
- def watching!
145
- @watching = @recording || @guarding
146
- end
147
-
148
- def touch(addr)
149
- @touched[addr] = @ram.peek(addr) unless @touched.key?(addr)
150
- end
151
-
152
- def watch_read(addr)
153
- if addr < 0x0800
154
- touch(addr) if @recording
155
- elsif addr >= 0x1800
156
- via1 = addr < 0x1c00
157
- watch(via1 ? VOLATILE_VIA1_READS : VOLATILE_VIA2_READS, addr) if @recording
158
- guard_read(via1, addr & 0x0f) if @guarding
159
- end
160
- end
161
-
162
- def watch_write(addr)
163
- if addr >= 0x1c00
164
- watch_via2_write(addr) if @recording
165
- guard_via2_write(addr & 0x0f) if @guarding
166
- elsif addr >= 0x1800
167
- volatile! if @recording
168
- taint! if @guarding
169
- end
170
- end
171
-
172
- # Any write to VIA 1 makes the stretch volatile (watch_write), and so
173
- # does a write to VIA 2 that leaves the motor on.
174
- def watch_via2_write(addr)
175
- watch(VOLATILE_VIA2_WRITES, addr)
176
- volatile! if @recording && @via2.port_b_output.anybits?(Mechanism::MOTOR)
177
- end
178
-
179
- def watch(volatile, addr)
180
- volatile! if volatile.anybits?(1 << (addr & 0x0f))
181
- end
182
-
183
- def volatile!
184
- @volatile = true
185
- @recording = false
186
- watching!
187
- end
188
-
189
- def guard_read(via1, offset)
190
- return taint! if via1 && offset.zero?
191
-
192
- @touched_timers |= via1 ? TIMER_REGISTERS[offset] : TIMER_REGISTERS[offset] << 2
193
- end
194
-
195
- def guard_via2_write(offset)
196
- return taint! if offset.between?(0x0a, 0x0b)
197
-
198
- @touched_timers |= TIMER_REGISTERS[offset] << 2
199
- lines = @via2.port_b_output
200
- taint! if lines.anybits?(Mechanism::MOTOR) || (lines & Mechanism::LED) != @led
201
- end
202
-
203
- def taint!
204
- @tainted = true
205
- @guarding = false
206
- watching!
207
- end
208
84
  end
209
85
  end
210
86
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1541
5
+ # The Mechanism's BYTE READY line: to VIA 2's CA1, to the CPU's SO pin
6
+ # through SOE, and on the 1571 to a latch VIA 1's PA7 reads.
7
+ module ByteReady
8
+ # VIA 2, which DiskVIA plugs in.
9
+ attr_writer :via
10
+
11
+ # Whether BYTE READY reached SO for the CPU to sample on the drive's
12
+ # next cycle.
13
+ attr_accessor :so_pending
14
+
15
+ # Whether SO is pending, which the drive's cycle samples and clears.
16
+ def take_so
17
+ so = @so_pending
18
+ @so_pending = false
19
+ so
20
+ end
21
+
22
+ # The read electronics signal a whole GCR byte. BYTE READY pulls VIA
23
+ # 2's CA1 low, a falling edge that sets its flag and, with latching
24
+ # on, latches port A. It reaches the CPU's SO pin while VIA 2's CA2
25
+ # (SOE) is high, which lets the DOS spin on BVC for each byte. The CPU
26
+ # samples SO on the next cycle.
27
+ def byte_ready!
28
+ @byte_latched = true
29
+ @via.ca1 = false
30
+ @so_pending = true if @via.ca2_output
31
+ end
32
+
33
+ # BYTE READY lets go of CA1 with the next bit.
34
+ def byte_ready_ended!
35
+ @via.ca1 = true
36
+ end
37
+
38
+ # Whether BYTE READY has come since the CPU last read VIA 2, as the
39
+ # 1571 latches it for VIA 1's PA7.
40
+ def byte_latched? = @byte_latched
41
+
42
+ # A read of VIA 2 clears the 1571's BYTE READY latch.
43
+ def release_byte_latch
44
+ @byte_latched = false
45
+ end
46
+ end
47
+ end
48
+ end
@@ -5,25 +5,7 @@ module Badline
5
5
  # The drive's 6502, on the drive's Bus.
6
6
  class CPU
7
7
  include Badline::CPU::Core
8
-
9
- # Everything the CPU holds but its cycle and instruction counts: the
10
- # registers, the interrupt lines and pipeline, and what the last
11
- # instruction left in its working registers. Drive1541::Idle compares
12
- # it at two instruction boundaries.
13
- def idle_state
14
- [@program_counter, @stack_pointer, @a, @x, @y, @status.value,
15
- @irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
16
- @boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
17
- @operation, @optional_dummy, @boundary_crossed, @branch_taken,
18
- @address, @pointer, @dummy_address, @value, @rmw_result]
19
- end
20
-
21
- # Counts +cycles+ cycles and +instructions+ instructions that ran
22
- # without changing anything idle_state holds.
23
- def fast_forward(cycles, instructions)
24
- @cycles += cycles
25
- @instructions += instructions
26
- end
8
+ include Drive::IdleCPU
27
9
  end
28
10
  end
29
11
  end
@@ -10,6 +10,11 @@ module Badline
10
10
  # nothing between them. A .d64 image fills the whole tracks; a .g64
11
11
  # can fill any of them, with tracks of any length.
12
12
  #
13
+ # The disk has a second side, for the 1571's second head, its half
14
+ # tracks numbered from SIDE on. A .d71 fills both sides, its tracks
15
+ # 36-70 being tracks 1-35 of the second, and a .g71 holds 84 half
16
+ # tracks a side. A 1541 reads the first side only.
17
+ #
13
18
  # The head writes into the tracks' bytes, and a write to a half track
14
19
  # without data gives it a blank Track first. A disk made from an image
15
20
  # remembers it: flush reads the sectors back off the tracks written
@@ -21,6 +26,15 @@ module Badline
21
26
 
22
27
  MAX_HALF_TRACK = 84
23
28
 
29
+ # Where the second side's half tracks start: its track 1 is SIDE + 2.
30
+ SIDE = MAX_HALF_TRACK + 1
31
+
32
+ # The tracks a side of a .d71 holds.
33
+ D71_SIDE_TRACKS = 35
34
+
35
+ # The half track entries a side of a .g71 holds.
36
+ G71_SIDE_ENTRIES = 84
37
+
24
38
  # GCR bytes around a track in each speed zone: 200 ms of rotation at
25
39
  # 300 rpm, in bytes of 32, 30, 28 and 26 µs.
26
40
  TRACK_LENGTHS = [6250, 6666, 7142, 7692].freeze
@@ -51,27 +65,63 @@ module Badline
51
65
  # the 325-byte GCR data block. The gap after it takes up the rest.
52
66
  SECTOR_LENGTH = SYNC_LENGTH + 10 + HEADER_GAP + SYNC_LENGTH + 325
53
67
 
54
- # A disk from the image at +path+: a .g64 as its tracks are, and
55
- # anything else as a .d64, formatted. `read_only` opens the image
56
- # write-protected.
68
+ # A disk from the image at +path+: a .g64 or .g71 as its tracks are,
69
+ # a .d71 formatted on both sides, and anything else as a .d64,
70
+ # formatted. `read_only` opens the image write-protected.
57
71
  def self.open(path, read_only: false)
58
72
  return from_g64(Storage::G64Image.new(path, read_only:)).opened(path, read_only) if State.g64?(path)
73
+ return from_d64(Storage::D71Image.new(path, read_only:)).opened(path, read_only) if State.d71?(path)
59
74
 
60
75
  from_d64(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
61
76
  end
62
77
 
63
- # A disk from a G64 image, each half track as the image stores it,
64
- # with its speed map. Flushing it writes the tracks back as they are.
78
+ # A disk from a G64 or G71 image, each half track as the image stores
79
+ # it, with its speed map. Flushing it writes the tracks back as they
80
+ # are.
65
81
  def self.from_g64(image)
66
82
  disk = new(image)
67
- (Mechanism::MIN_HALF_TRACK..MAX_HALF_TRACK).each do |half_track|
68
- entry = half_track - Mechanism::MIN_HALF_TRACK
83
+ image.half_tracks.times do |entry|
69
84
  track = image.track(entry)
70
- disk.write(half_track, Track.new(*track, image.speeds(entry))) if track
85
+ disk.write(half_track_of(entry), Track.new(*track, image.speeds(entry))) if track
71
86
  end
72
87
  disk
73
88
  end
74
89
 
90
+ # The half track a G64 or G71 table entry holds: entry 0 is track 1,
91
+ # and a .g71's entries from G71_SIDE_ENTRIES on are the second
92
+ # side's.
93
+ def self.half_track_of(entry)
94
+ side = entry / G71_SIDE_ENTRIES
95
+ (side * SIDE) + (entry % G71_SIDE_ENTRIES) + Mechanism::MIN_HALF_TRACK
96
+ end
97
+
98
+ # The table entry of a half track, or nil for one a G64 or G71 has
99
+ # no entry for.
100
+ def self.entry_of(half_track)
101
+ side = half_track / SIDE
102
+ entry = (half_track % SIDE) - Mechanism::MIN_HALF_TRACK
103
+ (side * G71_SIDE_ENTRIES) + entry if entry.between?(0, G71_SIDE_ENTRIES - 1)
104
+ end
105
+
106
+ # The half track an image's track number is on: a .d71's tracks past
107
+ # D71_SIDE_TRACKS are on the second side.
108
+ def self.d64_half_track(image, track)
109
+ return track * 2 unless image.sides == 2 && track > D71_SIDE_TRACKS
110
+
111
+ SIDE + ((track - D71_SIDE_TRACKS) * 2)
112
+ end
113
+
114
+ # The image's track number a half track holds, or nil for a half
115
+ # track or one past the image's last.
116
+ def self.d64_track(image, half_track)
117
+ side = half_track / SIDE
118
+ return unless (half_track % SIDE).even?
119
+
120
+ track = (half_track % SIDE) / 2
121
+ track += D71_SIDE_TRACKS if side == 1
122
+ track if track.positive? && track <= image.track_count && side < image.sides
123
+ end
124
+
75
125
  # A disk formatted from a D64 image, each sector laid out as the DOS
76
126
  # formats it, with the disk ID from the header block. A block the
77
127
  # image's error table marks 20, 21, 22, 23, 27 or 29 is written the
@@ -81,7 +131,8 @@ module Badline
81
131
  #
82
132
  # Each track starts sector 0 at the angle the DOS's N: leaves it: track
83
133
  # 1 at the index angle, and each track after it SKEWS[zone] of a turn
84
- # round from the last.
134
+ # round from the last. A .d71's second side starts over at the index
135
+ # angle.
85
136
  def self.from_d64(image)
86
137
  track, sector = image.header_block
87
138
  header = image.read_block(track, sector)
@@ -89,16 +140,18 @@ module Badline
89
140
  disk = new(image)
90
141
  angle = 0
91
142
  (1..image.track_count).each do |track|
92
- angle = (angle + SKEWS[zone(track)]) % SKEW_TURN if track > 1
93
- bytes = format_track(image, track, id)
94
- disk.write(track * 2, Track.new(bytes.rotate(-(bytes.length * angle / SKEW_TURN)), zone(track)))
143
+ half_track = d64_half_track(image, track)
144
+ zone = zone((half_track % SIDE) / 2)
145
+ angle = half_track == SIDE + 2 ? 0 : (angle + SKEWS[zone]) % SKEW_TURN if track > 1
146
+ bytes = format_track(image, track, id, zone)
147
+ disk.write(half_track, Track.new(bytes.rotate(-(bytes.length * angle / SKEW_TURN)), zone))
95
148
  end
96
149
  disk
97
150
  end
98
151
 
99
- def self.format_track(image, track, id)
152
+ def self.format_track(image, track, id, zone = zone(track))
100
153
  sectors = image.sectors_in(track)
101
- length = TRACK_LENGTHS[zone(track)]
154
+ length = TRACK_LENGTHS[zone]
102
155
  gap = (length - (sectors * SECTOR_LENGTH)) / sectors
103
156
  bytes = []
104
157
  sectors.times do |sector|
@@ -146,7 +199,7 @@ module Badline
146
199
 
147
200
  def initialize(image = nil)
148
201
  @image = image
149
- @tracks = Array.new(MAX_HALF_TRACK + 1)
202
+ @tracks = Array.new(SIDE * 2)
150
203
  @written = {}
151
204
  @path = nil
152
205
  @read_only = false
@@ -188,11 +241,14 @@ module Badline
188
241
  def flush
189
242
  return flush_tracks if @image.respond_to?(:store_tracks)
190
243
 
191
- tracks = @written.keys.select { |half| half.even? && (1..@image&.track_count.to_i).cover?(half / 2) }
244
+ tracks = @written.keys.filter_map do |half|
245
+ track = @image && Disk.d64_track(@image, half)
246
+ [half, track] if track
247
+ end
192
248
  @written.clear
193
249
  return if tracks.empty?
194
250
 
195
- sectors = tracks.to_h { |half| [half / 2, SectorReader.read(@tracks[half].bytes, half / 2)] }
251
+ sectors = tracks.to_h { |half, track| [track, SectorReader.read(@tracks[half].bytes, track)] }
196
252
  id = disk_id(sectors)
197
253
  warn_lost(sectors)
198
254
  @image.store_blocks(sectors.flat_map do |track, found|
@@ -203,9 +259,10 @@ module Badline
203
259
  private
204
260
 
205
261
  def flush_tracks
206
- tracks = @written.keys.sort.to_h do |half|
207
- [half - Mechanism::MIN_HALF_TRACK, [@tracks[half].bytes, @tracks[half].zone]]
208
- end
262
+ tracks = @written.keys.sort.filter_map do |half|
263
+ entry = Disk.entry_of(half)
264
+ [entry, [@tracks[half].bytes, @tracks[half].zone]] if entry
265
+ end.to_h
209
266
  @written.clear
210
267
  @image.store_tracks(tracks) unless tracks.empty?
211
268
  end
@@ -12,13 +12,18 @@ module Badline
12
12
  # for a disk from Disk.open. Nil for a disk made another way.
13
13
  attr_reader :path, :read_only
14
14
 
15
- # A .g64 image by its name, and anything else a .d64.
16
- def self.g64?(path) = File.extname(path).casecmp?(".g64")
15
+ # A .g64 or .g71 image by its name, a .d71 by its name, and anything
16
+ # else a .d64.
17
+ def self.g64?(path) = %w[.g64 .g71].include?(File.extname(path).downcase)
18
+
19
+ def self.d71?(path) = File.extname(path).casecmp?(".d71")
17
20
 
18
21
  # A disk for the image at the path, its tracks left for the state.
19
22
  def self.reopen(path, read_only)
20
23
  if g64?(path)
21
24
  Disk.new(Storage::G64Image.new(path, read_only:)).opened(path, read_only)
25
+ elsif d71?(path)
26
+ Disk.new(Storage::D71Image.new(path, read_only:)).opened(path, read_only)
22
27
  else
23
28
  Disk.new(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
24
29
  end
@@ -7,8 +7,11 @@ module Badline
7
7
  # hears both after every write and reset, since ORB, DDRB, the ACR and
8
8
  # the PCR can all change them.
9
9
  class DiskVIA < VIA
10
+ attr_reader :mechanism
11
+
10
12
  def initialize(start:, mechanism:)
11
13
  @mechanism = mechanism
14
+ mechanism.via = self
12
15
  super(start:, peripheral: mechanism)
13
16
  end
14
17