badline 0.8.0 → 0.10.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 +58 -0
- data/CLAUDE.md +6 -4
- data/README.md +60 -19
- data/doc/library.md +10 -3
- data/doc/media.md +11 -2
- data/doc/pinned-behaviour.md +199 -9
- data/doc/sid-player.md +8 -4
- data/doc/snapshots.md +79 -33
- data/lib/badline/address_bus/pla.rb +130 -0
- data/lib/badline/address_bus/roms.rb +32 -0
- data/lib/badline/address_bus.rb +13 -92
- data/lib/badline/audio/bare_player.rb +24 -0
- data/lib/badline/audio/checkpoints.rb +42 -0
- data/lib/badline/audio/jukebox.rb +11 -4
- data/lib/badline/audio/machine_player.rb +16 -0
- data/lib/badline/audio/renderer.rb +80 -9
- data/lib/badline/audio/stereo.rb +3 -5
- data/lib/badline/audio.rb +1 -0
- data/lib/badline/chrout_trap.rb +4 -3
- data/lib/badline/computer/attachments.rb +52 -12
- data/lib/badline/computer/saved_state.rb +1 -0
- data/lib/badline/computer.rb +50 -33
- data/lib/badline/control_ports.rb +10 -3
- data/lib/badline/core.rb +51 -0
- data/lib/badline/data_path.rb +37 -0
- data/lib/badline/datasette.rb +63 -10
- data/lib/badline/drive1541/disk.rb +3 -3
- data/lib/badline/drive1541/disk_state.rb +14 -5
- data/lib/badline/drive1541/serial_port.rb +5 -8
- data/lib/badline/drive1541/sleep.rb +5 -6
- data/lib/badline/drive1541.rb +3 -3
- data/lib/badline/frontend/app.rb +63 -64
- data/lib/badline/frontend/boot.rb +34 -4
- data/lib/badline/frontend/confirmation.rb +17 -11
- data/lib/badline/frontend/controls.rb +12 -6
- data/lib/badline/frontend/drive_led.rb +9 -10
- data/lib/badline/frontend/frame_rate.rb +4 -4
- data/lib/badline/frontend/machine_window.rb +87 -0
- data/lib/badline/frontend/menu_dialogs.rb +52 -14
- data/lib/badline/frontend/menu_media.rb +70 -16
- data/lib/badline/frontend/menu_pages.rb +47 -42
- data/lib/badline/frontend/menu_ports.rb +69 -0
- data/lib/badline/frontend/name_field.rb +88 -0
- data/lib/badline/frontend/pacer.rb +6 -5
- data/lib/badline/frontend/pause_menu.rb +52 -37
- data/lib/badline/frontend/player_screen.rb +1 -1
- data/lib/badline/frontend/player_window.rb +4 -4
- data/lib/badline/frontend/screen.rb +39 -37
- data/lib/badline/frontend/sid_view.rb +1 -1
- data/lib/badline/frontend/snapshots.rb +145 -17
- data/lib/badline/frontend/sound.rb +17 -9
- data/lib/badline/frontend/timeline.rb +36 -5
- data/lib/badline/frontend.rb +3 -0
- data/lib/badline/iec_bus.rb +16 -28
- data/lib/badline/kernal_trap/drive.rb +3 -0
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/layout.rb +112 -0
- data/lib/badline/kernal_trap/load.rb +24 -25
- data/lib/badline/kernal_trap/routine.rb +17 -5
- data/lib/badline/kernal_trap/save.rb +13 -23
- data/lib/badline/kernal_trap/serial.rb +38 -24
- data/lib/badline/kernal_trap.rb +1 -0
- data/lib/badline/keyboard.rb +17 -14
- data/lib/badline/machine.rb +60 -0
- data/lib/badline/media/disk_list.rb +75 -0
- data/lib/badline/media/disk_set.rb +13 -3
- data/lib/badline/media/vic20_basic.rb +54 -0
- data/lib/badline/media/vic20_cartridge.rb +74 -0
- data/lib/badline/media/vic20_media.rb +132 -0
- data/lib/badline/media.rb +39 -20
- data/lib/badline/model.rb +62 -0
- data/lib/badline/options/event.rb +10 -6
- data/lib/badline/options/help.rb +25 -13
- data/lib/badline/options/table.rb +15 -1
- data/lib/badline/options/validation.rb +63 -3
- data/lib/badline/options.rb +37 -34
- data/lib/badline/region.rb +16 -4
- data/lib/badline/reu/ram.rb +27 -0
- data/lib/badline/reu/saved_state.rb +27 -0
- data/lib/badline/roms/README +37 -0
- data/lib/badline/roms/kernal-sx64.rom +0 -0
- data/lib/badline/roms/vic20/basic.rom +0 -0
- data/lib/badline/roms/vic20/character.rom +0 -0
- data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
- data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
- data/lib/badline/sid/decimator.rb +13 -0
- data/lib/badline/sid/filter.rb +6 -3
- data/lib/badline/sid.rb +19 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/fields.rb +1 -0
- data/lib/badline/snapshot/image.rb +15 -0
- data/lib/badline/snapshot/setup.rb +20 -8
- data/lib/badline/snapshot/state.rb +15 -7
- data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
- data/lib/badline/snapshot/vice/peripherals.rb +12 -2
- data/lib/badline/snapshot/vice/restore.rb +3 -1
- data/lib/badline/snapshot/vice/reu1764.rb +55 -0
- data/lib/badline/snapshot/vice/vicii.rb +59 -13
- data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
- data/lib/badline/snapshot/vice.rb +25 -23
- data/lib/badline/snapshot.rb +10 -0
- data/lib/badline/storage/crt_file.rb +16 -5
- data/lib/badline/storage/disk_image.rb +3 -0
- data/lib/badline/storage/host_directory.rb +3 -0
- data/lib/badline/storage/t64.rb +10 -4
- data/lib/badline/storage/tap.rb +44 -8
- data/lib/badline/storage/unavailable.rb +3 -1
- data/lib/badline/storage.rb +5 -5
- data/lib/badline/timing.rb +12 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/shift_register.rb +43 -16
- data/lib/badline/vic/bank.rb +53 -16
- data/lib/badline/vic/registers.rb +20 -4
- data/lib/badline/vic/sprite/timing.rb +5 -9
- data/lib/badline/vic/sprite.rb +4 -3
- data/lib/badline/vic.rb +34 -15
- data/lib/badline/vic20/attachments.rb +103 -0
- data/lib/badline/vic20/bus.rb +308 -0
- data/lib/badline/vic20/cpu.rb +10 -0
- data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
- data/lib/badline/vic20/port_wiring.rb +50 -0
- data/lib/badline/vic20/saved_state.rb +152 -0
- data/lib/badline/vic20/sound/output.rb +83 -0
- data/lib/badline/vic20/sound.rb +233 -0
- data/lib/badline/vic20/user_via_ports.rb +47 -0
- data/lib/badline/vic20/vic/painter.rb +239 -0
- data/lib/badline/vic20/vic/saved_state.rb +55 -0
- data/lib/badline/vic20/vic/text_window.rb +71 -0
- data/lib/badline/vic20/vic.rb +267 -0
- data/lib/badline/vic20.rb +220 -0
- data/lib/badline.rb +2 -38
- metadata +37 -2
- data/lib/badline/address_bus/ultimax_pages.rb +0 -38
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# The
|
|
5
|
+
# The machine's sound source, the C64's SID or the VIC-20's VIC, on
|
|
6
|
+
# SDL's audio queue, as mono signed 16-bit samples. Both take
|
|
7
|
+
# #record(rate:, clock_hz:) and #drain_samples.
|
|
6
8
|
# Once the device plays, it is the clock: #wait holds each frame until
|
|
7
9
|
# the queue is down to `AHEAD`, so the machine runs at the device's pace.
|
|
8
10
|
#
|
|
@@ -13,6 +15,8 @@ module Badline
|
|
|
13
15
|
# only happens unpaced.
|
|
14
16
|
#
|
|
15
17
|
# Each frame's samples go to SDL through an IO::Buffer of 16-bit values.
|
|
18
|
+
# The chip's cycles are converted at `clock_hz`, the machine's clock, so
|
|
19
|
+
# an NTSC or Drean machine plays at its own pitch.
|
|
16
20
|
class Sound
|
|
17
21
|
RATE = 44_100
|
|
18
22
|
AHEAD = 0.08
|
|
@@ -20,8 +24,9 @@ module Badline
|
|
|
20
24
|
|
|
21
25
|
attr_reader :rate, :underruns, :dropped, :queued, :low, :high
|
|
22
26
|
|
|
23
|
-
def initialize(
|
|
24
|
-
@
|
|
27
|
+
def initialize(source, clock_hz, wanted, verbose)
|
|
28
|
+
@source = source
|
|
29
|
+
@clock_hz = clock_hz
|
|
25
30
|
@verbose = verbose
|
|
26
31
|
@device = 0
|
|
27
32
|
@rate = RATE
|
|
@@ -37,10 +42,11 @@ module Badline
|
|
|
37
42
|
|
|
38
43
|
def on? = @device != 0
|
|
39
44
|
|
|
40
|
-
# Plays another machine's
|
|
41
|
-
def
|
|
42
|
-
@
|
|
43
|
-
|
|
45
|
+
# Plays another machine's sound from here on, at that machine's clock.
|
|
46
|
+
def switch(source, clock_hz)
|
|
47
|
+
@source = source
|
|
48
|
+
@clock_hz = clock_hz
|
|
49
|
+
record if on?
|
|
44
50
|
end
|
|
45
51
|
|
|
46
52
|
def muted? = @muted
|
|
@@ -50,7 +56,7 @@ module Badline
|
|
|
50
56
|
def feed
|
|
51
57
|
return unless on?
|
|
52
58
|
|
|
53
|
-
samples = @
|
|
59
|
+
samples = @source.drain_samples
|
|
54
60
|
return if @muted || samples.empty?
|
|
55
61
|
|
|
56
62
|
level = queued_seconds
|
|
@@ -102,10 +108,12 @@ module Badline
|
|
|
102
108
|
return puts "No sound: #{SDL.SDL_GetError}" unless on?
|
|
103
109
|
|
|
104
110
|
@rate = SDL.read_i32(SDL.obtained)
|
|
105
|
-
|
|
111
|
+
record
|
|
106
112
|
puts "Sound at #{@rate} Hz" if @verbose
|
|
107
113
|
end
|
|
108
114
|
|
|
115
|
+
def record = @source.record(rate: @rate, clock_hz: @clock_hz)
|
|
116
|
+
|
|
109
117
|
def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (2.0 * @rate)
|
|
110
118
|
|
|
111
119
|
def play(samples, level)
|
|
@@ -21,13 +21,20 @@ module Badline
|
|
|
21
21
|
@failed = false
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# What's wrong with the events, the first key
|
|
25
|
-
# or an empty string.
|
|
24
|
+
# What's wrong with the events, the first key or menu page that
|
|
25
|
+
# names nothing, or an empty string.
|
|
26
26
|
def self.error(events)
|
|
27
27
|
event = events.find { |candidate| candidate.action == "key" && !key?(candidate.argument) }
|
|
28
|
-
|
|
28
|
+
return "no such key: #{event.argument}" unless event.nil?
|
|
29
|
+
|
|
30
|
+
event = events.find { |candidate| candidate.action == "menu" && !page?(candidate.argument) }
|
|
31
|
+
event.nil? ? "" : "no such menu page: #{event.argument}"
|
|
29
32
|
end
|
|
30
33
|
|
|
34
|
+
# Whether the pause menu has a page of the name, such as drive or
|
|
35
|
+
# sound. An empty one is its first.
|
|
36
|
+
def self.page?(name) = name.empty? || PauseMenu::PAGES.any? { |page| page.to_s == name }
|
|
37
|
+
|
|
31
38
|
def self.key?(name) = name == "restore" || !keyboard_key(name).nil? || !joystick_direction(name).nil?
|
|
32
39
|
|
|
33
40
|
# The Keyboard's key for a name such as space, return or a.
|
|
@@ -67,6 +74,22 @@ module Badline
|
|
|
67
74
|
# events do.
|
|
68
75
|
def press_freeze(computer, frame) = freeze(computer, Options::Event.new(frame, "freeze", ""))
|
|
69
76
|
|
|
77
|
+
# Whether the pause menu opens at the frame, or turns to another page.
|
|
78
|
+
def menu?(frame) = @events.any? { |event| event.frame == frame && event.action == "menu" }
|
|
79
|
+
|
|
80
|
+
# The index in PauseMenu::PAGES of the page the frame's menu event
|
|
81
|
+
# names, 0 for the first.
|
|
82
|
+
def menu_section(frame)
|
|
83
|
+
page = ""
|
|
84
|
+
@events.each { |event| page = event.argument if event.frame == frame && event.action == "menu" }
|
|
85
|
+
section = 0
|
|
86
|
+
PauseMenu::PAGES.each_with_index { |name, index| section = index if name.to_s == page }
|
|
87
|
+
section
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Whether the pause menu closes at the frame.
|
|
91
|
+
def resume?(frame) = @events.any? { |event| event.frame == frame && event.action == "resume" }
|
|
92
|
+
|
|
70
93
|
# Whether the run ends at the frame: at a quit, or once an event failed.
|
|
71
94
|
def quit?(frame) = @failed || @events.any? { |event| event.frame == frame && event.action == "quit" }
|
|
72
95
|
|
|
@@ -98,7 +121,7 @@ module Badline
|
|
|
98
121
|
name = event.argument
|
|
99
122
|
if name == "restore"
|
|
100
123
|
computer.press_restore
|
|
101
|
-
return
|
|
124
|
+
return hold(event)
|
|
102
125
|
end
|
|
103
126
|
|
|
104
127
|
direction = Timeline.joystick_direction(name)
|
|
@@ -110,7 +133,10 @@ module Badline
|
|
|
110
133
|
hold(event)
|
|
111
134
|
end
|
|
112
135
|
|
|
136
|
+
# The VIC-20 has no cartridge with a button yet.
|
|
113
137
|
def freeze(computer, event)
|
|
138
|
+
return if computer.family == :vic20
|
|
139
|
+
|
|
114
140
|
computer.press_cartridge_button
|
|
115
141
|
hold(event)
|
|
116
142
|
end
|
|
@@ -123,6 +149,8 @@ module Badline
|
|
|
123
149
|
return computer.release_cartridge_button if event.action == "freeze"
|
|
124
150
|
|
|
125
151
|
name = event.argument
|
|
152
|
+
return computer.release_restore if name == "restore"
|
|
153
|
+
|
|
126
154
|
direction = Timeline.joystick_direction(name)
|
|
127
155
|
if direction.nil?
|
|
128
156
|
computer.keyboard.release(Timeline.keyboard_key(name))
|
|
@@ -141,7 +169,7 @@ module Badline
|
|
|
141
169
|
end
|
|
142
170
|
|
|
143
171
|
def eject(computer, what)
|
|
144
|
-
return puts "No #{what} to eject" unless
|
|
172
|
+
return puts "No #{what} to eject" unless ejectable?(computer, what)
|
|
145
173
|
|
|
146
174
|
if what == "disk"
|
|
147
175
|
eject_disk(computer)
|
|
@@ -154,6 +182,9 @@ module Badline
|
|
|
154
182
|
puts "Ejected the #{what}"
|
|
155
183
|
end
|
|
156
184
|
|
|
185
|
+
# Whether there's one in to eject. A VIC-20 has no cartridge to take out yet.
|
|
186
|
+
def ejectable?(computer, what) = (computer.family == :c64 || what != "cartridge") && inserted?(computer, what)
|
|
187
|
+
|
|
157
188
|
def inserted?(computer, what)
|
|
158
189
|
return !computer.datasette.tape.nil? if what == "tape"
|
|
159
190
|
return !computer.address_bus.cartridge.nil? if what == "cartridge"
|
data/lib/badline/frontend.rb
CHANGED
|
@@ -15,6 +15,7 @@ require "badline/frontend/frame_report"
|
|
|
15
15
|
require "badline/frontend/controls"
|
|
16
16
|
require "badline/frontend/pad_port"
|
|
17
17
|
require "badline/frontend/gamepads"
|
|
18
|
+
require "badline/frontend/machine_window"
|
|
18
19
|
require "badline/frontend/app"
|
|
19
20
|
require "badline/frontend/audio_sink"
|
|
20
21
|
require "badline/frontend/painter"
|
|
@@ -34,6 +35,8 @@ require "badline/frontend/player_window"
|
|
|
34
35
|
require "badline/frontend/pause_menu"
|
|
35
36
|
require "badline/frontend/file_browser"
|
|
36
37
|
require "badline/frontend/confirmation"
|
|
38
|
+
require "badline/frontend/name_field"
|
|
37
39
|
require "badline/frontend/menu_dialogs"
|
|
38
40
|
require "badline/frontend/menu_media"
|
|
41
|
+
require "badline/frontend/menu_ports"
|
|
39
42
|
require "badline/frontend/menu_pages"
|
data/lib/badline/iec_bus.rb
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
# The serial bus between the C64 and its drives: ATN, CLK and DATA, each
|
|
5
5
|
# an open-collector line with a pull-up. A line is low while any device
|
|
6
|
-
# pulls it (wired-AND), and high otherwise.
|
|
7
|
-
#
|
|
6
|
+
# pulls it (wired-AND), and high otherwise. Every read works the levels
|
|
7
|
+
# out from what each side drives now.
|
|
8
8
|
#
|
|
9
9
|
# The C64 drives the lines from CIA 2's port A through 7406 inverters, so
|
|
10
10
|
# an output bit of 1 pulls its line low: PA3 ATN, PA4 CLK and PA5 DATA.
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# The machine pushes those bits into the bus (host_lines=), which holds
|
|
12
|
+
# them. PA6 and PA7 read CLK and DATA directly, 1 for a released line.
|
|
13
|
+
# The bus stands in as CIA 2's peripheral for those two inputs.
|
|
13
14
|
#
|
|
14
15
|
# A drive drives CLK and DATA from VIA 1's PB3 and PB1, the same way. It
|
|
15
16
|
# also pulls DATA while ATN IN differs from its ATN acknowledge on PB4
|
|
@@ -33,30 +34,21 @@ module Badline
|
|
|
33
34
|
CLK = 0x02
|
|
34
35
|
DATA = 0x04
|
|
35
36
|
|
|
36
|
-
attr_reader :
|
|
37
|
+
attr_reader :drives, :host_lines
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
def initialize(host: nil)
|
|
40
|
-
@host = host
|
|
39
|
+
def initialize
|
|
41
40
|
@drives = []
|
|
42
|
-
@
|
|
41
|
+
@host_lines = 0
|
|
43
42
|
end
|
|
44
43
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@
|
|
50
|
-
@
|
|
44
|
+
# The machine pushes the lines it pulls, as port A bits, whenever they
|
|
45
|
+
# may have moved: its output bits, with input bits floating high as the
|
|
46
|
+
# 7406 inputs see them. A bus with only drives on it keeps 0.
|
|
47
|
+
def host_lines=(lines)
|
|
48
|
+
@host_lines = lines
|
|
49
|
+
@drives.each(&:host_written!)
|
|
51
50
|
end
|
|
52
51
|
|
|
53
|
-
# Whether the host says when it may have moved a line (see plug_in!).
|
|
54
|
-
# Without, a drive asleep checks ATN every cycle.
|
|
55
|
-
def notifies? = @notifies
|
|
56
|
-
|
|
57
|
-
# CIA 2 wrote its port A, which may have moved a line.
|
|
58
|
-
def host_written = @drives.each(&:host_written!)
|
|
59
|
-
|
|
60
52
|
def attach(drive)
|
|
61
53
|
@drives << drive unless @drives.include?(drive)
|
|
62
54
|
end
|
|
@@ -68,7 +60,7 @@ module Badline
|
|
|
68
60
|
# One pass over everything on the bus, for a reader that wants more
|
|
69
61
|
# than one line. +host+ is the C64's port A lines as the reader sees
|
|
70
62
|
# them, which a drive takes a cycle late (see Drive1541::SerialPort).
|
|
71
|
-
def low_lines(host = host_lines)
|
|
63
|
+
def low_lines(host = @host_lines)
|
|
72
64
|
atn = host.anybits?(HOST_ATN_OUT)
|
|
73
65
|
low = atn ? ATN : 0
|
|
74
66
|
low |= CLK if host.anybits?(HOST_CLK_OUT)
|
|
@@ -83,7 +75,7 @@ module Badline
|
|
|
83
75
|
low
|
|
84
76
|
end
|
|
85
77
|
|
|
86
|
-
def atn_low?(host = host_lines) = host.anybits?(HOST_ATN_OUT)
|
|
78
|
+
def atn_low?(host = @host_lines) = host.anybits?(HOST_ATN_OUT)
|
|
87
79
|
|
|
88
80
|
def clk_low? = low_lines.anybits?(CLK)
|
|
89
81
|
|
|
@@ -101,9 +93,5 @@ module Badline
|
|
|
101
93
|
|
|
102
94
|
# Port B goes to the user port, not the serial bus.
|
|
103
95
|
def read_b(_port_a, _port_b) = 0xff
|
|
104
|
-
|
|
105
|
-
# What the C64 pulls, as port A bits: its output bits, with input bits
|
|
106
|
-
# floating high as the 7406 inputs see them.
|
|
107
|
-
def host_lines = @host ? @host.port_a_lines : 0
|
|
108
96
|
end
|
|
109
97
|
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
# Where a machine's KERNAL keeps the routines the traps stand in for,
|
|
6
|
+
# and the ROM code they hand over to, with the steps that depend on the
|
|
7
|
+
# machine around it: whether the KERNAL is mapped in, the timer that
|
|
8
|
+
# times each serial byte and the release of the serial lines.
|
|
9
|
+
#
|
|
10
|
+
# +load+ and +save+ are the default ILOAD and ISAVE vector targets.
|
|
11
|
+
# LOAD hands over to the SEARCHING FOR and LOADING/VERIFYING messages,
|
|
12
|
+
# the byte loop that retries a timed-out byte until RUN/STOP, the
|
|
13
|
+
# successful return (CLC, LDX $AE, LDY $AF, RTS) and the FILE NOT FOUND
|
|
14
|
+
# and MISSING FILE NAME error exits. SAVE hands over to the SAVING
|
|
15
|
+
# message, the two line releases that end UNLISTEN (clock, then data,
|
|
16
|
+
# leaving A as read from the port), the successful return (CLC, RTS)
|
|
17
|
+
# and the MISSING FILE NAME exit. The rest are the serial bus
|
|
18
|
+
# primitives' entry points.
|
|
19
|
+
#
|
|
20
|
+
# +banked+ says whether the 6510 port can bank the KERNAL out, and so
|
|
21
|
+
# whether RAM lies under every ROM and the I/O. Each
|
|
22
|
+
# serial byte starts a one-shot timer by writing the high byte of its
|
|
23
|
+
# count to +serial_timer+, and the control value TIMER_ONE_SHOT_START
|
|
24
|
+
# to +serial_timer_start+ where the timer has a control register to
|
|
25
|
+
# start it (a CIA's), or nil where the write to the high byte starts it
|
|
26
|
+
# (a VIA's timer 2). The ROM lets go of each serial line by reading a
|
|
27
|
+
# port, masking the line's bit off and writing it back: ATN at
|
|
28
|
+
# +atn_port+ with +atn_mask+, then the clock and the data line.
|
|
29
|
+
Layout = Data.define(:load, :save, :searching_message, :loading_message, :load_byte_loop, :load_done,
|
|
30
|
+
:file_not_found_exit, :missing_file_name_exit, :saving_message, :clock_release,
|
|
31
|
+
:data_release, :save_done, :talk, :listen, :second, :tksa, :ciout, :untalk,
|
|
32
|
+
:unlisten, :acptr, :banked, :serial_timer, :serial_timer_start, :atn_port, :atn_mask,
|
|
33
|
+
:clock_port, :clock_mask, :data_port, :data_mask) do
|
|
34
|
+
# Whether the CPU reads the KERNAL ROM at the trapped addresses: on a
|
|
35
|
+
# banked machine, the 6510 port's banking bits select it.
|
|
36
|
+
def kernal_mapped?(bus)
|
|
37
|
+
!banked || bus.io_port.kernal?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Stores a loaded file's bytes from +address+ on: on a banked machine
|
|
41
|
+
# into the RAM under whatever is banked in, and otherwise where the
|
|
42
|
+
# ROM's byte loop would store them, through the bus.
|
|
43
|
+
def store_file(bus, address, bytes)
|
|
44
|
+
return bus.ram.write(address, bytes) if banked
|
|
45
|
+
|
|
46
|
+
bytes.each_with_index { |byte, i| bus.poke(address + i, byte) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Starts the timer the ROM times a serial byte with, its count's high
|
|
50
|
+
# byte +timer_high+.
|
|
51
|
+
def time_serial_byte(bus, timer_high)
|
|
52
|
+
bus.poke(serial_timer, timer_high)
|
|
53
|
+
start = serial_timer_start
|
|
54
|
+
bus.poke(start, TIMER_ONE_SHOT_START) if start
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The ROM's release of the serial bus: ATN, then the clock, then the
|
|
58
|
+
# data line, each a read of its port, a mask and a write back, so the
|
|
59
|
+
# input bits written back follow the lines as each one is let go.
|
|
60
|
+
# Returns the last value written, which the ROM leaves in A.
|
|
61
|
+
def release_serial_lines(bus)
|
|
62
|
+
release_line(bus, atn_port, atn_mask)
|
|
63
|
+
release_line(bus, clock_port, clock_mask)
|
|
64
|
+
release_line(bus, data_port, data_mask)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def release_line(bus, port, mask)
|
|
70
|
+
value = bus.peek(port) & mask
|
|
71
|
+
bus.poke(port, value)
|
|
72
|
+
value
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A CIA's control value that starts its timer: force load, one-shot,
|
|
77
|
+
# start.
|
|
78
|
+
TIMER_ONE_SHOT_START = 0x19
|
|
79
|
+
|
|
80
|
+
# The C64 KERNAL, revision 3. The SX-64's, 251104-04, differs from it
|
|
81
|
+
# only in bytes none of these addresses reach. It times serial bytes
|
|
82
|
+
# with CIA 1's timer B, and drives the lines from CIA 2's port A
|
|
83
|
+
# ($EE03: ATN $EDBE, clock $EE85, data $EE97).
|
|
84
|
+
C64_LAYOUT = Layout.new(
|
|
85
|
+
load: 0xf4a5, save: 0xf5ed,
|
|
86
|
+
searching_message: 0xf5af, loading_message: 0xf5d2, load_byte_loop: 0xf4f3, load_done: 0xf5a9,
|
|
87
|
+
file_not_found_exit: 0xf704, missing_file_name_exit: 0xf710,
|
|
88
|
+
saving_message: 0xf68f, clock_release: 0xee85, data_release: 0xee97, save_done: 0xf657,
|
|
89
|
+
talk: 0xed09, listen: 0xed0c, second: 0xedb9, tksa: 0xedc7,
|
|
90
|
+
ciout: 0xeddd, untalk: 0xedef, unlisten: 0xedfe, acptr: 0xee13,
|
|
91
|
+
banked: true, serial_timer: 0xdc07, serial_timer_start: 0xdc0f,
|
|
92
|
+
atn_port: 0xdd00, atn_mask: 0xf7, clock_port: 0xdd00, clock_mask: 0xef, data_port: 0xdd00, data_mask: 0xdf
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# The VIC-20 KERNAL, 901486-07 (PAL). The NTSC one, 901486-06, differs
|
|
96
|
+
# from it only at $E475, $EDE4-$EDE5, $FE3F, $FE44 and $FF5C-$FF70,
|
|
97
|
+
# none of them here. Nothing banks it out. It times serial bytes with
|
|
98
|
+
# VIA 2's timer 2 ($9129), drives ATN from VIA 1's PA7 ($911F) and the
|
|
99
|
+
# clock and data lines from VIA 2's CA2 and CB2 ($912C), and UNLISTEN
|
|
100
|
+
# lets them go at $EF09: ATN $EEC5, clock $EF84, data $E4A0.
|
|
101
|
+
VIC20_LAYOUT = Layout.new(
|
|
102
|
+
load: 0xf549, save: 0xf685,
|
|
103
|
+
searching_message: 0xf647, loading_message: 0xf66a, load_byte_loop: 0xf58a, load_done: 0xf641,
|
|
104
|
+
file_not_found_exit: 0xf787, missing_file_name_exit: 0xf793,
|
|
105
|
+
saving_message: 0xf728, clock_release: 0xef84, data_release: 0xe4a0, save_done: 0xf6ef,
|
|
106
|
+
talk: 0xee14, listen: 0xee17, second: 0xeec0, tksa: 0xeece,
|
|
107
|
+
ciout: 0xeee4, untalk: 0xeef6, unlisten: 0xef04, acptr: 0xef19,
|
|
108
|
+
banked: false, serial_timer: 0x9129, serial_timer_start: nil,
|
|
109
|
+
atn_port: 0x911f, atn_mask: 0x7f, clock_port: 0x912c, clock_mask: 0xfd, data_port: 0x912c, data_mask: 0xdf
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -2,29 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module KernalTrap
|
|
5
|
-
# PC trap on the KERNAL serial LOAD routine (
|
|
6
|
-
#
|
|
7
|
-
# channel 0, then hands over to the
|
|
8
|
-
# SEARCHING FOR and LOADING (or VERIFYING)
|
|
9
|
-
# errors and returns with the routine's
|
|
10
|
-
# other devices fall through to the ROM.
|
|
5
|
+
# PC trap on the KERNAL serial LOAD routine (the default ILOAD vector
|
|
6
|
+
# target, $F4A5 on the C64, $F549 on the VIC-20). Reads device 8
|
|
7
|
+
# requests from the virtual drive's channel 0, then hands over to the
|
|
8
|
+
# ROM's own tail so it prints SEARCHING FOR and LOADING (or VERIFYING)
|
|
9
|
+
# in direct mode, reports errors and returns with the routine's
|
|
10
|
+
# register/zeropage contract; other devices fall through to the ROM.
|
|
11
|
+
# So does a load that reaches
|
|
11
12
|
# below $0334, into the zero page, the stack or the KERNAL vectors: the
|
|
12
13
|
# ROM's byte loop then loads it through the serial traps, and a loader
|
|
13
14
|
# that overwrites ISTOP takes over mid-load as it does on a real drive.
|
|
14
15
|
class Load < File
|
|
15
|
-
ADDRESS = 0xf4a5
|
|
16
|
-
|
|
17
|
-
# ROM entry points: the SEARCHING FOR and LOADING/VERIFYING messages,
|
|
18
|
-
# the byte loop that retries a timed-out byte until RUN/STOP, the
|
|
19
|
-
# successful return (CLC, LDX $AE, LDY $AF, RTS) and the
|
|
20
|
-
# FILE NOT FOUND and MISSING FILE NAME error exits
|
|
21
|
-
SEARCHING_MESSAGE = 0xf5af
|
|
22
|
-
LOADING_MESSAGE = 0xf5d2
|
|
23
|
-
BYTE_LOOP = 0xf4f3
|
|
24
|
-
LOAD_DONE = 0xf5a9
|
|
25
|
-
FILE_NOT_FOUND_EXIT = 0xf704
|
|
26
|
-
MISSING_FILE_NAME_EXIT = 0xf710
|
|
27
|
-
|
|
28
16
|
# ST bits at $90
|
|
29
17
|
EOI = 0x40
|
|
30
18
|
VERIFY_MISMATCH = 0x10
|
|
@@ -35,8 +23,8 @@ module Badline
|
|
|
35
23
|
# address even when the first one relocated.
|
|
36
24
|
LOAD_SECONDARY = 0x60
|
|
37
25
|
|
|
38
|
-
def initialize(cpu:, bus:, drive:)
|
|
39
|
-
super(cpu:, bus:)
|
|
26
|
+
def initialize(cpu:, bus:, layout:, drive:)
|
|
27
|
+
super(cpu:, bus:, layout:)
|
|
40
28
|
@drive = drive
|
|
41
29
|
end
|
|
42
30
|
|
|
@@ -46,7 +34,7 @@ module Badline
|
|
|
46
34
|
@bus.poke(0x93, @cpu.a)
|
|
47
35
|
@bus.poke(0x90, 0x00)
|
|
48
36
|
name = filename
|
|
49
|
-
return @cpu.program_counter =
|
|
37
|
+
return @cpu.program_counter = @layout.missing_file_name_exit if Storage.parse_name(name).first.empty?
|
|
50
38
|
|
|
51
39
|
data, complete = receive(name)
|
|
52
40
|
if low_memory?(data)
|
|
@@ -79,15 +67,26 @@ module Badline
|
|
|
79
67
|
def finish(data, complete)
|
|
80
68
|
if data.empty?
|
|
81
69
|
@bus.poke(0x90, EOI | READ_TIMEOUT)
|
|
82
|
-
|
|
70
|
+
leave_bus(ACPTR_TIMEOUT)
|
|
71
|
+
continue_with(@layout.searching_message, @layout.file_not_found_exit)
|
|
83
72
|
else
|
|
84
73
|
deliver(data)
|
|
85
74
|
@bus.poke(0x90, @bus.peek(0x90) & ~EOI) unless complete
|
|
86
|
-
|
|
75
|
+
leave_bus(ISOUR_TIMEOUT)
|
|
76
|
+
continue_with(@layout.searching_message, @layout.loading_message,
|
|
77
|
+
complete ? @layout.load_done : @layout.load_byte_loop)
|
|
87
78
|
end
|
|
88
79
|
@bus.poke(0xb9, LOAD_SECONDARY)
|
|
89
80
|
end
|
|
90
81
|
|
|
82
|
+
# A load ends with the UNLISTEN that closes the file, a byte sent,
|
|
83
|
+
# and a missing file with the ACPTR that timed out. Either way it
|
|
84
|
+
# releases ATN, the clock and the data line.
|
|
85
|
+
def leave_bus(timer_high)
|
|
86
|
+
time_serial_byte(timer_high)
|
|
87
|
+
@layout.release_serial_lines(@bus)
|
|
88
|
+
end
|
|
89
|
+
|
|
91
90
|
def low_memory?(data)
|
|
92
91
|
load? && data.length > 2 && load_address(data) < 0x0334
|
|
93
92
|
end
|
|
@@ -102,7 +101,7 @@ module Badline
|
|
|
102
101
|
payload = data[2..] || []
|
|
103
102
|
payload = payload[0, 0x10000 - addr] if addr + payload.length > 0x10000
|
|
104
103
|
if load?
|
|
105
|
-
@
|
|
104
|
+
@layout.store_file(@bus, addr, payload)
|
|
106
105
|
@bus.poke(0x90, EOI)
|
|
107
106
|
else
|
|
108
107
|
verify(addr, payload)
|
|
@@ -3,22 +3,34 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module KernalTrap
|
|
5
5
|
# Base class for the PC traps on KERNAL routines. Holds the device
|
|
6
|
-
# number they answer for
|
|
7
|
-
# routine's RTS.
|
|
6
|
+
# number they answer for, the layout of the KERNAL they trap, and the
|
|
7
|
+
# return that stands in for the trapped routine's RTS.
|
|
8
8
|
class Routine
|
|
9
9
|
include IntegerHelper
|
|
10
10
|
|
|
11
11
|
DEVICE = 8
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# The serial timer's high byte as ISOUR and ACPTR set it.
|
|
14
|
+
ISOUR_TIMEOUT = 0x04
|
|
15
|
+
ACPTR_TIMEOUT = 0x01
|
|
16
|
+
|
|
17
|
+
def initialize(cpu:, bus:, layout:)
|
|
14
18
|
@cpu = cpu
|
|
15
19
|
@bus = bus
|
|
20
|
+
@layout = layout
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
private
|
|
19
24
|
|
|
20
|
-
def
|
|
21
|
-
@
|
|
25
|
+
def kernal_mapped?
|
|
26
|
+
@layout.kernal_mapped?(@bus)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The ROM times each byte on the serial bus with a one-shot timer
|
|
30
|
+
# (Layout#time_serial_byte): ISOUR_TIMEOUT for a byte sent,
|
|
31
|
+
# ACPTR_TIMEOUT for one received.
|
|
32
|
+
def time_serial_byte(timer_high)
|
|
33
|
+
@layout.time_serial_byte(@bus, timer_high)
|
|
22
34
|
end
|
|
23
35
|
|
|
24
36
|
def return_to_caller
|
|
@@ -2,37 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module KernalTrap
|
|
5
|
-
# PC trap on the KERNAL serial SAVE routine (
|
|
6
|
-
#
|
|
7
|
-
# (load address followed by the memory
|
|
8
|
-
# through to the ROM, and so do saves to a
|
|
9
|
-
# whole. The ROM prints SAVING in direct
|
|
10
|
-
# into the trap, which then writes the file and leaves
|
|
11
|
-
# ROM's own tail with the registers its UNLISTEN and return
|
|
5
|
+
# PC trap on the KERNAL serial SAVE routine (the default ISAVE vector
|
|
6
|
+
# target, $F5ED on the C64, $F685 on the VIC-20). Hands device 8 saves
|
|
7
|
+
# to the virtual drive as a PRG (load address followed by the memory
|
|
8
|
+
# range); other devices fall through to the ROM, and so do saves to a
|
|
9
|
+
# disk that doesn't take them whole. The ROM prints SAVING in direct
|
|
10
|
+
# mode and returns into the trap, which then writes the file and leaves
|
|
11
|
+
# through the ROM's own tail with the registers its UNLISTEN and return
|
|
12
|
+
# leave. A
|
|
12
13
|
# write that fails, to a full disk, a name already on it or a host file
|
|
13
14
|
# that can't be written, ends the way a 1541 ends a SAVE it can't
|
|
14
15
|
# write: the drive stops listening, so ST reads DEVICE NOT PRESENT,
|
|
15
16
|
# and the ROM returns without an error. A "@" before the drive prefix
|
|
16
17
|
# writes over a file of the same name.
|
|
17
18
|
class Save < File
|
|
18
|
-
ADDRESS = 0xf5ed
|
|
19
|
-
|
|
20
|
-
# ROM entry points: the SAVING message, the two line releases that
|
|
21
|
-
# end UNLISTEN (clock, then data, leaving A as read from $DD00), the
|
|
22
|
-
# successful return (CLC, RTS) and the MISSING FILE NAME error exit
|
|
23
|
-
SAVING_MESSAGE = 0xf68f
|
|
24
|
-
CLOCK_RELEASE = 0xee85
|
|
25
|
-
DATA_RELEASE = 0xee97
|
|
26
|
-
SAVE_DONE = 0xf657
|
|
27
|
-
MISSING_FILE_NAME_EXIT = 0xf710
|
|
28
|
-
|
|
29
19
|
SECONDARY = 0x61
|
|
30
20
|
|
|
31
21
|
# ST bit at $90
|
|
32
22
|
DEVICE_NOT_PRESENT = 0x80
|
|
33
23
|
|
|
34
|
-
def initialize(cpu:, bus:, drive:)
|
|
35
|
-
super(cpu:, bus:)
|
|
24
|
+
def initialize(cpu:, bus:, layout:, drive:)
|
|
25
|
+
super(cpu:, bus:, layout:)
|
|
36
26
|
@drive = drive
|
|
37
27
|
@saving = false
|
|
38
28
|
end
|
|
@@ -51,11 +41,11 @@ module Badline
|
|
|
51
41
|
return unless @drive.saves?
|
|
52
42
|
|
|
53
43
|
@bus.poke(0xb9, SECONDARY)
|
|
54
|
-
return @cpu.program_counter =
|
|
44
|
+
return @cpu.program_counter = @layout.missing_file_name_exit if name.empty?
|
|
55
45
|
|
|
56
46
|
@bus.poke(0x90, 0x00)
|
|
57
47
|
@saving = true
|
|
58
|
-
continue_with(
|
|
48
|
+
continue_with(@layout.saving_message, @layout.save)
|
|
59
49
|
end
|
|
60
50
|
|
|
61
51
|
private
|
|
@@ -72,7 +62,7 @@ module Badline
|
|
|
72
62
|
@bus.poke(0xad, @bus.peek(0xaf))
|
|
73
63
|
@cpu.y = 0
|
|
74
64
|
@cpu.status.overflow = true
|
|
75
|
-
continue_with(
|
|
65
|
+
continue_with(@layout.clock_release, @layout.data_release, @layout.save_done)
|
|
76
66
|
end
|
|
77
67
|
|
|
78
68
|
# Start address at $C1/$C2 (STAL), end address (exclusive) at
|