badline 0.11.1 → 0.12.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +8 -7
- data/CONTRIBUTING.md +7 -6
- data/README.md +12 -5
- data/doc/media.md +28 -1
- data/doc/pinned-behaviour.md +31 -1
- data/exe/badline-ruby +1 -1
- data/lib/badline/c128/bus.rb +4 -17
- data/lib/badline/c128/drives.rb +35 -22
- data/lib/badline/c128/fast_serial.rb +1 -0
- data/lib/badline/c128/io_pages.rb +35 -0
- data/lib/badline/c128/mmu.rb +9 -7
- data/lib/badline/c128/modes.rb +17 -4
- data/lib/badline/c128/saved_state.rb +22 -0
- data/lib/badline/c128/z80.rb +49 -0
- data/lib/badline/c128/z80_pages.rb +118 -0
- data/lib/badline/c128/z80_turns.rb +106 -0
- data/lib/badline/c128.rb +16 -19
- data/lib/badline/cartridge/action_replay.rb +2 -2
- data/lib/badline/cartridge/comal80.rb +1 -7
- data/lib/badline/cartridge/dinamic.rb +0 -8
- data/lib/badline/cartridge/final_cartridge3.rb +6 -6
- data/lib/badline/cartridge/fun_play.rb +0 -8
- data/lib/badline/cartridge/game_system.rb +0 -8
- data/lib/badline/cartridge/kcs_power.rb +10 -7
- data/lib/badline/cartridge/mach5.rb +2 -4
- data/lib/badline/cartridge/magic_desk.rb +0 -8
- data/lib/badline/cartridge/ocean.rb +0 -8
- data/lib/badline/cartridge/pagefox.rb +2 -2
- data/lib/badline/cartridge/retro_replay.rb +7 -6
- data/lib/badline/cartridge/rex_utility.rb +2 -6
- data/lib/badline/cartridge/rgcd.rb +2 -2
- data/lib/badline/cartridge/simons_basic.rb +0 -5
- data/lib/badline/cartridge/super_games.rb +4 -2
- data/lib/badline/cartridge/westermann.rb +2 -6
- data/lib/badline/cartridge.rb +37 -9
- data/lib/badline/cia/fast_forward.rb +21 -3
- data/lib/badline/cia.rb +8 -5
- data/lib/badline/computer/attachments.rb +4 -56
- data/lib/badline/computer/true_drives.rb +80 -0
- data/lib/badline/computer.rb +4 -0
- data/lib/badline/core.rb +2 -0
- data/lib/badline/drive/core.rb +8 -33
- data/lib/badline/drive/fast_serial.rb +73 -0
- data/lib/badline/drive/idle.rb +1 -11
- data/lib/badline/drive/orbit.rb +2 -5
- data/lib/badline/drive/sleep.rb +0 -2
- data/lib/badline/drive/vias.rb +84 -0
- data/lib/badline/drive.rb +2 -0
- data/lib/badline/drive1541.rb +1 -0
- data/lib/badline/drive1571/wd1770.rb +94 -8
- data/lib/badline/drive1571.rb +8 -58
- data/lib/badline/drive1581/bus.rb +137 -0
- data/lib/badline/drive1581/cpu.rb +11 -0
- data/lib/badline/drive1581/disk.rb +135 -0
- data/lib/badline/drive1581/disk_state.rb +87 -0
- data/lib/badline/drive1581/mechanism.rb +117 -0
- data/lib/badline/drive1581/serial_port.rb +52 -0
- data/lib/badline/drive1581/slot.rb +78 -0
- data/lib/badline/drive1581/track.rb +100 -0
- data/lib/badline/drive1581/track_bytes.rb +75 -0
- data/lib/badline/drive1581/wd1772/registers.rb +89 -0
- data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
- data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
- data/lib/badline/drive1581/wd1772/state.rb +66 -0
- data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
- data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
- data/lib/badline/drive1581/wd1772/turn.rb +73 -0
- data/lib/badline/drive1581/wd1772.rb +216 -0
- data/lib/badline/drive1581.rb +233 -0
- data/lib/badline/frontend/app.rb +1 -3
- data/lib/badline/frontend/audio_device.rb +67 -0
- data/lib/badline/frontend/audio_sink.rb +16 -50
- data/lib/badline/frontend/boot.rb +18 -10
- data/lib/badline/frontend/buttons.rb +7 -7
- data/lib/badline/frontend/keys.rb +33 -8
- data/lib/badline/frontend/media_slots.rb +42 -0
- data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
- data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
- data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
- data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
- data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +5 -8
- data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
- data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
- data/lib/badline/frontend/menu/theme.rb +18 -0
- data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
- data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
- data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
- data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
- data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
- data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
- data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
- data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
- data/lib/badline/frontend/player/theme.rb +18 -0
- data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
- data/lib/badline/frontend/sound.rb +19 -38
- data/lib/badline/frontend/timeline.rb +3 -14
- data/lib/badline/frontend.rb +24 -20
- data/lib/badline/iec_bus.rb +14 -2
- data/lib/badline/machine.rb +5 -4
- data/lib/badline/media/boot_disk.rb +50 -0
- data/lib/badline/media/not_disk.rb +8 -0
- data/lib/badline/media/true_drive.rb +35 -15
- data/lib/badline/media.rb +9 -3
- data/lib/badline/options/event.rb +2 -2
- data/lib/badline/options/family.rb +3 -9
- data/lib/badline/options/help.rb +2 -1
- data/lib/badline/roms/README +9 -0
- data/lib/badline/roms/dos1581.rom +0 -0
- data/lib/badline/sdl.rb +37 -20
- data/lib/badline/snapshot/state.rb +5 -4
- data/lib/badline/storage/p00.rb +8 -0
- data/lib/badline/time_of_day.rb +3 -3
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic20/attachments.rb +21 -7
- data/lib/badline/vic20/saved_state.rb +2 -0
- data/lib/badline/vic20.rb +3 -0
- data/lib/badline/z80/core.rb +2 -0
- data/lib/badline/z80/saved_state.rb +69 -0
- metadata +54 -21
- /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
- /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
- /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
- /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
data/lib/badline/drive1541.rb
CHANGED
|
@@ -2,32 +2,118 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
class Drive1571
|
|
5
|
-
# The WD1770 floppy controller at $2000, which
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# The WD1770 floppy controller at $2000, which DOS 3.0 drives for MFM
|
|
6
|
+
# disks. The 1571 here only ever holds GCR disks, so the controller
|
|
7
|
+
# never finds an MFM sector: it runs each command for its time and ends
|
|
8
|
+
# it as the WD177x data sheet does on a disk without one.
|
|
8
9
|
#
|
|
9
10
|
# $2000 status (read), command (write)
|
|
10
11
|
# $2001 track
|
|
11
12
|
# $2002 sector
|
|
12
13
|
# $2003 data
|
|
14
|
+
#
|
|
15
|
+
# BUSY sets as a command is written. A restore, seek or step (type I)
|
|
16
|
+
# moves the track register and ends a few milliseconds later, with
|
|
17
|
+
# TRACK 0 reported at track 0. A sector read or write, a read address
|
|
18
|
+
# or a read track ends with RECORD NOT FOUND after five turns of the
|
|
19
|
+
# disk, and a write track after one, with nothing written. A force
|
|
20
|
+
# interrupt ends the command at once. Time is the drive CPU's cycles.
|
|
13
21
|
class WD1770
|
|
22
|
+
BUSY = 0x01
|
|
23
|
+
TRACK0 = 0x04
|
|
24
|
+
NOT_FOUND = 0x10
|
|
25
|
+
MOTOR_ON = 0x80
|
|
26
|
+
|
|
27
|
+
# Cycles at 2 MHz, the 1571's clock in 1571 mode: a turn of the disk
|
|
28
|
+
# at 300 rpm, and a step with the head settling.
|
|
29
|
+
TURN = 400_000
|
|
30
|
+
STEP = 12_000
|
|
31
|
+
|
|
32
|
+
# The CPU whose cycles time the commands.
|
|
33
|
+
attr_writer :cpu
|
|
34
|
+
|
|
14
35
|
def initialize
|
|
15
36
|
@registers = [0, 0, 0, 0]
|
|
37
|
+
@status = 0
|
|
38
|
+
@due = 0
|
|
39
|
+
@result = 0
|
|
40
|
+
@cpu = nil
|
|
16
41
|
end
|
|
17
42
|
|
|
18
|
-
# Register +offset+, 0 to 3
|
|
19
|
-
|
|
43
|
+
# Register +offset+, 0 to 3: the status, settled to the command's end
|
|
44
|
+
# once its time has run out.
|
|
45
|
+
def peek(offset)
|
|
46
|
+
return @registers[offset] unless offset.zero?
|
|
47
|
+
|
|
48
|
+
finish if @status.anybits?(BUSY) && @cpu.cycles >= @due
|
|
49
|
+
@status
|
|
50
|
+
end
|
|
20
51
|
|
|
21
52
|
def poke(offset, value)
|
|
22
|
-
@registers[offset] = value
|
|
53
|
+
offset.zero? ? command(value) : @registers[offset] = value
|
|
23
54
|
end
|
|
24
55
|
|
|
25
56
|
def save_state(out)
|
|
26
|
-
out.ints(@
|
|
57
|
+
out.ints([*@registers, @status, @due, @result])
|
|
27
58
|
end
|
|
28
59
|
|
|
60
|
+
# A state from before the controller ran commands holds the four
|
|
61
|
+
# registers alone.
|
|
29
62
|
def load_state(input)
|
|
30
|
-
|
|
63
|
+
values = input.ints
|
|
64
|
+
@registers = values[0, 4]
|
|
65
|
+
@status = values[4] || 0
|
|
66
|
+
@due = values[5] || 0
|
|
67
|
+
@result = values[6] || 0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def command(value)
|
|
73
|
+
return interrupt if (value & 0xf0) == 0xd0
|
|
74
|
+
|
|
75
|
+
@registers[0] = value
|
|
76
|
+
value < 0x80 ? step(value) : search(value)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A restore ($0x), seek ($1x) or step ($2x to $7x), its track register
|
|
80
|
+
# moved at once.
|
|
81
|
+
def step(value)
|
|
82
|
+
track = @registers[1]
|
|
83
|
+
target = case value >> 4
|
|
84
|
+
when 0 then 0
|
|
85
|
+
when 1 then @registers[3]
|
|
86
|
+
else track + step_direction(value)
|
|
87
|
+
end
|
|
88
|
+
@registers[1] = target.clamp(0, 255) if value < 0x20 || value.anybits?(0x10)
|
|
89
|
+
start(STEP * [(target - track).abs, 1].max, @registers[1].zero? ? TRACK0 : 0)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Step in ($4x) moves toward higher tracks, step out ($6x) toward
|
|
93
|
+
# track 0, and step ($2x) the way the last one went.
|
|
94
|
+
def step_direction(value)
|
|
95
|
+
@direction = 1 if (value >> 5) == 2
|
|
96
|
+
@direction = -1 if (value >> 5) == 3
|
|
97
|
+
@direction || 1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A sector or track command that finds no MFM sector.
|
|
101
|
+
def search(value)
|
|
102
|
+
value >= 0xf0 ? start(TURN, 0) : start(TURN * 5, NOT_FOUND)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def start(cycles, result)
|
|
106
|
+
@status = BUSY | MOTOR_ON
|
|
107
|
+
@due = @cpu.cycles + cycles
|
|
108
|
+
@result = result
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def finish
|
|
112
|
+
@status = @result
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def interrupt
|
|
116
|
+
@status &= ~BUSY & 0xff
|
|
31
117
|
end
|
|
32
118
|
end
|
|
33
119
|
end
|
data/lib/badline/drive1571.rb
CHANGED
|
@@ -19,15 +19,13 @@ module Badline
|
|
|
19
19
|
# same speed either way (Drive1541::Mechanism#clock_hz=).
|
|
20
20
|
#
|
|
21
21
|
# The CIA sits at $4000, on the IRQ line with both VIAs. Only its serial
|
|
22
|
-
# port is wired
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# pin, so each byte goes through both shift registers bit by bit. The
|
|
26
|
-
# CIA takes no part in a pass of the idle loop: the drive sleeps only
|
|
27
|
-
# while the CIA is quiet (CIA#quiet?), and any access to it, or the
|
|
28
|
-
# host moving its fast serial pins, wakes the drive.
|
|
22
|
+
# port is wired, for fast serial through buffers PA1 turns round
|
|
23
|
+
# (Drive::FastSerial). The drive sleeps only while the CIA is quiet
|
|
24
|
+
# (CIA#quiet?).
|
|
29
25
|
class Drive1571
|
|
30
26
|
include Drive::Core
|
|
27
|
+
include Drive::VIAs
|
|
28
|
+
include Drive::FastSerial
|
|
31
29
|
|
|
32
30
|
CLOCK_HZ = 1_000_000
|
|
33
31
|
FAST_CLOCK_HZ = 2_000_000
|
|
@@ -40,10 +38,6 @@ module Badline
|
|
|
40
38
|
SIDE = 0x04
|
|
41
39
|
FAST = 0x20
|
|
42
40
|
|
|
43
|
-
# Whether PA1 turns the fast serial buffers outwards, the drive's CIA
|
|
44
|
-
# driving DATA and SRQ.
|
|
45
|
-
attr_reader :fast_serial_out
|
|
46
|
-
|
|
47
41
|
def cia
|
|
48
42
|
settle!
|
|
49
43
|
@cia
|
|
@@ -62,8 +56,7 @@ module Badline
|
|
|
62
56
|
def initialize(rom: nil, host_clock_hz: CLOCK_HZ, device: 8, debug: false)
|
|
63
57
|
@device = device
|
|
64
58
|
@clock_hz = CLOCK_HZ
|
|
65
|
-
|
|
66
|
-
@fast_output = 0
|
|
59
|
+
init_fast_serial
|
|
67
60
|
@mechanism = Drive1541::Mechanism.new(slip: 2)
|
|
68
61
|
@serial_port = SerialPort.new(@mechanism, device:)
|
|
69
62
|
@via1 = SerialVIA.new(start: 0x1800, peripheral: @serial_port, drive: self)
|
|
@@ -72,6 +65,7 @@ module Badline
|
|
|
72
65
|
@fdc = WD1770.new
|
|
73
66
|
@bus = Bus.new(rom: rom || ROM.load("dos1571.rom", 0x8000), via1: @via1, via2: @via2, cia: @cia, fdc: @fdc)
|
|
74
67
|
@cpu = CPU.new(@bus, debug:)
|
|
68
|
+
@fdc.cpu = @cpu
|
|
75
69
|
@idle_loop = IDLE_LOOP
|
|
76
70
|
@host_clock_hz = host_clock_hz
|
|
77
71
|
@phase = 0
|
|
@@ -114,13 +108,6 @@ module Badline
|
|
|
114
108
|
# (IECBus::DRIVE_FAST_DATA and DRIVE_FAST_SRQ) above it.
|
|
115
109
|
def serial_output = @via1.port_b_output | @fast_output
|
|
116
110
|
|
|
117
|
-
# The host moved its fast serial pins: the CIA hears SRQ on CNT and
|
|
118
|
-
# DATA on SP while PA1 turns the buffers inwards.
|
|
119
|
-
def fast_lines_moved
|
|
120
|
-
settle!
|
|
121
|
-
hear_fast_serial unless @fast_serial_out
|
|
122
|
-
end
|
|
123
|
-
|
|
124
111
|
# The drive's whole state, as the 1541's (Drive1541#save_state), with
|
|
125
112
|
# the CIA and the WD1770's registers. The clock rate, the side and the
|
|
126
113
|
# fast serial direction follow from VIA 1's port A.
|
|
@@ -181,45 +168,8 @@ module Badline
|
|
|
181
168
|
@cia.quiet? ? [@via1.quiet_cycles, @via2.quiet_cycles].min : 0
|
|
182
169
|
end
|
|
183
170
|
|
|
184
|
-
def
|
|
171
|
+
def fast_forward_more(cycles)
|
|
185
172
|
@cia.fast_forward(cycles)
|
|
186
173
|
end
|
|
187
|
-
|
|
188
|
-
# PA1 turned the buffers round: outwards the CIA's pins drive the
|
|
189
|
-
# lines, and inwards they let go and hear them.
|
|
190
|
-
def turn_fast_serial(out)
|
|
191
|
-
@fast_serial_out = out
|
|
192
|
-
return unless @serial_bus
|
|
193
|
-
|
|
194
|
-
push_fast_output(out ? fast_pins : 0)
|
|
195
|
-
hear_fast_serial unless out
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def drive_fast_serial
|
|
199
|
-
low = fast_pins
|
|
200
|
-
push_fast_output(low) if low != @fast_output
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# The lines the CIA's CNT and SP pull while its serial port drives
|
|
204
|
-
# them.
|
|
205
|
-
def fast_pins
|
|
206
|
-
serial = @cia.serial
|
|
207
|
-
return 0 unless serial.output?
|
|
208
|
-
|
|
209
|
-
low = serial.cnt ? 0 : IECBus::DRIVE_FAST_SRQ
|
|
210
|
-
serial.sp_out ? low : low | IECBus::DRIVE_FAST_DATA
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def push_fast_output(low)
|
|
214
|
-
@fast_output = low
|
|
215
|
-
@serial_bus&.drives_fast_moved!
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def hear_fast_serial
|
|
219
|
-
low = @serial_bus.low_lines
|
|
220
|
-
serial = @cia.serial
|
|
221
|
-
serial.cnt_in = low.nobits?(IECBus::SRQ)
|
|
222
|
-
serial.sp_in = low.nobits?(IECBus::DATA)
|
|
223
|
-
end
|
|
224
174
|
end
|
|
225
175
|
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1581
|
|
5
|
+
# The drive CPU's bus, as the 74LS139 decodes A13-A15 (1581 Service
|
|
6
|
+
# Manual, memory map and schematic sheet 1):
|
|
7
|
+
#
|
|
8
|
+
# $0000-$1FFF - 8 KB of RAM
|
|
9
|
+
# $2000-$3FFF - nothing: open bus
|
|
10
|
+
# $4000-$5FFF - the 8520, its sixteen registers mirrored
|
|
11
|
+
# $6000-$7FFF - the WD1772, its four registers mirrored
|
|
12
|
+
# $8000-$FFFF - the DOS ROM
|
|
13
|
+
#
|
|
14
|
+
# Writes to the ROM and to open bus go nowhere, and a read from open
|
|
15
|
+
# bus finds the last byte on the data bus, as on the 1541. A write to
|
|
16
|
+
# either of the 8520's ports or their direction registers moves the
|
|
17
|
+
# drive's lines (Drive1581#ports_written).
|
|
18
|
+
#
|
|
19
|
+
# While Idle records a pass of the DOS's idle loop, the bus watches it
|
|
20
|
+
# (watch!): the RAM it reads and writes, and whether it touched the
|
|
21
|
+
# 8520 or the WD1772, either of which makes the pass volatile. While
|
|
22
|
+
# Idle looks for an orbit, the bus guards the drive (guard!), and any
|
|
23
|
+
# access to either chip taints it.
|
|
24
|
+
class Bus
|
|
25
|
+
include Addressable
|
|
26
|
+
|
|
27
|
+
attr_reader :ram, :rom, :cia, :fdc, :data, :volatile, :touched, :tainted, :touched_timers
|
|
28
|
+
|
|
29
|
+
def initialize(rom:, cia:, fdc:, drive:)
|
|
30
|
+
addressable_at(0, length: 2**16)
|
|
31
|
+
@ram = Memory.new([], length: 0x2000, start: 0)
|
|
32
|
+
@rom = rom
|
|
33
|
+
@cia = cia
|
|
34
|
+
@fdc = fdc
|
|
35
|
+
@drive = drive
|
|
36
|
+
@data = 0
|
|
37
|
+
@recording = @guarding = @watching = false
|
|
38
|
+
@volatile = @tainted = false
|
|
39
|
+
@touched = {}
|
|
40
|
+
@touched_timers = 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The last byte on the data bus and the RAM. What a watched stretch
|
|
44
|
+
# touched is Idle's, and a restored bus isn't watching.
|
|
45
|
+
def save_state(out)
|
|
46
|
+
out.int(@data)
|
|
47
|
+
@ram.save_state(out)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def load_state(input)
|
|
51
|
+
@data = input.int
|
|
52
|
+
@ram.load_state(input)
|
|
53
|
+
@watching = false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def peek(addr)
|
|
57
|
+
return @data = @rom.peek(addr) if addr >= 0x8000
|
|
58
|
+
return @data = chip_peek(addr) if addr >= 0x2000
|
|
59
|
+
|
|
60
|
+
touch(addr) if @recording
|
|
61
|
+
@data = @ram.peek(addr)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def poke(addr, value)
|
|
65
|
+
@data = value
|
|
66
|
+
return if addr >= 0x8000
|
|
67
|
+
return chip_poke(addr, value) if addr >= 0x2000
|
|
68
|
+
|
|
69
|
+
touch(addr) if @recording
|
|
70
|
+
@ram.poke(addr, value)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Starts watching: see volatile and touched.
|
|
74
|
+
def watch!
|
|
75
|
+
@recording = true
|
|
76
|
+
@volatile = false
|
|
77
|
+
@touched = {}
|
|
78
|
+
watching!
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def unwatch!
|
|
82
|
+
@recording = false
|
|
83
|
+
watching!
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Starts guarding: see tainted.
|
|
87
|
+
def guard!
|
|
88
|
+
@guarding = true
|
|
89
|
+
@tainted = false
|
|
90
|
+
watching!
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def chip_peek(addr)
|
|
96
|
+
watch_chip if @watching
|
|
97
|
+
if addr >= 0x6000
|
|
98
|
+
@fdc.peek(addr & 0x03)
|
|
99
|
+
elsif addr >= 0x4000
|
|
100
|
+
@cia.peek(0x4000 | (addr & 0x0f))
|
|
101
|
+
else
|
|
102
|
+
@data
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def chip_poke(addr, value)
|
|
107
|
+
watch_chip if @watching
|
|
108
|
+
if addr >= 0x6000
|
|
109
|
+
@fdc.poke(addr & 0x03, value)
|
|
110
|
+
elsif addr >= 0x4000
|
|
111
|
+
@cia.poke(0x4000 | (addr & 0x0f), value)
|
|
112
|
+
@drive.ports_written if addr.nobits?(0x0c)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def touch(addr)
|
|
117
|
+
@touched[addr] = @ram.peek(addr) unless @touched.key?(addr)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def watching!
|
|
121
|
+
@watching = @recording || @guarding
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def watch_chip
|
|
125
|
+
if @recording
|
|
126
|
+
@volatile = true
|
|
127
|
+
@recording = false
|
|
128
|
+
end
|
|
129
|
+
if @guarding
|
|
130
|
+
@tainted = true
|
|
131
|
+
@guarding = false
|
|
132
|
+
end
|
|
133
|
+
watching!
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/drive1581/disk_state"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
class Drive1581
|
|
7
|
+
# A 3.5" double-sided disk as the WD1772 sees it: a Track on each side
|
|
8
|
+
# of 80 cylinders, or none where it was never formatted.
|
|
9
|
+
#
|
|
10
|
+
# A .d81 image holds the 1581 DOS's logical blocks in order, 40 of 256
|
|
11
|
+
# bytes on each logical track 1 to 80 (1581 Service Manual, format
|
|
12
|
+
# organization). Logical track t is cylinder t - 1, and the DOS puts
|
|
13
|
+
# its sectors 0 to 19 on side 0 and 20 to 39 on side 1, two to each
|
|
14
|
+
# 512-byte physical sector, numbered from 1. So physical sector s on
|
|
15
|
+
# side h of cylinder c holds logical sectors 20h + 2(s - 1) and the one
|
|
16
|
+
# after it, and every cylinder of the image is a standard Track
|
|
17
|
+
# (Track.standard) on both sides. A block the image's error table
|
|
18
|
+
# marks 20 or 21 loses its sector's ID field, 27 spoils the ID's CRC,
|
|
19
|
+
# 22 loses the data field and 23 spoils its CRC.
|
|
20
|
+
#
|
|
21
|
+
# The WD1772 writes into the tracks, and flush stores the standard
|
|
22
|
+
# sectors of each one written back in the image, which writes its host
|
|
23
|
+
# file. A track formatted another way stays on the disk alone. The disk
|
|
24
|
+
# is write-protected when the image won't take writes.
|
|
25
|
+
class Disk
|
|
26
|
+
include State
|
|
27
|
+
|
|
28
|
+
CYLINDERS = 80
|
|
29
|
+
SIDES = 2
|
|
30
|
+
|
|
31
|
+
# Logical blocks a side of a cylinder holds.
|
|
32
|
+
SIDE_BLOCKS = 20
|
|
33
|
+
|
|
34
|
+
# A logical block's bytes, and how many a physical sector holds.
|
|
35
|
+
BLOCK_SIZE = 256
|
|
36
|
+
BLOCKS = Track::SECTOR_SIZE / BLOCK_SIZE
|
|
37
|
+
|
|
38
|
+
# A disk from the .d81 image at +path+. `read_only` opens the image
|
|
39
|
+
# write-protected.
|
|
40
|
+
def self.open(path, read_only: false)
|
|
41
|
+
new(Storage::D81Image.new(path, read_only:)).opened(path, read_only)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
attr_reader :image
|
|
45
|
+
|
|
46
|
+
def initialize(image = nil)
|
|
47
|
+
@image = image
|
|
48
|
+
@tracks = Array.new(CYLINDERS * SIDES)
|
|
49
|
+
@written = {}
|
|
50
|
+
@changed = {}
|
|
51
|
+
@path = nil
|
|
52
|
+
@read_only = false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The Track on +side+ of +cylinder+, or nil where there is none: past
|
|
56
|
+
# the image's cylinders, or on a disk without an image that the head
|
|
57
|
+
# never formatted.
|
|
58
|
+
def track(cylinder, side)
|
|
59
|
+
index = (cylinder * SIDES) + side
|
|
60
|
+
return @tracks[index] if @tracks[index] || !@image || cylinder >= CYLINDERS
|
|
61
|
+
|
|
62
|
+
@tracks[index] = image_track(cylinder, side)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A write track laid +track+ down on +side+ of +cylinder+, or a write
|
|
66
|
+
# sector changed it.
|
|
67
|
+
def write(cylinder, side, track)
|
|
68
|
+
return if cylinder >= CYLINDERS
|
|
69
|
+
|
|
70
|
+
index = (cylinder * SIDES) + side
|
|
71
|
+
@tracks[index] = track
|
|
72
|
+
@written[index] = true
|
|
73
|
+
@changed[index] = true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Whether the write-protect hole is open: the disk's image won't take
|
|
77
|
+
# writes, or a disk without an image was opened read-only.
|
|
78
|
+
def write_protected? = @image.nil? ? @read_only : !@image.writable?
|
|
79
|
+
|
|
80
|
+
# Whether any track was written since the last flush.
|
|
81
|
+
def written? = !@written.empty?
|
|
82
|
+
|
|
83
|
+
# Stores the standard sectors of each track written since the last
|
|
84
|
+
# flush in the image, all in one write to its host file.
|
|
85
|
+
def flush
|
|
86
|
+
written = @written.keys.sort
|
|
87
|
+
@written.clear
|
|
88
|
+
return if @image.nil? || written.empty?
|
|
89
|
+
|
|
90
|
+
@image.store_blocks(written.flat_map { |index| track_blocks(index / SIDES, index % SIDES) })
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def image_track(cylinder, side)
|
|
96
|
+
track = Track.standard(cylinder, side) { |sector| physical_block(cylinder, side, sector) }
|
|
97
|
+
track.sectors.each { |sector| mark_errors(cylinder, side, sector) }
|
|
98
|
+
track.sectors.reject! { |sector| sector.id.nil? }
|
|
99
|
+
track
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The logical track and first sector physical sector +sector+ holds.
|
|
103
|
+
def logical(cylinder, side, sector) = [cylinder + 1, (side * SIDE_BLOCKS) + ((sector - 1) * BLOCKS)]
|
|
104
|
+
|
|
105
|
+
def physical_block(cylinder, side, sector)
|
|
106
|
+
track, first = logical(cylinder, side, sector)
|
|
107
|
+
Array.new(BLOCKS) { |i| @image.read_block(track, first + i) || [] }.flatten
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def mark_errors(cylinder, side, sector)
|
|
111
|
+
track, first = logical(cylinder, side, sector.id.bytes[2])
|
|
112
|
+
errors = Array.new(BLOCKS) { |i| @image.block_error(track, first + i) }
|
|
113
|
+
sector.id.good = false if errors.include?(27)
|
|
114
|
+
sector.data.good = false if errors.include?(23)
|
|
115
|
+
sector.data = nil if errors.include?(22)
|
|
116
|
+
sector.id = nil if errors.include?(20) || errors.include?(21)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# [track, sector, data, nil] for DiskImage#store_blocks, for each
|
|
120
|
+
# logical block a standard sector of the track holds.
|
|
121
|
+
def track_blocks(cylinder, side)
|
|
122
|
+
track = @tracks[(cylinder * SIDES) + side]
|
|
123
|
+
Track::SECTORS.times.flat_map do |index|
|
|
124
|
+
data = track.sector([cylinder, side, index + 1, Track::SIZE_CODE])&.data
|
|
125
|
+
next [] unless data
|
|
126
|
+
|
|
127
|
+
logical_track, first = logical(cylinder, side, index + 1)
|
|
128
|
+
Array.new(BLOCKS) do |i|
|
|
129
|
+
[logical_track, first + i, data.bytes[i * BLOCK_SIZE, BLOCK_SIZE], nil]
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1581
|
|
5
|
+
class Disk
|
|
6
|
+
# Saving and restoring a Disk for a snapshot: the image by its path,
|
|
7
|
+
# and every track the head wrote since the disk went in, with those
|
|
8
|
+
# written since the last flush. The rest come from the image, read
|
|
9
|
+
# again from its host file where there still is one.
|
|
10
|
+
module State
|
|
11
|
+
# The image's path, expanded, and whether it was opened read-only,
|
|
12
|
+
# for a disk from Disk.open. Nil for a disk made another way.
|
|
13
|
+
attr_reader :path, :read_only
|
|
14
|
+
|
|
15
|
+
# The disk a state from save_state describes: +current+ when it was
|
|
16
|
+
# opened from the same image the same way, and otherwise a disk for
|
|
17
|
+
# the image at the path the state names. Where no file is at that
|
|
18
|
+
# path, the disk has no image and is write-protected, and a
|
|
19
|
+
# detached reader always gets one without an image.
|
|
20
|
+
def self.load(input, current)
|
|
21
|
+
path = input.optional_string
|
|
22
|
+
read_only = input.boolean?
|
|
23
|
+
disk = current if current && !path.nil? && current.path == path && current.read_only == read_only
|
|
24
|
+
disk ||= fresh(path, read_only, input.detached?)
|
|
25
|
+
disk.load_tracks(input)
|
|
26
|
+
disk
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.fresh(path, read_only, detached)
|
|
30
|
+
return Disk.new if path.nil? || detached
|
|
31
|
+
return Disk.new.opened(path, true) unless File.file?(path)
|
|
32
|
+
|
|
33
|
+
Disk.open(path, read_only:)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Notes the image's path and how it was opened.
|
|
37
|
+
def opened(path, read_only)
|
|
38
|
+
@path = File.expand_path(path)
|
|
39
|
+
@read_only = read_only
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def save_state(out)
|
|
44
|
+
out.optional_string(@path).boolean(@read_only)
|
|
45
|
+
changed = @changed.keys.sort
|
|
46
|
+
out.ints(changed)
|
|
47
|
+
changed.each { |index| save_track(@tracks[index], out) }
|
|
48
|
+
out.ints(@written.keys)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def load_tracks(input)
|
|
52
|
+
@changed.clear
|
|
53
|
+
input.ints.each do |index|
|
|
54
|
+
@tracks[index] = load_track(input)
|
|
55
|
+
@changed[index] = true
|
|
56
|
+
end
|
|
57
|
+
@written.clear
|
|
58
|
+
input.ints.each { |index| @written[index] = true }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def save_track(track, out)
|
|
64
|
+
out.int(track.sectors.length)
|
|
65
|
+
track.sectors.each do |sector|
|
|
66
|
+
save_field(sector.id, out)
|
|
67
|
+
out.boolean(!sector.data.nil?)
|
|
68
|
+
save_field(sector.data, out) if sector.data
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def save_field(field, out)
|
|
73
|
+
out.int(field.mark).ints(field.bytes).boolean(field.good).boolean(field.deleted)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def load_track(input)
|
|
77
|
+
Track.new(Array.new(input.int) do
|
|
78
|
+
id = load_field(input)
|
|
79
|
+
Track::Sector.new(id, input.boolean? ? load_field(input) : nil)
|
|
80
|
+
end)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def load_field(input) = Track::Field.new(input.int, input.ints, input.boolean?, input.boolean?)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|