badline 0.18.0 → 0.19.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 +40 -0
- data/CLAUDE.md +8 -0
- data/CONTRIBUTING.md +4 -0
- data/README.md +10 -7
- data/doc/library.md +5 -1
- data/doc/media.md +3 -2
- data/doc/pinned-behaviour.md +41 -1
- data/doc/snapshot-format.md +56 -24
- data/doc/snapshots.md +45 -8
- data/doc/vice.md +344 -0
- data/lib/badline/audio/renderer.rb +4 -8
- data/lib/badline/c128.rb +4 -0
- data/lib/badline/c64/bus/fittings.rb +6 -0
- data/lib/badline/c64.rb +6 -0
- data/lib/badline/cbm2/p500.rb +4 -0
- data/lib/badline/cbm2.rb +4 -0
- data/lib/badline/chips/cpu/core.rb +29 -4
- data/lib/badline/chips/cpu/interrupts.rb +9 -0
- data/lib/badline/chips/cpu/keyed_state.rb +15 -9
- data/lib/badline/chips/cpu/microcode.rb +10 -0
- data/lib/badline/chips/rriot.rb +4 -1
- data/lib/badline/chips/sid/decimator.rb +1 -1
- data/lib/badline/chips/sid/keyed_parts.rb +51 -18
- data/lib/badline/chips/sid.rb +6 -2
- data/lib/badline/chips/vic_i/keyed_parts.rb +6 -1
- data/lib/badline/chips/vic_i/light_pen.rb +89 -0
- data/lib/badline/chips/vic_i.rb +12 -7
- data/lib/badline/drives/model1541/sector_reader.rb +20 -3
- data/lib/badline/drives/model1541.rb +1 -0
- data/lib/badline/drives/model1551.rb +1 -0
- data/lib/badline/drives/model1571.rb +1 -0
- data/lib/badline/drives/model1581.rb +1 -0
- data/lib/badline/drives/model2031.rb +1 -0
- data/lib/badline/drives/model4040/mechanism.rb +6 -3
- data/lib/badline/drives/model4040.rb +17 -1
- data/lib/badline/drives/model8050/board.rb +2 -0
- data/lib/badline/drives/model8050/dos_board.rb +49 -6
- data/lib/badline/drives/model8050/fdc_bus.rb +7 -0
- data/lib/badline/drives/model8050/idle.rb +137 -24
- data/lib/badline/drives/model8050.rb +14 -0
- data/lib/badline/drives/model9060/idle.rb +30 -10
- data/lib/badline/drives/model9060.rb +2 -0
- data/lib/badline/input/joystick.rb +15 -2
- data/lib/badline/kim1/tape.rb +3 -1
- data/lib/badline/kim1.rb +4 -0
- data/lib/badline/machine.rb +3 -0
- data/lib/badline/media/true_drive.rb +10 -0
- data/lib/badline/media/vic20_media.rb +4 -2
- data/lib/badline/media.rb +5 -1
- data/lib/badline/memory.rb +2 -4
- data/lib/badline/options/table.rb +1 -1
- data/lib/badline/pet/superpet/dongle.rb +13 -2
- data/lib/badline/pet/superpet.rb +5 -0
- data/lib/badline/pet.rb +4 -0
- data/lib/badline/plus4.rb +4 -0
- data/lib/badline/roms/README +11 -1
- data/lib/badline/snapshot/container.rb +6 -4
- data/lib/badline/snapshot/field_doc.rb +8 -4
- data/lib/badline/snapshot/image.rb +15 -6
- data/lib/badline/snapshot/vice/pet.rb +1 -0
- data/lib/badline/snapshot/vice/pet_dongle.rb +72 -0
- data/lib/badline/snapshot/vice/pet_mem.rb +11 -5
- data/lib/badline/snapshot/vice/vic20.rb +70 -0
- data/lib/badline/snapshot/vice/vic20_cpu.rb +61 -0
- data/lib/badline/snapshot/vice/vic20_mem.rb +85 -0
- data/lib/badline/snapshot/vice/vic20_restore.rb +124 -0
- data/lib/badline/snapshot/vice/vic20_vici.rb +200 -0
- data/lib/badline/snapshot/vice.rb +1 -0
- data/lib/badline/snapshot.rb +4 -4
- data/lib/badline/storage/disk_image/writing.rb +2 -2
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic20/port_wiring.rb +11 -0
- data/lib/badline/vic20.rb +4 -0
- metadata +9 -1
|
@@ -95,6 +95,14 @@ module Badline
|
|
|
95
95
|
op_int_vector_low op_int_vector_high
|
|
96
96
|
].freeze
|
|
97
97
|
|
|
98
|
+
# RES runs BRK's sequence with its three stack writes turned into
|
|
99
|
+
# reads, which still count S down, and reads the vector at $FFFC. Its
|
|
100
|
+
# first two cycles read the program counter and leave it where it was.
|
|
101
|
+
RESET_PLAN = %i[
|
|
102
|
+
op_reset_dummy op_reset_dummy op_reset_stack op_reset_stack op_reset_stack
|
|
103
|
+
op_int_vector_low op_int_vector_high
|
|
104
|
+
].freeze
|
|
105
|
+
|
|
98
106
|
SPECIAL_PLANS = {
|
|
99
107
|
brk: BRK_PLAN, jam: JAM_PLAN, jsr: JSR_PLAN, rts: RTS_PLAN,
|
|
100
108
|
rti: RTI_PLAN, pha: PUSH_PLAN, php: PUSH_PLAN,
|
|
@@ -181,6 +189,7 @@ module Badline
|
|
|
181
189
|
FETCH_WRITES = Microcode.write_mask(FETCH_PLAN)
|
|
182
190
|
INTERRUPT_WRITES = Microcode.write_mask(INTERRUPT_PLAN)
|
|
183
191
|
JAMMED_WRITES = Microcode.write_mask(JAMMED_PLAN)
|
|
192
|
+
RESET_WRITES = Microcode.write_mask(RESET_PLAN)
|
|
184
193
|
MICROCODE = Microcode.table
|
|
185
194
|
|
|
186
195
|
# The instruction methods, in a fixed order a snapshot names them by.
|
|
@@ -190,6 +199,7 @@ module Badline
|
|
|
190
199
|
FETCH = -1
|
|
191
200
|
INTERRUPT = -2
|
|
192
201
|
JAMMED = -3
|
|
202
|
+
RESET = -4
|
|
193
203
|
end
|
|
194
204
|
end
|
|
195
205
|
end
|
data/lib/badline/chips/rriot.rb
CHANGED
|
@@ -74,7 +74,10 @@ module Badline
|
|
|
74
74
|
|
|
75
75
|
# Everything the RRIOT holds but its timer's counter, for comparing
|
|
76
76
|
# it at two points.
|
|
77
|
-
def idle_state = [*@ram,
|
|
77
|
+
def idle_state = [*@ram, *port_state]
|
|
78
|
+
|
|
79
|
+
# The ports and the timer but for its counter.
|
|
80
|
+
def port_state = [@ora, @orb, @ddra, @ddrb, *@timer.state, @timer.flag]
|
|
78
81
|
|
|
79
82
|
def read_rom(addr) = @rom[addr & 0x3ff]
|
|
80
83
|
|
|
@@ -21,7 +21,7 @@ module Badline
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# Cycles until the current window closes, counting the next one as 1.
|
|
24
|
-
attr_reader :cycles_to_close
|
|
24
|
+
attr_reader :cycles_to_close, :rate, :clock_hz
|
|
25
25
|
|
|
26
26
|
# How far the current window has got, in a keyed snapshot module
|
|
27
27
|
# (Snapshot::Keyed) under `prefix`. The clock and the rate are the
|
|
@@ -221,7 +221,11 @@ module Badline
|
|
|
221
221
|
# voice's under voice1. to voice3., and the filter's under filter.
|
|
222
222
|
# and ext. Saving catches the DSP up first, so no span or deferred
|
|
223
223
|
# write is left to save. What the registers decode to is worked out
|
|
224
|
-
# from them on load.
|
|
224
|
+
# from them on load. A recording SID also saves how far its recording
|
|
225
|
+
# has got under rec., with the rate, the clock and the filter chunk it
|
|
226
|
+
# records at: the samples' windows bound the filter's steps, so a SID
|
|
227
|
+
# loaded from it that records at the same rate and clock carries on
|
|
228
|
+
# from that window and integrates the steps the saved one does.
|
|
225
229
|
module KeyedState
|
|
226
230
|
def save_fields(out)
|
|
227
231
|
catch_up
|
|
@@ -230,6 +234,8 @@ module Badline
|
|
|
230
234
|
@voice2.save_fields(out, "voice2.")
|
|
231
235
|
@voice3.save_fields(out, "voice3.")
|
|
232
236
|
@filter.save_fields(out, "filter.")
|
|
237
|
+
recording = @decimator || @resumed_recording
|
|
238
|
+
save_recording(out, recording) if recording
|
|
233
239
|
out
|
|
234
240
|
end
|
|
235
241
|
|
|
@@ -243,23 +249,7 @@ module Badline
|
|
|
243
249
|
@voice2.load_fields(input, "voice2.")
|
|
244
250
|
@voice3.load_fields(input, "voice3.")
|
|
245
251
|
@filter.load_fields(input, "filter.")
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
# How far the recording has got into the sample it is averaging,
|
|
249
|
-
# under `prefix`, for a host that carries on recording from a
|
|
250
|
-
# snapshot: it calls #record, then #load_recording_fields.
|
|
251
|
-
def save_recording_fields(out, prefix)
|
|
252
|
-
decimator = @decimator
|
|
253
|
-
raise ArgumentError, "the SID isn't recording" if decimator.nil?
|
|
254
|
-
|
|
255
|
-
decimator.save_fields(out, prefix)
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def load_recording_fields(input, prefix)
|
|
259
|
-
decimator = @decimator
|
|
260
|
-
raise ArgumentError, "the SID isn't recording" if decimator.nil?
|
|
261
|
-
|
|
262
|
-
decimator.load_fields(input, prefix)
|
|
252
|
+
load_recording(input)
|
|
263
253
|
end
|
|
264
254
|
|
|
265
255
|
def restored!
|
|
@@ -272,6 +262,49 @@ module Badline
|
|
|
272
262
|
|
|
273
263
|
private
|
|
274
264
|
|
|
265
|
+
def save_recording(out, decimator)
|
|
266
|
+
out.int("rec.rate", decimator.rate).int("rec.clock_hz", decimator.clock_hz)
|
|
267
|
+
out.int("rec.filter_chunk", @filter_chunk)
|
|
268
|
+
decimator.save_fields(out, "rec.")
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# A SID recording already carries on from the loaded window when it
|
|
272
|
+
# records at its rate, clock and filter chunk, and starts a new
|
|
273
|
+
# window otherwise.
|
|
274
|
+
def load_recording(input)
|
|
275
|
+
@resumed_recording = nil
|
|
276
|
+
live = @decimator
|
|
277
|
+
loaded = loaded_recording(input)
|
|
278
|
+
chunk = loaded ? input.int("rec.filter_chunk", @filter_chunk) : @filter_chunk
|
|
279
|
+
if live.nil?
|
|
280
|
+
@filter_chunk = chunk
|
|
281
|
+
@resumed_recording = loaded
|
|
282
|
+
elsif loaded && chunk == @filter_chunk && loaded.rate == live.rate && loaded.clock_hz == live.clock_hz
|
|
283
|
+
@decimator = loaded
|
|
284
|
+
else
|
|
285
|
+
@decimator = Decimator.new(clock_hz: live.clock_hz, rate: live.rate)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def loaded_recording(input)
|
|
290
|
+
rate = input.int("rec.rate", 0)
|
|
291
|
+
return unless rate.positive?
|
|
292
|
+
|
|
293
|
+
decimator = Decimator.new(clock_hz: input.int("rec.clock_hz", Region::PAL.clock_hz), rate:)
|
|
294
|
+
decimator.load_fields(input, "rec.")
|
|
295
|
+
decimator
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# The window a loaded snapshot's recording had got to, when this
|
|
299
|
+
# recording is at its rate, clock and filter chunk.
|
|
300
|
+
def resumed_recording(rate, filter_chunk, clock_hz)
|
|
301
|
+
resumed = @resumed_recording
|
|
302
|
+
@resumed_recording = nil
|
|
303
|
+
return if resumed.nil? || filter_chunk != @filter_chunk
|
|
304
|
+
|
|
305
|
+
resumed if resumed.rate == rate && resumed.clock_hz == clock_hz
|
|
306
|
+
end
|
|
307
|
+
|
|
275
308
|
def restore_voice(voice, base)
|
|
276
309
|
control = register(base + 4)
|
|
277
310
|
voice.waveform.restore_registers(register(base) | (register(base + 1) << 8),
|
data/lib/badline/chips/sid.rb
CHANGED
|
@@ -70,6 +70,7 @@ module Badline
|
|
|
70
70
|
@pending_cycles = 0
|
|
71
71
|
@deferred_writes = []
|
|
72
72
|
@decimator = nil
|
|
73
|
+
@resumed_recording = nil
|
|
73
74
|
@samples = []
|
|
74
75
|
@voice_samples = nil
|
|
75
76
|
@filter_chunk = filter_chunk
|
|
@@ -120,12 +121,15 @@ module Badline
|
|
|
120
121
|
# Starts collecting the output, averaged down to `rate` samples a
|
|
121
122
|
# second, for #drain_samples. `filter_chunk` overrides the one the SID
|
|
122
123
|
# was built with, for a machine that built its own. `clock_hz` is the
|
|
123
|
-
# rate the SID's cycles are taken to run at.
|
|
124
|
+
# rate the SID's cycles are taken to run at. A SID loaded from a
|
|
125
|
+
# recording one carries on from the window that one had got to, at the
|
|
126
|
+
# same rate, clock and filter chunk.
|
|
124
127
|
def record(rate:, filter_chunk: @filter_chunk, clock_hz: Region::PAL.clock_hz)
|
|
125
128
|
synthesize!
|
|
126
129
|
catch_up
|
|
130
|
+
resumed = resumed_recording(rate, filter_chunk, clock_hz)
|
|
127
131
|
@filter_chunk = filter_chunk
|
|
128
|
-
@decimator = Decimator.new(clock_hz:, rate:)
|
|
132
|
+
@decimator = resumed || Decimator.new(clock_hz:, rate:)
|
|
129
133
|
@samples = []
|
|
130
134
|
end
|
|
131
135
|
|
|
@@ -88,7 +88,8 @@ module Badline
|
|
|
88
88
|
# The VIC-I's fields in a keyed snapshot module (Snapshot::Keyed): the
|
|
89
89
|
# registers, the beam, the frame's length in lines and whether the
|
|
90
90
|
# next interlaced field is the long one, the text window's counters
|
|
91
|
-
# and the fetch in flight, the
|
|
91
|
+
# and the fetch in flight, the light pen's latch and line under pen.*,
|
|
92
|
+
# the painter's lines under line.*, the picture, and the sound
|
|
92
93
|
# under sound.* What the registers decode to, where the line's fetches
|
|
93
94
|
# end and the next event are worked out on load.
|
|
94
95
|
module KeyedState
|
|
@@ -98,6 +99,7 @@ module Badline
|
|
|
98
99
|
out.bool("window_open", @window_open).bool("window_done", @window_done).int("row_start", @row_start)
|
|
99
100
|
out.int("matrix", @matrix).int("char_line", @char_line).int("text_rows", @text_rows)
|
|
100
101
|
out.int("fetch_from", @fetch_from).int("fetched", @fetched).int("code", @code)
|
|
102
|
+
out.int("pen.x", @pen_x).int("pen.y", @pen_y).bool("pen.low", @pen_low)
|
|
101
103
|
out.int("line.layout", LINE_LAYOUT)
|
|
102
104
|
@painter.save_fields(out)
|
|
103
105
|
@sound&.save_fields(out, "sound.")
|
|
@@ -119,6 +121,9 @@ module Badline
|
|
|
119
121
|
@fetch_from = input.int("fetch_from", @fetch_from)
|
|
120
122
|
@fetched = input.int("fetched", @fetched)
|
|
121
123
|
@code = input.byte("code", @code)
|
|
124
|
+
@pen_x = input.byte("pen.x", @pen_x)
|
|
125
|
+
@pen_y = input.byte("pen.y", @pen_y)
|
|
126
|
+
@pen_low = input.bool("pen.low", @pen_low)
|
|
122
127
|
line = input.int("line.layout", 0) == LINE_LAYOUT
|
|
123
128
|
input.adjust("line in progress from another renderer, restarted") unless line || input.missing?
|
|
124
129
|
@painter.load_fields(input, line)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Chips
|
|
5
|
+
class VICI
|
|
6
|
+
# The light pen input, the line the VIC-20 shares with the joystick's
|
|
7
|
+
# fire button, and the position a falling edge on it latches into
|
|
8
|
+
# $9006 and $9007.
|
|
9
|
+
#
|
|
10
|
+
# $9006 reads 2h + 1, where h is the edge's column plus two, wrapping
|
|
11
|
+
# at the line's end, and $9007 bits 8-1 of the raster count in the
|
|
12
|
+
# cycle after the edge. Around the cycle where the count turns to 0
|
|
13
|
+
# the latch reads otherwise:
|
|
14
|
+
#
|
|
15
|
+
# - An edge from 8 cycles before that cycle to 1 after it latches as
|
|
16
|
+
# an edge in the cycle after it would, with $9007 at 0, but for an
|
|
17
|
+
# edge 2 cycles before it, which latches an even $9006, one less.
|
|
18
|
+
# - Before that, an edge latches nothing while the count read its
|
|
19
|
+
# last value of the frame in the cycle before the edge: line 311 on
|
|
20
|
+
# the 6561, and 261, past the last line, on the 6560.
|
|
21
|
+
module LightPen
|
|
22
|
+
# Where the window around the count's turn to 0 starts and ends,
|
|
23
|
+
# in cycles from that turn, and the edge in it that latches an even
|
|
24
|
+
# $9006.
|
|
25
|
+
WRAP_WINDOW = (-8..1)
|
|
26
|
+
WRAP_EVEN = -2
|
|
27
|
+
|
|
28
|
+
# The pen's line, low while the pen sees the beam or fire is held.
|
|
29
|
+
def lightpen_level(high)
|
|
30
|
+
low = !high
|
|
31
|
+
return if low == @pen_low
|
|
32
|
+
|
|
33
|
+
@pen_low = low
|
|
34
|
+
pen_fell if low
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def power_on_pen!
|
|
40
|
+
@pen_x = @pen_y = 0
|
|
41
|
+
@pen_low = false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def pen_fell
|
|
45
|
+
line = @rasterline
|
|
46
|
+
column = @column
|
|
47
|
+
from_wrap = cycles_from_wrap(line, column)
|
|
48
|
+
if WRAP_WINDOW.cover?(from_wrap)
|
|
49
|
+
latch_pen(@frame_wrap + 1, 0, from_wrap == WRAP_EVEN)
|
|
50
|
+
elsif !pen_blocked?(line, column)
|
|
51
|
+
next_column = column + 1
|
|
52
|
+
if next_column == @cycles_per_line
|
|
53
|
+
next_column = 0
|
|
54
|
+
line = line == @frame_lines - 1 ? 0 : line + 1
|
|
55
|
+
end
|
|
56
|
+
latch_pen(column, count_at(line, next_column) >> 1, false)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# How many cycles the beam is past the cycle where the count turns
|
|
61
|
+
# to 0, negative before it. On the 6561 the count turns as the
|
|
62
|
+
# frame starts, so the frame's first cycles are past it.
|
|
63
|
+
def cycles_from_wrap(line, column)
|
|
64
|
+
position = (line * @cycles_per_line) + column
|
|
65
|
+
return position if position < 2 && @frame_wrap == @cycles_per_line
|
|
66
|
+
|
|
67
|
+
position - ((@frame_lines - 1) * @cycles_per_line) - @frame_wrap
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Whether the count read its last value of the frame in the cycle
|
|
71
|
+
# before this one.
|
|
72
|
+
def pen_blocked?(line, column)
|
|
73
|
+
column -= 1
|
|
74
|
+
if column.negative?
|
|
75
|
+
column = @cycles_per_line - 1
|
|
76
|
+
line = line.zero? ? @frame_lines - 1 : line - 1
|
|
77
|
+
end
|
|
78
|
+
last = @frame_wrap > @count_column ? @frame_lines : @frame_lines - 1
|
|
79
|
+
count_at(line, column) == last
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def latch_pen(column, count, even)
|
|
83
|
+
@pen_x = (2 * ((column + 2) % @cycles_per_line)) + (even ? 0 : 1)
|
|
84
|
+
@pen_y = count
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/badline/chips/vic_i.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "badline/chips/vic_i/painter"
|
|
4
4
|
require "badline/chips/vic_i/text_window"
|
|
5
|
+
require "badline/chips/vic_i/light_pen"
|
|
5
6
|
require "badline/chips/vic_i/sound"
|
|
6
7
|
require "badline/chips/vic_i/keyed_parts"
|
|
7
8
|
|
|
@@ -16,7 +17,7 @@ module Badline
|
|
|
16
17
|
# The registers repeat through $9000-$90FF. $9003 bit 7 and $9004 read
|
|
17
18
|
# the raster count, its bit 0 and bits 8-1, which on the 6561 is the
|
|
18
19
|
# raster line (see Profile for the 6560's). $9006 and $9007 read the
|
|
19
|
-
# light pen
|
|
20
|
+
# light pen's latch (LightPen), and $9008 and $9009 the two
|
|
20
21
|
# pots, which read $FF with nothing plugged in. The other registers
|
|
21
22
|
# read back what was written to them.
|
|
22
23
|
#
|
|
@@ -45,6 +46,7 @@ module Badline
|
|
|
45
46
|
# blanked.
|
|
46
47
|
class VICI
|
|
47
48
|
include TextWindow
|
|
49
|
+
include LightPen
|
|
48
50
|
|
|
49
51
|
# What a VIC-I sets for the machine: the clock, the raster's cycles
|
|
50
52
|
# per line and lines per frame, how many lines at the top of the frame
|
|
@@ -149,6 +151,7 @@ module Badline
|
|
|
149
151
|
@fetched = @slot = 0
|
|
150
152
|
@code = 0
|
|
151
153
|
@painter.power_on!(@frame_lines - 1)
|
|
154
|
+
power_on_pen!
|
|
152
155
|
schedule
|
|
153
156
|
end
|
|
154
157
|
|
|
@@ -195,7 +198,8 @@ module Badline
|
|
|
195
198
|
case register
|
|
196
199
|
when 0x03 then ((raster_count & 0x01) << 7) | (@registers[3] & 0x7f)
|
|
197
200
|
when 0x04 then raster_count >> 1
|
|
198
|
-
when 0x06
|
|
201
|
+
when 0x06 then @pen_x
|
|
202
|
+
when 0x07 then @pen_y
|
|
199
203
|
when 0x08, 0x09 then 0xff
|
|
200
204
|
else @registers[register]
|
|
201
205
|
end
|
|
@@ -218,17 +222,18 @@ module Badline
|
|
|
218
222
|
end
|
|
219
223
|
|
|
220
224
|
# The raster count $9003 and $9004 read (see Profile).
|
|
221
|
-
def raster_count
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
def raster_count = count_at(@rasterline, @column)
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
# What the count reads at +column+ of +line+ in this frame.
|
|
230
|
+
def count_at(line, column)
|
|
224
231
|
return line if column < @count_column
|
|
225
232
|
return 0 if line == @frame_lines - 1 && column >= @frame_wrap
|
|
226
233
|
|
|
227
234
|
line + 1
|
|
228
235
|
end
|
|
229
236
|
|
|
230
|
-
private
|
|
231
|
-
|
|
232
237
|
def event(column)
|
|
233
238
|
if column == @cycles_per_line
|
|
234
239
|
column = @column = 0
|
|
@@ -13,12 +13,22 @@ module Badline
|
|
|
13
13
|
# that sector's data block, whatever it holds.
|
|
14
14
|
module SectorReader
|
|
15
15
|
HEADER_BITS = 10 * 8
|
|
16
|
-
DATA_BITS = 325 * 8
|
|
17
16
|
SYNC = /1{10,}/
|
|
18
17
|
|
|
18
|
+
# A data block as far as its checksum: its ID, 256 bytes and the
|
|
19
|
+
# checksum. The DOS writes two more after them, and switches back to
|
|
20
|
+
# read mode a few bits into the last, so the gap that was there
|
|
21
|
+
# before finishes its last code.
|
|
22
|
+
CHECKED_BYTES = 258
|
|
23
|
+
CHECKED_BITS = CHECKED_BYTES * 10
|
|
24
|
+
|
|
25
|
+
# The codes of two $0 nibbles, which finish the last group of
|
|
26
|
+
# CHECKED_BITS.
|
|
27
|
+
PADDING = "01010" * 4
|
|
28
|
+
|
|
19
29
|
# A sector as read: its decoded header, [ID, checksum, sector, track,
|
|
20
30
|
# the second ID byte, the first, ...], and its decoded data block,
|
|
21
|
-
# [ID, 256 data bytes, checksum
|
|
31
|
+
# [ID, 256 data bytes, checksum], nil when there isn't one that
|
|
22
32
|
# decodes.
|
|
23
33
|
Sector = Struct.new(:header, :data)
|
|
24
34
|
|
|
@@ -62,10 +72,17 @@ module Badline
|
|
|
62
72
|
next unless header && header[0] == Disk::HEADER_ID
|
|
63
73
|
|
|
64
74
|
data_sync = SYNC.match(bits, pos + HEADER_BITS)
|
|
65
|
-
yield header, data_sync &&
|
|
75
|
+
yield header, data_sync && decode_data(bits, data_sync.end(0))
|
|
66
76
|
end
|
|
67
77
|
end
|
|
68
78
|
|
|
79
|
+
def decode_data(bits, pos)
|
|
80
|
+
slice = bits[pos, CHECKED_BITS]
|
|
81
|
+
return unless slice.length == CHECKED_BITS
|
|
82
|
+
|
|
83
|
+
GCR.decode([slice + PADDING].pack("B*").bytes)&.first(CHECKED_BYTES)
|
|
84
|
+
end
|
|
85
|
+
|
|
69
86
|
def decode(bits, pos, count)
|
|
70
87
|
slice = bits[pos, count]
|
|
71
88
|
return unless slice.length == count
|
|
@@ -39,6 +39,7 @@ module Badline
|
|
|
39
39
|
@via2 = DiskVIA.new(start: 0x1c00, mechanism: @mechanism)
|
|
40
40
|
@bus = Bus.new(rom: rom || ROM.load("dos1541.rom", 0xc000), via1: @via1, via2: @via2)
|
|
41
41
|
@cpu = CPU.new(@bus, debug:)
|
|
42
|
+
@cpu.power_on!
|
|
42
43
|
@clock_hz = CLOCK_HZ
|
|
43
44
|
@idle_loop = IDLE_LOOP
|
|
44
45
|
@host_clock_hz = host_clock_hz
|
|
@@ -97,6 +97,7 @@ module Badline
|
|
|
97
97
|
@host_tia = Chips::TIA.new
|
|
98
98
|
@bus = Bus.new(rom: rom || ROM.load("dos1551.rom", 0xc000), port: @port, tia: @tia)
|
|
99
99
|
@cpu = CPU.new(@bus, debug:)
|
|
100
|
+
@cpu.power_on!
|
|
100
101
|
@idle_loop = IDLE_LOOP
|
|
101
102
|
@host_clock_hz = host_clock_hz
|
|
102
103
|
@phase = 0
|
|
@@ -68,6 +68,7 @@ module Badline
|
|
|
68
68
|
@fdc = WD1770.new
|
|
69
69
|
@bus = Bus.new(rom: rom || ROM.load("dos1571.rom", 0x8000), via1: @via1, via2: @via2, cia: @cia, fdc: @fdc)
|
|
70
70
|
@cpu = CPU.new(@bus, debug:)
|
|
71
|
+
@cpu.power_on!
|
|
71
72
|
@fdc.cpu = @cpu
|
|
72
73
|
@idle_loop = IDLE_LOOP
|
|
73
74
|
@host_clock_hz = host_clock_hz
|
|
@@ -87,6 +87,7 @@ module Badline
|
|
|
87
87
|
@cia = Chips::CIA.new(start: 0x4000, peripheral: @serial_port, tod_hz: 0)
|
|
88
88
|
@bus = Bus.new(rom: rom || ROM.load("dos1581.rom", 0x8000), cia: @cia, fdc: @fdc, drive: self)
|
|
89
89
|
@cpu = CPU.new(@bus, debug:)
|
|
90
|
+
@cpu.power_on!
|
|
90
91
|
@idle_loop = IDLE_LOOP
|
|
91
92
|
@host_clock_hz = host_clock_hz
|
|
92
93
|
@phase = 0
|
|
@@ -52,6 +52,7 @@ module Badline
|
|
|
52
52
|
@via2 = Model1541::DiskVIA.new(start: 0x1c00, mechanism: @mechanism)
|
|
53
53
|
@bus = Bus.new(rom: rom || ROM.load("dos2031.rom", 0xc000), via1: @via1, via2: @via2)
|
|
54
54
|
@cpu = CPU.new(@bus, debug:)
|
|
55
|
+
@cpu.power_on!
|
|
55
56
|
@clock_hz = CLOCK_HZ
|
|
56
57
|
@idle_loop = IDLE_LOOP
|
|
57
58
|
@host_clock_hz = host_clock_hz
|
|
@@ -79,9 +79,10 @@ module Badline
|
|
|
79
79
|
turn(1, drives.nobits?(0x10), drives & 0x03)
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
# Whether a cycle would change nothing:
|
|
83
|
-
#
|
|
84
|
-
|
|
82
|
+
# Whether a cycle would change nothing: no byte ready under way, and
|
|
83
|
+
# each drive's motor off or turning with no disk in it, as both turn
|
|
84
|
+
# from the bump at boot until the run-down.
|
|
85
|
+
def quiet? = !@ca1_low && still?(0) && still?(1)
|
|
85
86
|
|
|
86
87
|
# What the drives hold, for comparing them at two points.
|
|
87
88
|
def idle_state = [*@half_tracks, *@phases, *@motors, *@syncs, @data_in]
|
|
@@ -98,6 +99,8 @@ module Badline
|
|
|
98
99
|
|
|
99
100
|
def selected_drive = @rriot.port_b_output & 0x01
|
|
100
101
|
|
|
102
|
+
def still?(drive) = !@motors[drive] || @disks[drive].nil?
|
|
103
|
+
|
|
101
104
|
def turn(drive, motor, phase)
|
|
102
105
|
step(drive, phase) if phase != @phases[drive]
|
|
103
106
|
motor_off(drive) if @motors[drive] && !motor
|
|
@@ -26,7 +26,11 @@ module Badline
|
|
|
26
26
|
# At reset the controller waits for the common RAM's $0400 to read 0,
|
|
27
27
|
# starts, and writes its timer's period there. The interface processor
|
|
28
28
|
# tests its RAM and ROMs, has the controller reset its drives, and
|
|
29
|
-
# bumps both heads to track 1.
|
|
29
|
+
# bumps both heads to track 1. The motors run from then until the
|
|
30
|
+
# run-down after the first job, about 9.5 million host cycles in for a
|
|
31
|
+
# directory LOAD, or 8.8 million with no job. A motor turning with no
|
|
32
|
+
# disk in its drive changes nothing, so a unit with no disks sleeps
|
|
33
|
+
# from the bump on, and one with a disk only after the run-down.
|
|
30
34
|
class Model4040
|
|
31
35
|
include Model8050::Board
|
|
32
36
|
include Model8050::Idle
|
|
@@ -61,7 +65,9 @@ module Badline
|
|
|
61
65
|
build_ip(dos_rom || ROM.load(dos_file, dos_start))
|
|
62
66
|
build_fdc(fdc_rom || ROM.read(fdc_file))
|
|
63
67
|
@ip_cpu = IPCPU.new(@ip_bus)
|
|
68
|
+
@ip_cpu.power_on!
|
|
64
69
|
@fdc_cpu = FDCCPU.new(@fdc_bus)
|
|
70
|
+
@fdc_cpu.power_on!
|
|
65
71
|
init_idle(ip_loop, fdc_loop)
|
|
66
72
|
@ieee_bus.attach(self)
|
|
67
73
|
end
|
|
@@ -128,6 +134,16 @@ module Badline
|
|
|
128
134
|
@fdc_cpu.cycle!
|
|
129
135
|
@cycles += 1
|
|
130
136
|
end
|
|
137
|
+
|
|
138
|
+
# One cycle of the FDC's side alone, as step runs it (Model8050::Idle).
|
|
139
|
+
def fdc_step
|
|
140
|
+
@mechanism.cycle!
|
|
141
|
+
@via.cycle!
|
|
142
|
+
@rriot.cycle!
|
|
143
|
+
@fdc_cpu.irq = @rriot.irq?
|
|
144
|
+
@fdc_cpu.cycle!
|
|
145
|
+
@cycles += 1
|
|
146
|
+
end
|
|
131
147
|
end
|
|
132
148
|
|
|
133
149
|
# The 3040: the 4040's board with the first DOS 2.1, as xpet's 3040
|
|
@@ -15,7 +15,9 @@ module Badline
|
|
|
15
15
|
# Drives::Engine::Core does: each host cycle adds 1 MHz of phase, and
|
|
16
16
|
# every whole host period in it runs a cycle of both processors. While
|
|
17
17
|
# both processors idle, it sleeps instead, and the readers of its
|
|
18
|
-
# parts catch it up first.
|
|
18
|
+
# parts catch it up first. Asleep, a host cycle only counts towards
|
|
19
|
+
# the one planned to wake the unit, which its own doze wakes, and a
|
|
20
|
+
# line the controller moves wakes it at once (host_written!).
|
|
19
21
|
module DOSBoard
|
|
20
22
|
attr_reader :device, :ieee_bus
|
|
21
23
|
|
|
@@ -51,9 +53,6 @@ module Badline
|
|
|
51
53
|
# (IEEEPort).
|
|
52
54
|
def ieee_low_lines(atn) = @ieee_port.ieee_low_lines(atn)
|
|
53
55
|
|
|
54
|
-
# The controller pushed its lines into the bus.
|
|
55
|
-
def host_written! = nil
|
|
56
|
-
|
|
57
56
|
# Which LEDs are lit, as the unit's port drives them. They hold
|
|
58
57
|
# still while the unit sleeps, so reading them leaves it asleep.
|
|
59
58
|
def leds = @ieee_port.leds
|
|
@@ -62,16 +61,20 @@ module Badline
|
|
|
62
61
|
|
|
63
62
|
# Runs the drive cycles that fall in one host cycle, then takes the
|
|
64
63
|
# controller's lines for the next. Asleep, the unit owes the cycles
|
|
65
|
-
# instead, and wakes once the controller has moved a line
|
|
64
|
+
# instead, and wakes once the controller has moved a line, as it
|
|
65
|
+
# does with a processor owing cycles (dozing?).
|
|
66
66
|
def host_cycle!
|
|
67
|
+
return if slept_through?
|
|
68
|
+
|
|
67
69
|
phase = @phase + CLOCK_HZ
|
|
68
70
|
while phase >= @host_clock_hz
|
|
69
71
|
@asleep ? owe_cycle : run_cycle
|
|
70
72
|
phase -= @host_clock_hz
|
|
71
73
|
end
|
|
72
74
|
@phase = phase
|
|
73
|
-
settle! if
|
|
75
|
+
settle! if dozing? && @ieee_port.lines_moved?
|
|
74
76
|
@ieee_port.latch_host
|
|
77
|
+
plan_sleep if @asleep
|
|
75
78
|
end
|
|
76
79
|
|
|
77
80
|
def cycle!
|
|
@@ -79,12 +82,52 @@ module Badline
|
|
|
79
82
|
run_cycle
|
|
80
83
|
end
|
|
81
84
|
|
|
85
|
+
# The controller pushed its lines into the bus. A line it moved
|
|
86
|
+
# wakes a unit that owes cycles, which saw the lines as they stood.
|
|
87
|
+
def host_written!
|
|
88
|
+
settle! if dozing? && @ieee_port.lines_moved?
|
|
89
|
+
end
|
|
90
|
+
|
|
82
91
|
private
|
|
83
92
|
|
|
84
93
|
def settled(part)
|
|
85
94
|
settle!
|
|
86
95
|
part
|
|
87
96
|
end
|
|
97
|
+
|
|
98
|
+
# Asleep, a host cycle only counts towards the one planned to wake
|
|
99
|
+
# the unit (plan_sleep). Returns whether it did.
|
|
100
|
+
def slept_through?
|
|
101
|
+
return false unless @planned
|
|
102
|
+
|
|
103
|
+
@slept += 1
|
|
104
|
+
doze if @slept == @wake_at
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Plans the host cycle that brings the cycles owed up to the budget,
|
|
109
|
+
# once the unit has fallen asleep and the host cycle it fell asleep
|
|
110
|
+
# in is over.
|
|
111
|
+
def plan_sleep
|
|
112
|
+
return if @planned
|
|
113
|
+
|
|
114
|
+
@planned = true
|
|
115
|
+
@slept = 0
|
|
116
|
+
needed = ((@budget - @owed) * @host_clock_hz) - @phase
|
|
117
|
+
@wake_at = needed.positive? ? (needed + CLOCK_HZ - 1) / CLOCK_HZ : 1
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Turns the host cycles slept into cycles owed, through the phase
|
|
121
|
+
# accumulator.
|
|
122
|
+
def owe_slept
|
|
123
|
+
return unless @planned
|
|
124
|
+
|
|
125
|
+
@planned = false
|
|
126
|
+
phase = @phase + (@slept * CLOCK_HZ)
|
|
127
|
+
@owed += phase / @host_clock_hz
|
|
128
|
+
@phase = phase % @host_clock_hz
|
|
129
|
+
@slept = 0
|
|
130
|
+
end
|
|
88
131
|
end
|
|
89
132
|
end
|
|
90
133
|
end
|
|
@@ -31,6 +31,10 @@ module Badline
|
|
|
31
31
|
# last cleared (Model8050::Idle).
|
|
32
32
|
attr_accessor :volatile
|
|
33
33
|
|
|
34
|
+
# The bytes of the 6530's RAM read or written since it was last
|
|
35
|
+
# cleared, a bit for each pair of them (Model8050::Idle).
|
|
36
|
+
attr_accessor :ram_touched
|
|
37
|
+
|
|
34
38
|
def initialize(common:, via:, rriot:)
|
|
35
39
|
addressable_at(0, length: 2**16)
|
|
36
40
|
@common = common
|
|
@@ -38,6 +42,7 @@ module Badline
|
|
|
38
42
|
@rriot = rriot
|
|
39
43
|
@data = 0
|
|
40
44
|
@volatile = false
|
|
45
|
+
@ram_touched = 0
|
|
41
46
|
@common_changes = 0
|
|
42
47
|
end
|
|
43
48
|
|
|
@@ -78,6 +83,7 @@ module Badline
|
|
|
78
83
|
end
|
|
79
84
|
return peek_via(addr & 0x0f) if addr.anybits?(0x40)
|
|
80
85
|
|
|
86
|
+
@ram_touched |= 1 << ((addr & 0x3f) >> 1)
|
|
81
87
|
@rriot.read_ram(addr)
|
|
82
88
|
end
|
|
83
89
|
|
|
@@ -93,6 +99,7 @@ module Badline
|
|
|
93
99
|
elsif addr.anybits?(0x40)
|
|
94
100
|
@via.poke(0x40 | (addr & 0x0f), value)
|
|
95
101
|
else
|
|
102
|
+
@ram_touched |= 1 << ((addr & 0x3f) >> 1)
|
|
96
103
|
@rriot.write_ram(addr, value)
|
|
97
104
|
end
|
|
98
105
|
end
|