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
|
@@ -35,10 +35,15 @@ module Badline
|
|
|
35
35
|
|
|
36
36
|
# Where #stream starts: the frames before it are rendered as fast as
|
|
37
37
|
# they go and yielded without their samples, so the seconds rendered
|
|
38
|
-
# still count up to it.
|
|
39
|
-
#
|
|
38
|
+
# still count up to it. Rendering picks up from the latest of the
|
|
39
|
+
# checkpoints at or before it, without the frames before that. Set
|
|
40
|
+
# while streaming, it skips ahead from the frame rendered last.
|
|
40
41
|
attr_writer :from
|
|
41
42
|
|
|
43
|
+
# The Checkpoints this subtune's renderers keep as they go and start
|
|
44
|
+
# from, for a player that seeks. Without them nothing is kept.
|
|
45
|
+
attr_writer :checkpoints
|
|
46
|
+
|
|
42
47
|
def initialize(tune, seconds:, subtune: nil, rate: DEFAULT_RATE, sid_models: tune.sid_models)
|
|
43
48
|
@tune = tune
|
|
44
49
|
@seconds = seconds
|
|
@@ -52,6 +57,8 @@ module Badline
|
|
|
52
57
|
@rendered = 0.0
|
|
53
58
|
@observer = ->(_samples, _rendered) {}
|
|
54
59
|
@from = 0.0
|
|
60
|
+
@played = 0
|
|
61
|
+
@checkpoints = nil
|
|
55
62
|
end
|
|
56
63
|
|
|
57
64
|
def silence=(seconds)
|
|
@@ -65,7 +72,6 @@ module Badline
|
|
|
65
72
|
# Yields the seconds rendered so far after every frame.
|
|
66
73
|
def render(path)
|
|
67
74
|
container = container_for(path)
|
|
68
|
-
player.start
|
|
69
75
|
container.open(path, rate: @rate, channels: 2) do |writer|
|
|
70
76
|
each_frame do |samples, seconds|
|
|
71
77
|
samples.each { |sample| writer << sample }
|
|
@@ -76,9 +82,16 @@ module Badline
|
|
|
76
82
|
|
|
77
83
|
# Starts the tune, then yields each frame's samples along with the
|
|
78
84
|
# seconds rendered so far.
|
|
79
|
-
def stream(&)
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
def stream(&) = each_frame(&)
|
|
86
|
+
|
|
87
|
+
# Whether the latest checkpoint at or before `seconds` lies past the
|
|
88
|
+
# frame rendered last, so that a seek there gets there sooner from it
|
|
89
|
+
# than on from here.
|
|
90
|
+
def checkpoint_ahead?(seconds)
|
|
91
|
+
checkpoints = @checkpoints
|
|
92
|
+
return false if checkpoints.nil?
|
|
93
|
+
|
|
94
|
+
checkpoints.reach((seconds * player.clock_hz).round) > @played
|
|
82
95
|
end
|
|
83
96
|
|
|
84
97
|
private
|
|
@@ -101,19 +114,77 @@ module Badline
|
|
|
101
114
|
end
|
|
102
115
|
|
|
103
116
|
def each_frame
|
|
104
|
-
|
|
117
|
+
clock_hz = player.clock_hz
|
|
105
118
|
total = total_cycles
|
|
106
|
-
|
|
119
|
+
@played = start((@from * clock_hz).round)
|
|
120
|
+
remaining = total - @played
|
|
107
121
|
while remaining.positive?
|
|
108
122
|
samples = []
|
|
109
123
|
remaining -= player.frame(remaining) { |sample| samples << sample }
|
|
110
|
-
|
|
124
|
+
played = total - remaining
|
|
125
|
+
@played = played
|
|
126
|
+
@rendered = played.fdiv(clock_hz)
|
|
111
127
|
@observer.call(samples, @rendered)
|
|
112
128
|
yield(@rendered < @from ? [] : samples, @rendered)
|
|
113
129
|
break if fallen_silent?(samples)
|
|
130
|
+
|
|
131
|
+
save_checkpoint(played, clock_hz)
|
|
114
132
|
end
|
|
115
133
|
end
|
|
116
134
|
|
|
135
|
+
# Starts the player and its recording, from the latest checkpoint at
|
|
136
|
+
# or before `cycles` if there is one, and returns the cycles into the
|
|
137
|
+
# subtune it starts at.
|
|
138
|
+
def start(cycles)
|
|
139
|
+
state = @checkpoints&.latest(cycles)
|
|
140
|
+
return start_afresh if state.nil?
|
|
141
|
+
|
|
142
|
+
input = Snapshot::StateReader.new(state)
|
|
143
|
+
played = input.int
|
|
144
|
+
resume(input)
|
|
145
|
+
record
|
|
146
|
+
player.stereo.sids.each { |sid| sid.load_recording(input) }
|
|
147
|
+
@level = input.int
|
|
148
|
+
@still = input.int
|
|
149
|
+
played
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def start_afresh
|
|
153
|
+
player.start
|
|
154
|
+
record
|
|
155
|
+
0
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def record = player.stereo.record(rate: @rate, filter_chunk: @filter_chunk, clock_hz: player.clock_hz)
|
|
159
|
+
|
|
160
|
+
# Restores the player as it was, then puts back the SID models chosen
|
|
161
|
+
# for this renderer.
|
|
162
|
+
def resume(input)
|
|
163
|
+
sids = player.stereo.sids
|
|
164
|
+
chosen = sids.map(&:model)
|
|
165
|
+
sids.each { |sid| refit(sid, Snapshot::Setup::SID_MODELS.fetch(input.int)) }
|
|
166
|
+
player.load_state(input)
|
|
167
|
+
sids.each_with_index { |sid, index| refit(sid, chosen[index]) }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def refit(sid, model)
|
|
171
|
+
sid.model = model unless sid.model == model
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def save_checkpoint(played, clock_hz)
|
|
175
|
+
checkpoints = @checkpoints
|
|
176
|
+
return if checkpoints.nil? || !checkpoints.due?(played, clock_hz)
|
|
177
|
+
|
|
178
|
+
sids = player.stereo.sids
|
|
179
|
+
out = Snapshot::StateWriter.new
|
|
180
|
+
out.int(played)
|
|
181
|
+
sids.each { |sid| out.int(Snapshot::Setup::SID_MODELS.index(sid.model) || 0) }
|
|
182
|
+
player.save_state(out)
|
|
183
|
+
sids.each { |sid| sid.save_recording(out) }
|
|
184
|
+
out.int(@level).int(@still)
|
|
185
|
+
checkpoints.keep(played, out.state)
|
|
186
|
+
end
|
|
187
|
+
|
|
117
188
|
# Counts the samples since the output last strayed more than QUIET
|
|
118
189
|
# from where it settled.
|
|
119
190
|
def fallen_silent?(samples)
|
data/lib/badline/audio/stereo.rb
CHANGED
data/lib/badline/audio.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "badline/audio/aiff"
|
|
|
6
6
|
require "badline/audio/stereo"
|
|
7
7
|
require "badline/audio/bare_player"
|
|
8
8
|
require "badline/audio/machine_player"
|
|
9
|
+
require "badline/audio/checkpoints"
|
|
9
10
|
require "badline/audio/renderer"
|
|
10
11
|
require "badline/audio/playback"
|
|
11
12
|
require "badline/audio/terminal"
|
data/lib/badline/chrout_trap.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
# Observe-only PC trap on the KERNAL CHROUT routine ($FFD2). Records each
|
|
5
|
-
# character written while the KERNAL
|
|
5
|
+
# character written while the layout finds the KERNAL mapped in, then lets execution fall
|
|
6
6
|
# through to the ROM, so the screen output is unaffected. Used for headless
|
|
7
7
|
# capture of program output.
|
|
8
8
|
class ChroutTrap
|
|
@@ -10,14 +10,15 @@ module Badline
|
|
|
10
10
|
|
|
11
11
|
attr_reader :output
|
|
12
12
|
|
|
13
|
-
def initialize(cpu:, bus:)
|
|
13
|
+
def initialize(cpu:, bus:, layout:)
|
|
14
14
|
@cpu = cpu
|
|
15
15
|
@bus = bus
|
|
16
|
+
@layout = layout
|
|
16
17
|
@output = +""
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def call
|
|
20
|
-
@output << ascii(@cpu.a) if @
|
|
21
|
+
@output << ascii(@cpu.a) if @layout.kernal_mapped?(@bus)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def inspect
|
|
@@ -22,11 +22,13 @@ module Badline
|
|
|
22
22
|
return @drive.insert(storage) if @drive
|
|
23
23
|
|
|
24
24
|
@drive = KernalTrap::Drive.new(storage)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
layout = KernalTrap::C64_LAYOUT
|
|
26
|
+
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, layout:, drive: @drive)
|
|
27
|
+
cpu.install_trap(layout.load) { load_trap.call }
|
|
28
|
+
@serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, layout:, drive: @drive,
|
|
29
|
+
device: serial_trap_device).install
|
|
30
|
+
save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, layout:, drive: @drive)
|
|
31
|
+
cpu.install_trap(layout.save) { save_trap.call }
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
# Takes device 8's mounted storage out, and with it the LOAD, SAVE and
|
|
@@ -35,8 +37,8 @@ module Badline
|
|
|
35
37
|
def unmount
|
|
36
38
|
return unless @drive
|
|
37
39
|
|
|
38
|
-
cpu.remove_trap(KernalTrap::
|
|
39
|
-
cpu.remove_trap(KernalTrap::
|
|
40
|
+
cpu.remove_trap(KernalTrap::C64_LAYOUT.load)
|
|
41
|
+
cpu.remove_trap(KernalTrap::C64_LAYOUT.save)
|
|
40
42
|
@serial_trap.device = nil
|
|
41
43
|
@serial_trap = nil
|
|
42
44
|
@save_trap = nil
|
|
@@ -46,17 +48,55 @@ module Badline
|
|
|
46
48
|
# Whether device 8 serves a disk or directory through the traps.
|
|
47
49
|
def mounted? = !@drive.nil?
|
|
48
50
|
|
|
51
|
+
# The serial bus, which CIA 2's port A drives through the C64's own
|
|
52
|
+
# inverters and reads back on PA6 and PA7, with or without a drive on
|
|
53
|
+
# it.
|
|
54
|
+
attr_reader :iec_bus
|
|
55
|
+
|
|
56
|
+
# Plugs in a Drive1541, which then runs alongside the C64 on its own
|
|
57
|
+
# clock and talks to it over the serial bus. The serial traps stop
|
|
58
|
+
# answering the drive's device number, so the KERNAL's TALK, LISTEN and
|
|
59
|
+
# byte transfers reach the drive. The LOAD and SAVE traps stay, and
|
|
60
|
+
# still serve a mounted image.
|
|
61
|
+
def attach_drive1541(drive)
|
|
62
|
+
@iec_bus.detach(@drive1541) if @drive1541
|
|
63
|
+
drive.host_clock_hz = region.clock_hz
|
|
64
|
+
@drive1541 = drive
|
|
65
|
+
drive.connect(iec_bus)
|
|
66
|
+
@serial_trap&.device = serial_trap_device
|
|
67
|
+
end
|
|
68
|
+
|
|
49
69
|
# Unplugs the Drive1541, leaving the serial bus with nothing on it.
|
|
50
70
|
def detach_drive1541
|
|
51
71
|
return unless @drive1541
|
|
52
72
|
|
|
53
|
-
@iec_bus
|
|
73
|
+
@iec_bus.detach(@drive1541)
|
|
54
74
|
@drive1541 = nil
|
|
55
75
|
@serial_trap&.device = serial_trap_device
|
|
56
76
|
end
|
|
57
77
|
|
|
58
78
|
private
|
|
59
79
|
|
|
80
|
+
# The device number the serial traps answer: device 8, unless a true
|
|
81
|
+
# drive is on the bus as device 8.
|
|
82
|
+
def serial_trap_device
|
|
83
|
+
@drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# CIA 2's port A drives the serial bus, which reads CLK and DATA back
|
|
87
|
+
# into it.
|
|
88
|
+
def plug_serial_bus
|
|
89
|
+
@iec_bus = IECBus.new
|
|
90
|
+
@cia2.peripheral = @iec_bus
|
|
91
|
+
@cia2.on_port_a_write { push_serial_lines }
|
|
92
|
+
push_serial_lines
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Pushes the lines CIA 2's port A pulls into the serial bus.
|
|
96
|
+
def push_serial_lines
|
|
97
|
+
@iec_bus.host_lines = @cia2.port_a_lines
|
|
98
|
+
end
|
|
99
|
+
|
|
60
100
|
def save_cartridge(out)
|
|
61
101
|
cartridge = address_bus.cartridge
|
|
62
102
|
out.boolean(!cartridge.nil?)
|
|
@@ -95,10 +135,10 @@ module Badline
|
|
|
95
135
|
@save_trap.load_state(input)
|
|
96
136
|
end
|
|
97
137
|
|
|
98
|
-
#
|
|
99
|
-
#
|
|
138
|
+
# A true 1541 on the serial bus. The leading flag is the bus itself,
|
|
139
|
+
# which every machine now has, kept so older snapshots still load.
|
|
100
140
|
def save_serial_bus(out)
|
|
101
|
-
out.boolean(
|
|
141
|
+
out.boolean(true).boolean(!@drive1541.nil?)
|
|
102
142
|
return unless @drive1541
|
|
103
143
|
|
|
104
144
|
out.int(@drive1541.device)
|
|
@@ -106,7 +146,7 @@ module Badline
|
|
|
106
146
|
end
|
|
107
147
|
|
|
108
148
|
def load_serial_bus(input)
|
|
109
|
-
|
|
149
|
+
input.boolean?
|
|
110
150
|
return detach_drive1541 unless input.boolean?
|
|
111
151
|
|
|
112
152
|
device = input.int
|
data/lib/badline/computer.rb
CHANGED
|
@@ -12,6 +12,12 @@ module Badline
|
|
|
12
12
|
|
|
13
13
|
attr_reader :address_bus, :cpu, :cycles, :drive1541
|
|
14
14
|
|
|
15
|
+
# The path of the disk or directory device 8 serves through the traps
|
|
16
|
+
# (Attachments), or an empty one.
|
|
17
|
+
def mounted_path = @drive.nil? ? "" : @drive.path
|
|
18
|
+
|
|
19
|
+
def family = :c64
|
|
20
|
+
|
|
15
21
|
def region = address_bus.region
|
|
16
22
|
|
|
17
23
|
def vic = address_bus.vic
|
|
@@ -42,9 +48,13 @@ module Badline
|
|
|
42
48
|
# TOD clocks count. The KERNAL tells PAL from NTSC by the raster, so
|
|
43
49
|
# every region boots the same ROMs. reu plugs an REU of that many K
|
|
44
50
|
# into the expansion port, where it drives the IRQ line and takes the
|
|
45
|
-
# bus for its transfers.
|
|
46
|
-
|
|
51
|
+
# bus for its transfers. kernal: names the KERNAL ROM
|
|
52
|
+
# (AddressBus::ROMs::KERNALS), and datasette: false leaves the cassette port
|
|
53
|
+
# empty, as on the SX-64.
|
|
54
|
+
def initialize(debug: false, reu: nil, kernal: :c64, datasette: true, **machine)
|
|
47
55
|
@address_bus = AddressBus.new(**machine)
|
|
56
|
+
@address_bus.kernal = kernal unless kernal == :c64
|
|
57
|
+
@address_bus.datasette.disconnect! unless datasette
|
|
48
58
|
@cpu = CPU.new(@address_bus, debug:)
|
|
49
59
|
@vic = @address_bus.vic
|
|
50
60
|
@vic.open_bus = -> { @address_bus.ram.peek(@cpu.program_counter) }
|
|
@@ -66,7 +76,7 @@ module Badline
|
|
|
66
76
|
@serial_trap = nil
|
|
67
77
|
@save_trap = nil
|
|
68
78
|
@drive1541 = nil
|
|
69
|
-
|
|
79
|
+
plug_serial_bus
|
|
70
80
|
@reu = reu ? plug_reu(reu) : nil
|
|
71
81
|
end
|
|
72
82
|
|
|
@@ -94,6 +104,40 @@ module Badline
|
|
|
94
104
|
@cycles += 1
|
|
95
105
|
end
|
|
96
106
|
|
|
107
|
+
# Runs `count` cycles, one #cycle! after another.
|
|
108
|
+
def run_cycles(count)
|
|
109
|
+
i = 0
|
|
110
|
+
while i < count
|
|
111
|
+
cycle!
|
|
112
|
+
i += 1
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Runs cycles until the block returns true or the cycle count passes
|
|
117
|
+
# `limit`, checking before each cycle.
|
|
118
|
+
def run_until(limit)
|
|
119
|
+
cycle! until yield || @cycles > limit
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The cycle at which #on_init's handlers run, once the KERNAL has booted.
|
|
123
|
+
def init_threshold
|
|
124
|
+
INIT_THRESHOLD
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The clock, the raster and the crop of the machine's region, a pixel
|
|
128
|
+
# to a window pixel.
|
|
129
|
+
def timing
|
|
130
|
+
region = address_bus.region
|
|
131
|
+
Timing.new(clock_hz: region.clock_hz, cycles_per_line: region.cycles_per_line,
|
|
132
|
+
lines_per_frame: region.lines_per_frame, crop: region.crop, pixel_width: 1)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The chip whose #display a front end shows.
|
|
136
|
+
def video = @vic
|
|
137
|
+
|
|
138
|
+
# The chip a front end records the machine's sound from.
|
|
139
|
+
def sound_source = @sid
|
|
140
|
+
|
|
97
141
|
def load_prg(data)
|
|
98
142
|
uint16(data[0], data[1]).tap do |load_addr|
|
|
99
143
|
ram.write(load_addr, data[2..])
|
|
@@ -155,28 +199,11 @@ module Badline
|
|
|
155
199
|
@restore_pulse = true
|
|
156
200
|
end
|
|
157
201
|
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
# answering the drive's device number, so the KERNAL's TALK, LISTEN and
|
|
161
|
-
# byte transfers reach the drive. The LOAD and SAVE traps stay, and
|
|
162
|
-
# still serve a mounted image.
|
|
163
|
-
def attach_drive1541(drive)
|
|
164
|
-
@iec_bus.detach(@drive1541) if @iec_bus && @drive1541
|
|
165
|
-
drive.host_clock_hz = region.clock_hz
|
|
166
|
-
@drive1541 = drive
|
|
167
|
-
drive.connect(iec_bus)
|
|
168
|
-
@serial_trap&.device = serial_trap_device
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# The serial bus. CIA 2's port A joins it the first time it's asked
|
|
172
|
-
# for, which plugging in a drive does. Until then port A's serial
|
|
173
|
-
# inputs float high.
|
|
174
|
-
def iec_bus
|
|
175
|
-
@iec_bus ||= IECBus.new(host: @cia2).tap(&:plug_in!)
|
|
176
|
-
end
|
|
202
|
+
# The one-shot has pulsed already, so letting go does nothing.
|
|
203
|
+
def release_restore; end
|
|
177
204
|
|
|
178
205
|
def capture_output
|
|
179
|
-
@capture_output ||= ChroutTrap.new(cpu:, bus: address_bus).tap do |trap|
|
|
206
|
+
@capture_output ||= ChroutTrap.new(cpu:, bus: address_bus, layout: KernalTrap::C64_LAYOUT).tap do |trap|
|
|
180
207
|
cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
|
|
181
208
|
end
|
|
182
209
|
end
|
|
@@ -195,12 +222,6 @@ module Badline
|
|
|
195
222
|
|
|
196
223
|
private
|
|
197
224
|
|
|
198
|
-
# The device number the serial traps answer: device 8, unless a true
|
|
199
|
-
# drive is on the bus as device 8.
|
|
200
|
-
def serial_trap_device
|
|
201
|
-
@drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
|
|
202
|
-
end
|
|
203
|
-
|
|
204
225
|
# The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
|
|
205
226
|
# key, and the CPU takes an interrupt on its falling edge.
|
|
206
227
|
def drive_nmi
|
|
@@ -256,9 +277,5 @@ module Badline
|
|
|
256
277
|
def handle_init
|
|
257
278
|
@init_handlers.each(&:call)
|
|
258
279
|
end
|
|
259
|
-
|
|
260
|
-
def init_threshold
|
|
261
|
-
INIT_THRESHOLD
|
|
262
|
-
end
|
|
263
280
|
end
|
|
264
281
|
end
|
|
@@ -16,13 +16,19 @@ module Badline
|
|
|
16
16
|
# resistances; close neither and the lines float high. Reading a pot asks the
|
|
17
17
|
# CIA for the current select lines, since a program sets them and then goes
|
|
18
18
|
# straight to SID without touching the ports again.
|
|
19
|
+
#
|
|
20
|
+
# A matrix may have rows beyond port A's eight. Their select lines come from
|
|
21
|
+
# outside the CIA and are pushed into +extra_rows+, bit 0 for the ninth row
|
|
22
|
+
# upwards, active low like port A; idle, every line is high and none of
|
|
23
|
+
# those rows is selected. They settle with port A's lines as rows 8 and up,
|
|
24
|
+
# so their keys reach port B, but port A reads only its own eight.
|
|
19
25
|
class ControlPorts
|
|
20
26
|
PORT1_POTS = 0x40
|
|
21
27
|
PORT2_POTS = 0x80
|
|
22
28
|
FIRE = 0x10
|
|
23
29
|
|
|
24
30
|
attr_reader :keyboard, :joystick1, :joystick2
|
|
25
|
-
attr_accessor :device1, :device2, :port_a_source
|
|
31
|
+
attr_accessor :device1, :device2, :port_a_source, :extra_rows
|
|
26
32
|
|
|
27
33
|
def initialize(keyboard:, joystick1:, joystick2:)
|
|
28
34
|
@keyboard = keyboard
|
|
@@ -31,9 +37,10 @@ module Badline
|
|
|
31
37
|
@device1 = nil
|
|
32
38
|
@device2 = nil
|
|
33
39
|
@port_a_source = nil
|
|
40
|
+
@extra_rows = 0xff
|
|
34
41
|
end
|
|
35
42
|
|
|
36
|
-
def read_a(port_a, port_b) = scan(port_a, port_b).first
|
|
43
|
+
def read_a(port_a, port_b) = scan(port_a, port_b).first & 0xff
|
|
37
44
|
|
|
38
45
|
def read_b(port_a, port_b) = scan(port_a, port_b).last
|
|
39
46
|
|
|
@@ -58,7 +65,7 @@ module Badline
|
|
|
58
65
|
end
|
|
59
66
|
|
|
60
67
|
def scan(port_a, port_b)
|
|
61
|
-
keyboard.scan(port_a & port_bits(@joystick2, @device2),
|
|
68
|
+
keyboard.scan((port_a & port_bits(@joystick2, @device2)) | (@extra_rows << 8),
|
|
62
69
|
port_b & port_bits(@joystick1, @device1))
|
|
63
70
|
end
|
|
64
71
|
|
data/lib/badline/core.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The emulator core: the machine, its chips, media and traps, and the
|
|
4
|
+
# snapshot state Computer saves and restores with, without the options,
|
|
5
|
+
# the rest of the snapshot code, audio or front end. lib/badline.rb and
|
|
6
|
+
# the Spinel harnesses in spinel/ load it, so a file the core needs is
|
|
7
|
+
# added here.
|
|
8
|
+
require "badline/version"
|
|
9
|
+
require "badline/integer_helper"
|
|
10
|
+
require "badline/region"
|
|
11
|
+
require "badline/timing"
|
|
12
|
+
require "badline/model"
|
|
13
|
+
require "badline/addressable"
|
|
14
|
+
require "badline/memory"
|
|
15
|
+
require "badline/color_memory"
|
|
16
|
+
require "badline/rom"
|
|
17
|
+
require "badline/ram_expansion"
|
|
18
|
+
require "badline/address_bus"
|
|
19
|
+
require "badline/instruction"
|
|
20
|
+
require "badline/instruction_set"
|
|
21
|
+
require "badline/status"
|
|
22
|
+
require "badline/keyboard"
|
|
23
|
+
require "badline/joystick"
|
|
24
|
+
require "badline/control_ports"
|
|
25
|
+
require "badline/input"
|
|
26
|
+
require "badline/cycleable"
|
|
27
|
+
require "badline/datasette"
|
|
28
|
+
require "badline/time_of_day"
|
|
29
|
+
require "badline/cia"
|
|
30
|
+
require "badline/via"
|
|
31
|
+
require "badline/sid"
|
|
32
|
+
require "badline/debug_register"
|
|
33
|
+
require "badline/reu"
|
|
34
|
+
require "badline/interrupts"
|
|
35
|
+
require "badline/traps"
|
|
36
|
+
require "badline/cpu"
|
|
37
|
+
require "badline/vic"
|
|
38
|
+
require "badline/keyboard_buffer"
|
|
39
|
+
require "badline/iec_bus"
|
|
40
|
+
require "badline/drive1541"
|
|
41
|
+
require "badline/snapshot/state"
|
|
42
|
+
require "badline/snapshot/setup"
|
|
43
|
+
require "badline/computer"
|
|
44
|
+
require "badline/vic20"
|
|
45
|
+
require "badline/checkpoint"
|
|
46
|
+
require "badline/storage"
|
|
47
|
+
require "badline/cartridge"
|
|
48
|
+
require "badline/kernal_trap"
|
|
49
|
+
require "badline/chrout_trap"
|
|
50
|
+
require "badline/media"
|
|
51
|
+
require "badline/machine"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
class << self
|
|
7
|
+
attr_writer :data_path
|
|
8
|
+
|
|
9
|
+
# The per-user folder badline keeps its own files in. Defaults to
|
|
10
|
+
# $BADLINE_DATA_PATH, then to the platform's folder for application
|
|
11
|
+
# data. Assigning nil restores the default. Nothing is created until
|
|
12
|
+
# data_folder is asked for a subfolder.
|
|
13
|
+
def data_path
|
|
14
|
+
@data_path ||= ENV.fetch("BADLINE_DATA_PATH", nil) || default_data_path(RUBY_PLATFORM)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# ~/Library/Application Support/badline on macOS, and
|
|
18
|
+
# $XDG_DATA_HOME/badline or ~/.local/share/badline elsewhere.
|
|
19
|
+
def default_data_path(platform)
|
|
20
|
+
return File.join(Dir.home, "Library", "Application Support", "badline") if platform.include?("darwin")
|
|
21
|
+
|
|
22
|
+
xdg = ENV.fetch("XDG_DATA_HOME", nil)
|
|
23
|
+
xdg = File.join(Dir.home, ".local", "share") if xdg.nil? || !xdg.start_with?("/")
|
|
24
|
+
File.join(xdg, "badline")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The quicksaves, autosaves or saves subfolder of data_path, created
|
|
28
|
+
# along with data_path if it doesn't exist yet.
|
|
29
|
+
def data_folder(name)
|
|
30
|
+
raise ArgumentError, "unknown data folder: #{name}" unless %w[quicksaves autosaves saves].include?(name)
|
|
31
|
+
|
|
32
|
+
path = File.join(data_path, name)
|
|
33
|
+
FileUtils.mkdir_p(path)
|
|
34
|
+
path
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|