badline 0.5.0 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/CLAUDE.md +23 -23
- data/CONTRIBUTING.md +5 -0
- data/README.md +1 -1
- data/doc/pinned-behaviour.md +4 -2
- data/lib/badline/cia.rb +16 -2
- data/lib/badline/computer.rb +1 -1
- data/lib/badline/drive1541/bus.rb +77 -14
- data/lib/badline/drive1541/idle.rb +9 -57
- data/lib/badline/drive1541/orbit.rb +81 -0
- data/lib/badline/drive1541/sleep.rb +114 -0
- data/lib/badline/drive1541.rb +18 -2
- data/lib/badline/iec_bus.rb +16 -0
- data/lib/badline/memory.rb +3 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/fast_forward.rb +15 -0
- data/lib/badline/via/timer1.rb +6 -0
- data/lib/badline/via/timer2.rb +6 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9175400f11b212812092f887bea13e0880417fb151011fdbc568c4789050dcf1
|
|
4
|
+
data.tar.gz: dbd07633e45bfcc06fd6b4ab1b680389276ff81340cc6c72d26bd4e7fdf6baad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d00a54fba0e530ff578a35b3b5953c0f011b6ea32a2b5ac3f8ccaf19dc8c8f3e26bb81dedfac4c53b026b0e2d0a8e3a35c9f212369624995765c0c8a99678542
|
|
7
|
+
data.tar.gz: '0918a31ea33114c1f46d76ec3c0e9978a5e922bd7b39f6abe9494ee03423fad48f4aecbf8c4a2e0af4414ee19b2ce2d33c6cb21774a473138db15a588c149f2b'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.1](https://github.com/elektronaut/badline/compare/v0.5.0...v0.5.1) (2026-09-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* build the native badline with a Spinel that writes .d64 images through the true drive ([97f537a](https://github.com/elektronaut/badline/commit/97f537afe69b10ae044f83ccf4dd018402ebe1be))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Performance Improvements
|
|
12
|
+
|
|
13
|
+
* sleep the idle true drive through its timer interrupts ([0482e43](https://github.com/elektronaut/badline/commit/0482e439cfb160ab69027b166b1059ed8fc9dfb3)), closes [#367](https://github.com/elektronaut/badline/issues/367)
|
|
14
|
+
|
|
3
15
|
## [0.5.0](https://github.com/elektronaut/badline/compare/v0.4.2...v0.5.0) (2026-09-29)
|
|
4
16
|
|
|
5
17
|
|
data/CLAUDE.md
CHANGED
|
@@ -48,6 +48,7 @@ requires only the namespace file.
|
|
|
48
48
|
| Wolfgang Lorenz suite | `bin/lorenz` | CPU, CIA, interrupts |
|
|
49
49
|
| VICE testbench | `bin/testbench <subtree>` | `VICII/`, `CIA/`, `interrupts/`, `CPU/`, cartridges with `--carts`, and the true 1541 drive with `--drive` |
|
|
50
50
|
| VICE SID testprogs | `bin/sidtests` | SID |
|
|
51
|
+
| Drive scenarios | `bin/drive_scenarios` | The true 1541 running the DOS ROM: save, format, the error channel, idle, write protect and autostart |
|
|
51
52
|
| CIA offline grids | `bundle exec rspec --tag slow spec/badline/cia` | CIA timers and shift register, against a bare CIA in about 2 min |
|
|
52
53
|
|
|
53
54
|
The CIA offline grids replay Lorenz's `cia1ta` and `cia1tb` sweeps (about
|
|
@@ -156,29 +157,29 @@ the rows your change can't reach tell you nothing about it.
|
|
|
156
157
|
`ruby --yjit bin/lorenz [image] [start_test] [max_cycles]` (resumes the
|
|
157
158
|
chain at a named test, and `--stop-after NAME` ends it once that test is
|
|
158
159
|
done). These take seconds to minutes
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
- A pull request's CI is the verdict. It runs every suite but
|
|
161
|
+
`testbench-drive` on the Spinel build, compared row by row against
|
|
162
|
+
`test/baselines/`, and every job is a required check. A row that moved
|
|
163
|
+
fails it. Re-record only those rows, in the same change:
|
|
164
|
+
`rake "regression:record:<suite>[filter,...]"` (quote it, because zsh
|
|
165
|
+
globs the brackets). It runs only the matching tests on CRuby and
|
|
166
|
+
splices their rows into the baseline, and every other row keeps its
|
|
167
|
+
verdict. CI runs the same rows on Spinel, so the two have to agree.
|
|
163
168
|
`lorenz` chains itself, so it takes a stretch of the chain instead:
|
|
164
169
|
`rake "regression:record:lorenz[first,last]"` resumes just ahead of
|
|
165
170
|
`first`, stops after `last` and leaves the `(suite)` row alone.
|
|
166
171
|
`[first,(suite)]` runs on to the end of the chain and records the
|
|
167
|
-
`(suite)` row too.
|
|
168
|
-
|
|
169
|
-
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
- The one exception is a change whose reach you can't bound to a set of
|
|
179
|
-
filters, such as reordering `Computer#cycle!` or changing the LOAD trap
|
|
180
|
-
every suite loads through. Ask before running a full suite for it, and
|
|
181
|
-
don't start one on your own judgement
|
|
172
|
+
`(suite)` row too. Explain every moved row in the pull request. Never
|
|
173
|
+
re-record just to make a diff go away
|
|
174
|
+
- For a change whose reach you can't bound to a set of filters, such as
|
|
175
|
+
reordering `Computer#cycle!` or changing the LOAD trap every suite loads
|
|
176
|
+
through, push and let CI run the suites
|
|
177
|
+
- `testbench-drive` needs the true drive, which the Spinel testbench
|
|
178
|
+
harness doesn't have, so CI leaves it out. Run the rows your change can
|
|
179
|
+
reach locally
|
|
180
|
+
- The Regression workflow runs the suites on CRuby, started by hand from
|
|
181
|
+
the Actions tab, for a person checking that CRuby and Spinel agree.
|
|
182
|
+
Agents don't start it
|
|
182
183
|
|
|
183
184
|
Pick the filters from the suites your change can move: VIC → `testbench`,
|
|
184
185
|
plus `testbench-vicii-new` (`bin/testbench --vicii-new`, the 8565) for
|
|
@@ -190,10 +191,9 @@ CPU, interrupts or timing → the matching `testbench-*` suite, plus
|
|
|
190
191
|
the 6526A) for anything the interrupt register or the CIA model reaches; cartridge mappers, banking or power-on state →
|
|
191
192
|
`testbench-carts`, plus `testbench-expansions` for banking; GEO-RAM, +60K,
|
|
192
193
|
+256K, the REU or the VIC's BA line the REU follows → `testbench-expansions`; the 1541 drive, VIA or IEC bus →
|
|
193
|
-
`testbench-drive` (`bin/testbench --drive`); SID → `sid`, plus `sid-8580` for anything the 8580
|
|
194
|
-
model reaches (`bin/sidtests --sid 8580`). Lorenz
|
|
195
|
-
|
|
196
|
-
exception is code whose rule in `doc/pinned-behaviour.md` names Lorenz
|
|
194
|
+
`testbench-drive` (`bin/testbench --drive`) and `drive-scenarios` (`bin/drive_scenarios <filter>`); SID → `sid`, plus `sid-8580` for anything the 8580
|
|
195
|
+
model reaches (`bin/sidtests --sid 8580`). Leave the Lorenz chain to CI,
|
|
196
|
+
unless your code has a rule in `doc/pinned-behaviour.md` that names Lorenz
|
|
197
197
|
tests: the interrupt polling, CPU port and CIA timer rules. Run just the
|
|
198
198
|
tests that rule names, one at a time, with
|
|
199
199
|
`ruby --yjit bin/lorenz --resume <test> --stop-after <test>`. A stretch
|
data/CONTRIBUTING.md
CHANGED
|
@@ -85,6 +85,11 @@ bundle exec rubocop
|
|
|
85
85
|
## Tests and commits
|
|
86
86
|
|
|
87
87
|
- Add tests for any behavior you change.
|
|
88
|
+
- CI runs rubocop, the specs and every regression suite on each pull
|
|
89
|
+
request, and fails on any suite row that differs from its baseline in
|
|
90
|
+
`test/baselines/`. When a change moves rows on purpose, re-record just
|
|
91
|
+
those rows as [test/baselines/README.md](test/baselines/README.md)
|
|
92
|
+
describes, and say why in the pull request.
|
|
88
93
|
- Write commit messages using
|
|
89
94
|
[Conventional Commits](https://www.conventionalcommits.org). The
|
|
90
95
|
changelog and releases are generated from them, so the `feat:` and
|
data/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[](https://rubygems.org/gems/badline)
|
|
2
|
-
[](https://github.com/elektronaut/badline/actions/workflows/ci.yml)
|
|
3
3
|
|
|
4
4
|
# Badline
|
|
5
5
|
|
data/doc/pinned-behaviour.md
CHANGED
|
@@ -1203,8 +1203,10 @@ and each was knocked out: removing it fails the rows named.
|
|
|
1203
1203
|
- Spec guard: *steps in from the stop with the first phase after the
|
|
1204
1204
|
bump* in
|
|
1205
1205
|
[`mechanism_spec.rb`](../spec/badline/drive1541/mechanism_spec.rb),
|
|
1206
|
-
and
|
|
1207
|
-
|
|
1206
|
+
and the `format/name-and-id`, `format/bam-free` and
|
|
1207
|
+
`format/trap-readable` rows of the `drive-scenarios` suite
|
|
1208
|
+
([`drive_scenarios.rb`](../test/drive_scenarios.rb)), which read the
|
|
1209
|
+
formatted disk back from its image.
|
|
1208
1210
|
- The disk turns at 300 rpm whatever bit rate VIA 2's PB5-6 select: a
|
|
1209
1211
|
turn is 200,000 drive cycles over every track and over a half track
|
|
1210
1212
|
without data. Each track's bits pass under the head at the rate they
|
data/lib/badline/cia.rb
CHANGED
|
@@ -62,6 +62,7 @@ module Badline
|
|
|
62
62
|
|
|
63
63
|
@peripheral = peripheral
|
|
64
64
|
@port_b4_handler = nil
|
|
65
|
+
@port_a_handler = nil
|
|
65
66
|
@port_b4_high = true
|
|
66
67
|
reset!
|
|
67
68
|
end
|
|
@@ -87,6 +88,7 @@ module Badline
|
|
|
87
88
|
@ta = Timer.new(@control_a)
|
|
88
89
|
@tb = Timer.new(@control_b)
|
|
89
90
|
@serial = Serial.new(@control_a)
|
|
91
|
+
@port_a_handler&.call
|
|
90
92
|
end
|
|
91
93
|
|
|
92
94
|
# Register a change handler on the PB4 line. On CIA 1, this feeds the
|
|
@@ -95,6 +97,13 @@ module Badline
|
|
|
95
97
|
@port_b4_handler = handler
|
|
96
98
|
end
|
|
97
99
|
|
|
100
|
+
# Register a handler for writes that can move port A's lines: to the
|
|
101
|
+
# data register or DDR, and the reset. On CIA 2, this tells the drives
|
|
102
|
+
# on the serial bus that the C64 may have moved a line.
|
|
103
|
+
def on_port_a_write(&handler)
|
|
104
|
+
@port_a_handler = handler
|
|
105
|
+
end
|
|
106
|
+
|
|
98
107
|
# A falling edge on the FLAG pin. On CIA 1 the datasette's tape read
|
|
99
108
|
# line drives it, on CIA 2 the serial bus SRQ.
|
|
100
109
|
def flag! = raise_interrupt(:flag)
|
|
@@ -153,9 +162,9 @@ module Badline
|
|
|
153
162
|
|
|
154
163
|
def poke(addr, value)
|
|
155
164
|
case offset_of(addr) & 0x0f
|
|
156
|
-
when 0x00 then @data_port_a = value
|
|
165
|
+
when 0x00 then update_port_a { @data_port_a = value }
|
|
157
166
|
when 0x01 then update_port_b { @data_port_b = value }
|
|
158
|
-
when 0x02 then @data_dir_a = value
|
|
167
|
+
when 0x02 then update_port_a { @data_dir_a = value }
|
|
159
168
|
when 0x03 then update_port_b { @data_dir_b = value }
|
|
160
169
|
when 0x04 then @ta.write_latch_low(value)
|
|
161
170
|
when 0x05 then @ta.write_latch_high(value)
|
|
@@ -174,6 +183,11 @@ module Badline
|
|
|
174
183
|
|
|
175
184
|
private
|
|
176
185
|
|
|
186
|
+
def update_port_a
|
|
187
|
+
yield
|
|
188
|
+
@port_a_handler&.call
|
|
189
|
+
end
|
|
190
|
+
|
|
177
191
|
def update_port_b
|
|
178
192
|
yield
|
|
179
193
|
@port_b4_driven_high = driven_lines(@data_port_b, @data_dir_b).anybits?(0x10)
|
data/lib/badline/computer.rb
CHANGED
|
@@ -195,7 +195,7 @@ module Badline
|
|
|
195
195
|
# for, which plugging in a drive does. Until then port A's serial
|
|
196
196
|
# inputs float high.
|
|
197
197
|
def iec_bus
|
|
198
|
-
@iec_bus ||= IECBus.new(host: @cia2).tap
|
|
198
|
+
@iec_bus ||= IECBus.new(host: @cia2).tap(&:plug_in!)
|
|
199
199
|
end
|
|
200
200
|
|
|
201
201
|
def capture_output
|
|
@@ -19,7 +19,8 @@ module Badline
|
|
|
19
19
|
# both on a real 1541.
|
|
20
20
|
#
|
|
21
21
|
# While Idle records a pass of the DOS's idle loop, the bus watches it
|
|
22
|
-
# (see watch!).
|
|
22
|
+
# (see watch!). While Idle looks for an orbit, the bus guards the drive
|
|
23
|
+
# (see guard!).
|
|
23
24
|
class Bus
|
|
24
25
|
include Addressable
|
|
25
26
|
|
|
@@ -31,6 +32,10 @@ module Badline
|
|
|
31
32
|
VOLATILE_VIA2_READS = 0x0730
|
|
32
33
|
VOLATILE_VIA2_WRITES = 0x0ff0
|
|
33
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
|
|
38
|
+
|
|
34
39
|
attr_reader :ram, :rom, :via1, :via2, :data
|
|
35
40
|
|
|
36
41
|
# Whether a watched stretch did something that makes it unrepeatable.
|
|
@@ -40,6 +45,14 @@ module Badline
|
|
|
40
45
|
# from before the first access.
|
|
41
46
|
attr_reader :touched
|
|
42
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
|
+
|
|
43
56
|
def initialize(rom:, via1:, via2:)
|
|
44
57
|
addressable_at(0, length: 2**16)
|
|
45
58
|
@ram = Memory.new([], length: 0x0800, start: 0)
|
|
@@ -47,21 +60,38 @@ module Badline
|
|
|
47
60
|
@via1 = via1
|
|
48
61
|
@via2 = via2
|
|
49
62
|
@data = 0
|
|
50
|
-
@watching = false
|
|
51
|
-
@volatile = false
|
|
63
|
+
@recording = @guarding = @watching = false
|
|
64
|
+
@volatile = @tainted = false
|
|
52
65
|
@touched = {}
|
|
66
|
+
@touched_timers = 0
|
|
67
|
+
@led = 0
|
|
53
68
|
end
|
|
54
69
|
|
|
55
70
|
# Starts watching: see volatile and touched. The bus stops watching
|
|
56
71
|
# once the stretch turns volatile.
|
|
57
72
|
def watch!
|
|
58
|
-
@
|
|
73
|
+
@recording = true
|
|
59
74
|
@volatile = false
|
|
60
75
|
@touched = {}
|
|
76
|
+
watching!
|
|
61
77
|
end
|
|
62
78
|
|
|
63
79
|
def unwatch!
|
|
64
|
-
@
|
|
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!
|
|
65
95
|
end
|
|
66
96
|
|
|
67
97
|
def peek(addr)
|
|
@@ -86,7 +116,7 @@ module Badline
|
|
|
86
116
|
|
|
87
117
|
addr &= 0x1fff
|
|
88
118
|
if addr < 0x0800
|
|
89
|
-
touch(addr) if @
|
|
119
|
+
touch(addr) if @recording
|
|
90
120
|
@ram.poke(addr, value)
|
|
91
121
|
elsif addr >= 0x1c00
|
|
92
122
|
@via2.poke(addr, value)
|
|
@@ -98,36 +128,69 @@ module Badline
|
|
|
98
128
|
|
|
99
129
|
private
|
|
100
130
|
|
|
131
|
+
def watching!
|
|
132
|
+
@watching = @recording || @guarding
|
|
133
|
+
end
|
|
134
|
+
|
|
101
135
|
def touch(addr)
|
|
102
136
|
@touched[addr] = @ram.peek(addr) unless @touched.key?(addr)
|
|
103
137
|
end
|
|
104
138
|
|
|
105
139
|
def watch_read(addr)
|
|
106
140
|
if addr < 0x0800
|
|
107
|
-
touch(addr)
|
|
141
|
+
touch(addr) if @recording
|
|
108
142
|
elsif addr >= 0x1800
|
|
109
|
-
|
|
143
|
+
via1 = addr < 0x1c00
|
|
144
|
+
watch(via1 ? VOLATILE_VIA1_READS : VOLATILE_VIA2_READS, addr) if @recording
|
|
145
|
+
guard_read(via1, addr & 0x0f) if @guarding
|
|
110
146
|
end
|
|
111
147
|
end
|
|
112
148
|
|
|
113
|
-
# Any write to VIA 1 makes the stretch volatile, and so does a write
|
|
114
|
-
# to VIA 2 that leaves the motor on.
|
|
115
149
|
def watch_write(addr)
|
|
116
150
|
if addr >= 0x1c00
|
|
117
|
-
|
|
118
|
-
|
|
151
|
+
watch_via2_write(addr) if @recording
|
|
152
|
+
guard_via2_write(addr & 0x0f) if @guarding
|
|
119
153
|
elsif addr >= 0x1800
|
|
120
|
-
volatile!
|
|
154
|
+
volatile! if @recording
|
|
155
|
+
taint! if @guarding
|
|
121
156
|
end
|
|
122
157
|
end
|
|
123
158
|
|
|
159
|
+
# Any write to VIA 1 makes the stretch volatile (watch_write), and so
|
|
160
|
+
# does a write to VIA 2 that leaves the motor on.
|
|
161
|
+
def watch_via2_write(addr)
|
|
162
|
+
watch(VOLATILE_VIA2_WRITES, addr)
|
|
163
|
+
volatile! if @recording && @via2.port_b_output.anybits?(Mechanism::MOTOR)
|
|
164
|
+
end
|
|
165
|
+
|
|
124
166
|
def watch(volatile, addr)
|
|
125
167
|
volatile! if volatile.anybits?(1 << (addr & 0x0f))
|
|
126
168
|
end
|
|
127
169
|
|
|
128
170
|
def volatile!
|
|
129
171
|
@volatile = true
|
|
130
|
-
@
|
|
172
|
+
@recording = false
|
|
173
|
+
watching!
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def guard_read(via1, offset)
|
|
177
|
+
return taint! if via1 && offset.zero?
|
|
178
|
+
|
|
179
|
+
@touched_timers |= via1 ? TIMER_REGISTERS[offset] : TIMER_REGISTERS[offset] << 2
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def guard_via2_write(offset)
|
|
183
|
+
return taint! if offset.between?(0x0a, 0x0b)
|
|
184
|
+
|
|
185
|
+
@touched_timers |= TIMER_REGISTERS[offset] << 2
|
|
186
|
+
lines = @via2.port_b_output
|
|
187
|
+
taint! if lines.anybits?(Mechanism::MOTOR) || (lines & Mechanism::LED) != @led
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def taint!
|
|
191
|
+
@tainted = true
|
|
192
|
+
@guarding = false
|
|
193
|
+
watching!
|
|
131
194
|
end
|
|
132
195
|
end
|
|
133
196
|
end
|
|
@@ -27,7 +27,8 @@ module Badline
|
|
|
27
27
|
#
|
|
28
28
|
# Such a pass runs the same way again from where it ended, for as
|
|
29
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
|
|
30
|
+
# sleeps: host_cycle! only counts the host cycles that go by (see
|
|
31
|
+
# Sleep). It wakes
|
|
31
32
|
# when ATN moves, when a counter is due to set a flag, or when something
|
|
32
33
|
# outside reads or changes the drive (the readers in Drive1541 call
|
|
33
34
|
# settle!). Waking counts off the whole passes it owes in bulk,
|
|
@@ -41,6 +42,9 @@ module Badline
|
|
|
41
42
|
# A pass starts at $EBFF, where the CPU always comes from the loop's
|
|
42
43
|
# last instruction, a JMP, so what that leaves in the CPU's working
|
|
43
44
|
# registers is the same each time round.
|
|
45
|
+
#
|
|
46
|
+
# The timer's interrupts come round too, and once they do, the drive
|
|
47
|
+
# sleeps on through them (see Orbit).
|
|
44
48
|
module Idle
|
|
45
49
|
# Where the DOS 2.6 idle loop starts over.
|
|
46
50
|
IDLE_LOOP = 0xebff
|
|
@@ -54,38 +58,15 @@ module Badline
|
|
|
54
58
|
@idle_skip = on
|
|
55
59
|
end
|
|
56
60
|
|
|
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
61
|
private
|
|
72
62
|
|
|
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
63
|
def init_idle(debug)
|
|
80
64
|
@idle_skip = !debug
|
|
81
|
-
@asleep = false
|
|
82
65
|
@recording = false
|
|
83
|
-
@owed = 0
|
|
84
|
-
@budget = 0
|
|
85
|
-
@slept = 0
|
|
86
|
-
@wake_at = 0
|
|
87
66
|
@pass_cycles = 0
|
|
88
67
|
@pass_instructions = 0
|
|
68
|
+
init_sleep
|
|
69
|
+
init_orbits
|
|
89
70
|
end
|
|
90
71
|
|
|
91
72
|
# The CPU is at $EBFF. At an instruction boundary, the drive sleeps if
|
|
@@ -130,39 +111,10 @@ module Badline
|
|
|
130
111
|
@pass_cycles = @cycles - @record_cycles
|
|
131
112
|
@pass_instructions = @cpu.instructions - @record_instructions
|
|
132
113
|
@asleep = true
|
|
114
|
+
@host_still = false
|
|
133
115
|
@owed = 0
|
|
134
116
|
@budget = quiet_cycles
|
|
135
|
-
|
|
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 }
|
|
117
|
+
anchor
|
|
166
118
|
end
|
|
167
119
|
|
|
168
120
|
def quiet_cycles
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# Sleeping through the DOS's timer interrupts, exactly, once they come
|
|
6
|
+
# round.
|
|
7
|
+
#
|
|
8
|
+
# VIA 2's timer 1 interrupts the idle loop for the job loop, which
|
|
9
|
+
# finds no job, restarts the timer and returns into the pass it cut
|
|
10
|
+
# into, 14,998 cycles and more apart. The interrupts land at a
|
|
11
|
+
# different point in the pass each time, until, six of them on, the
|
|
12
|
+
# whole drive is back where it was: an orbit of 90,044 cycles.
|
|
13
|
+
#
|
|
14
|
+
# Each place the drive falls asleep (see Idle) is an anchor, and the
|
|
15
|
+
# drive keeps everything it holds at each one: RAM, the counters of
|
|
16
|
+
# the timers that are armed or that it read or wrote since, and the
|
|
17
|
+
# rest as Idle compares a pass. It keeps them only while it keeps to
|
|
18
|
+
# itself, with Bus#guard! checking that it doesn't read the serial
|
|
19
|
+
# bus, write VIA 1, turn the motor on or change the LED. ATN moving,
|
|
20
|
+
# or a reader settling the drive, forgets them.
|
|
21
|
+
#
|
|
22
|
+
# Coming to an anchor it kept before closes an orbit, which runs the
|
|
23
|
+
# same way again from here for as long as ATN holds still, so the
|
|
24
|
+
# drive sleeps on without waking for its counters. Waking counts off
|
|
25
|
+
# the whole orbits it owes in bulk, moving on only the counters it
|
|
26
|
+
# never touched, and runs the rest as Idle does.
|
|
27
|
+
module Orbit
|
|
28
|
+
# How many anchors the drive keeps while it looks for an orbit.
|
|
29
|
+
ANCHORS = 32
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def init_orbits
|
|
34
|
+
@anchors = {}
|
|
35
|
+
@orbit_cycles = nil
|
|
36
|
+
@orbit_instructions = 0
|
|
37
|
+
@bus.guard!
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The drive falls asleep at an anchor. One it came to before closes
|
|
41
|
+
# an orbit: everything from there to here runs the same way again.
|
|
42
|
+
def anchor
|
|
43
|
+
forget_orbits if @bus.tainted || @anchors.size >= ANCHORS
|
|
44
|
+
key = anchor_state
|
|
45
|
+
if (seen = @anchors[key])
|
|
46
|
+
@orbit_cycles = @cycles - seen[0]
|
|
47
|
+
@orbit_instructions = @cpu.instructions - seen[1]
|
|
48
|
+
else
|
|
49
|
+
@anchors[key] = [@cycles, @cpu.instructions]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def forget_orbits
|
|
54
|
+
@anchors.clear
|
|
55
|
+
@orbit_cycles = nil
|
|
56
|
+
@bus.guard!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Runs whole orbits, which bring the drive back to the anchor it
|
|
60
|
+
# sleeps at but for the counters it never touched. Returns the
|
|
61
|
+
# cycles.
|
|
62
|
+
def skip_orbits(orbits)
|
|
63
|
+
cycles = orbits * @orbit_cycles
|
|
64
|
+
touched = @bus.touched_timers
|
|
65
|
+
@cpu.fast_forward(cycles, orbits * @orbit_instructions)
|
|
66
|
+
@via1.skip_orbits(cycles, touched & 0x03)
|
|
67
|
+
@via2.skip_orbits(cycles, touched >> 2)
|
|
68
|
+
@cycles += cycles
|
|
69
|
+
cycles
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Everything the drive holds at an anchor, but for the counters of
|
|
73
|
+
# the unarmed timers it hasn't touched since it was last tainted.
|
|
74
|
+
def anchor_state
|
|
75
|
+
touched = @bus.touched_timers
|
|
76
|
+
[*idle_state, *@via1.counter_state(touched & 0x03), *@via2.counter_state(touched >> 2),
|
|
77
|
+
@bus.ram.snapshot]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# The drive asleep, as Idle and Orbit let it: host_cycle! counts the
|
|
6
|
+
# host cycles that go by, and waking turns them into drive cycles
|
|
7
|
+
# through the phase accumulator and runs them. Whole orbits and whole
|
|
8
|
+
# passes go in bulk, and the rest cycle by cycle, which may find the
|
|
9
|
+
# idle loop and put the drive back to sleep.
|
|
10
|
+
#
|
|
11
|
+
# The drive looks at ATN only when the C64 may have moved it, on a
|
|
12
|
+
# bus that says when (IECBus#notifies?), and every host cycle on one
|
|
13
|
+
# that doesn't.
|
|
14
|
+
module Sleep
|
|
15
|
+
# Whether the drive is skipping its idle loop right now.
|
|
16
|
+
def asleep? = @asleep
|
|
17
|
+
|
|
18
|
+
# Brings the drive up to the present, cycle for cycle, and drops the
|
|
19
|
+
# pass it was recording and the orbits it found, since whoever called
|
|
20
|
+
# may change it. Every reader of the drive's parts calls this.
|
|
21
|
+
def settle!
|
|
22
|
+
if @asleep
|
|
23
|
+
wake!
|
|
24
|
+
@serial_port.latch_host
|
|
25
|
+
@asleep = false
|
|
26
|
+
@orbit_cycles = nil
|
|
27
|
+
end
|
|
28
|
+
stop_recording if @recording
|
|
29
|
+
forget_orbits
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def init_sleep
|
|
35
|
+
@asleep = false
|
|
36
|
+
@host_still = false
|
|
37
|
+
@owed = 0
|
|
38
|
+
@budget = 0
|
|
39
|
+
@slept = 0
|
|
40
|
+
@wake_at = 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A host cycle asleep that may wake the drive. The bus says whether
|
|
44
|
+
# the next ones need a look at ATN.
|
|
45
|
+
def doze
|
|
46
|
+
@host_still = @serial_bus.notifies?
|
|
47
|
+
if @serial_port.atn_moved?
|
|
48
|
+
settle!
|
|
49
|
+
elsif @slept == @wake_at
|
|
50
|
+
wake!
|
|
51
|
+
@serial_port.latch_host
|
|
52
|
+
plan_wake if @asleep
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Asleep, host_cycle! counts host cycles, and leaves it to wake! to
|
|
57
|
+
# turn them into drive cycles through the phase accumulator. This sets
|
|
58
|
+
# the host cycle that brings the drive cycles owed up to the budget.
|
|
59
|
+
# On an orbit the drive sleeps until something outside wakes it:
|
|
60
|
+
# @slept counts from 1, so it never comes to 0.
|
|
61
|
+
def plan_wake
|
|
62
|
+
@slept = 0
|
|
63
|
+
return @wake_at = 0 if @orbit_cycles
|
|
64
|
+
|
|
65
|
+
needed = ((@budget - @owed) * @host_clock_hz) - @phase
|
|
66
|
+
@wake_at = needed.positive? ? (needed + CLOCK_HZ - 1) / CLOCK_HZ : 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Runs the cycles owed.
|
|
70
|
+
def wake!
|
|
71
|
+
phase = @phase + (@slept * CLOCK_HZ)
|
|
72
|
+
@phase = phase % @host_clock_hz
|
|
73
|
+
owed = @owed + (phase / @host_clock_hz)
|
|
74
|
+
@owed = 0
|
|
75
|
+
@slept = 0
|
|
76
|
+
catch_up(owed)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Runs +owed+ drive cycles: whole orbits and whole passes in bulk,
|
|
80
|
+
# passes only up to the budget, and the rest cycle by cycle, which
|
|
81
|
+
# may put the drive back to sleep.
|
|
82
|
+
def catch_up(owed)
|
|
83
|
+
while owed.positive?
|
|
84
|
+
if @asleep
|
|
85
|
+
if @orbit_cycles && owed >= @orbit_cycles
|
|
86
|
+
owed -= skip_orbits(owed / @orbit_cycles)
|
|
87
|
+
next
|
|
88
|
+
end
|
|
89
|
+
@orbit_cycles = nil
|
|
90
|
+
passes = [owed, @budget].min / @pass_cycles
|
|
91
|
+
if passes.positive?
|
|
92
|
+
owed -= skip_passes(passes)
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
@asleep = false
|
|
96
|
+
end
|
|
97
|
+
run_cycle
|
|
98
|
+
owed -= 1
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Runs whole passes, moving the counters on. Returns the cycles.
|
|
103
|
+
def skip_passes(passes)
|
|
104
|
+
cycles = passes * @pass_cycles
|
|
105
|
+
@cpu.fast_forward(cycles, passes * @pass_instructions)
|
|
106
|
+
@via1.fast_forward(cycles)
|
|
107
|
+
@via2.fast_forward(cycles)
|
|
108
|
+
@cycles += cycles
|
|
109
|
+
@budget -= cycles
|
|
110
|
+
cycles
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/badline/drive1541.rb
CHANGED
|
@@ -7,7 +7,9 @@ require "badline/drive1541/track"
|
|
|
7
7
|
require "badline/drive1541/disk"
|
|
8
8
|
require "badline/drive1541/mechanism"
|
|
9
9
|
require "badline/drive1541/disk_via"
|
|
10
|
+
require "badline/drive1541/sleep"
|
|
10
11
|
require "badline/drive1541/idle"
|
|
12
|
+
require "badline/drive1541/orbit"
|
|
11
13
|
|
|
12
14
|
module Badline
|
|
13
15
|
# The 1541 disk drive as a machine of its own: a 6502 running the DOS
|
|
@@ -27,9 +29,12 @@ module Badline
|
|
|
27
29
|
# While the DOS idles, host_cycle! skips the drive's cycles and catches
|
|
28
30
|
# up on them later, exactly (see Idle). The readers of the drive's parts
|
|
29
31
|
# catch up first, so they find the drive as running every cycle would
|
|
30
|
-
# have left it.
|
|
32
|
+
# have left it. Once the DOS's timer interrupts come round, the drive
|
|
33
|
+
# sleeps through them too (see Orbit).
|
|
31
34
|
class Drive1541
|
|
35
|
+
include Sleep
|
|
32
36
|
include Idle
|
|
37
|
+
include Orbit
|
|
33
38
|
|
|
34
39
|
CLOCK_HZ = 1_000_000
|
|
35
40
|
|
|
@@ -143,8 +148,14 @@ module Badline
|
|
|
143
148
|
# The host has run this cycle already, and the drive sees what it did
|
|
144
149
|
# from the next one on (see SerialPort). Asleep, the drive owes the
|
|
145
150
|
# cycles instead, until ATN moves or its counters are due (see Idle).
|
|
151
|
+
# It looks at ATN only after the C64 wrote CIA 2's port A, on a bus
|
|
152
|
+
# that says so (see host_written!).
|
|
146
153
|
def host_cycle!
|
|
147
|
-
|
|
154
|
+
if @asleep
|
|
155
|
+
return if (@slept += 1) != @wake_at && @host_still
|
|
156
|
+
|
|
157
|
+
return doze
|
|
158
|
+
end
|
|
148
159
|
|
|
149
160
|
phase = @phase + CLOCK_HZ
|
|
150
161
|
while phase >= @host_clock_hz
|
|
@@ -157,6 +168,11 @@ module Badline
|
|
|
157
168
|
plan_wake if @asleep
|
|
158
169
|
end
|
|
159
170
|
|
|
171
|
+
# The C64 wrote CIA 2's port A, so ATN may have moved.
|
|
172
|
+
def host_written!
|
|
173
|
+
@host_still = false
|
|
174
|
+
end
|
|
175
|
+
|
|
160
176
|
# Runs one drive cycle, catching up first on any the drive owes (see
|
|
161
177
|
# step).
|
|
162
178
|
def cycle!
|
data/lib/badline/iec_bus.rb
CHANGED
|
@@ -39,8 +39,24 @@ module Badline
|
|
|
39
39
|
def initialize(host: nil)
|
|
40
40
|
@host = host
|
|
41
41
|
@drives = []
|
|
42
|
+
@notifies = false
|
|
42
43
|
end
|
|
43
44
|
|
|
45
|
+
# Stands in as the host's peripheral, CIA 2's, and has it say when it
|
|
46
|
+
# writes port A (see notifies?).
|
|
47
|
+
def plug_in!
|
|
48
|
+
@host.peripheral = self
|
|
49
|
+
@host.on_port_a_write { host_written }
|
|
50
|
+
@notifies = true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Whether the host says when it may have moved a line (see plug_in!).
|
|
54
|
+
# Without, a drive asleep checks ATN every cycle.
|
|
55
|
+
def notifies? = @notifies
|
|
56
|
+
|
|
57
|
+
# CIA 2 wrote its port A, which may have moved a line.
|
|
58
|
+
def host_written = @drives.each(&:host_written!)
|
|
59
|
+
|
|
44
60
|
def attach(drive)
|
|
45
61
|
@drives << drive unless @drives.include?(drive)
|
|
46
62
|
end
|
data/lib/badline/memory.rb
CHANGED
data/lib/badline/version.rb
CHANGED
|
@@ -26,6 +26,21 @@ module Badline
|
|
|
26
26
|
@t2.fast_forward(cycles)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# The counters an orbit (see Drive1541::Idle) has to bring back: those
|
|
30
|
+
# of the armed timers, and of the timers +touched+ names (bit 0 for
|
|
31
|
+
# timer 1, bit 1 for timer 2). The others only count.
|
|
32
|
+
def counter_state(touched)
|
|
33
|
+
[(@t1.count_state if @t1.armed? || touched.anybits?(1)),
|
|
34
|
+
(@t2.count_state if @t2.armed? || touched.anybits?(2))]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Runs +cycles+ cycles of whole orbits at once: the counters
|
|
38
|
+
# counter_state leaves out move on, and the rest come back round.
|
|
39
|
+
def skip_orbits(cycles, touched)
|
|
40
|
+
@t1.fast_forward(cycles) unless @t1.armed? || touched.anybits?(1)
|
|
41
|
+
@t2.fast_forward(cycles) unless @t2.armed? || touched.anybits?(2)
|
|
42
|
+
end
|
|
43
|
+
|
|
29
44
|
# Everything the VIA holds but its counters, for comparing it at two
|
|
30
45
|
# points. Timer 1's PB7 level counts only while ACR bit 7 puts it on
|
|
31
46
|
# the pin: nothing reads it otherwise.
|
data/lib/badline/via/timer1.rb
CHANGED
|
@@ -96,6 +96,12 @@ module Badline
|
|
|
96
96
|
# points.
|
|
97
97
|
def state = [@latch, @armed]
|
|
98
98
|
|
|
99
|
+
# Whether a timeout sets the flag.
|
|
100
|
+
def armed? = @armed
|
|
101
|
+
|
|
102
|
+
# The count, for comparing the timer at two points where it matters.
|
|
103
|
+
def count_state = [@counter, @hold, @reload]
|
|
104
|
+
|
|
99
105
|
private
|
|
100
106
|
|
|
101
107
|
# The counter reloads either way. Armed, PB7 inverts and the flag
|
data/lib/badline/via/timer2.rb
CHANGED
|
@@ -88,6 +88,12 @@ module Badline
|
|
|
88
88
|
# comparing the timer at two points.
|
|
89
89
|
def state = [@latch_low, @armed, @hold, @low_reload, @count_pulses, @counting_pulses]
|
|
90
90
|
|
|
91
|
+
# Whether the next underflow sets the flag.
|
|
92
|
+
def armed? = @armed
|
|
93
|
+
|
|
94
|
+
# The count, for comparing the timer at two points where it matters.
|
|
95
|
+
def count_state = [@counter, @low_underflowed]
|
|
96
|
+
|
|
91
97
|
private
|
|
92
98
|
|
|
93
99
|
# A cycle while counting pulses, or held after a load or a low byte
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: badline
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Inge Jørgensen
|
|
@@ -115,9 +115,11 @@ files:
|
|
|
115
115
|
- lib/badline/drive1541/gcr.rb
|
|
116
116
|
- lib/badline/drive1541/idle.rb
|
|
117
117
|
- lib/badline/drive1541/mechanism.rb
|
|
118
|
+
- lib/badline/drive1541/orbit.rb
|
|
118
119
|
- lib/badline/drive1541/rotation.rb
|
|
119
120
|
- lib/badline/drive1541/sector_reader.rb
|
|
120
121
|
- lib/badline/drive1541/serial_port.rb
|
|
122
|
+
- lib/badline/drive1541/sleep.rb
|
|
121
123
|
- lib/badline/drive1541/track.rb
|
|
122
124
|
- lib/badline/gui.rb
|
|
123
125
|
- lib/badline/gui/application.rb
|