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
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/drive1581/wd1772/registers"
|
|
4
|
+
require "badline/drive1581/wd1772/turn"
|
|
5
|
+
require "badline/drive1581/wd1772/stepping"
|
|
6
|
+
require "badline/drive1581/wd1772/sectors"
|
|
7
|
+
require "badline/drive1581/wd1772/sector_writes"
|
|
8
|
+
require "badline/drive1581/wd1772/tracks"
|
|
9
|
+
require "badline/drive1581/wd1772/state"
|
|
10
|
+
|
|
11
|
+
module Badline
|
|
12
|
+
class Drive1581
|
|
13
|
+
# The WD1772 floppy disk controller at $6000, as the WD177x data sheet
|
|
14
|
+
# describes it, working on the Mechanism's Track at sector level: it
|
|
15
|
+
# finds ID and data fields where they lie round the turn and moves
|
|
16
|
+
# their bytes at the MFM double-density rate, a byte every 32 µs, but
|
|
17
|
+
# keeps no flux or bitstream.
|
|
18
|
+
#
|
|
19
|
+
# $6000 status (read), command (write)
|
|
20
|
+
# $6001 track
|
|
21
|
+
# $6002 sector
|
|
22
|
+
# $6003 data
|
|
23
|
+
#
|
|
24
|
+
# It runs on the drive's clock, 2 MHz here, so the data sheet's times,
|
|
25
|
+
# given for 8 MHz on the chip's clock pin, come out as BYTE cycles a
|
|
26
|
+
# byte, STEP_RATES between steps and SETTLE for the head to settle.
|
|
27
|
+
#
|
|
28
|
+
# A command runs as a chain of phases, each due at a cycle (@due): a
|
|
29
|
+
# step, an index pulse, the end of a byte under the head. Between them
|
|
30
|
+
# nothing happens, so cycle! only counts. BUSY sets as the command is
|
|
31
|
+
# written, and the chip takes it up START cycles later, 16 µs, so a
|
|
32
|
+
# command with nothing to do, such as a seek to the track the head is
|
|
33
|
+
# on, still shows BUSY to the DOS, which polls for it before waiting
|
|
34
|
+
# for it to clear.
|
|
35
|
+
#
|
|
36
|
+
# The 1581 leaves INTRQ and DRQ unconnected and polls the status
|
|
37
|
+
# register instead (schematic sheet 2), but both are here (intrq?,
|
|
38
|
+
# drq?). Its DOS sets h in every command, so the motor spin-up wait
|
|
39
|
+
# never runs; the spindle follows the 8520's /MOTOR line instead.
|
|
40
|
+
class WD1772
|
|
41
|
+
include Registers
|
|
42
|
+
include Turn
|
|
43
|
+
include Stepping
|
|
44
|
+
include Sectors
|
|
45
|
+
include SectorWrites
|
|
46
|
+
include Tracks
|
|
47
|
+
include State
|
|
48
|
+
|
|
49
|
+
BYTE = 64
|
|
50
|
+
START = 32
|
|
51
|
+
STEP_RATES = [12_000, 24_000, 4_000, 6_000].freeze
|
|
52
|
+
SETTLE = 30_000
|
|
53
|
+
|
|
54
|
+
NEVER = 1 << 62
|
|
55
|
+
|
|
56
|
+
# Status bits. Bits 1 and 2 report the index pulse and track 0 after
|
|
57
|
+
# a type I command, and DRQ and lost data after the others; bit 5
|
|
58
|
+
# reports spin-up after a type I command and a deleted data mark
|
|
59
|
+
# after a read.
|
|
60
|
+
BUSY = 0x01
|
|
61
|
+
INDEX = 0x02
|
|
62
|
+
DATA_REQUEST = 0x02
|
|
63
|
+
TRACK0 = 0x04
|
|
64
|
+
LOST_DATA = 0x04
|
|
65
|
+
CRC_ERROR = 0x08
|
|
66
|
+
NOT_FOUND = 0x10
|
|
67
|
+
SPUN_UP = 0x20
|
|
68
|
+
DELETED = 0x20
|
|
69
|
+
PROTECTED = 0x40
|
|
70
|
+
MOTOR_ON = 0x80
|
|
71
|
+
|
|
72
|
+
# Command flags.
|
|
73
|
+
NO_SPIN_UP = 0x08
|
|
74
|
+
VERIFY = 0x04
|
|
75
|
+
SETTLE_DELAY = 0x04
|
|
76
|
+
MULTIPLE = 0x10
|
|
77
|
+
UPDATE = 0x10
|
|
78
|
+
DELETED_MARK = 0x01
|
|
79
|
+
|
|
80
|
+
# Phases.
|
|
81
|
+
IDLE = 0
|
|
82
|
+
SPIN_UP = 1
|
|
83
|
+
STEPPED = 2
|
|
84
|
+
SETTLED = 3
|
|
85
|
+
SEARCH = 4
|
|
86
|
+
READ_DATA = 5
|
|
87
|
+
WRITE_GATE = 6
|
|
88
|
+
WRITE_DATA = 7
|
|
89
|
+
READ_TRACK = 8
|
|
90
|
+
TRACK_INDEX = 9
|
|
91
|
+
WRITE_TRACK = 10
|
|
92
|
+
MOTOR_IDLE = 11
|
|
93
|
+
INDEX_INTERRUPT = 12
|
|
94
|
+
SETTLE_SEARCH = 13
|
|
95
|
+
READ_ADDRESS = 14
|
|
96
|
+
WRITE_TRACK_START = 15
|
|
97
|
+
STARTING = 16
|
|
98
|
+
|
|
99
|
+
# What a search for an ID field is for.
|
|
100
|
+
READ = 0
|
|
101
|
+
WRITE = 1
|
|
102
|
+
ADDRESS = 2
|
|
103
|
+
VERIFY_TRACK = 3
|
|
104
|
+
|
|
105
|
+
attr_reader :now, :track, :sector, :data, :command
|
|
106
|
+
|
|
107
|
+
def initialize(mechanism)
|
|
108
|
+
@mechanism = mechanism
|
|
109
|
+
@now = 0
|
|
110
|
+
@due = NEVER
|
|
111
|
+
@phase = IDLE
|
|
112
|
+
reset!
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The MR line: the registers as the data sheet leaves them, $03 in
|
|
116
|
+
# the command register and 1 in the sector register, and no command
|
|
117
|
+
# running.
|
|
118
|
+
def reset!
|
|
119
|
+
@command = 0x03
|
|
120
|
+
@track = 0
|
|
121
|
+
@sector = 1
|
|
122
|
+
@data = 0
|
|
123
|
+
@status = 0
|
|
124
|
+
@type1 = true
|
|
125
|
+
@drq = false
|
|
126
|
+
@intrq = false
|
|
127
|
+
@motor_on = false
|
|
128
|
+
@inward = true
|
|
129
|
+
clear_command
|
|
130
|
+
idle
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def cycle!
|
|
134
|
+
@now += 1
|
|
135
|
+
advance if @now == @due
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# How many cycles can go by before the next phase is due.
|
|
139
|
+
def quiet_cycles = @due - @now - 1
|
|
140
|
+
|
|
141
|
+
# Runs +cycles+ cycles at once, as many as quiet_cycles allows.
|
|
142
|
+
def fast_forward(cycles)
|
|
143
|
+
@now += cycles
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def busy? = @phase != IDLE && @phase != MOTOR_IDLE && @phase != INDEX_INTERRUPT
|
|
147
|
+
|
|
148
|
+
def drq? = @drq
|
|
149
|
+
|
|
150
|
+
def intrq? = @intrq
|
|
151
|
+
|
|
152
|
+
def motor_on? = @motor_on
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
# A command ends: INTRQ, and the motor turns off after nine more
|
|
157
|
+
# index pulses unless a command comes first.
|
|
158
|
+
def finish
|
|
159
|
+
@intrq = true
|
|
160
|
+
@phase = MOTOR_IDLE
|
|
161
|
+
@count = 9
|
|
162
|
+
count_index
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# What a command keeps between its phases.
|
|
166
|
+
def clear_command
|
|
167
|
+
@steps = 0
|
|
168
|
+
@count = 0
|
|
169
|
+
@after_spin_up = IDLE
|
|
170
|
+
@mode = 0
|
|
171
|
+
@deadline = NEVER
|
|
172
|
+
@id_at = NEVER
|
|
173
|
+
@found = -1
|
|
174
|
+
@bytes = []
|
|
175
|
+
@syncs = []
|
|
176
|
+
@index = 0
|
|
177
|
+
@crc = 0xffff
|
|
178
|
+
@crc_good = true
|
|
179
|
+
@write_id = 0
|
|
180
|
+
@size = 0
|
|
181
|
+
@gate_open = false
|
|
182
|
+
@late = false
|
|
183
|
+
@settling = false
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def idle
|
|
187
|
+
@phase = IDLE
|
|
188
|
+
@due = NEVER
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def advance
|
|
192
|
+
case @phase
|
|
193
|
+
when STARTING then start_command
|
|
194
|
+
when SPIN_UP, MOTOR_IDLE, INDEX_INTERRUPT then index_counted
|
|
195
|
+
when STEPPED then step_done
|
|
196
|
+
when SETTLED then verify_track
|
|
197
|
+
when SETTLE_SEARCH then begin_search
|
|
198
|
+
when SEARCH then id_passed
|
|
199
|
+
when READ_DATA, READ_ADDRESS then byte_read
|
|
200
|
+
else advance_write
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def advance_write
|
|
205
|
+
case @phase
|
|
206
|
+
when WRITE_GATE then write_gate
|
|
207
|
+
when WRITE_DATA then byte_written
|
|
208
|
+
when READ_TRACK then track_byte_read
|
|
209
|
+
when TRACK_INDEX then track_index
|
|
210
|
+
when WRITE_TRACK_START then write_track_start
|
|
211
|
+
when WRITE_TRACK then track_byte_written
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/drive"
|
|
4
|
+
require "badline/drive1581/track"
|
|
5
|
+
require "badline/drive1581/track_bytes"
|
|
6
|
+
require "badline/drive1581/disk"
|
|
7
|
+
require "badline/drive1581/mechanism"
|
|
8
|
+
require "badline/drive1581/wd1772"
|
|
9
|
+
require "badline/drive1581/cpu"
|
|
10
|
+
require "badline/drive1581/serial_port"
|
|
11
|
+
require "badline/drive1581/bus"
|
|
12
|
+
|
|
13
|
+
module Badline
|
|
14
|
+
# The 1581 3.5" disk drive: a 6502 at 2 MHz with 8 KB of RAM, an 8520
|
|
15
|
+
# for the serial bus and the mechanism's lines, a WD1772 for the MFM
|
|
16
|
+
# disk, and the DOS in 32 KB of ROM (1581 Service Manual). Drive::Core
|
|
17
|
+
# clocks it against the host.
|
|
18
|
+
#
|
|
19
|
+
# The 8520's port B faces the serial bus as VIA 1's does on the 1541,
|
|
20
|
+
# with the same bits, and ATN reaches its FLAG pin, so ATN going low
|
|
21
|
+
# interrupts the DOS. Its SP and CNT carry fast serial on DATA and SRQ,
|
|
22
|
+
# through buffers PB5 turns round (Drive::FastSerial), as the 1571's
|
|
23
|
+
# 6526 does. The ATN acknowledge differs: a NAND of ATN IN and
|
|
24
|
+
# ATN ACK pulls DATA, so DATA follows ATN only while ATN is asserted
|
|
25
|
+
# and PB4 is high, where the 1541's XOR pulls it whenever the two
|
|
26
|
+
# differ (IECBus::DRIVE_ATN_GATED). Port A drives the motor, the side
|
|
27
|
+
# and the two LEDs, and reads the switches and the mechanism's /RDY and
|
|
28
|
+
# /DISK CHNG (SerialPort). Timer B interrupts every 10 ms for the
|
|
29
|
+
# controller code that runs the WD1772.
|
|
30
|
+
#
|
|
31
|
+
# The 8520 differs from the 6526 in its TOD, a 24-bit binary event
|
|
32
|
+
# counter where the 6526 keeps a BCD clock. The 1581 holds the TOD pin
|
|
33
|
+
# high when it carries a WD1772 (jumper J1 out), so the counter never
|
|
34
|
+
# counts, and the DOS's one use of it, writing $00 to the low byte and
|
|
35
|
+
# reading it back 20 µs later to tell a WD1770 from a WD1772, reads $00
|
|
36
|
+
# on either chip: the drive's CIA gets no TOD pulses (CIA's tod_hz).
|
|
37
|
+
class Drive1581
|
|
38
|
+
include Drive::Core
|
|
39
|
+
include Drive::FastSerial
|
|
40
|
+
|
|
41
|
+
CLOCK_HZ = 2_000_000
|
|
42
|
+
|
|
43
|
+
# Where the DOS's idle loop starts over (see Drive::Idle).
|
|
44
|
+
IDLE_LOOP = 0xb105
|
|
45
|
+
|
|
46
|
+
# The host cycles, at about 1 MHz, by which a 1581 switched on with the
|
|
47
|
+
# machine has run its DOS's reset, the RAM test, the ROM checksum and
|
|
48
|
+
# the controller's half-second wait, and waits on the bus: 2.32 s,
|
|
49
|
+
# with a margin.
|
|
50
|
+
BOOT_CYCLES = 2_700_000
|
|
51
|
+
|
|
52
|
+
# The 8520's port A outputs.
|
|
53
|
+
SIDE = 0x01
|
|
54
|
+
MOTOR = 0x04
|
|
55
|
+
POWER_LED = 0x20
|
|
56
|
+
ACTIVITY_LED = 0x40
|
|
57
|
+
|
|
58
|
+
# Port B's ATN acknowledge, and the fast serial direction.
|
|
59
|
+
ATN_ACK = 0x10
|
|
60
|
+
FAST_SERIAL_OUT = 0x20
|
|
61
|
+
|
|
62
|
+
def cia
|
|
63
|
+
settle!
|
|
64
|
+
@cia
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def fdc
|
|
68
|
+
settle!
|
|
69
|
+
@fdc
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def model_name = "1581"
|
|
73
|
+
|
|
74
|
+
# +rom+ covers $8000-$FFFF, and defaults to the DOS image in the ROM
|
|
75
|
+
# path. +device+ is the number the switches on port A's PA3 and PA4
|
|
76
|
+
# set, 8 to 11.
|
|
77
|
+
def initialize(rom: nil, host_clock_hz: Drive1541::CLOCK_HZ, device: 8, debug: false)
|
|
78
|
+
@device = device
|
|
79
|
+
@clock_hz = CLOCK_HZ
|
|
80
|
+
@mechanism = Mechanism.new
|
|
81
|
+
@fdc = WD1772.new(@mechanism)
|
|
82
|
+
@serial_port = SerialPort.new(@mechanism, device:)
|
|
83
|
+
@cia = CIA.new(start: 0x4000, peripheral: @serial_port, tod_hz: 0)
|
|
84
|
+
@bus = Bus.new(rom: rom || ROM.load("dos1581.rom", 0x8000), cia: @cia, fdc: @fdc, drive: self)
|
|
85
|
+
@cpu = CPU.new(@bus, debug:)
|
|
86
|
+
@idle_loop = IDLE_LOOP
|
|
87
|
+
@host_clock_hz = host_clock_hz
|
|
88
|
+
@phase = 0
|
|
89
|
+
@cycles = 0
|
|
90
|
+
@serial_bus = nil
|
|
91
|
+
@atn = false
|
|
92
|
+
@activity = false
|
|
93
|
+
@power = false
|
|
94
|
+
init_fast_serial
|
|
95
|
+
init_idle(debug)
|
|
96
|
+
ports_written
|
|
97
|
+
connect(IECBus.new)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The serial bus's RESET line reaches the CPU, the 8520 and the
|
|
101
|
+
# WD1772. RAM keeps its contents.
|
|
102
|
+
def reset!
|
|
103
|
+
settle!
|
|
104
|
+
@cia.reset!
|
|
105
|
+
@fdc.reset!
|
|
106
|
+
ports_written
|
|
107
|
+
@cpu.reset!
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def insert(disk)
|
|
111
|
+
settle!
|
|
112
|
+
@mechanism.insert(disk, @fdc.now)
|
|
113
|
+
@fdc.spin_changed
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Puts in a Disk from the .d81 image at +path+, write-protected with
|
|
117
|
+
# `read_only`.
|
|
118
|
+
def insert_image(path, read_only: false)
|
|
119
|
+
insert(Disk.open(path, read_only:))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Whether the activity LED is lit. The power LED stays lit, and
|
|
123
|
+
# flashes on an error.
|
|
124
|
+
def led_on? = @activity
|
|
125
|
+
|
|
126
|
+
def power_led_on? = @power
|
|
127
|
+
|
|
128
|
+
# The 8520's port B as it drives the serial bus, in VIA 1's bits, the
|
|
129
|
+
# ATN acknowledge turned round for the NAND that gates it, with the
|
|
130
|
+
# fast serial pins above it. It holds still while the drive sleeps, so
|
|
131
|
+
# reading it leaves the drive asleep.
|
|
132
|
+
def serial_output
|
|
133
|
+
lines = @cia.port_b_lines
|
|
134
|
+
output = (lines & (IECBus::DRIVE_DATA_OUT | IECBus::DRIVE_CLK_OUT)) | IECBus::DRIVE_ATN_GATED | @fast_output
|
|
135
|
+
lines.anybits?(ATN_ACK) ? output : output | IECBus::DRIVE_ATNA
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The 8520's ports drive the motor, the side, the LEDs and the fast
|
|
139
|
+
# serial buffers: the drive follows them after every write to a port
|
|
140
|
+
# or direction register.
|
|
141
|
+
def ports_written
|
|
142
|
+
fast_serial_out = @cia.port_b_lines.anybits?(FAST_SERIAL_OUT)
|
|
143
|
+
turn_fast_serial(fast_serial_out) if fast_serial_out != @fast_serial_out
|
|
144
|
+
lines = @cia.port_a_lines
|
|
145
|
+
@mechanism.side = lines.anybits?(SIDE) ? 1 : 0
|
|
146
|
+
@activity = lines.anybits?(ACTIVITY_LED)
|
|
147
|
+
@power = lines.anybits?(POWER_LED)
|
|
148
|
+
motor = lines.nobits?(MOTOR)
|
|
149
|
+
return if motor == @mechanism.motor_on?
|
|
150
|
+
|
|
151
|
+
@mechanism.motor(motor, @fdc.now)
|
|
152
|
+
@fdc.spin_changed
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The drive's whole state, as the 1541's (Drive1541#save_state), with
|
|
156
|
+
# the 8520 and the WD1772 in place of the VIAs.
|
|
157
|
+
def save_state(out)
|
|
158
|
+
settle!
|
|
159
|
+
out.marker("DRIVE1581")
|
|
160
|
+
out.int(@phase).int(@cycles).boolean(@atn)
|
|
161
|
+
@serial_port.save_state(out)
|
|
162
|
+
@cia.save_state(out)
|
|
163
|
+
@fdc.save_state(out)
|
|
164
|
+
@bus.save_state(out)
|
|
165
|
+
@cpu.save_state(out)
|
|
166
|
+
@mechanism.save_state(out)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def load_state(input)
|
|
170
|
+
settle!
|
|
171
|
+
input.marker("DRIVE1581")
|
|
172
|
+
@phase = input.int
|
|
173
|
+
@cycles = input.int
|
|
174
|
+
@atn = input.boolean?
|
|
175
|
+
@serial_port.load_state(input)
|
|
176
|
+
@cia.load_state(input)
|
|
177
|
+
@fdc.load_state(input)
|
|
178
|
+
@bus.load_state(input)
|
|
179
|
+
@cpu.load_state(input)
|
|
180
|
+
@mechanism.load_state(input)
|
|
181
|
+
@fast_serial_out = @cia.port_b_lines.anybits?(FAST_SERIAL_OUT)
|
|
182
|
+
ports_written
|
|
183
|
+
push_fast_output(@fast_serial_out ? fast_pins : 0)
|
|
184
|
+
forget_orbits
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
# One drive cycle: ATN going low pulls FLAG, the WD1772 and the 8520
|
|
190
|
+
# clock ahead of the CPU, and the 8520 alone pulls IRQ.
|
|
191
|
+
def step
|
|
192
|
+
atn = @serial_port.atn_low?
|
|
193
|
+
if atn != @atn
|
|
194
|
+
@atn = atn
|
|
195
|
+
@cia.flag! if atn
|
|
196
|
+
end
|
|
197
|
+
@fdc.cycle!
|
|
198
|
+
@cia.cycle!
|
|
199
|
+
drive_fast_serial if @fast_serial_out
|
|
200
|
+
@cpu.irq = @cia.interrupted?
|
|
201
|
+
@cpu.cycle!
|
|
202
|
+
@cycles += 1
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def hear_atn
|
|
206
|
+
@atn = @serial_port.atn_low?
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# A pass of the idle loop reads and writes RAM alone, so it repeats
|
|
210
|
+
# while the 8520 pulls no IRQ, for as long as the 8520's counters and
|
|
211
|
+
# the WD1772's next phase leave room: until timer B's next interrupt
|
|
212
|
+
# for the controller code. Neither chip changes but by the clock, so
|
|
213
|
+
# only the CPU is compared.
|
|
214
|
+
def busy_for_pass? = @cia.interrupted?
|
|
215
|
+
|
|
216
|
+
def quiet_cycles = [@cia.quiet_cycles, @fdc.quiet_cycles].min
|
|
217
|
+
|
|
218
|
+
def idle_state = [*@cpu.idle_state, @bus.data, @atn]
|
|
219
|
+
|
|
220
|
+
def fast_forward_chips(cycles)
|
|
221
|
+
@cia.fast_forward(cycles)
|
|
222
|
+
@fdc.fast_forward(cycles)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Every timer B interrupt reads the 8520, so no orbit (Drive::Orbit)
|
|
226
|
+
# comes round, and the drive keeps no anchors.
|
|
227
|
+
def anchor = nil
|
|
228
|
+
|
|
229
|
+
def counter_state(_touched) = []
|
|
230
|
+
|
|
231
|
+
def skip_chip_orbits(_cycles, _touched) = nil
|
|
232
|
+
end
|
|
233
|
+
end
|
data/lib/badline/frontend/app.rb
CHANGED
|
@@ -10,8 +10,6 @@ module Badline
|
|
|
10
10
|
# On the C128 it follows the machine between the VIC-IIe's 40 columns
|
|
11
11
|
# and the VDC's 80, and F8 switches them until the machine next does.
|
|
12
12
|
class App
|
|
13
|
-
DROPFILE = 0x1000
|
|
14
|
-
|
|
15
13
|
# Takes the frame limit, the pacing, the snapshot path, the sound and
|
|
16
14
|
# the verbosity from Options, and runs the timeline's events.
|
|
17
15
|
def initialize(computer, options, timeline)
|
|
@@ -109,7 +107,7 @@ module Badline
|
|
|
109
107
|
@controls.mouse_button(SDL.event_button(SDL.event), type == SDL::MOUSEBUTTONDOWN)
|
|
110
108
|
when SDL::CONTROLLERDEVICEADDED, SDL::CONTROLLERDEVICEREMOVED
|
|
111
109
|
@gamepads.rescan
|
|
112
|
-
when DROPFILE then resume(false) unless @menu.drop(@computer, @controls, @sound)
|
|
110
|
+
when SDL::DROPFILE then resume(false) unless @menu.drop(@computer, @controls, @sound)
|
|
113
111
|
end
|
|
114
112
|
end
|
|
115
113
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# An SDL audio device on SDL's queue: signed 16-bit samples, `channels`
|
|
6
|
+
# of them interleaved per frame, go in at the device's rate and SDL
|
|
7
|
+
# plays them out behind us. When #open fails the device stays closed,
|
|
8
|
+
# with the reason in SDL_GetError for the caller to report.
|
|
9
|
+
class AudioDevice
|
|
10
|
+
attr_reader :rate
|
|
11
|
+
|
|
12
|
+
def initialize(channels:, rate:, samples:)
|
|
13
|
+
@channels = channels
|
|
14
|
+
@rate = rate
|
|
15
|
+
@samples = samples
|
|
16
|
+
@device = 0
|
|
17
|
+
@buffer = IO::Buffer.new(4096)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def open? = @device != 0
|
|
21
|
+
|
|
22
|
+
# With `exact_rate` false SDL may pick the device's own rate instead,
|
|
23
|
+
# which #rate then reports.
|
|
24
|
+
def open(exact_rate)
|
|
25
|
+
return unless SDL.SDL_InitSubSystem(SDL::INIT_AUDIO).zero?
|
|
26
|
+
|
|
27
|
+
SDL.spec_freq(SDL.wanted, @rate)
|
|
28
|
+
SDL.spec_format(SDL.wanted, SDL::AUDIO_S16LSB)
|
|
29
|
+
SDL.spec_channels(SDL.wanted, @channels)
|
|
30
|
+
SDL.spec_samples(SDL.wanted, @samples)
|
|
31
|
+
allowed_changes = exact_rate ? 0 : SDL::ALLOW_FREQUENCY_CHANGE
|
|
32
|
+
@device = SDL.SDL_OpenAudioDevice(nil, 0, SDL.wanted, SDL.obtained, allowed_changes)
|
|
33
|
+
@rate = SDL.read_i32(SDL.obtained) if open?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns SDL_QueueAudio's result, negative on failure.
|
|
37
|
+
def queue(samples)
|
|
38
|
+
count = samples.size
|
|
39
|
+
bytes = count * 2
|
|
40
|
+
@buffer.resize(bytes) if bytes > @buffer.size
|
|
41
|
+
buffer = @buffer
|
|
42
|
+
i = 0
|
|
43
|
+
while i < count
|
|
44
|
+
buffer.set_value(:s16, i * 2, samples[i])
|
|
45
|
+
i += 1
|
|
46
|
+
end
|
|
47
|
+
SDL.SDL_QueueAudio(@device, buffer, bytes)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (2.0 * @channels * @rate)
|
|
51
|
+
|
|
52
|
+
def start = SDL.SDL_PauseAudioDevice(@device, 0)
|
|
53
|
+
|
|
54
|
+
def pause = SDL.SDL_PauseAudioDevice(@device, 1)
|
|
55
|
+
|
|
56
|
+
def clear = SDL.SDL_ClearQueuedAudio(@device)
|
|
57
|
+
|
|
58
|
+
def close
|
|
59
|
+
return unless open?
|
|
60
|
+
|
|
61
|
+
SDL.SDL_CloseAudioDevice(@device)
|
|
62
|
+
SDL.SDL_QuitSubSystem(SDL::INIT_AUDIO)
|
|
63
|
+
@device = 0
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -2,72 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# The host's audio device for --headless and the SID player
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# us. It answers what Audio::Playback asks of a sink.
|
|
5
|
+
# The host's audio device for --headless and the SID player: a stereo
|
|
6
|
+
# AudioDevice, the left and right samples interleaved, that raises on
|
|
7
|
+
# SDL's failures. It answers what Audio::Playback asks of a sink.
|
|
9
8
|
class AudioSink
|
|
10
9
|
class Error < Audio::Playback::DeviceError; end
|
|
11
10
|
|
|
12
|
-
attr_reader :rate
|
|
13
|
-
|
|
14
11
|
# With `exact_rate` false SDL may pick the device's own rate instead,
|
|
15
12
|
# which #rate then reports.
|
|
16
13
|
def initialize(rate:, exact_rate: false, buffer: 1024)
|
|
17
|
-
@device =
|
|
18
|
-
@
|
|
19
|
-
|
|
20
|
-
check(SDL.SDL_InitSubSystem(SDL::INIT_AUDIO))
|
|
21
|
-
open_device(rate, exact_rate ? 0 : SDL::ALLOW_FREQUENCY_CHANGE, buffer)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def queue(samples)
|
|
25
|
-
count = samples.size
|
|
26
|
-
bytes = count * 2
|
|
27
|
-
@buffer.resize(bytes) if bytes > @buffer.size
|
|
28
|
-
buffer = @buffer
|
|
29
|
-
i = 0
|
|
30
|
-
while i < count
|
|
31
|
-
buffer.set_value(:s16, i * 2, samples[i])
|
|
32
|
-
i += 1
|
|
33
|
-
end
|
|
34
|
-
check(SDL.SDL_QueueAudio(@device, buffer, bytes))
|
|
14
|
+
@device = AudioDevice.new(channels: 2, rate:, samples: buffer)
|
|
15
|
+
@device.open(exact_rate)
|
|
16
|
+
raise Error, SDL.SDL_GetError unless @device.open?
|
|
35
17
|
end
|
|
36
18
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
def start = SDL.SDL_PauseAudioDevice(@device, 0)
|
|
40
|
-
|
|
41
|
-
def pause = SDL.SDL_PauseAudioDevice(@device, 1)
|
|
42
|
-
|
|
43
|
-
def clear = SDL.SDL_ClearQueuedAudio(@device)
|
|
19
|
+
def rate = @device.rate
|
|
44
20
|
|
|
45
|
-
def
|
|
46
|
-
|
|
21
|
+
def queue(samples)
|
|
22
|
+
result = @device.queue(samples)
|
|
23
|
+
raise Error, SDL.SDL_GetError if result.negative?
|
|
47
24
|
|
|
48
|
-
|
|
49
|
-
SDL.SDL_QuitSubSystem(SDL::INIT_AUDIO)
|
|
50
|
-
@device = 0
|
|
25
|
+
result
|
|
51
26
|
end
|
|
52
27
|
|
|
53
|
-
|
|
28
|
+
def queued_seconds = @device.queued_seconds
|
|
54
29
|
|
|
55
|
-
def
|
|
56
|
-
SDL.spec_freq(SDL.wanted, rate)
|
|
57
|
-
SDL.spec_format(SDL.wanted, SDL::AUDIO_S16LSB)
|
|
58
|
-
SDL.spec_channels(SDL.wanted, 2)
|
|
59
|
-
SDL.spec_samples(SDL.wanted, buffer)
|
|
60
|
-
@device = SDL.SDL_OpenAudioDevice(nil, 0, SDL.wanted, SDL.obtained, allowed_changes)
|
|
61
|
-
raise Error, SDL.SDL_GetError if @device.zero?
|
|
30
|
+
def start = @device.start
|
|
62
31
|
|
|
63
|
-
|
|
64
|
-
end
|
|
32
|
+
def pause = @device.pause
|
|
65
33
|
|
|
66
|
-
def
|
|
67
|
-
raise Error, SDL.SDL_GetError if result.negative?
|
|
34
|
+
def clear = @device.clear
|
|
68
35
|
|
|
69
|
-
|
|
70
|
-
end
|
|
36
|
+
def close = @device.close
|
|
71
37
|
end
|
|
72
38
|
end
|
|
73
39
|
end
|
|
@@ -16,13 +16,10 @@ module Badline
|
|
|
16
16
|
return 1
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Cartridge::UnsupportedTypeError, Snapshot::FormatError, Media::DiskList::Error,
|
|
24
|
-
Datasette::Missing, Unsupported => e
|
|
25
|
-
warn "#{options.program}: #{options.media_path}: #{e.message}"
|
|
19
|
+
computer = nil
|
|
20
|
+
problem = media_problem { computer = boot(options) }
|
|
21
|
+
unless problem.empty?
|
|
22
|
+
warn "#{options.program}: #{options.media_path}: #{problem}"
|
|
26
23
|
return 1
|
|
27
24
|
end
|
|
28
25
|
timeline = Timeline.new(options)
|
|
@@ -30,6 +27,19 @@ module Badline
|
|
|
30
27
|
timeline.failed? ? 1 : 0
|
|
31
28
|
end
|
|
32
29
|
|
|
30
|
+
# Runs the block and returns the message of the error it raised over
|
|
31
|
+
# media that won't go in or start, or an empty string. Other errors,
|
|
32
|
+
# a bare ArgumentError among them, go through.
|
|
33
|
+
def self.media_problem
|
|
34
|
+
yield
|
|
35
|
+
""
|
|
36
|
+
rescue Unsupported, Media::TrueDrive::Error, Media::DiskList::Error, Media::NotDisk, Datasette::Missing,
|
|
37
|
+
SystemCallError, Storage::SIDFile::FormatError, Storage::T64::FormatError, Storage::TAP::FormatError,
|
|
38
|
+
Storage::CRTFile::FormatError, Storage::G64Image::FormatError, Cartridge::UnsupportedTypeError,
|
|
39
|
+
Snapshot::FormatError => e
|
|
40
|
+
e.message
|
|
41
|
+
end
|
|
42
|
+
|
|
33
43
|
# The machine a .vsf snapshot holds, built as the saved one was, or a
|
|
34
44
|
# new one built from the options with the media attached.
|
|
35
45
|
def self.boot(options)
|
|
@@ -88,9 +98,7 @@ module Badline
|
|
|
88
98
|
return true if extension == ".sid"
|
|
89
99
|
return false unless %w[.prg .p00].include?(extension)
|
|
90
100
|
|
|
91
|
-
|
|
92
|
-
bytes = Storage::P00.data(bytes) if Storage::P00.wraps?(bytes)
|
|
93
|
-
bytes.length >= 2 && [Media::BASIC_START, Media::BASIC_START - 1].include?(bytes[0] | (bytes[1] << 8))
|
|
101
|
+
[Media::BASIC_START, Media::BASIC_START - 1].include?(Storage::P00.load_address(media))
|
|
94
102
|
end
|
|
95
103
|
|
|
96
104
|
# A VIC-20 with the RAM --ram names, or else the RAM `media` needs
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# The SID player
|
|
6
|
-
# and remembers its place, so a click finds the
|
|
7
|
-
# one under the pointer lights up, and one that's
|
|
8
|
-
# They take
|
|
9
|
-
# under the pointer, the background and, if any, a fill
|
|
10
|
-
# button.
|
|
5
|
+
# The buttons of the SID player and the pause menu: each is drawn where
|
|
6
|
+
# it goes on every frame, and remembers its place, so a click finds the
|
|
7
|
+
# action under it. The one under the pointer lights up, and one that's
|
|
8
|
+
# on is drawn reversed. They take their colours from the caller: the
|
|
9
|
+
# text, the text under the pointer, the background and, if any, a fill
|
|
10
|
+
# behind each button.
|
|
11
11
|
class Buttons
|
|
12
12
|
PAD = 2
|
|
13
13
|
HEIGHT = Painter::GLYPH + (PAD * 2)
|
|
14
14
|
|
|
15
|
-
def initialize(painter, colors
|
|
15
|
+
def initialize(painter, colors)
|
|
16
16
|
@painter = painter
|
|
17
17
|
@text = colors[0]
|
|
18
18
|
@bright = colors[1]
|