badline 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/CLAUDE.md +10 -5
- data/README.md +54 -23
- data/doc/pinned-behaviour.md +221 -0
- data/exe/badline-ruby +7 -4
- data/lib/badline/address_bus/ultimax_pages.rb +38 -0
- data/lib/badline/address_bus.rb +19 -29
- data/lib/badline/audio/jukebox.rb +6 -4
- data/lib/badline/audio/machine_player.rb +10 -6
- data/lib/badline/cartridge/kcs_power.rb +3 -3
- data/lib/badline/cia.rb +5 -1
- data/lib/badline/computer.rb +86 -7
- data/lib/badline/cpu.rb +39 -0
- data/lib/badline/drive1541/bus.rb +134 -0
- data/lib/badline/drive1541/disk.rb +256 -0
- data/lib/badline/drive1541/disk_via.rb +33 -0
- data/lib/badline/drive1541/gcr.rb +57 -0
- data/lib/badline/drive1541/idle.rb +184 -0
- data/lib/badline/drive1541/mechanism.rb +277 -0
- data/lib/badline/drive1541/rotation.rb +111 -0
- data/lib/badline/drive1541/sector_reader.rb +76 -0
- data/lib/badline/drive1541/serial_port.rb +67 -0
- data/lib/badline/drive1541/track.rb +98 -0
- data/lib/badline/drive1541.rb +218 -0
- data/lib/badline/gui/application.rb +15 -3
- data/lib/badline/gui/drive_led_pane.rb +29 -0
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/iec_bus.rb +93 -0
- data/lib/badline/kernal_trap/channel.rb +9 -1
- data/lib/badline/kernal_trap/serial.rb +9 -3
- data/lib/badline/media/true_drive.rb +54 -0
- data/lib/badline/media.rb +22 -1
- data/lib/badline/options.rb +24 -4
- data/lib/badline/region.rb +35 -10
- data/lib/badline/reu/dma.rb +228 -0
- data/lib/badline/reu/ram.rb +65 -0
- data/lib/badline/reu/trigger.rb +32 -0
- data/lib/badline/reu.rb +276 -0
- data/lib/badline/roms/README +10 -0
- data/lib/badline/roms/dos1541.rom +0 -0
- data/lib/badline/sdl/functions.rb +1 -0
- data/lib/badline/storage/d64_image.rb +18 -9
- data/lib/badline/storage/d81_image.rb +1 -0
- data/lib/badline/storage/disk_image/catalog.rb +52 -0
- data/lib/badline/storage/disk_image/writing.rb +16 -0
- data/lib/badline/storage/disk_image.rb +2 -0
- data/lib/badline/storage/g64_image.rb +222 -0
- data/lib/badline/storage/host_directory.rb +21 -1
- data/lib/badline/storage/listing.rb +118 -0
- data/lib/badline/storage/t64.rb +14 -0
- data/lib/badline/storage.rb +2 -0
- data/lib/badline/traps.rb +4 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +3 -0
- data/lib/badline/via/fast_forward.rb +38 -0
- data/lib/badline/via/shift_register.rb +8 -3
- data/lib/badline/via/timer1.rb +30 -0
- data/lib/badline/via/timer2.rb +25 -0
- data/lib/badline/via.rb +2 -0
- data/lib/badline/vic/display_state.rb +7 -7
- data/lib/badline/vic/sprite/internal_bus.rb +11 -7
- data/lib/badline/vic/sprite/shifter.rb +10 -15
- data/lib/badline/vic/sprite/timing.rb +60 -0
- data/lib/badline/vic/sprite.rb +14 -12
- data/lib/badline/vic/sprites.rb +2 -2
- data/lib/badline/vic.rb +86 -39
- data/lib/badline.rb +3 -0
- metadata +26 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/drive1541/bus"
|
|
4
|
+
require "badline/drive1541/serial_port"
|
|
5
|
+
require "badline/drive1541/gcr"
|
|
6
|
+
require "badline/drive1541/track"
|
|
7
|
+
require "badline/drive1541/disk"
|
|
8
|
+
require "badline/drive1541/mechanism"
|
|
9
|
+
require "badline/drive1541/disk_via"
|
|
10
|
+
require "badline/drive1541/idle"
|
|
11
|
+
|
|
12
|
+
module Badline
|
|
13
|
+
# The 1541 disk drive as a machine of its own: a 6502 running the DOS
|
|
14
|
+
# ROM at 1 MHz, with 2 KB of RAM and two VIAs on its bus. VIA 1 faces
|
|
15
|
+
# the serial bus and VIA 2 the disk mechanism.
|
|
16
|
+
#
|
|
17
|
+
# The drive runs on its own crystal, so it clocks against the host's
|
|
18
|
+
# clock through a fractional accumulator: each host cycle adds 1 MHz
|
|
19
|
+
# worth of phase, and every whole host period in it runs a drive cycle.
|
|
20
|
+
# Against the PAL C64's 985,248 Hz that's one drive cycle per host cycle
|
|
21
|
+
# and a second one about every 67. The host sets its clock on attaching
|
|
22
|
+
# the drive, and until then the drive runs one cycle per host cycle.
|
|
23
|
+
#
|
|
24
|
+
# VIA 2's port B runs the Mechanism, which reads a Disk put in with
|
|
25
|
+
# insert.
|
|
26
|
+
#
|
|
27
|
+
# While the DOS idles, host_cycle! skips the drive's cycles and catches
|
|
28
|
+
# up on them later, exactly (see Idle). The readers of the drive's parts
|
|
29
|
+
# catch up first, so they find the drive as running every cycle would
|
|
30
|
+
# have left it.
|
|
31
|
+
class Drive1541
|
|
32
|
+
include Idle
|
|
33
|
+
|
|
34
|
+
CLOCK_HZ = 1_000_000
|
|
35
|
+
|
|
36
|
+
attr_reader :device, :serial_bus
|
|
37
|
+
|
|
38
|
+
def cpu
|
|
39
|
+
settle!
|
|
40
|
+
@cpu
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def bus
|
|
44
|
+
settle!
|
|
45
|
+
@bus
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def via1
|
|
49
|
+
settle!
|
|
50
|
+
@via1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def via2
|
|
54
|
+
settle!
|
|
55
|
+
@via2
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def mechanism
|
|
59
|
+
settle!
|
|
60
|
+
@mechanism
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def cycles
|
|
64
|
+
settle!
|
|
65
|
+
@cycles
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ram = bus.ram
|
|
69
|
+
|
|
70
|
+
def host_clock_hz=(clock_hz)
|
|
71
|
+
settle!
|
|
72
|
+
@host_clock_hz = clock_hz
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# +rom+ covers $C000-$FFFF, and defaults to the DOS image in the ROM
|
|
76
|
+
# path. +device+ is the number the jumpers on VIA 1's PB5 and PB6 set,
|
|
77
|
+
# 8 to 11.
|
|
78
|
+
def initialize(rom: nil, host_clock_hz: CLOCK_HZ, device: 8, debug: false)
|
|
79
|
+
@device = device
|
|
80
|
+
@serial_port = SerialPort.new(device:)
|
|
81
|
+
@via1 = VIA.new(start: 0x1800, peripheral: @serial_port)
|
|
82
|
+
@mechanism = Mechanism.new(self)
|
|
83
|
+
@via2 = DiskVIA.new(start: 0x1c00, mechanism: @mechanism)
|
|
84
|
+
@bus = Bus.new(rom: rom || ROM.load("dos1541.rom", 0xc000), via1: @via1, via2: @via2)
|
|
85
|
+
@cpu = CPU.new(@bus, debug:)
|
|
86
|
+
@host_clock_hz = host_clock_hz
|
|
87
|
+
@phase = 0
|
|
88
|
+
@cycles = 0
|
|
89
|
+
@so_pending = false
|
|
90
|
+
@serial_bus = nil
|
|
91
|
+
init_idle(debug)
|
|
92
|
+
# CA1 powers up at the level of a released ATN, without an edge.
|
|
93
|
+
@via1.ca1 = false
|
|
94
|
+
@via1.reset!
|
|
95
|
+
connect(IECBus.new)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Plugs the drive into a serial bus, leaving the one it was on. A drive
|
|
99
|
+
# starts out on a bus of its own, with nothing else on it.
|
|
100
|
+
def connect(serial_bus)
|
|
101
|
+
settle!
|
|
102
|
+
@serial_bus&.detach(self)
|
|
103
|
+
@serial_bus = serial_bus
|
|
104
|
+
@serial_port.bus = serial_bus
|
|
105
|
+
serial_bus.attach(self)
|
|
106
|
+
@via1.ca1 = @serial_port.atn_low?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Puts a Disk in the drive (Disk.from_d64 makes one from an image).
|
|
110
|
+
# Nil takes the disk out.
|
|
111
|
+
def insert(disk)
|
|
112
|
+
settle!
|
|
113
|
+
@mechanism.insert(disk)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def disk = mechanism.disk
|
|
117
|
+
|
|
118
|
+
# Whether the LED is lit. Reading it leaves the drive asleep: the LED
|
|
119
|
+
# is the same at the end of every pass the drive sleeps through.
|
|
120
|
+
def led_on? = @mechanism.led_on?
|
|
121
|
+
|
|
122
|
+
# VIA 1's port B as it drives the serial bus. It holds still while the
|
|
123
|
+
# drive sleeps, so reading it leaves the drive asleep.
|
|
124
|
+
def serial_output = @via1.port_b_output
|
|
125
|
+
|
|
126
|
+
# Stores what the head wrote since the motor last stopped in the
|
|
127
|
+
# disk's image, as the motor stopping does.
|
|
128
|
+
def flush
|
|
129
|
+
settle!
|
|
130
|
+
@mechanism.flush
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The serial bus's RESET line reaches the CPU and both VIAs. RAM keeps
|
|
134
|
+
# its contents.
|
|
135
|
+
def reset!
|
|
136
|
+
settle!
|
|
137
|
+
@via1.reset!
|
|
138
|
+
@via2.reset!
|
|
139
|
+
@cpu.reset!
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Runs the drive cycles that fall in one host cycle: none, one or two.
|
|
143
|
+
# The host has run this cycle already, and the drive sees what it did
|
|
144
|
+
# from the next one on (see SerialPort). Asleep, the drive owes the
|
|
145
|
+
# cycles instead, until ATN moves or its counters are due (see Idle).
|
|
146
|
+
def host_cycle!
|
|
147
|
+
return doze if @asleep
|
|
148
|
+
|
|
149
|
+
phase = @phase + CLOCK_HZ
|
|
150
|
+
while phase >= @host_clock_hz
|
|
151
|
+
@asleep ? @owed += 1 : run_cycle
|
|
152
|
+
phase -= @host_clock_hz
|
|
153
|
+
end
|
|
154
|
+
@phase = phase
|
|
155
|
+
settle! if @serial_port.atn_moved?
|
|
156
|
+
@serial_port.latch_host
|
|
157
|
+
plan_wake if @asleep
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Runs one drive cycle, catching up first on any the drive owes (see
|
|
161
|
+
# step).
|
|
162
|
+
def cycle!
|
|
163
|
+
settle!
|
|
164
|
+
step
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def inspect
|
|
168
|
+
"#<#{self.class.name} cycles=#{cycles} cpu=(#{cpu.inspect})>"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# The read electronics signal a whole GCR byte. BYTE READY pulls VIA 2's
|
|
172
|
+
# CA1 low, a falling edge that sets its flag and, with latching on,
|
|
173
|
+
# latches port A. It reaches the CPU's SO pin while VIA 2's CA2 (SOE)
|
|
174
|
+
# is high, which lets the DOS spin on BVC for each byte. The CPU
|
|
175
|
+
# samples SO on the next cycle (see step).
|
|
176
|
+
def byte_ready!
|
|
177
|
+
@via2.ca1 = false
|
|
178
|
+
@so_pending = true if @via2.ca2_output
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# BYTE READY lets go of CA1 with the next bit.
|
|
182
|
+
def byte_ready_ended!
|
|
183
|
+
@via2.ca1 = true
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
# A drive cycle from host_cycle!, which may find the idle loop.
|
|
189
|
+
def run_cycle
|
|
190
|
+
step
|
|
191
|
+
idle_loop_reached if @cpu.program_counter == IDLE_LOOP && @idle_skip
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# One drive cycle. The VIAs clock ahead of the CPU, as the C64's chips
|
|
195
|
+
# do, and either one pulls IRQ. Nothing drives NMI on the 1541. The CPU
|
|
196
|
+
# runs whether or not the motor turns, since it answers ATN.
|
|
197
|
+
#
|
|
198
|
+
# The disk mechanism runs first, so BYTE READY lands on the VIA ahead
|
|
199
|
+
# of the CPU's cycle. The CPU samples SO a cycle late: BYTE READY sets
|
|
200
|
+
# V for the next cycle's instruction step.
|
|
201
|
+
#
|
|
202
|
+
# ATN reaches VIA 1's CA1 through the same inverter as PB7, so CA1 goes
|
|
203
|
+
# high as the C64 asserts ATN, as the serial port sees it: from the
|
|
204
|
+
# host cycle after the one that asserts it.
|
|
205
|
+
def step
|
|
206
|
+
so = @so_pending
|
|
207
|
+
@so_pending = false
|
|
208
|
+
@mechanism.cycle!
|
|
209
|
+
@via1.ca1 = @serial_port.atn_low?
|
|
210
|
+
@via1.cycle!
|
|
211
|
+
@via2.cycle!
|
|
212
|
+
@cpu.irq = @via1.irq? || @via2.irq?
|
|
213
|
+
@cpu.so! if so
|
|
214
|
+
@cpu.cycle!
|
|
215
|
+
@cycles += 1
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -28,15 +28,20 @@ module Badline
|
|
|
28
28
|
}.freeze
|
|
29
29
|
MOUSE_BUTTONS = { 1 => :left, 3 => :right }.freeze
|
|
30
30
|
|
|
31
|
-
# The
|
|
32
|
-
|
|
31
|
+
# The machine options (sid_model:, reu:, true_drive:, ntsc:) build
|
|
32
|
+
# the machine: reu plugs in an REU of that many K, true_drive puts a
|
|
33
|
+
# true 1541 on device 8, and ntsc makes it an NTSC C64. The media
|
|
34
|
+
# options (autostart:, song:, disk:) go to Media.attach.
|
|
35
|
+
def initialize(media_path: nil, machine: {}, sound: false, verbose: false, **media)
|
|
33
36
|
@verbose = verbose
|
|
34
|
-
@computer = Computer.new(
|
|
37
|
+
@computer = Computer.new(**machine_options(machine, media_path))
|
|
38
|
+
Media::TrueDrive.plug(@computer) if machine[:true_drive]
|
|
35
39
|
puts Media.attach(@computer, media_path, **media) if media_path
|
|
36
40
|
|
|
37
41
|
@mode = :keyboard
|
|
38
42
|
@pot_device = nil
|
|
39
43
|
@panes = [ScreenPane.new(@computer)]
|
|
44
|
+
@panes << DriveLedPane.new(@computer.drive1541, @panes.first) if @computer.drive1541
|
|
40
45
|
@stream = open_stream if sound
|
|
41
46
|
@paced = ENV["NOVSYNC"].nil?
|
|
42
47
|
@window = Window.new(
|
|
@@ -60,6 +65,7 @@ module Badline
|
|
|
60
65
|
@stream.pace(@frame_seconds) if @stream && @paced
|
|
61
66
|
end
|
|
62
67
|
ensure
|
|
68
|
+
@computer.drive1541&.flush
|
|
63
69
|
@stream&.close
|
|
64
70
|
@gamepads.close
|
|
65
71
|
@window.close
|
|
@@ -163,6 +169,12 @@ module Badline
|
|
|
163
169
|
warn "badline: no sound, can't open the audio device: #{e.message}"
|
|
164
170
|
end
|
|
165
171
|
|
|
172
|
+
def machine_options(machine, media_path)
|
|
173
|
+
options = { sid_model: machine[:sid_model] || Media.sid_model(media_path), **machine.slice(:reu) }
|
|
174
|
+
options[:region] = Region::NTSC if machine[:ntsc]
|
|
175
|
+
options
|
|
176
|
+
end
|
|
177
|
+
|
|
166
178
|
def fit_frame
|
|
167
179
|
rate = @window.refresh_rate
|
|
168
180
|
clock_hz = @computer.region.clock_hz
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module GUI
|
|
5
|
+
# The true drive's red LED, in the bottom right corner of the screen
|
|
6
|
+
# pane's border: bright while the DOS lights it, as it does while a file
|
|
7
|
+
# is open or after an error, and dim otherwise.
|
|
8
|
+
class DriveLedPane < Pane
|
|
9
|
+
WIDTH = 12
|
|
10
|
+
HEIGHT = 4
|
|
11
|
+
|
|
12
|
+
LIT = [0xff, 0x20, 0x20].freeze
|
|
13
|
+
DARK = [0x40, 0x00, 0x00].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(drive, screen)
|
|
16
|
+
super(width: WIDTH, height: HEIGHT,
|
|
17
|
+
left: screen.left + screen.width - WIDTH - 8, top: screen.top + screen.height - HEIGHT - 6)
|
|
18
|
+
@drive = drive
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def color = @drive.led_on? ? LIT : DARK
|
|
22
|
+
|
|
23
|
+
def render(renderer)
|
|
24
|
+
SDL::SetRenderDrawColor.call(renderer, *color, 0xff)
|
|
25
|
+
SDL::RenderFillRect.call(renderer, @rect)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/badline/gui.rb
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
# The serial bus between the C64 and its drives: ATN, CLK and DATA, each
|
|
5
|
+
# an open-collector line with a pull-up. A line is low while any device
|
|
6
|
+
# pulls it (wired-AND), and high otherwise. The bus holds no state of its
|
|
7
|
+
# own: every read works the levels out from what each side drives now.
|
|
8
|
+
#
|
|
9
|
+
# The C64 drives the lines from CIA 2's port A through 7406 inverters, so
|
|
10
|
+
# an output bit of 1 pulls its line low: PA3 ATN, PA4 CLK and PA5 DATA.
|
|
11
|
+
# PA6 and PA7 read CLK and DATA directly, 1 for a released line. The bus
|
|
12
|
+
# stands in as CIA 2's peripheral for those two inputs.
|
|
13
|
+
#
|
|
14
|
+
# A drive drives CLK and DATA from VIA 1's PB3 and PB1, the same way. It
|
|
15
|
+
# also pulls DATA while ATN IN differs from its ATN acknowledge on PB4
|
|
16
|
+
# (an XOR gate), which answers ATN in hardware before the DOS gets to it.
|
|
17
|
+
# SerialPort reads the lines back into VIA 1.
|
|
18
|
+
class IECBus
|
|
19
|
+
# CIA 2 port A
|
|
20
|
+
HOST_ATN_OUT = 0x08
|
|
21
|
+
HOST_CLK_OUT = 0x10
|
|
22
|
+
HOST_DATA_OUT = 0x20
|
|
23
|
+
HOST_CLK_IN = 0x40
|
|
24
|
+
HOST_DATA_IN = 0x80
|
|
25
|
+
|
|
26
|
+
# VIA 1 port B
|
|
27
|
+
DRIVE_DATA_OUT = 0x02
|
|
28
|
+
DRIVE_CLK_OUT = 0x08
|
|
29
|
+
DRIVE_ATNA = 0x10
|
|
30
|
+
|
|
31
|
+
# The lines pulled low, as ATN, CLK and DATA bits
|
|
32
|
+
ATN = 0x01
|
|
33
|
+
CLK = 0x02
|
|
34
|
+
DATA = 0x04
|
|
35
|
+
|
|
36
|
+
attr_reader :host, :drives
|
|
37
|
+
|
|
38
|
+
# +host+ is CIA 2, or nil for a bus with only drives on it.
|
|
39
|
+
def initialize(host: nil)
|
|
40
|
+
@host = host
|
|
41
|
+
@drives = []
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def attach(drive)
|
|
45
|
+
@drives << drive unless @drives.include?(drive)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def detach(drive)
|
|
49
|
+
@drives.delete(drive)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# One pass over everything on the bus, for a reader that wants more
|
|
53
|
+
# than one line. +host+ is the C64's port A lines as the reader sees
|
|
54
|
+
# them, which a drive takes a cycle late (see Drive1541::SerialPort).
|
|
55
|
+
def low_lines(host = host_lines)
|
|
56
|
+
atn = host.anybits?(HOST_ATN_OUT)
|
|
57
|
+
low = atn ? ATN : 0
|
|
58
|
+
low |= CLK if host.anybits?(HOST_CLK_OUT)
|
|
59
|
+
low |= DATA if host.anybits?(HOST_DATA_OUT)
|
|
60
|
+
i = 0
|
|
61
|
+
while i < @drives.length
|
|
62
|
+
drive = @drives[i].serial_output
|
|
63
|
+
low |= CLK if drive.anybits?(DRIVE_CLK_OUT)
|
|
64
|
+
low |= DATA if drive.anybits?(DRIVE_DATA_OUT) || atn != drive.anybits?(DRIVE_ATNA)
|
|
65
|
+
i += 1
|
|
66
|
+
end
|
|
67
|
+
low
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def atn_low?(host = host_lines) = host.anybits?(HOST_ATN_OUT)
|
|
71
|
+
|
|
72
|
+
def clk_low? = low_lines.anybits?(CLK)
|
|
73
|
+
|
|
74
|
+
def data_low? = low_lines.anybits?(DATA)
|
|
75
|
+
|
|
76
|
+
# CIA 2's port A pins as the bus leaves them: CLK IN and DATA IN low
|
|
77
|
+
# while their lines are.
|
|
78
|
+
def read_a(_port_a, _port_b)
|
|
79
|
+
low = low_lines
|
|
80
|
+
value = 0xff
|
|
81
|
+
value &= ~HOST_CLK_IN if low.anybits?(CLK)
|
|
82
|
+
value &= ~HOST_DATA_IN if low.anybits?(DATA)
|
|
83
|
+
value
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Port B goes to the user port, not the serial bus.
|
|
87
|
+
def read_b(_port_a, _port_b) = 0xff
|
|
88
|
+
|
|
89
|
+
# What the C64 pulls, as port A bits: its output bits, with input bits
|
|
90
|
+
# floating high as the 7406 inputs see them.
|
|
91
|
+
def host_lines = @host ? @host.port_a_lines : 0
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -53,8 +53,12 @@ module Badline
|
|
|
53
53
|
# a LOAD (secondary address 0) opens at $7E and $026F of its memory,
|
|
54
54
|
# and a LOAD of a name starting with "*" reopens that file rather than
|
|
55
55
|
# the first one on the disk. Loaders write the block there with M-W
|
|
56
|
-
# to load a file the directory doesn't list.
|
|
56
|
+
# to load a file the directory doesn't list. A LOAD of "$" reads the
|
|
57
|
+
# directory as a BASIC program, which a pattern after a colon narrows
|
|
58
|
+
# to the files it names.
|
|
57
59
|
def self.for_name(storage, memory, secondary, name, type)
|
|
60
|
+
return new(storage.directory.bytes(name)) if listing?(storage, secondary, name)
|
|
61
|
+
|
|
58
62
|
file = Storage.parse_name(name).first
|
|
59
63
|
return for_file(storage, file, type) unless secondary.zero? && storage.respond_to?(:first_block)
|
|
60
64
|
|
|
@@ -66,6 +70,10 @@ module Badline
|
|
|
66
70
|
for_file(storage, file, type)
|
|
67
71
|
end
|
|
68
72
|
|
|
73
|
+
def self.listing?(storage, secondary, name)
|
|
74
|
+
secondary.zero? && name.start_with?("$") && storage.respond_to?(:directory)
|
|
75
|
+
end
|
|
76
|
+
|
|
69
77
|
# The file whose chain starts at the block.
|
|
70
78
|
def self.at_block(storage, track, sector)
|
|
71
79
|
data = storage.read_file_at(track, sector)
|
|
@@ -6,7 +6,12 @@ module Badline
|
|
|
6
6
|
# device 8 go to a virtual drive instead of out over the IEC lines, so
|
|
7
7
|
# OPEN/CHKIN/CHRIN reach a CBM DOS, and so do the block reads that
|
|
8
8
|
# loaders make through them. Other devices fall through to the ROM.
|
|
9
|
+
#
|
|
10
|
+
# With +device+ nil the traps answer no device, and every frame goes
|
|
11
|
+
# out over the bus, to a true drive if one is plugged in.
|
|
9
12
|
class Serial < Routine
|
|
13
|
+
attr_accessor :device
|
|
14
|
+
|
|
10
15
|
ROUTINES = {
|
|
11
16
|
0xed09 => :talk,
|
|
12
17
|
0xed0c => :listen,
|
|
@@ -29,9 +34,10 @@ module Badline
|
|
|
29
34
|
|
|
30
35
|
NO_DATA = [0x0d, EOI | READ_TIMEOUT].freeze
|
|
31
36
|
|
|
32
|
-
def initialize(cpu:, bus:, drive:)
|
|
37
|
+
def initialize(cpu:, bus:, drive:, device: DEVICE)
|
|
33
38
|
super(cpu:, bus:)
|
|
34
39
|
@drive = drive
|
|
40
|
+
@device = device
|
|
35
41
|
@listening = false
|
|
36
42
|
@talking = false
|
|
37
43
|
@listen_channel = nil
|
|
@@ -66,13 +72,13 @@ module Badline
|
|
|
66
72
|
# in the secondary address that follows.
|
|
67
73
|
def talk
|
|
68
74
|
@talk_channel = nil
|
|
69
|
-
@talking = @cpu.a ==
|
|
75
|
+
@talking = @cpu.a == @device
|
|
70
76
|
return_to_caller if @talking
|
|
71
77
|
end
|
|
72
78
|
|
|
73
79
|
def listen
|
|
74
80
|
@listen_channel = nil
|
|
75
|
-
@listening = @cpu.a ==
|
|
81
|
+
@listening = @cpu.a == @device
|
|
76
82
|
return_to_caller if @listening
|
|
77
83
|
end
|
|
78
84
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
# Device 8 as a true 1541 instead of the KERNAL traps. Once a Drive1541
|
|
6
|
+
# is plugged in as device 8, disk media go into it rather than into the
|
|
7
|
+
# traps, so a LOAD runs the DOS and every byte crosses the serial bus.
|
|
8
|
+
# A .g64 holds the disk's raw GCR, which only a true drive reads, so it
|
|
9
|
+
# plugs one in when there's none yet. The 1541 reads only .d64 and .g64
|
|
10
|
+
# images, so the traps' other disk media, a .d71, .d81 or .t64 or a host
|
|
11
|
+
# directory, raise Error.
|
|
12
|
+
module TrueDrive
|
|
13
|
+
class Error < ArgumentError; end
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Plugs a Drive1541 in as device 8, unless one is there already, and
|
|
17
|
+
# returns it.
|
|
18
|
+
def plug(computer)
|
|
19
|
+
drive(computer) || Drive1541.new.tap { |drive| computer.attach_drive1541(drive) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The Drive1541 on device 8, or nil.
|
|
23
|
+
def drive(computer)
|
|
24
|
+
drive = computer.drive1541
|
|
25
|
+
drive if drive && drive.device == KernalTrap::Routine::DEVICE
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Whether the path goes in a true drive: a .g64 always, and other
|
|
29
|
+
# disk media, which the traps would mount, when a true drive is
|
|
30
|
+
# device 8.
|
|
31
|
+
def takes?(computer, path)
|
|
32
|
+
return true if File.extname(path).casecmp?(".g64")
|
|
33
|
+
|
|
34
|
+
!drive(computer).nil? && (File.directory?(path) || MOUNT_TYPES.key?(File.extname(path).downcase))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Puts the .d64 or .g64 in the drive on device 8, plugging one in
|
|
38
|
+
# when there's none, and takes out what was mounted through the
|
|
39
|
+
# traps, so LOAD and SAVE reach the drive too. `read_only` puts it
|
|
40
|
+
# in write-protected.
|
|
41
|
+
def insert(computer, path, read_only: false)
|
|
42
|
+
unless %w[.d64 .g64].include?(File.extname(path).downcase)
|
|
43
|
+
raise Error, "#{path} is not a .d64 or .g64 image, which the 1541 reads"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
disk = Drive1541::Disk.open(path, read_only:)
|
|
47
|
+
computer.unmount
|
|
48
|
+
plug(computer).insert(disk)
|
|
49
|
+
"Inserted #{path} in the 1541 as device 8"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/badline/media.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/media/true_drive"
|
|
4
|
+
|
|
3
5
|
module Badline
|
|
4
6
|
module Media
|
|
5
7
|
AUTOSTART = %(lO"*",8,1\rrun\r)
|
|
@@ -19,8 +21,14 @@ module Badline
|
|
|
19
21
|
# the Retro Replay's flash mode, and `disk: { read_only: true }` mounts
|
|
20
22
|
# a disk image write-protected, so nothing the program does writes to
|
|
21
23
|
# its file. Media they don't apply to ignore them.
|
|
24
|
+
#
|
|
25
|
+
# A .g64 plugs in a true drive as device 8 (TrueDrive), and with one
|
|
26
|
+
# there, a .d64 goes into it instead of the KERNAL traps too. The
|
|
27
|
+
# autostart then loads through it.
|
|
22
28
|
def attach(computer, path, autostart: true, song: nil, **options)
|
|
23
|
-
if
|
|
29
|
+
if TrueDrive.takes?(computer, path)
|
|
30
|
+
attach_true_drive(computer, path, options.fetch(:disk, {}), autostart:)
|
|
31
|
+
elsif File.directory?(path)
|
|
24
32
|
computer.mount(Storage::HostDirectory.new(path))
|
|
25
33
|
"Mounted #{path} as device 8"
|
|
26
34
|
elsif File.extname(path).downcase == ".crt"
|
|
@@ -40,7 +48,14 @@ module Badline
|
|
|
40
48
|
# while the machine runs, without loading anything. The drive keeps
|
|
41
49
|
# its RAM and its status. `read_only` inserts a disk image
|
|
42
50
|
# write-protected.
|
|
51
|
+
#
|
|
52
|
+
# A .g64 goes in a true 1541, which is plugged in as device 8 when
|
|
53
|
+
# there's none yet (TrueDrive). Once a true 1541 is device 8, a .d64
|
|
54
|
+
# goes in its drive as well, and other disks, which a 1541 can't
|
|
55
|
+
# read, raise TrueDrive::Error, an ArgumentError.
|
|
43
56
|
def insert_disk(computer, path, read_only: false)
|
|
57
|
+
return TrueDrive.insert(computer, path, read_only:) if TrueDrive.takes?(computer, path)
|
|
58
|
+
|
|
44
59
|
raise ArgumentError, "#{path} is not a disk image or a directory" unless disk?(path)
|
|
45
60
|
|
|
46
61
|
computer.mount(open_storage(path, { read_only: }))
|
|
@@ -98,6 +113,12 @@ module Badline
|
|
|
98
113
|
"Inserted #{path} in the datasette"
|
|
99
114
|
end
|
|
100
115
|
|
|
116
|
+
def attach_true_drive(computer, path, disk, autostart:)
|
|
117
|
+
message = TrueDrive.insert(computer, path, read_only: disk.fetch(:read_only, false))
|
|
118
|
+
computer.type_text(AUTOSTART) if autostart
|
|
119
|
+
message
|
|
120
|
+
end
|
|
121
|
+
|
|
101
122
|
def attach_storage(computer, path, disk, autostart:)
|
|
102
123
|
computer.mount(open_storage(path, disk))
|
|
103
124
|
computer.type_text(AUTOSTART) if autostart
|
data/lib/badline/options.rb
CHANGED
|
@@ -21,9 +21,13 @@ module Badline
|
|
|
21
21
|
badline-ruby --headless [options] tune.sid
|
|
22
22
|
badline-ruby [options] tune.sid --audio-out FILE
|
|
23
23
|
|
|
24
|
-
Media can be a .prg/.p00 program, a .d64/.d71/.d81 disk image,
|
|
25
|
-
|
|
26
|
-
a
|
|
24
|
+
Media can be a .prg/.p00 program, a .d64/.d71/.d81 disk image, a
|
|
25
|
+
.g64 disk image for the true 1541, a .t64 tape archive, a .tap
|
|
26
|
+
tape, a .crt cartridge, a .sid tune, or a directory to mount as
|
|
27
|
+
device 8. It opens in the emulator window. Device 8 answers through
|
|
28
|
+
traps on the KERNAL's disk routines, unless --true-drive puts an
|
|
29
|
+
emulated 1541 there, which runs its own DOS and reads .d64 and .g64
|
|
30
|
+
images only.
|
|
27
31
|
|
|
28
32
|
--headless plays a .sid tune on the host's audio device without the
|
|
29
33
|
window, and --audio-out renders it to 16-bit PCM instead. The
|
|
@@ -43,7 +47,7 @@ module Badline
|
|
|
43
47
|
|
|
44
48
|
BANNER
|
|
45
49
|
|
|
46
|
-
attr_reader :media_path, :audio_out, :song, :seconds, :songlengths, :sid_model, :filter_chunk
|
|
50
|
+
attr_reader :media_path, :audio_out, :song, :seconds, :songlengths, :sid_model, :filter_chunk, :reu
|
|
47
51
|
|
|
48
52
|
alias tune_path media_path
|
|
49
53
|
|
|
@@ -54,6 +58,8 @@ module Badline
|
|
|
54
58
|
@read_only = false
|
|
55
59
|
@sound = false
|
|
56
60
|
@verbose = false
|
|
61
|
+
@true_drive = false
|
|
62
|
+
@ntsc = false
|
|
57
63
|
@headless = false
|
|
58
64
|
@jit = true
|
|
59
65
|
@quiet = false
|
|
@@ -99,6 +105,10 @@ module Badline
|
|
|
99
105
|
|
|
100
106
|
def verbose? = @verbose
|
|
101
107
|
|
|
108
|
+
def true_drive? = @true_drive
|
|
109
|
+
|
|
110
|
+
def ntsc? = @ntsc
|
|
111
|
+
|
|
102
112
|
def quiet? = @quiet
|
|
103
113
|
|
|
104
114
|
def tui? = @tui
|
|
@@ -168,6 +178,16 @@ module Badline
|
|
|
168
178
|
opts.on("--sound", "Play the SID through the host's audio device (F10 mutes)") do
|
|
169
179
|
window_only("--sound") { @sound = true }
|
|
170
180
|
end
|
|
181
|
+
opts.on("--true-drive", "Put a true 1541 on device 8 instead of the KERNAL traps") do
|
|
182
|
+
window_only("--true-drive") { @true_drive = true }
|
|
183
|
+
end
|
|
184
|
+
opts.on("--reu SIZE", %w[128 256 512 1024 2048 4096 8192 16384],
|
|
185
|
+
"Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384") do |size|
|
|
186
|
+
window_only("--reu") { @reu = size.to_i }
|
|
187
|
+
end
|
|
188
|
+
opts.on("--ntsc", "Run an NTSC C64, with the 6567R8 VIC-II, instead of a PAL one") do
|
|
189
|
+
window_only("--ntsc") { @ntsc = true }
|
|
190
|
+
end
|
|
171
191
|
opts.on("--verbose", "Print the display, sound and gamepad setup and the frame timing") do
|
|
172
192
|
window_only("--verbose") { @verbose = true }
|
|
173
193
|
end
|