badline 0.6.0 → 0.7.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 +77 -0
- data/CLAUDE.md +10 -9
- data/README.md +135 -42
- data/doc/pinned-behaviour.md +131 -30
- data/exe/badline-ruby +28 -24
- data/lib/badline/address_bus/extra_sids.rb +30 -0
- data/lib/badline/address_bus/sid_slots.rb +42 -0
- data/lib/badline/address_bus.rb +15 -1
- data/lib/badline/audio/aiff.rb +2 -2
- data/lib/badline/audio/bare_player.rb +23 -12
- data/lib/badline/audio/cli.rb +167 -59
- data/lib/badline/audio/console.rb +2 -1
- data/lib/badline/audio/jukebox.rb +121 -36
- data/lib/badline/audio/machine_player.rb +25 -10
- data/lib/badline/audio/pcm_writer.rb +10 -6
- data/lib/badline/audio/playback.rb +6 -5
- data/lib/badline/audio/queued_tune.rb +89 -0
- data/lib/badline/audio/renderer.rb +57 -10
- data/lib/badline/audio/stereo.rb +97 -0
- data/lib/badline/audio/terminal.rb +39 -6
- data/lib/badline/audio/wav.rb +1 -1
- data/lib/badline/audio.rb +2 -3
- data/lib/badline/computer.rb +6 -3
- data/lib/badline/cpu/boundary.rb +54 -0
- data/lib/badline/cpu/core.rb +165 -0
- data/lib/badline/cpu.rb +3 -214
- data/lib/badline/drive1541/cpu.rb +29 -0
- data/lib/badline/drive1541.rb +1 -0
- data/lib/badline/ffi.rb +157 -0
- data/lib/badline/frontend/app.rb +225 -0
- data/lib/badline/frontend/audio_sink.rb +73 -0
- data/lib/badline/frontend/boot.rb +52 -0
- data/lib/badline/frontend/buttons.rb +90 -0
- data/lib/badline/frontend/controls.rb +163 -0
- data/lib/badline/frontend/drive_led.rb +51 -0
- data/lib/badline/frontend/filter_view.rb +96 -0
- data/lib/badline/frontend/frame_rate.rb +83 -0
- data/lib/badline/frontend/gamepads.rb +76 -0
- data/lib/badline/frontend/info_view.rb +120 -0
- data/lib/badline/frontend/keys.rb +48 -0
- data/lib/badline/frontend/pacer.rb +100 -0
- data/lib/badline/frontend/pad_port.rb +61 -0
- data/lib/badline/frontend/painter.rb +175 -0
- data/lib/badline/frontend/player_commands.rb +33 -0
- data/lib/badline/frontend/player_footer.rb +86 -0
- data/lib/badline/frontend/player_header.rb +60 -0
- data/lib/badline/frontend/player_screen.rb +124 -0
- data/lib/badline/frontend/player_state.rb +67 -0
- data/lib/badline/frontend/player_window.rb +228 -0
- data/lib/badline/frontend/scope.rb +42 -0
- data/lib/badline/frontend/screen.rb +65 -0
- data/lib/badline/frontend/screenshot.rb +22 -0
- data/lib/badline/frontend/sid_history.rb +175 -0
- data/lib/badline/frontend/sid_view.rb +140 -0
- data/lib/badline/frontend/snapshots.rb +60 -0
- data/lib/badline/frontend/sound.rb +153 -0
- data/lib/badline/frontend/timeline.rb +171 -0
- data/lib/badline/frontend/visualizer_view.rb +101 -0
- data/lib/badline/frontend.rb +32 -0
- data/lib/badline/media/queue.rb +158 -0
- data/lib/badline/media.rb +13 -8
- data/lib/badline/options/event.rb +94 -0
- data/lib/badline/options/help.rb +107 -0
- data/lib/badline/options/option.rb +29 -0
- data/lib/badline/options/table.rb +64 -0
- data/lib/badline/options/validation.rb +54 -0
- data/lib/badline/options.rb +204 -149
- data/lib/badline/reu/dma/saved_state.rb +50 -0
- data/lib/badline/reu/dma.rb +56 -47
- data/lib/badline/reu/saved_state.rb +2 -1
- data/lib/badline/reu.rb +19 -10
- data/lib/badline/sdl.rb +133 -51
- data/lib/badline/sid/catch_up.rb +11 -1
- data/lib/badline/sid/filter.rb +9 -0
- data/lib/badline/sid/voice.rb +6 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
- data/lib/badline/sid/waveform.rb +22 -8
- data/lib/badline/sid.rb +35 -3
- data/lib/badline/snapshot/state.rb +1 -1
- data/lib/badline/storage/hvsc.rb +41 -0
- data/lib/badline/storage/sid_file.rb +45 -25
- data/lib/badline/storage/song_lengths.rb +6 -29
- data/lib/badline/storage/stil/credit.rb +50 -0
- data/lib/badline/storage/stil.rb +126 -0
- data/lib/badline/storage.rb +8 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +21 -0
- data/lib/badline/vic.rb +15 -6
- metadata +49 -19
- data/.claude/hooks/session-start.sh +0 -57
- data/.claude/settings.json +0 -14
- data/lib/badline/audio/sdl_sink.rb +0 -70
- data/lib/badline/audio/stream.rb +0 -123
- data/lib/badline/gui/application.rb +0 -243
- data/lib/badline/gui/drive_led_pane.rb +0 -29
- data/lib/badline/gui/gamepads.rb +0 -104
- data/lib/badline/gui/joy_map.rb +0 -27
- data/lib/badline/gui/key_map.rb +0 -39
- data/lib/badline/gui/palette.rb +0 -35
- data/lib/badline/gui/pane.rb +0 -36
- data/lib/badline/gui/screen_pane.rb +0 -46
- data/lib/badline/gui/snapshots.rb +0 -67
- data/lib/badline/gui/window.rb +0 -61
- data/lib/badline/gui.rb +0 -14
- data/lib/badline/sdl/events.rb +0 -49
- data/lib/badline/sdl/functions.rb +0 -74
- data/vendor/.gitkeep +0 -0
data/lib/badline/options.rb
CHANGED
|
@@ -1,102 +1,137 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "badline/options/option"
|
|
4
|
+
require "badline/options/event"
|
|
5
|
+
require "badline/options/table"
|
|
6
|
+
require "badline/options/help"
|
|
7
|
+
require "badline/options/validation"
|
|
4
8
|
|
|
5
9
|
module Badline
|
|
6
|
-
# The command line of
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
+
# The command line of both builds, badline-ruby and the native badline.
|
|
11
|
+
# Either opens the window for any media, or with --headless or
|
|
12
|
+
# --audio-out plays or renders a .sid tune without one. `sid`, given
|
|
13
|
+
# first, plays .sid tunes and directories of them in the terminal. The
|
|
14
|
+
# native build adds --no-sound and --version, and badline-ruby adds
|
|
15
|
+
# --disable-jit.
|
|
16
|
+
#
|
|
17
|
+
# TABLE lists the options, and both the parser and the help read it. It
|
|
18
|
+
# parses by hand, inside the subset of Ruby Spinel compiles. Values come
|
|
19
|
+
# as `--subtune 2` or `--subtune=2`, and `--` ends the options. It loads
|
|
20
|
+
# nothing else from badline, so badline-ruby can parse and reject its
|
|
21
|
+
# arguments before paying for the emulator.
|
|
10
22
|
class Options
|
|
23
|
+
include Validation
|
|
24
|
+
|
|
11
25
|
class Error < StandardError; end
|
|
12
26
|
|
|
13
27
|
FALLBACK_SECONDS = 60.0
|
|
14
28
|
|
|
15
|
-
|
|
29
|
+
# A tune on the fallback length ends sooner once it has been silent
|
|
30
|
+
# this long.
|
|
31
|
+
SILENCE_SECONDS = 5.0
|
|
16
32
|
|
|
17
33
|
SID_MODELS = { "6581" => :mos6581, "8580" => :mos8580 }.freeze
|
|
18
34
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
filter cycle by cycle, which is exact but takes about twice as long.
|
|
48
|
-
|
|
49
|
-
BANNER
|
|
50
|
-
|
|
51
|
-
attr_reader :media_path, :audio_out, :song, :seconds, :songlengths, :sid_model, :filter_chunk, :reu
|
|
52
|
-
|
|
53
|
-
alias tune_path media_path
|
|
54
|
-
|
|
55
|
-
def self.parse(argv) = new.parse(argv)
|
|
56
|
-
|
|
57
|
-
def initialize
|
|
35
|
+
# What `sid` leaves out besides the window's options.
|
|
36
|
+
NOT_FOR_SID = %w[--audio-out].freeze
|
|
37
|
+
|
|
38
|
+
REU_SIZES = %w[128 256 512 1024 2048 4096 8192 16384].freeze
|
|
39
|
+
|
|
40
|
+
# A Float as OptionParser took one: digits with an optional sign,
|
|
41
|
+
# fraction and exponent.
|
|
42
|
+
DECIMAL = /\A[-+]?(\d+(\.\d+)?|\.\d+)([eE][-+]?\d+)?\z/
|
|
43
|
+
|
|
44
|
+
# tune_paths holds the media given, which `sid` takes more than one of:
|
|
45
|
+
# tunes and directories of tunes, in the order given.
|
|
46
|
+
attr_reader :program, :media_path, :tune_paths, :subtune, :sid_model, :reu, :frames, :screenshot, :save_snapshot,
|
|
47
|
+
:audio_out, :seconds, :songlengths, :filter_chunk, :timeline
|
|
48
|
+
|
|
49
|
+
def self.parse(argv, native: false) = new(native:).parse(argv)
|
|
50
|
+
|
|
51
|
+
# The native build plays the SID in the window unless --no-sound says
|
|
52
|
+
# otherwise, and badline-ruby, which runs below real time, only with
|
|
53
|
+
# --sound.
|
|
54
|
+
def initialize(native: false)
|
|
55
|
+
@native = native
|
|
56
|
+
@program = native ? "badline" : "badline-ruby"
|
|
57
|
+
@media_path = nil
|
|
58
|
+
@sid_command = false
|
|
59
|
+
@tune_paths = []
|
|
60
|
+
@subtune = nil
|
|
61
|
+
@sid_model = nil
|
|
62
|
+
@reu = nil
|
|
58
63
|
@autostart = true
|
|
59
64
|
@read_only = false
|
|
60
|
-
@sound =
|
|
65
|
+
@sound = native
|
|
66
|
+
@frames = 0
|
|
67
|
+
@paced = true
|
|
68
|
+
@vsync = true
|
|
61
69
|
@verbose = false
|
|
62
70
|
@true_drive = false
|
|
63
71
|
@ntsc = false
|
|
64
|
-
@
|
|
72
|
+
@screenshot = ""
|
|
73
|
+
@save_snapshot = ""
|
|
74
|
+
@timeline = []
|
|
65
75
|
@jit = true
|
|
76
|
+
@help = false
|
|
77
|
+
@version = false
|
|
78
|
+
@headless = false
|
|
79
|
+
@audio_out = nil
|
|
80
|
+
@seconds = nil
|
|
81
|
+
@songlengths = nil
|
|
82
|
+
@rate = nil
|
|
83
|
+
@filter_chunk = nil
|
|
66
84
|
@quiet = false
|
|
67
85
|
@tui = true
|
|
68
|
-
@
|
|
86
|
+
@all_subtunes = false
|
|
69
87
|
@window_only = []
|
|
70
88
|
@headless_only = []
|
|
71
89
|
end
|
|
72
90
|
|
|
73
91
|
def parse(argv)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
92
|
+
args = argv.dup
|
|
93
|
+
sid_command(args) if args.first == "sid"
|
|
94
|
+
argument(args.shift, args) until args.empty?
|
|
95
|
+
play_lone_tune
|
|
96
|
+
validate unless help? || version?
|
|
78
97
|
self
|
|
79
|
-
rescue OptionParser::ParseError => e
|
|
80
|
-
raise Error, e.message
|
|
81
98
|
end
|
|
82
99
|
|
|
100
|
+
# Whether the media is a .vsf snapshot to restore.
|
|
101
|
+
def snapshot? = !@media_path.nil? && File.extname(@media_path).casecmp?(".vsf")
|
|
102
|
+
|
|
83
103
|
# Plays or renders a .sid tune without the window.
|
|
84
|
-
def headless? = @headless || render?
|
|
104
|
+
def headless? = @headless || render? || @sid_command
|
|
105
|
+
|
|
106
|
+
# Whether `sid` came first, to play the tunes and directories given.
|
|
107
|
+
def sid_command? = @sid_command
|
|
108
|
+
|
|
109
|
+
# Whether `sid` plays in the SID player's window rather than the
|
|
110
|
+
# terminal.
|
|
111
|
+
def player_window? = @sid_command && !@headless
|
|
85
112
|
|
|
86
113
|
def window? = !headless?
|
|
87
114
|
|
|
88
|
-
def render? =
|
|
115
|
+
def render? = !@audio_out.nil?
|
|
89
116
|
|
|
90
|
-
|
|
117
|
+
# The tune --headless and --audio-out play, for Audio::CLI.
|
|
118
|
+
def tune_path = @media_path
|
|
91
119
|
|
|
92
120
|
# How long to play a tune whose length nothing gives.
|
|
93
121
|
def fallback_seconds = FALLBACK_SECONDS
|
|
94
122
|
|
|
123
|
+
# How long a tune playing for fallback_seconds may stay silent.
|
|
124
|
+
def silence_seconds = SILENCE_SECONDS
|
|
125
|
+
|
|
126
|
+
def rate = @rate.nil? ? DEFAULT_RATE : @rate
|
|
127
|
+
|
|
95
128
|
def rate_given? = !@rate.nil?
|
|
96
129
|
|
|
97
|
-
def
|
|
130
|
+
def quiet? = @quiet
|
|
98
131
|
|
|
99
|
-
def
|
|
132
|
+
def tui? = @tui
|
|
133
|
+
|
|
134
|
+
def all_subtunes? = @all_subtunes
|
|
100
135
|
|
|
101
136
|
def autostart? = @autostart
|
|
102
137
|
|
|
@@ -104,133 +139,153 @@ module Badline
|
|
|
104
139
|
|
|
105
140
|
def sound? = @sound
|
|
106
141
|
|
|
142
|
+
def paced? = @paced
|
|
143
|
+
|
|
144
|
+
def vsync? = @vsync
|
|
145
|
+
|
|
107
146
|
def verbose? = @verbose
|
|
108
147
|
|
|
109
148
|
def true_drive? = @true_drive
|
|
110
149
|
|
|
111
150
|
def ntsc? = @ntsc
|
|
112
151
|
|
|
113
|
-
def
|
|
152
|
+
def jit? = @jit
|
|
114
153
|
|
|
115
|
-
def
|
|
154
|
+
def help? = @help
|
|
116
155
|
|
|
117
|
-
def
|
|
156
|
+
def version? = @version
|
|
118
157
|
|
|
119
158
|
private
|
|
120
159
|
|
|
121
|
-
def
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
160
|
+
def takes?(option)
|
|
161
|
+
return false if @sid_command && (option.needs == :window || NOT_FOR_SID.include?(option.name))
|
|
162
|
+
|
|
163
|
+
[:both, @native ? :native : :ruby].include?(option.build)
|
|
125
164
|
end
|
|
126
165
|
|
|
127
|
-
def
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
raise Error, "no tune given" unless media_path
|
|
131
|
-
raise Error, "not a .sid tune: #{media_path}" unless File.extname(media_path).casecmp?(".sid")
|
|
132
|
-
elsif @headless_only.any?
|
|
133
|
-
raise Error, "#{@headless_only.first} needs --headless or --audio-out"
|
|
134
|
-
end
|
|
166
|
+
def sid_command(args)
|
|
167
|
+
args.shift
|
|
168
|
+
@sid_command = true
|
|
135
169
|
end
|
|
136
170
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
171
|
+
# A .sid on its own plays in the SID player, as `sid` would, unless an
|
|
172
|
+
# option of the emulator's window asks for the machine.
|
|
173
|
+
def play_lone_tune
|
|
174
|
+
return if @sid_command || headless? || !@window_only.empty? || @media_path.nil?
|
|
175
|
+
|
|
176
|
+
@sid_command = File.extname(@media_path).casecmp?(".sid")
|
|
142
177
|
end
|
|
143
178
|
|
|
144
|
-
def
|
|
145
|
-
|
|
146
|
-
|
|
179
|
+
def argument(arg, args)
|
|
180
|
+
if arg == "--"
|
|
181
|
+
media_argument(args.shift) until args.empty?
|
|
182
|
+
elsif arg.start_with?("-") && arg != "-"
|
|
183
|
+
option(arg, args)
|
|
184
|
+
else
|
|
185
|
+
media_argument(arg)
|
|
147
186
|
end
|
|
148
187
|
end
|
|
149
188
|
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
define_headless_options(opts)
|
|
189
|
+
def option(arg, args)
|
|
190
|
+
flag = arg.start_with?("--") && arg.include?("=") ? arg[0, arg.index("=")] : arg
|
|
191
|
+
option = find(flag)
|
|
192
|
+
restrict(option, flag)
|
|
193
|
+
if flag == arg
|
|
194
|
+
option.valued? ? valued_option(option.name, flag, take_value(flag, args)) : switch(option.name)
|
|
195
|
+
elsif option.valued?
|
|
196
|
+
valued_option(option.name, flag, arg[flag.size + 1, arg.size - flag.size - 1])
|
|
197
|
+
else
|
|
198
|
+
raise Error, "needless argument: #{arg}"
|
|
161
199
|
end
|
|
162
200
|
end
|
|
163
201
|
|
|
164
|
-
def
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
opts.on("-h", "--help", "Show this help") { @help = true }
|
|
202
|
+
def find(flag)
|
|
203
|
+
option = TABLE.find { |candidate| candidate.named?(flag) && takes?(candidate) }
|
|
204
|
+
raise Error, "invalid option: #{flag}" if option.nil?
|
|
205
|
+
|
|
206
|
+
option
|
|
170
207
|
end
|
|
171
208
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
opts.on("--read-only", "Mount a disk image write-protected, leaving its file unchanged") do
|
|
177
|
-
window_only("--read-only") { @read_only = true }
|
|
178
|
-
end
|
|
179
|
-
opts.on("--sound", "Play the SID through the host's audio device (F10 mutes)") do
|
|
180
|
-
window_only("--sound") { @sound = true }
|
|
181
|
-
end
|
|
182
|
-
opts.on("--true-drive", "Put a true 1541 on device 8 instead of the KERNAL traps") do
|
|
183
|
-
window_only("--true-drive") { @true_drive = true }
|
|
184
|
-
end
|
|
185
|
-
opts.on("--reu SIZE", %w[128 256 512 1024 2048 4096 8192 16384],
|
|
186
|
-
"Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384") do |size|
|
|
187
|
-
window_only("--reu") { @reu = size.to_i }
|
|
188
|
-
end
|
|
189
|
-
opts.on("--ntsc", "Run an NTSC C64, with the 6567R8 VIC-II, instead of a PAL one") do
|
|
190
|
-
window_only("--ntsc") { @ntsc = true }
|
|
191
|
-
end
|
|
192
|
-
opts.on("--verbose", "Print the display, sound and gamepad setup and the frame timing") do
|
|
193
|
-
window_only("--verbose") { @verbose = true }
|
|
194
|
-
end
|
|
209
|
+
# Notes an option only the window or only the headless player takes.
|
|
210
|
+
def restrict(option, flag)
|
|
211
|
+
@window_only << flag if option.needs == :window
|
|
212
|
+
@headless_only << flag if option.needs == :headless
|
|
195
213
|
end
|
|
196
214
|
|
|
197
|
-
def
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
define_rendering_options(opts)
|
|
202
|
-
opts.on("--quiet", "Don't report progress") { headless_only("--quiet") { @quiet = true } }
|
|
203
|
-
opts.on("--no-tui", "Play without the interactive display, even on a terminal") do
|
|
204
|
-
headless_only("--no-tui") { @tui = false }
|
|
205
|
-
end
|
|
215
|
+
def take_value(flag, args)
|
|
216
|
+
raise Error, "missing argument: #{flag}" if args.empty?
|
|
217
|
+
|
|
218
|
+
args.shift
|
|
206
219
|
end
|
|
207
220
|
|
|
208
|
-
def
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
221
|
+
def valued_option(name, flag, value)
|
|
222
|
+
case name
|
|
223
|
+
when "--subtune" then @subtune = number(flag, value)
|
|
224
|
+
when "--sid" then @sid_model = sid_model_for(value)
|
|
225
|
+
when "--reu" then @reu = reu_size(value)
|
|
226
|
+
when "--frames" then @frames = number(flag, value)
|
|
227
|
+
when "--screenshot" then @screenshot = value
|
|
228
|
+
when "--save-snapshot" then @save_snapshot = value
|
|
229
|
+
when "--at", "--script" then @timeline.concat(Event.given(name, value))
|
|
230
|
+
when "--audio-out" then @audio_out = value
|
|
231
|
+
when "--seconds" then @seconds = decimal(flag, value)
|
|
232
|
+
when "--songlengths" then @songlengths = value
|
|
233
|
+
when "--rate" then @rate = number(flag, value)
|
|
234
|
+
else @filter_chunk = number(flag, value)
|
|
214
235
|
end
|
|
215
236
|
end
|
|
216
237
|
|
|
217
|
-
def
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
238
|
+
def switch(name)
|
|
239
|
+
case name
|
|
240
|
+
when "--no-autostart" then @autostart = false
|
|
241
|
+
when "--read-only" then @read_only = true
|
|
242
|
+
when "--sound" then @sound = true
|
|
243
|
+
when "--no-sound" then @sound = false
|
|
244
|
+
when "--no-vsync" then @vsync = false
|
|
245
|
+
when "--verbose" then @verbose = true
|
|
246
|
+
when "--true-drive" then @true_drive = true
|
|
247
|
+
when "--ntsc" then @ntsc = true
|
|
248
|
+
when "--unpaced" then @paced = false
|
|
249
|
+
when "--disable-jit" then @jit = false
|
|
250
|
+
when "--help" then @help = true
|
|
251
|
+
when "--version" then @version = true
|
|
252
|
+
when "--headless" then @headless = true
|
|
253
|
+
when "--quiet" then @quiet = true
|
|
254
|
+
when "--all-subtunes" then @all_subtunes = true
|
|
255
|
+
else @tui = false
|
|
223
256
|
end
|
|
224
257
|
end
|
|
225
258
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
|
|
259
|
+
def sid_model_for(value)
|
|
260
|
+
return if value == "auto"
|
|
261
|
+
raise Error, "invalid argument: --sid #{value}" unless SID_MODELS.key?(value)
|
|
262
|
+
|
|
263
|
+
SID_MODELS[value]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def reu_size(value)
|
|
267
|
+
raise Error, "invalid argument: --reu #{value}" unless REU_SIZES.include?(value)
|
|
268
|
+
|
|
269
|
+
value.to_i
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def number(flag, value)
|
|
273
|
+
raise Error, "invalid argument: #{flag} #{value}" unless value.match?(/\A\d+\z/)
|
|
274
|
+
|
|
275
|
+
value.to_i
|
|
229
276
|
end
|
|
230
277
|
|
|
231
|
-
def
|
|
232
|
-
|
|
233
|
-
|
|
278
|
+
def decimal(flag, value)
|
|
279
|
+
raise Error, "invalid argument: #{flag} #{value}" unless value.match?(DECIMAL)
|
|
280
|
+
|
|
281
|
+
value.to_f
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def media_argument(arg)
|
|
285
|
+
raise Error, "unexpected argument: #{arg}" unless @sid_command || @media_path.nil?
|
|
286
|
+
|
|
287
|
+
@media_path = arg if @media_path.nil?
|
|
288
|
+
@tune_paths << arg
|
|
234
289
|
end
|
|
235
290
|
end
|
|
236
291
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class REU
|
|
5
|
+
class DMA
|
|
6
|
+
# A transfer's state for a snapshot (REU::SavedState).
|
|
7
|
+
module SavedState
|
|
8
|
+
# Everything a transfer holds between cycles. Before the first
|
|
9
|
+
# transfer, the fields it sets on starting are empty.
|
|
10
|
+
def save_state(out)
|
|
11
|
+
out.int(@c64).int(@expansion).int(@length).int(@events)
|
|
12
|
+
[@type, @c64_step, @expansion_step, @last, @ba_low_after_writes, @swap_byte, @held_read].each do |value|
|
|
13
|
+
out.optional_int(value)
|
|
14
|
+
end
|
|
15
|
+
[@holding, @waiting, @stopped_after_write, @swap_write_due, @swap_read_again, @moved_all,
|
|
16
|
+
@extra_cycle].each do |flag|
|
|
17
|
+
out.boolean(flag)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def load_state(input)
|
|
22
|
+
@c64 = input.int
|
|
23
|
+
@expansion = input.int
|
|
24
|
+
@length = input.int
|
|
25
|
+
@events = input.int
|
|
26
|
+
load_steps(input)
|
|
27
|
+
@holding = input.boolean?
|
|
28
|
+
@waiting = input.boolean?
|
|
29
|
+
@stopped_after_write = input.boolean?
|
|
30
|
+
@swap_write_due = input.boolean?
|
|
31
|
+
@swap_read_again = input.boolean?
|
|
32
|
+
@moved_all = input.boolean?
|
|
33
|
+
@extra_cycle = input.boolean?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def load_steps(input)
|
|
39
|
+
@type = input.optional_int
|
|
40
|
+
@c64_step = input.optional_int
|
|
41
|
+
@expansion_step = input.optional_int
|
|
42
|
+
@last = input.optional_int
|
|
43
|
+
@ba_low_after_writes = input.optional_int
|
|
44
|
+
@swap_byte = input.optional_int
|
|
45
|
+
@held_read = input.optional_int
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/badline/reu/dma.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/reu/dma/saved_state"
|
|
4
|
+
|
|
3
5
|
module Badline
|
|
4
6
|
class REU
|
|
5
7
|
# One transfer between the C64 and the REU's RAM, clocked a cycle at a
|
|
@@ -8,6 +10,8 @@ module Badline
|
|
|
8
10
|
# After a write it goes on through the first BA-low cycle and stops at
|
|
9
11
|
# the second. See doc/pinned-behaviour.md, REU DMA.
|
|
10
12
|
class DMA
|
|
13
|
+
include SavedState
|
|
14
|
+
|
|
11
15
|
# Transfer types, as the command register's bottom two bits give
|
|
12
16
|
# them.
|
|
13
17
|
STASH = 0
|
|
@@ -52,16 +56,32 @@ module Badline
|
|
|
52
56
|
@ba_low_after_writes = 0
|
|
53
57
|
@stopped_after_write = false
|
|
54
58
|
@swap_write_due = false
|
|
59
|
+
@swap_read_again = false
|
|
55
60
|
@swap_byte = 0
|
|
56
61
|
@held_read = 0
|
|
57
62
|
@moved_all = false
|
|
58
63
|
@extra_cycle = false
|
|
59
64
|
end
|
|
60
65
|
|
|
66
|
+
# Writes the byte a fetch from these addresses starts with, without
|
|
67
|
+
# counting it. Other transfers start with a read.
|
|
68
|
+
def write_ahead(type, c64, expansion)
|
|
69
|
+
return unless type == FETCH
|
|
70
|
+
|
|
71
|
+
@c64 = c64
|
|
72
|
+
byte = @ram.peek(expansion)
|
|
73
|
+
@ram.latch = byte
|
|
74
|
+
write_c64(byte)
|
|
75
|
+
end
|
|
76
|
+
|
|
61
77
|
# One cycle of the transfer. The cycle the REC lets go of the bus on
|
|
62
|
-
# is the CPU's.
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
# is the CPU's. late says BA fell on this cycle but a REC that moved a
|
|
79
|
+
# byte on the last one doesn't see it yet (VIC#reu_ba_late?), and
|
|
80
|
+
# handed_on that BA runs on from a bad line's DMA into a sprite's
|
|
81
|
+
# here (VIC#reu_ba_handed_on?).
|
|
82
|
+
def cycle!(ba_low, late, handed_on)
|
|
83
|
+
note_ba(ba_low, late)
|
|
84
|
+
@swap_read_again ||= handed_on && @swap_write_due
|
|
65
85
|
settle_held_read(ba_low) if @held_read.positive?
|
|
66
86
|
return if @waiting && ba_low
|
|
67
87
|
|
|
@@ -69,60 +89,38 @@ module Badline
|
|
|
69
89
|
@moved_all ? wind_down : move(ba_low)
|
|
70
90
|
end
|
|
71
91
|
|
|
72
|
-
# Everything a transfer holds between cycles. Before the first
|
|
73
|
-
# transfer, the fields it sets on starting are empty.
|
|
74
|
-
def save_state(out)
|
|
75
|
-
out.int(@c64).int(@expansion).int(@length).int(@events)
|
|
76
|
-
[@type, @c64_step, @expansion_step, @last, @ba_low_after_writes, @swap_byte, @held_read].each do |value|
|
|
77
|
-
out.optional_int(value)
|
|
78
|
-
end
|
|
79
|
-
[@holding, @waiting, @stopped_after_write, @swap_write_due, @moved_all, @extra_cycle].each do |flag|
|
|
80
|
-
out.boolean(flag)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def load_state(input)
|
|
85
|
-
@c64 = input.int
|
|
86
|
-
@expansion = input.int
|
|
87
|
-
@length = input.int
|
|
88
|
-
@events = input.int
|
|
89
|
-
load_steps(input)
|
|
90
|
-
@holding = input.boolean?
|
|
91
|
-
@waiting = input.boolean?
|
|
92
|
-
@stopped_after_write = input.boolean?
|
|
93
|
-
@swap_write_due = input.boolean?
|
|
94
|
-
@moved_all = input.boolean?
|
|
95
|
-
@extra_cycle = input.boolean?
|
|
96
|
-
end
|
|
97
|
-
|
|
98
92
|
private
|
|
99
93
|
|
|
100
|
-
def load_steps(input)
|
|
101
|
-
@type = input.optional_int
|
|
102
|
-
@c64_step = input.optional_int
|
|
103
|
-
@expansion_step = input.optional_int
|
|
104
|
-
@last = input.optional_int
|
|
105
|
-
@ba_low_after_writes = input.optional_int
|
|
106
|
-
@swap_byte = input.optional_int
|
|
107
|
-
@held_read = input.optional_int
|
|
108
|
-
end
|
|
109
|
-
|
|
110
94
|
# The second BA-low cycle in a row after writes stops the REC, and so
|
|
111
95
|
# does any BA-low cycle after a read. It then waits for BA to rise.
|
|
112
|
-
def note_ba(ba_low)
|
|
96
|
+
def note_ba(ba_low, late)
|
|
97
|
+
seen = ba_low && !late
|
|
113
98
|
if @last == READ
|
|
114
|
-
|
|
99
|
+
note_ba_after_read(seen)
|
|
115
100
|
elsif @last == WRITE
|
|
116
|
-
|
|
117
|
-
@stopped_after_write = @ba_low_after_writes == 2
|
|
118
|
-
if @stopped_after_write
|
|
119
|
-
@waiting = true
|
|
120
|
-
@ba_low_after_writes = 0
|
|
121
|
-
end
|
|
101
|
+
note_ba_after_write(seen)
|
|
122
102
|
end
|
|
123
103
|
@last = IDLE
|
|
124
104
|
end
|
|
125
105
|
|
|
106
|
+
# A swap's read that BA stops before its write is made again.
|
|
107
|
+
def note_ba_after_read(ba_low)
|
|
108
|
+
@waiting = ba_low
|
|
109
|
+
@ba_low_after_writes = 0
|
|
110
|
+
@swap_read_again = ba_low && @swap_write_due && @held_read.zero?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# A fetch's last write doesn't go through the first BA-low cycle.
|
|
114
|
+
# Pinned by REU/reutiming2/d and d2.
|
|
115
|
+
def note_ba_after_write(ba_low)
|
|
116
|
+
@ba_low_after_writes = ba_low ? @ba_low_after_writes + 1 : 0
|
|
117
|
+
@stopped_after_write = @ba_low_after_writes == 2 || (ba_low && @type == FETCH && @length == 1)
|
|
118
|
+
return unless @stopped_after_write
|
|
119
|
+
|
|
120
|
+
@waiting = true
|
|
121
|
+
@ba_low_after_writes = 0
|
|
122
|
+
end
|
|
123
|
+
|
|
126
124
|
# A fetch or a swap whose last write ran into BA, or a verify that
|
|
127
125
|
# found a difference before its last byte, takes one more cycle before
|
|
128
126
|
# the REC lets go of the bus.
|
|
@@ -166,6 +164,8 @@ module Badline
|
|
|
166
164
|
# Two cycles a byte: the C64's byte is read and stored while the
|
|
167
165
|
# REU's is fetched, and the REU's is written to the C64 on the next.
|
|
168
166
|
def swap(ba_low)
|
|
167
|
+
return read_swap_again if @swap_read_again
|
|
168
|
+
|
|
169
169
|
if @swap_write_due
|
|
170
170
|
@swap_write_due = false
|
|
171
171
|
write_c64(@swap_byte)
|
|
@@ -196,6 +196,15 @@ module Badline
|
|
|
196
196
|
@ba_low_after_writes = 0
|
|
197
197
|
end
|
|
198
198
|
|
|
199
|
+
# A swap's read that BA cut off from its write is made again when BA
|
|
200
|
+
# rises, and the write follows it. Pinned by REU/reutiming2/e4-m2,
|
|
201
|
+
# e6-m2 and g3-m2.
|
|
202
|
+
def read_swap_again
|
|
203
|
+
@swap_read_again = false
|
|
204
|
+
@ram.poke(@expansion, read_c64)
|
|
205
|
+
@last = READ
|
|
206
|
+
end
|
|
207
|
+
|
|
199
208
|
def settle_held_read(ba_low)
|
|
200
209
|
@held_read -= 1
|
|
201
210
|
return if @held_read.positive? && ba_low
|
|
@@ -13,7 +13,7 @@ module Badline
|
|
|
13
13
|
out.marker("REU")
|
|
14
14
|
[@status, @command, @c64, @c64_start, @expansion, @expansion_start, @bank, @bank_start, @length,
|
|
15
15
|
@length_start, @interrupt_mask, @address_control].each { |value| out.int(value) }
|
|
16
|
-
out.boolean(@armed).boolean(@requested).boolean(@running).boolean(@irq)
|
|
16
|
+
out.boolean(@armed).boolean(@requested).boolean(@running).boolean(@irq).boolean(@ba_was_low)
|
|
17
17
|
@dma.save_state(out)
|
|
18
18
|
@ram.save_state(out)
|
|
19
19
|
end
|
|
@@ -27,6 +27,7 @@ module Badline
|
|
|
27
27
|
@requested = input.boolean?
|
|
28
28
|
@running = input.boolean?
|
|
29
29
|
@irq = input.boolean?
|
|
30
|
+
@ba_was_low = input.boolean?
|
|
30
31
|
@dma.load_state(input)
|
|
31
32
|
@ram.load_state(input)
|
|
32
33
|
end
|