badline 0.6.0 → 0.6.1
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 +40 -0
- data/CLAUDE.md +10 -9
- data/README.md +24 -10
- data/doc/pinned-behaviour.md +131 -30
- data/exe/badline-ruby +20 -24
- data/lib/badline/address_bus.rb +10 -1
- data/lib/badline/audio/cli.rb +18 -5
- data/lib/badline/audio/console.rb +2 -1
- data/lib/badline/audio.rb +0 -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 +72 -0
- data/lib/badline/frontend/boot.rb +52 -0
- data/lib/badline/frontend/controls.rb +163 -0
- data/lib/badline/frontend/drive_led.rb +51 -0
- data/lib/badline/frontend/frame_rate.rb +83 -0
- data/lib/badline/frontend/gamepads.rb +76 -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/screen.rb +65 -0
- data/lib/badline/frontend/screenshot.rb +22 -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.rb +18 -0
- data/lib/badline/options/event.rb +94 -0
- data/lib/badline/options/help.rb +78 -0
- data/lib/badline/options/option.rb +29 -0
- data/lib/badline/options/table.rb +62 -0
- data/lib/badline/options.rb +181 -145
- 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 +123 -51
- data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
- data/lib/badline/sid/waveform.rb +2 -3
- data/lib/badline/snapshot/state.rb +1 -1
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +21 -0
- data/lib/badline/vic.rb +15 -6
- metadata +26 -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,106 @@
|
|
|
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"
|
|
4
7
|
|
|
5
8
|
module Badline
|
|
6
|
-
# The command line of
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
9
|
+
# The command line of both builds, badline-ruby and the native badline.
|
|
10
|
+
# Either opens the window for any media, or with --headless or
|
|
11
|
+
# --audio-out plays or renders a .sid tune without one. The native build
|
|
12
|
+
# adds --no-sound and --version, and badline-ruby adds --disable-jit.
|
|
13
|
+
#
|
|
14
|
+
# TABLE lists the options, and both the parser and the help read it. It
|
|
15
|
+
# parses by hand, inside the subset of Ruby Spinel compiles. Values come
|
|
16
|
+
# as `--song 2` or `--song=2`, and `--` ends the options. It loads
|
|
17
|
+
# nothing else from badline, so badline-ruby can parse and reject its
|
|
18
|
+
# arguments before paying for the emulator.
|
|
10
19
|
class Options
|
|
11
20
|
class Error < StandardError; end
|
|
12
21
|
|
|
13
22
|
FALLBACK_SECONDS = 60.0
|
|
14
23
|
|
|
15
|
-
DEFAULT_RATE = 44_100
|
|
16
|
-
|
|
17
24
|
SID_MODELS = { "6581" => :mos6581, "8580" => :mos8580 }.freeze
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
Usage: badline-ruby [options] [media]
|
|
21
|
-
badline-ruby --headless [options] tune.sid
|
|
22
|
-
badline-ruby [options] tune.sid --audio-out FILE
|
|
23
|
-
|
|
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, a .vsf snapshot, or a directory
|
|
27
|
-
to mount as device 8. It opens in the emulator window, where F11
|
|
28
|
-
saves a snapshot and F12 restores it. Device 8 answers through
|
|
29
|
-
traps on the KERNAL's disk routines, unless --true-drive puts an
|
|
30
|
-
emulated 1541 there, which runs its own DOS and reads .d64 and .g64
|
|
31
|
-
images only.
|
|
32
|
-
|
|
33
|
-
--headless plays a .sid tune on the host's audio device without the
|
|
34
|
-
window, and --audio-out renders it to 16-bit PCM instead. The
|
|
35
|
-
container follows the file's extension, .wav or .aiff. Played on a
|
|
36
|
-
terminal, n or → skips to the next song, p or ← to the previous one,
|
|
37
|
-
space pauses and q quits.
|
|
38
|
-
|
|
39
|
-
A .sid file carries no length of its own. Without --seconds the tune is
|
|
40
|
-
looked up by MD5 in HVSC's Songlengths.md5, taken from --songlengths, from
|
|
41
|
-
the DOCUMENTS directory of an HVSC collection above the tune, or from
|
|
42
|
-
$HVSC_BASE. Failing all of those it runs for #{FALLBACK_SECONDS.to_i} seconds.
|
|
43
|
-
|
|
44
|
-
Without the window, PSID tunes run on a bare CPU and SID, faster than
|
|
45
|
-
real time. RSID tunes boot the whole machine and run at about half
|
|
46
|
-
real time, so they stutter when played. --filter-chunk 1 runs the
|
|
47
|
-
filter cycle by cycle, which is exact but takes about twice as long.
|
|
26
|
+
REU_SIZES = %w[128 256 512 1024 2048 4096 8192 16384].freeze
|
|
48
27
|
|
|
49
|
-
|
|
28
|
+
# A Float as OptionParser took one: digits with an optional sign,
|
|
29
|
+
# fraction and exponent.
|
|
30
|
+
DECIMAL = /\A[-+]?(\d+(\.\d+)?|\.\d+)([eE][-+]?\d+)?\z/
|
|
50
31
|
|
|
51
|
-
attr_reader :
|
|
32
|
+
attr_reader :program, :media_path, :song, :sid_model, :reu, :frames, :screenshot, :save_snapshot, :audio_out,
|
|
33
|
+
:seconds, :songlengths, :filter_chunk, :timeline
|
|
52
34
|
|
|
53
|
-
|
|
35
|
+
def self.parse(argv, native: false) = new(native:).parse(argv)
|
|
54
36
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
37
|
+
# The native build plays the SID in the window unless --no-sound says
|
|
38
|
+
# otherwise, and badline-ruby, which runs below real time, only with
|
|
39
|
+
# --sound.
|
|
40
|
+
def initialize(native: false)
|
|
41
|
+
@native = native
|
|
42
|
+
@program = native ? "badline" : "badline-ruby"
|
|
43
|
+
@media_path = nil
|
|
44
|
+
@song = nil
|
|
45
|
+
@sid_model = nil
|
|
46
|
+
@reu = nil
|
|
58
47
|
@autostart = true
|
|
59
48
|
@read_only = false
|
|
60
|
-
@sound =
|
|
49
|
+
@sound = native
|
|
50
|
+
@frames = 0
|
|
51
|
+
@paced = true
|
|
52
|
+
@vsync = true
|
|
61
53
|
@verbose = false
|
|
62
54
|
@true_drive = false
|
|
63
55
|
@ntsc = false
|
|
64
|
-
@
|
|
56
|
+
@screenshot = ""
|
|
57
|
+
@save_snapshot = ""
|
|
58
|
+
@timeline = []
|
|
65
59
|
@jit = true
|
|
60
|
+
@help = false
|
|
61
|
+
@version = false
|
|
62
|
+
@headless = false
|
|
63
|
+
@audio_out = nil
|
|
64
|
+
@seconds = nil
|
|
65
|
+
@songlengths = nil
|
|
66
|
+
@rate = nil
|
|
67
|
+
@filter_chunk = nil
|
|
66
68
|
@quiet = false
|
|
67
69
|
@tui = true
|
|
68
|
-
@help = false
|
|
69
70
|
@window_only = []
|
|
70
71
|
@headless_only = []
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
def parse(argv)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
validate unless help?
|
|
75
|
+
args = argv.dup
|
|
76
|
+
argument(args.shift, args) until args.empty?
|
|
77
|
+
validate unless help? || version?
|
|
78
78
|
self
|
|
79
|
-
rescue OptionParser::ParseError => e
|
|
80
|
-
raise Error, e.message
|
|
81
79
|
end
|
|
82
80
|
|
|
81
|
+
# Whether the media is a .vsf snapshot to restore.
|
|
82
|
+
def snapshot? = !@media_path.nil? && File.extname(@media_path).casecmp?(".vsf")
|
|
83
|
+
|
|
83
84
|
# Plays or renders a .sid tune without the window.
|
|
84
85
|
def headless? = @headless || render?
|
|
85
86
|
|
|
86
87
|
def window? = !headless?
|
|
87
88
|
|
|
88
|
-
def render? =
|
|
89
|
+
def render? = !@audio_out.nil?
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
# The tune --headless and --audio-out play, for Audio::CLI.
|
|
92
|
+
def tune_path = @media_path
|
|
91
93
|
|
|
92
94
|
# How long to play a tune whose length nothing gives.
|
|
93
95
|
def fallback_seconds = FALLBACK_SECONDS
|
|
94
96
|
|
|
97
|
+
def rate = @rate.nil? ? DEFAULT_RATE : @rate
|
|
98
|
+
|
|
95
99
|
def rate_given? = !@rate.nil?
|
|
96
100
|
|
|
97
|
-
def
|
|
101
|
+
def quiet? = @quiet
|
|
98
102
|
|
|
99
|
-
def
|
|
103
|
+
def tui? = @tui
|
|
100
104
|
|
|
101
105
|
def autostart? = @autostart
|
|
102
106
|
|
|
@@ -104,133 +108,165 @@ module Badline
|
|
|
104
108
|
|
|
105
109
|
def sound? = @sound
|
|
106
110
|
|
|
111
|
+
def paced? = @paced
|
|
112
|
+
|
|
113
|
+
def vsync? = @vsync
|
|
114
|
+
|
|
107
115
|
def verbose? = @verbose
|
|
108
116
|
|
|
109
117
|
def true_drive? = @true_drive
|
|
110
118
|
|
|
111
119
|
def ntsc? = @ntsc
|
|
112
120
|
|
|
113
|
-
def
|
|
121
|
+
def jit? = @jit
|
|
114
122
|
|
|
115
|
-
def
|
|
123
|
+
def help? = @help
|
|
116
124
|
|
|
117
|
-
def
|
|
125
|
+
def version? = @version
|
|
118
126
|
|
|
119
127
|
private
|
|
120
128
|
|
|
121
|
-
def
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
def takes?(option) = [:both, @native ? :native : :ruby].include?(option.build)
|
|
130
|
+
|
|
131
|
+
def argument(arg, args)
|
|
132
|
+
if arg == "--"
|
|
133
|
+
media_argument(args.shift) until args.empty?
|
|
134
|
+
elsif arg.start_with?("-") && arg != "-"
|
|
135
|
+
option(arg, args)
|
|
136
|
+
else
|
|
137
|
+
media_argument(arg)
|
|
138
|
+
end
|
|
125
139
|
end
|
|
126
140
|
|
|
127
|
-
def
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
def option(arg, args)
|
|
142
|
+
flag = arg.start_with?("--") && arg.include?("=") ? arg[0, arg.index("=")] : arg
|
|
143
|
+
option = find(flag)
|
|
144
|
+
restrict(option, flag)
|
|
145
|
+
if flag == arg
|
|
146
|
+
option.valued? ? valued_option(option.name, flag, take_value(flag, args)) : switch(option.name)
|
|
147
|
+
elsif option.valued?
|
|
148
|
+
valued_option(option.name, flag, arg[flag.size + 1, arg.size - flag.size - 1])
|
|
149
|
+
else
|
|
150
|
+
raise Error, "needless argument: #{arg}"
|
|
134
151
|
end
|
|
135
152
|
end
|
|
136
153
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
raise Error, "invalid
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
def find(flag)
|
|
155
|
+
option = TABLE.find { |candidate| candidate.named?(flag) && takes?(candidate) }
|
|
156
|
+
raise Error, "invalid option: #{flag}" if option.nil?
|
|
157
|
+
|
|
158
|
+
option
|
|
142
159
|
end
|
|
143
160
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
161
|
+
# Notes an option only the window or only the headless player takes.
|
|
162
|
+
def restrict(option, flag)
|
|
163
|
+
@window_only << flag if option.needs == :window
|
|
164
|
+
@headless_only << flag if option.needs == :headless
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def take_value(flag, args)
|
|
168
|
+
raise Error, "missing argument: #{flag}" if args.empty?
|
|
169
|
+
|
|
170
|
+
args.shift
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def valued_option(name, flag, value)
|
|
174
|
+
case name
|
|
175
|
+
when "--song" then @song = number(flag, value)
|
|
176
|
+
when "--sid" then @sid_model = sid_model_for(value)
|
|
177
|
+
when "--reu" then @reu = reu_size(value)
|
|
178
|
+
when "--frames" then @frames = number(flag, value)
|
|
179
|
+
when "--screenshot" then @screenshot = value
|
|
180
|
+
when "--save-snapshot" then @save_snapshot = value
|
|
181
|
+
when "--at", "--script" then @timeline.concat(Event.given(name, value))
|
|
182
|
+
when "--audio-out" then @audio_out = value
|
|
183
|
+
when "--seconds" then @seconds = decimal(flag, value)
|
|
184
|
+
when "--songlengths" then @songlengths = value
|
|
185
|
+
when "--rate" then @rate = number(flag, value)
|
|
186
|
+
else @filter_chunk = number(flag, value)
|
|
147
187
|
end
|
|
148
188
|
end
|
|
149
189
|
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
190
|
+
def switch(name)
|
|
191
|
+
case name
|
|
192
|
+
when "--no-autostart" then @autostart = false
|
|
193
|
+
when "--read-only" then @read_only = true
|
|
194
|
+
when "--sound" then @sound = true
|
|
195
|
+
when "--no-sound" then @sound = false
|
|
196
|
+
when "--no-vsync" then @vsync = false
|
|
197
|
+
when "--verbose" then @verbose = true
|
|
198
|
+
when "--true-drive" then @true_drive = true
|
|
199
|
+
when "--ntsc" then @ntsc = true
|
|
200
|
+
when "--unpaced" then @paced = false
|
|
201
|
+
when "--disable-jit" then @jit = false
|
|
202
|
+
when "--help" then @help = true
|
|
203
|
+
when "--version" then @version = true
|
|
204
|
+
when "--headless" then @headless = true
|
|
205
|
+
when "--quiet" then @quiet = true
|
|
206
|
+
else @tui = false
|
|
161
207
|
end
|
|
162
208
|
end
|
|
163
209
|
|
|
164
|
-
def
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
opts.on("--disable-jit", "Run without enabling YJIT") { @jit = false }
|
|
169
|
-
opts.on("-h", "--help", "Show this help") { @help = true }
|
|
210
|
+
def sid_model_for(value)
|
|
211
|
+
raise Error, "invalid argument: --sid #{value}" unless SID_MODELS.key?(value)
|
|
212
|
+
|
|
213
|
+
SID_MODELS[value]
|
|
170
214
|
end
|
|
171
215
|
|
|
172
|
-
def
|
|
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
|
|
216
|
+
def reu_size(value)
|
|
217
|
+
raise Error, "invalid argument: --reu #{value}" unless REU_SIZES.include?(value)
|
|
218
|
+
|
|
219
|
+
value.to_i
|
|
195
220
|
end
|
|
196
221
|
|
|
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
|
|
222
|
+
def number(flag, value)
|
|
223
|
+
raise Error, "invalid argument: #{flag} #{value}" unless value.match?(/\A\d+\z/)
|
|
224
|
+
|
|
225
|
+
value.to_i
|
|
206
226
|
end
|
|
207
227
|
|
|
208
|
-
def
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
opts.on("--songlengths PATH", "HVSC Songlengths.md5 to take the length from") do |path|
|
|
213
|
-
headless_only("--songlengths") { @songlengths = path }
|
|
214
|
-
end
|
|
228
|
+
def decimal(flag, value)
|
|
229
|
+
raise Error, "invalid argument: #{flag} #{value}" unless value.match?(DECIMAL)
|
|
230
|
+
|
|
231
|
+
value.to_f
|
|
215
232
|
end
|
|
216
233
|
|
|
217
|
-
def
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
234
|
+
def media_argument(arg)
|
|
235
|
+
raise Error, "unexpected argument: #{arg}" unless @media_path.nil?
|
|
236
|
+
|
|
237
|
+
@media_path = arg
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def validate
|
|
241
|
+
validate_mode
|
|
242
|
+
validate_numbers
|
|
243
|
+
validate_paths
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def validate_mode
|
|
247
|
+
if headless?
|
|
248
|
+
raise Error, "#{@window_only.first} needs the window" unless @window_only.empty?
|
|
249
|
+
raise Error, "no tune given" if @media_path.nil?
|
|
250
|
+
raise Error, "not a .sid tune: #{@media_path}" unless File.extname(@media_path).casecmp?(".sid")
|
|
251
|
+
elsif !@headless_only.empty?
|
|
252
|
+
raise Error, "#{@headless_only.first} needs --headless or --audio-out"
|
|
223
253
|
end
|
|
224
254
|
end
|
|
225
255
|
|
|
226
|
-
def
|
|
227
|
-
@
|
|
228
|
-
|
|
256
|
+
def validate_numbers
|
|
257
|
+
positive("--song", @song)
|
|
258
|
+
positive("--seconds", @seconds)
|
|
259
|
+
positive("--rate", @rate)
|
|
260
|
+
positive("--filter-chunk", @filter_chunk)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def positive(name, value)
|
|
264
|
+
raise Error, "invalid argument: #{name} #{value}" if !value.nil? && value <= 0
|
|
229
265
|
end
|
|
230
266
|
|
|
231
|
-
def
|
|
232
|
-
@
|
|
233
|
-
|
|
267
|
+
def validate_paths
|
|
268
|
+
raise Error, "no such file or directory: #{@media_path}" unless @media_path.nil? || File.exist?(@media_path)
|
|
269
|
+
raise Error, "no such file or directory: #{@songlengths}" unless @songlengths.nil? || File.exist?(@songlengths)
|
|
234
270
|
end
|
|
235
271
|
end
|
|
236
272
|
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
|