badline 0.4.2 → 0.5.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 +25 -0
- data/CLAUDE.md +10 -5
- data/README.md +54 -23
- data/doc/pinned-behaviour.md +221 -0
- data/exe/badline-ruby +7 -4
- data/lib/badline/address_bus/ultimax_pages.rb +38 -0
- data/lib/badline/address_bus.rb +19 -29
- data/lib/badline/audio/jukebox.rb +6 -4
- data/lib/badline/audio/machine_player.rb +10 -6
- data/lib/badline/cartridge/kcs_power.rb +3 -3
- data/lib/badline/cia.rb +5 -1
- data/lib/badline/computer.rb +86 -7
- data/lib/badline/cpu.rb +39 -0
- data/lib/badline/drive1541/bus.rb +134 -0
- data/lib/badline/drive1541/disk.rb +256 -0
- data/lib/badline/drive1541/disk_via.rb +33 -0
- data/lib/badline/drive1541/gcr.rb +57 -0
- data/lib/badline/drive1541/idle.rb +184 -0
- data/lib/badline/drive1541/mechanism.rb +277 -0
- data/lib/badline/drive1541/rotation.rb +111 -0
- data/lib/badline/drive1541/sector_reader.rb +76 -0
- data/lib/badline/drive1541/serial_port.rb +67 -0
- data/lib/badline/drive1541/track.rb +98 -0
- data/lib/badline/drive1541.rb +218 -0
- data/lib/badline/gui/application.rb +15 -3
- data/lib/badline/gui/drive_led_pane.rb +29 -0
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/iec_bus.rb +93 -0
- data/lib/badline/kernal_trap/channel.rb +9 -1
- data/lib/badline/kernal_trap/serial.rb +9 -3
- data/lib/badline/media/true_drive.rb +54 -0
- data/lib/badline/media.rb +22 -1
- data/lib/badline/options.rb +24 -4
- data/lib/badline/region.rb +35 -10
- data/lib/badline/reu/dma.rb +228 -0
- data/lib/badline/reu/ram.rb +65 -0
- data/lib/badline/reu/trigger.rb +32 -0
- data/lib/badline/reu.rb +276 -0
- data/lib/badline/roms/README +10 -0
- data/lib/badline/roms/dos1541.rom +0 -0
- data/lib/badline/sdl/functions.rb +1 -0
- data/lib/badline/storage/d64_image.rb +18 -9
- data/lib/badline/storage/d81_image.rb +1 -0
- data/lib/badline/storage/disk_image/catalog.rb +52 -0
- data/lib/badline/storage/disk_image/writing.rb +16 -0
- data/lib/badline/storage/disk_image.rb +2 -0
- data/lib/badline/storage/g64_image.rb +222 -0
- data/lib/badline/storage/host_directory.rb +21 -1
- data/lib/badline/storage/listing.rb +118 -0
- data/lib/badline/storage/t64.rb +14 -0
- data/lib/badline/storage.rb +2 -0
- data/lib/badline/traps.rb +4 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +3 -0
- data/lib/badline/via/fast_forward.rb +38 -0
- data/lib/badline/via/shift_register.rb +8 -3
- data/lib/badline/via/timer1.rb +30 -0
- data/lib/badline/via/timer2.rb +25 -0
- data/lib/badline/via.rb +2 -0
- data/lib/badline/vic/display_state.rb +7 -7
- data/lib/badline/vic/sprite/internal_bus.rb +11 -7
- data/lib/badline/vic/sprite/shifter.rb +10 -15
- data/lib/badline/vic/sprite/timing.rb +60 -0
- data/lib/badline/vic/sprite.rb +14 -12
- data/lib/badline/vic/sprites.rb +2 -2
- data/lib/badline/vic.rb +86 -39
- data/lib/badline.rb +3 -0
- metadata +26 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/drive1541/sector_reader"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
class Drive1541
|
|
7
|
+
# A disk as the head sees it: a Track for each half track that holds
|
|
8
|
+
# data, numbered from 2 (track 1) to MAX_HALF_TRACK (track 42), and
|
|
9
|
+
# nothing between them. A .d64 image fills the whole tracks; a .g64
|
|
10
|
+
# can fill any of them, with tracks of any length.
|
|
11
|
+
#
|
|
12
|
+
# The head writes into the tracks' bytes, and a write to a half track
|
|
13
|
+
# without data gives it a blank Track first. A disk made from an image
|
|
14
|
+
# remembers it: flush reads the sectors back off the tracks written
|
|
15
|
+
# since the last flush and stores them in the image, which writes its
|
|
16
|
+
# host file. The disk is write-protected when the image won't take
|
|
17
|
+
# writes.
|
|
18
|
+
class Disk
|
|
19
|
+
MAX_HALF_TRACK = 84
|
|
20
|
+
|
|
21
|
+
# GCR bytes around a track in each speed zone: 200 ms of rotation at
|
|
22
|
+
# 300 rpm, in bytes of 32, 30, 28 and 26 µs.
|
|
23
|
+
TRACK_LENGTHS = [6250, 6666, 7142, 7692].freeze
|
|
24
|
+
|
|
25
|
+
# The speed zone each track is written in: 3 for the 21 sectors of
|
|
26
|
+
# tracks 1-17, 2 for 18-24, 1 for 25-30 and 0 from 31 on.
|
|
27
|
+
def self.zone(track)
|
|
28
|
+
if track <= 17 then 3
|
|
29
|
+
elsif track <= 24 then 2
|
|
30
|
+
elsif track <= 30 then 1
|
|
31
|
+
else 0
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# How far round the DOS's N: starts each track from the one before,
|
|
36
|
+
# in each zone, in 1/10000 of a turn: it steps in and starts writing
|
|
37
|
+
# the next track that long after it started the last.
|
|
38
|
+
SKEWS = [2916, 897, 8915, 6869].freeze
|
|
39
|
+
SKEW_TURN = 10_000
|
|
40
|
+
|
|
41
|
+
SYNC_LENGTH = 5
|
|
42
|
+
HEADER_GAP = 9
|
|
43
|
+
GAP = 0x55
|
|
44
|
+
HEADER_ID = 0x08
|
|
45
|
+
DATA_ID = 0x07
|
|
46
|
+
|
|
47
|
+
# A sector: SYNC, the 10-byte GCR header, the header gap, SYNC and
|
|
48
|
+
# the 325-byte GCR data block. The gap after it takes up the rest.
|
|
49
|
+
SECTOR_LENGTH = SYNC_LENGTH + 10 + HEADER_GAP + SYNC_LENGTH + 325
|
|
50
|
+
|
|
51
|
+
# A disk from the image at +path+: a .g64 as its tracks are, and
|
|
52
|
+
# anything else as a .d64, formatted. `read_only` opens the image
|
|
53
|
+
# write-protected.
|
|
54
|
+
def self.open(path, read_only: false)
|
|
55
|
+
return from_g64(Storage::G64Image.new(path, read_only:)) if File.extname(path).casecmp?(".g64")
|
|
56
|
+
|
|
57
|
+
from_d64(Storage::D64Image.new(path, read_only:))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A disk from a G64 image, each half track as the image stores it,
|
|
61
|
+
# with its speed map. Flushing it writes the tracks back as they are.
|
|
62
|
+
def self.from_g64(image)
|
|
63
|
+
disk = new(image)
|
|
64
|
+
(Mechanism::MIN_HALF_TRACK..MAX_HALF_TRACK).each do |half_track|
|
|
65
|
+
entry = half_track - Mechanism::MIN_HALF_TRACK
|
|
66
|
+
track = image.track(entry)
|
|
67
|
+
disk.write(half_track, Track.new(*track, image.speeds(entry))) if track
|
|
68
|
+
end
|
|
69
|
+
disk
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A disk formatted from a D64 image, each sector laid out as the DOS
|
|
73
|
+
# formats it, with the disk ID from the header block. A block the
|
|
74
|
+
# image's error table marks 20, 21, 22, 23, 27 or 29 is written the
|
|
75
|
+
# way that DOS error would read. The table's other codes, 24, 25, 26
|
|
76
|
+
# and 28, describe write or decoding faults the layout can't carry,
|
|
77
|
+
# so their blocks are written good and read without an error.
|
|
78
|
+
#
|
|
79
|
+
# Each track starts sector 0 at the angle the DOS's N: leaves it: track
|
|
80
|
+
# 1 at the index angle, and each track after it SKEWS[zone] of a turn
|
|
81
|
+
# round from the last.
|
|
82
|
+
def self.from_d64(image)
|
|
83
|
+
track, sector = image.header_block
|
|
84
|
+
header = image.read_block(track, sector)
|
|
85
|
+
id = [header[0xa2], header[0xa3]]
|
|
86
|
+
disk = new(image)
|
|
87
|
+
angle = 0
|
|
88
|
+
(1..image.track_count).each do |track|
|
|
89
|
+
angle = (angle + SKEWS[zone(track)]) % SKEW_TURN if track > 1
|
|
90
|
+
bytes = format_track(image, track, id)
|
|
91
|
+
disk.write(track * 2, Track.new(bytes.rotate(-(bytes.length * angle / SKEW_TURN)), zone(track)))
|
|
92
|
+
end
|
|
93
|
+
disk
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.format_track(image, track, id)
|
|
97
|
+
sectors = image.sectors_in(track)
|
|
98
|
+
length = TRACK_LENGTHS[zone(track)]
|
|
99
|
+
gap = (length - (sectors * SECTOR_LENGTH)) / sectors
|
|
100
|
+
bytes = []
|
|
101
|
+
sectors.times do |sector|
|
|
102
|
+
format_sector(bytes, image, track, sector, id)
|
|
103
|
+
gap.times { bytes << GAP }
|
|
104
|
+
end
|
|
105
|
+
bytes << GAP while bytes.length < length
|
|
106
|
+
bytes
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Error 21 leaves the block without SYNC marks, and 29 writes its
|
|
110
|
+
# header with the wrong first ID byte.
|
|
111
|
+
def self.format_sector(bytes, image, track, sector, id)
|
|
112
|
+
error = image.block_error(track, sector)
|
|
113
|
+
sync = error == 21 ? GAP : 0xff
|
|
114
|
+
id = [id[0] ^ 0xff, id[1]] if error == 29
|
|
115
|
+
SYNC_LENGTH.times { bytes << sync }
|
|
116
|
+
bytes.concat(GCR.encode(header(track, sector, id, error)))
|
|
117
|
+
HEADER_GAP.times { bytes << GAP }
|
|
118
|
+
SYNC_LENGTH.times { bytes << sync }
|
|
119
|
+
bytes.concat(GCR.encode(data_block(image.read_block(track, sector), error)))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# ID, checksum, sector, track, the second ID byte, the first and two
|
|
123
|
+
# $0F fillers. Error 20 hides the block's ID and 27 spoils its
|
|
124
|
+
# checksum.
|
|
125
|
+
def self.header(track, sector, id, error)
|
|
126
|
+
checksum = sector ^ track ^ id[1] ^ id[0]
|
|
127
|
+
checksum ^= 0xff if error == 27
|
|
128
|
+
[error == 20 ? 0x00 : HEADER_ID, checksum, sector, track, id[1], id[0], 0x0f, 0x0f]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# ID, 256 data bytes, their checksum and two $00 fillers. Error 22
|
|
132
|
+
# hides the block's ID and 23 spoils its checksum.
|
|
133
|
+
def self.data_block(data, error)
|
|
134
|
+
checksum = 0
|
|
135
|
+
data.each { |byte| checksum ^= byte }
|
|
136
|
+
checksum ^= 0xff if error == 23
|
|
137
|
+
block = [error == 22 ? 0x00 : DATA_ID]
|
|
138
|
+
block.concat(data)
|
|
139
|
+
block << checksum << 0x00 << 0x00
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
attr_reader :image
|
|
143
|
+
|
|
144
|
+
def initialize(image = nil)
|
|
145
|
+
@image = image
|
|
146
|
+
@tracks = Array.new(MAX_HALF_TRACK + 1)
|
|
147
|
+
@written = {}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# The Track at the half track, or nil where there's no data.
|
|
151
|
+
def track(half_track) = @tracks[half_track]
|
|
152
|
+
|
|
153
|
+
def write(half_track, track)
|
|
154
|
+
@tracks[half_track] = track
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Whether the write-protect notch is covered: the disk's image won't
|
|
158
|
+
# take writes. A disk without an image takes them, and keeps them
|
|
159
|
+
# only in its tracks.
|
|
160
|
+
def write_protected? = !@image.nil? && !@image.writable?
|
|
161
|
+
|
|
162
|
+
# A half track the head writes to, blank to begin with: no flux, and
|
|
163
|
+
# as many bytes as a turn holds at the bit rate of the +zone+ it's
|
|
164
|
+
# written in.
|
|
165
|
+
def writable_track(half_track, zone)
|
|
166
|
+
@tracks[half_track] ||= Track.new(Array.new(TRACK_LENGTHS[zone], 0), zone)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# The head wrote to the half track.
|
|
170
|
+
def written(half_track)
|
|
171
|
+
@written[half_track] = true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Whether any track was written since the last flush.
|
|
175
|
+
def written? = !@written.empty?
|
|
176
|
+
|
|
177
|
+
# Stores each track written since the last flush in the image, all
|
|
178
|
+
# in one write to its host file. A G64 image takes the tracks as they
|
|
179
|
+
# are. A D64 image takes the sectors read back off them, and keeps
|
|
180
|
+
# whole tracks only, so a half track, or a track past its last, stays
|
|
181
|
+
# on the disk alone. A sector that no longer reads back keeps its old
|
|
182
|
+
# data, and the flush warns once, naming the tracks that lost one.
|
|
183
|
+
def flush
|
|
184
|
+
return flush_tracks if @image.respond_to?(:store_tracks)
|
|
185
|
+
|
|
186
|
+
tracks = @written.keys.select { |half| half.even? && (1..@image&.track_count.to_i).cover?(half / 2) }
|
|
187
|
+
@written.clear
|
|
188
|
+
return if tracks.empty?
|
|
189
|
+
|
|
190
|
+
sectors = tracks.to_h { |half| [half / 2, SectorReader.read(@tracks[half].bytes, half / 2)] }
|
|
191
|
+
id = disk_id(sectors)
|
|
192
|
+
warn_lost(sectors)
|
|
193
|
+
@image.store_blocks(sectors.flat_map do |track, found|
|
|
194
|
+
Array.new(@image.sectors_in(track)) { |sector| stored_block(track, sector, found[sector], id) }
|
|
195
|
+
end)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
def flush_tracks
|
|
201
|
+
tracks = @written.keys.sort.to_h do |half|
|
|
202
|
+
[half - Mechanism::MIN_HALF_TRACK, [@tracks[half].bytes, @tracks[half].zone]]
|
|
203
|
+
end
|
|
204
|
+
@written.clear
|
|
205
|
+
@image.store_tracks(tracks) unless tracks.empty?
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def warn_lost(sectors)
|
|
209
|
+
lost = sectors.keys.select do |track|
|
|
210
|
+
Array.new(@image.sectors_in(track)) { |sector| lost?(track, sector, sectors[track][sector]) }.any?
|
|
211
|
+
end
|
|
212
|
+
return if lost.empty?
|
|
213
|
+
|
|
214
|
+
warn "1541: tracks written that no longer read back whole: #{lost.join(', ')}. The .d64 keeps the old " \
|
|
215
|
+
"data of the sectors lost, and a .g64 image keeps tracks as the drive writes them."
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# A sector the head no longer finds, which the image doesn't already
|
|
219
|
+
# hold as unreadable.
|
|
220
|
+
def lost?(track, sector, found)
|
|
221
|
+
found.nil? && ![20, 21].include?(@image.block_error(track, sector))
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# The disk ID the headers must carry, from the header block's $A2 and
|
|
225
|
+
# $A3 as Disk.from_d64 takes it: as just read back, or as the image
|
|
226
|
+
# holds it.
|
|
227
|
+
def disk_id(sectors)
|
|
228
|
+
track, sector = @image.header_block
|
|
229
|
+
header = sectors.dig(track, sector)&.data
|
|
230
|
+
data = header ? header[1, 256] : @image.read_block(track, sector)
|
|
231
|
+
[data[0xa2], data[0xa3]]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# [track, sector, data, error] for DiskImage#store_blocks: the data as
|
|
235
|
+
# read, nil where it can't be, and the DOS error a read of it would
|
|
236
|
+
# raise, the way Disk.from_d64 writes each one. A sector whose header
|
|
237
|
+
# isn't found keeps its data, and reads as error 20, or 21 where the
|
|
238
|
+
# image already has it so.
|
|
239
|
+
def stored_block(track, sector, found, id)
|
|
240
|
+
return [track, sector, nil, @image.block_error(track, sector) == 21 ? 21 : 20] unless found
|
|
241
|
+
|
|
242
|
+
header = found.header
|
|
243
|
+
data = found.data
|
|
244
|
+
[track, sector, data && data[1, 256], block_error(header, data, id)]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def block_error(header, data, id)
|
|
248
|
+
if header[1] != header[2] ^ header[3] ^ header[4] ^ header[5] then 27
|
|
249
|
+
elsif header[5] != id[0] || header[4] != id[1] then 29
|
|
250
|
+
elsif data.nil? || data[0] != DATA_ID then 22
|
|
251
|
+
elsif data[257] != data[1, 256].reduce(:^) then 23
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# VIA 2, the one facing the disk mechanism. Its port B drives the
|
|
6
|
+
# Mechanism, and CB2 selects its read or write mode. The mechanism
|
|
7
|
+
# hears both after every write and reset, since ORB, DDRB, the ACR and
|
|
8
|
+
# the PCR can all change them.
|
|
9
|
+
class DiskVIA < VIA
|
|
10
|
+
def initialize(start:, mechanism:)
|
|
11
|
+
@mechanism = mechanism
|
|
12
|
+
super(start:, peripheral: mechanism)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def reset!
|
|
16
|
+
super
|
|
17
|
+
tell_mechanism
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def poke(addr, value)
|
|
21
|
+
super
|
|
22
|
+
tell_mechanism
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def tell_mechanism
|
|
28
|
+
@mechanism.port_b_written(port_b_output)
|
|
29
|
+
@mechanism.cb2_written(cb2_output)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# Group code recording, the 1541's on-disk code. Each nibble becomes
|
|
6
|
+
# five bits, chosen so that no code has more than two 0 bits in a row
|
|
7
|
+
# and no run of codes has ten 1 bits in a row, which only a SYNC mark
|
|
8
|
+
# has. Four bytes become five.
|
|
9
|
+
module GCR
|
|
10
|
+
CODES = [0x0a, 0x0b, 0x12, 0x13, 0x0e, 0x0f, 0x16, 0x17,
|
|
11
|
+
0x09, 0x19, 0x1a, 0x1b, 0x0d, 0x1d, 0x1e, 0x15].freeze
|
|
12
|
+
|
|
13
|
+
# Five-bit code => nibble, nil for the codes that aren't GCR.
|
|
14
|
+
NIBBLES = Array.new(32) { |code| CODES.index(code) }.freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Encodes a whole number of four-byte groups.
|
|
19
|
+
def encode(bytes)
|
|
20
|
+
out = []
|
|
21
|
+
i = 0
|
|
22
|
+
while i < bytes.length
|
|
23
|
+
bits = 0
|
|
24
|
+
4.times do |n|
|
|
25
|
+
byte = bytes[i + n]
|
|
26
|
+
bits = (bits << 10) | (CODES[byte >> 4] << 5) | CODES[byte & 0x0f]
|
|
27
|
+
end
|
|
28
|
+
4.downto(0) { |n| out << ((bits >> (n * 8)) & 0xff) }
|
|
29
|
+
i += 4
|
|
30
|
+
end
|
|
31
|
+
out
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Decodes a whole number of five-byte groups, nil when any code
|
|
35
|
+
# isn't GCR.
|
|
36
|
+
def decode(bytes)
|
|
37
|
+
out = []
|
|
38
|
+
i = 0
|
|
39
|
+
while i < bytes.length
|
|
40
|
+
bits = 0
|
|
41
|
+
5.times { |n| bits = (bits << 8) | bytes[i + n] }
|
|
42
|
+
n = 7
|
|
43
|
+
while n.positive?
|
|
44
|
+
high = NIBBLES[(bits >> (n * 5)) & 0x1f]
|
|
45
|
+
low = NIBBLES[(bits >> ((n - 1) * 5)) & 0x1f]
|
|
46
|
+
return nil if high.nil? || low.nil?
|
|
47
|
+
|
|
48
|
+
out << ((high << 4) | low)
|
|
49
|
+
n -= 2
|
|
50
|
+
end
|
|
51
|
+
i += 5
|
|
52
|
+
end
|
|
53
|
+
out
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# Skipping the drive's cycles while the DOS idles, exactly.
|
|
6
|
+
#
|
|
7
|
+
# With nothing to do, the DOS goes round its idle loop from $EBFF,
|
|
8
|
+
# 446 cycles a pass, until VIA 2's timer 1 interrupts it every 14,850
|
|
9
|
+
# cycles for the job loop, or ATN interrupts it through VIA 1's CA1. A
|
|
10
|
+
# pass writes RAM and VIA 2's port B, but puts back what it found, so
|
|
11
|
+
# the drive comes round to $EBFF as it left it. Only the timers'
|
|
12
|
+
# counters and the cycle counts move.
|
|
13
|
+
#
|
|
14
|
+
# Each time the CPU comes to $EBFF, the drive records the pass that
|
|
15
|
+
# follows. At the next $EBFF it checks that the pass
|
|
16
|
+
#
|
|
17
|
+
# - left the CPU, both VIAs but for their counters, the bus's last
|
|
18
|
+
# byte, the SO pin, the mechanism (Mechanism#idle_state) and what
|
|
19
|
+
# the ports read from it, and every byte of RAM it read or wrote as
|
|
20
|
+
# it found them (Bus#watch!)
|
|
21
|
+
# - read nothing that changes from pass to pass: no counter, no shift
|
|
22
|
+
# register, and not VIA 1's port B, which reads the serial bus
|
|
23
|
+
# - wrote nothing that reaches outside the drive: not VIA 1, whose port
|
|
24
|
+
# B drives the serial bus, and not the motor on
|
|
25
|
+
# - ran while no counter could set a flag (VIA#quiet_cycles), with the
|
|
26
|
+
# motor off, no interrupt pulled and no trap on the CPU.
|
|
27
|
+
#
|
|
28
|
+
# Such a pass runs the same way again from where it ended, for as
|
|
29
|
+
# long as ATN holds still and no counter sets a flag, so the drive
|
|
30
|
+
# sleeps: host_cycle! only counts the host cycles that go by. It wakes
|
|
31
|
+
# when ATN moves, when a counter is due to set a flag, or when something
|
|
32
|
+
# outside reads or changes the drive (the readers in Drive1541 call
|
|
33
|
+
# settle!). Waking counts off the whole passes it owes in bulk,
|
|
34
|
+
# moving the counters on, and runs the rest cycle by cycle.
|
|
35
|
+
#
|
|
36
|
+
# The loop reads neither CLK nor DATA, so the drive sleeps through them
|
|
37
|
+
# moving and takes the C64's lines as they stand once it wakes. Its
|
|
38
|
+
# own lines hold still while it sleeps, so the C64 reads them without
|
|
39
|
+
# waking it (serial_output).
|
|
40
|
+
#
|
|
41
|
+
# A pass starts at $EBFF, where the CPU always comes from the loop's
|
|
42
|
+
# last instruction, a JMP, so what that leaves in the CPU's working
|
|
43
|
+
# registers is the same each time round.
|
|
44
|
+
module Idle
|
|
45
|
+
# Where the DOS 2.6 idle loop starts over.
|
|
46
|
+
IDLE_LOOP = 0xebff
|
|
47
|
+
|
|
48
|
+
# Whether the drive may sleep through its idle loop. On by default,
|
|
49
|
+
# but for a drive whose CPU logs each instruction.
|
|
50
|
+
attr_reader :idle_skip
|
|
51
|
+
|
|
52
|
+
def idle_skip=(on)
|
|
53
|
+
settle!
|
|
54
|
+
@idle_skip = on
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Whether the drive is skipping its idle loop right now.
|
|
58
|
+
def asleep? = @asleep
|
|
59
|
+
|
|
60
|
+
# Brings the drive up to the present, cycle for cycle, and drops the
|
|
61
|
+
# pass it was recording, since whoever called may change it. Every
|
|
62
|
+
# reader of the drive's parts calls this.
|
|
63
|
+
def settle!
|
|
64
|
+
if @asleep
|
|
65
|
+
wake!
|
|
66
|
+
@serial_port.latch_host
|
|
67
|
+
end
|
|
68
|
+
stop_recording if @recording
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# A host cycle asleep.
|
|
74
|
+
def doze
|
|
75
|
+
@slept += 1
|
|
76
|
+
settle! if @slept == @wake_at || @serial_port.atn_moved?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def init_idle(debug)
|
|
80
|
+
@idle_skip = !debug
|
|
81
|
+
@asleep = false
|
|
82
|
+
@recording = false
|
|
83
|
+
@owed = 0
|
|
84
|
+
@budget = 0
|
|
85
|
+
@slept = 0
|
|
86
|
+
@wake_at = 0
|
|
87
|
+
@pass_cycles = 0
|
|
88
|
+
@pass_instructions = 0
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The CPU is at $EBFF. At an instruction boundary, the drive sleeps if
|
|
92
|
+
# the pass it recorded can repeat, and otherwise records the next.
|
|
93
|
+
def idle_loop_reached
|
|
94
|
+
return unless @cpu.boundary?
|
|
95
|
+
|
|
96
|
+
if @recording
|
|
97
|
+
stop_recording
|
|
98
|
+
return fall_asleep if repeatable_pass? && room_for_pass?
|
|
99
|
+
end
|
|
100
|
+
start_recording
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def start_recording
|
|
104
|
+
return if @mechanism.motor_on? || @via1.irq? || @via2.irq? || @cpu.trapped?
|
|
105
|
+
|
|
106
|
+
@recording = true
|
|
107
|
+
@record_state = idle_state
|
|
108
|
+
@record_cycles = @cycles
|
|
109
|
+
@record_instructions = @cpu.instructions
|
|
110
|
+
@record_quiet = quiet_cycles
|
|
111
|
+
@bus.watch!
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def stop_recording
|
|
115
|
+
@recording = false
|
|
116
|
+
@bus.unwatch!
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Whether the pass just recorded can repeat.
|
|
120
|
+
def repeatable_pass?
|
|
121
|
+
!@bus.volatile && @cycles - @record_cycles <= @record_quiet && idle_state == @record_state &&
|
|
122
|
+
ram_as_found?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Whether the counters leave room for a whole pass.
|
|
126
|
+
def room_for_pass? = quiet_cycles >= @cycles - @record_cycles
|
|
127
|
+
|
|
128
|
+
# Sleeps through the pass just recorded, over and over.
|
|
129
|
+
def fall_asleep
|
|
130
|
+
@pass_cycles = @cycles - @record_cycles
|
|
131
|
+
@pass_instructions = @cpu.instructions - @record_instructions
|
|
132
|
+
@asleep = true
|
|
133
|
+
@owed = 0
|
|
134
|
+
@budget = quiet_cycles
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Asleep, host_cycle! counts host cycles, and leaves it to wake! to
|
|
138
|
+
# turn them into drive cycles through the phase accumulator. This sets
|
|
139
|
+
# the host cycle that brings the drive cycles owed up to the budget.
|
|
140
|
+
def plan_wake
|
|
141
|
+
@slept = 0
|
|
142
|
+
needed = ((@budget - @owed) * @host_clock_hz) - @phase
|
|
143
|
+
@wake_at = needed.positive? ? (needed + CLOCK_HZ - 1) / CLOCK_HZ : 1
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Runs the cycles owed: whole passes in bulk, up to the budget, and
|
|
147
|
+
# the rest one by one.
|
|
148
|
+
def wake!
|
|
149
|
+
@asleep = false
|
|
150
|
+
phase = @phase + (@slept * CLOCK_HZ)
|
|
151
|
+
@phase = phase % @host_clock_hz
|
|
152
|
+
owed = @owed + (phase / @host_clock_hz)
|
|
153
|
+
@owed = 0
|
|
154
|
+
@slept = 0
|
|
155
|
+
span = [owed, @budget].min
|
|
156
|
+
passes = span / @pass_cycles
|
|
157
|
+
if passes.positive?
|
|
158
|
+
cycles = passes * @pass_cycles
|
|
159
|
+
@cpu.fast_forward(cycles, passes * @pass_instructions)
|
|
160
|
+
@via1.fast_forward(cycles)
|
|
161
|
+
@via2.fast_forward(cycles)
|
|
162
|
+
@cycles += cycles
|
|
163
|
+
owed -= cycles
|
|
164
|
+
end
|
|
165
|
+
owed.times { step }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def quiet_cycles
|
|
169
|
+
[@via1.quiet_cycles, @via2.quiet_cycles].min
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def ram_as_found?
|
|
173
|
+
ram = @bus.ram
|
|
174
|
+
@bus.touched.all? { |addr, value| ram.peek(addr) == value }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def idle_state
|
|
178
|
+
mechanism = @mechanism
|
|
179
|
+
[*@cpu.idle_state, *@via1.idle_state, *@via2.idle_state, @bus.data, @so_pending,
|
|
180
|
+
*mechanism.idle_state, mechanism.read_a(0xff), mechanism.read_b(0xff)]
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|