badline 0.3.0 → 0.4.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/.claude/hooks/session-start.sh +57 -0
- data/.claude/settings.json +14 -0
- data/CHANGELOG.md +58 -0
- data/CLAUDE.md +45 -16
- data/CONTRIBUTING.md +95 -3
- data/README.md +111 -61
- data/SECURITY.md +9 -0
- data/doc/pinned-behaviour.md +275 -33
- data/exe/badline-ruby +59 -0
- data/lib/badline/address_bus.rb +22 -10
- data/lib/badline/audio/aiff.rb +8 -3
- data/lib/badline/audio/cli.rb +17 -12
- data/lib/badline/audio/console.rb +12 -58
- data/lib/badline/audio/playback.rb +3 -0
- data/lib/badline/audio/sdl_sink.rb +20 -52
- data/lib/badline/audio/terminal.rb +90 -0
- data/lib/badline/audio.rb +2 -2
- data/lib/badline/cartridge/geo_ram.rb +63 -0
- data/lib/badline/cartridge.rb +1 -0
- data/lib/badline/cia/interrupt_register.rb +34 -10
- data/lib/badline/cia.rb +15 -8
- data/lib/badline/computer.rb +28 -13
- data/lib/badline/gui/application.rb +33 -21
- data/lib/badline/gui/gamepads.rb +22 -22
- data/lib/badline/gui/joy_map.rb +1 -1
- data/lib/badline/gui/key_map.rb +6 -1
- data/lib/badline/gui/pane.rb +7 -6
- data/lib/badline/gui/screen_pane.rb +9 -13
- data/lib/badline/gui/window.rb +35 -20
- data/lib/badline/gui.rb +1 -1
- data/lib/badline/input/mouse1351.rb +21 -6
- data/lib/badline/input/paddles.rb +11 -6
- data/lib/badline/kernal_trap/channel.rb +3 -0
- data/lib/badline/kernal_trap/drive/block_commands.rb +33 -4
- data/lib/badline/kernal_trap/drive/status.rb +3 -0
- data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
- data/lib/badline/kernal_trap/drive/writes.rb +92 -0
- data/lib/badline/kernal_trap/drive.rb +51 -51
- data/lib/badline/kernal_trap/file.rb +6 -9
- data/lib/badline/kernal_trap/save.rb +9 -4
- data/lib/badline/keyboard.rb +19 -3
- data/lib/badline/media.rb +42 -11
- data/lib/badline/options.rb +209 -0
- data/lib/badline/ram_expansion/banking.rb +30 -0
- data/lib/badline/ram_expansion/plus256k.rb +38 -0
- data/lib/badline/ram_expansion/plus60k.rb +31 -0
- data/lib/badline/ram_expansion/unexpanded.rb +22 -0
- data/lib/badline/ram_expansion.rb +21 -0
- data/lib/badline/region.rb +51 -0
- data/lib/badline/sdl/events.rb +47 -0
- data/lib/badline/sdl/functions.rb +73 -0
- data/lib/badline/sdl.rb +67 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
- data/lib/badline/sid/waveform.rb +1 -1
- data/lib/badline/sid.rb +3 -1
- data/lib/badline/storage/d64_image.rb +10 -0
- data/lib/badline/storage/d71_image.rb +12 -0
- data/lib/badline/storage/d81_image.rb +9 -0
- data/lib/badline/storage/disk_image/bam.rb +111 -0
- data/lib/badline/storage/disk_image/directory.rb +79 -0
- data/lib/badline/storage/disk_image/writing.rb +110 -0
- data/lib/badline/storage/disk_image.rb +27 -4
- data/lib/badline/storage/host_directory.rb +2 -1
- data/lib/badline/storage.rb +15 -0
- data/lib/badline/time_of_day.rb +4 -2
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +104 -0
- data/lib/badline/via/interrupt_register.rb +58 -0
- data/lib/badline/via/shift_register.rb +135 -0
- data/lib/badline/via/timer1.rb +82 -0
- data/lib/badline/via/timer2.rb +84 -0
- data/lib/badline/via.rb +262 -0
- data/lib/badline/vic/bank.rb +4 -3
- data/lib/badline/vic/color_patches.rb +6 -3
- data/lib/badline/vic/display_state.rb +4 -0
- data/lib/badline/vic/graphics_shifter.rb +42 -7
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic/sequencer.rb +18 -18
- data/lib/badline/vic/sequencer_output.rb +4 -4
- data/lib/badline/vic/sprite/internal_bus.rb +4 -0
- data/lib/badline/vic/sprite/shifter.rb +25 -3
- data/lib/badline/vic/sprite.rb +5 -4
- data/lib/badline/vic/sprites.rb +8 -3
- data/lib/badline/vic.rb +93 -44
- data/lib/badline.rb +4 -0
- data/packaging/homebrew/badline.rb +34 -0
- metadata +30 -20
- data/exe/badline +0 -78
- data/exe/badline-sid +0 -34
- data/lib/badline/audio/options.rb +0 -137
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
# The command line of `badline-ruby`. It opens the window for any media,
|
|
7
|
+
# or with --headless or --audio-out plays or renders a .sid tune without
|
|
8
|
+
# one. It loads nothing else from badline, so the executable can parse
|
|
9
|
+
# and reject its arguments before paying for the emulator.
|
|
10
|
+
class Options
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
FALLBACK_SECONDS = 60.0
|
|
14
|
+
|
|
15
|
+
DEFAULT_RATE = 44_100
|
|
16
|
+
|
|
17
|
+
SID_MODELS = { "6581" => :mos6581, "8580" => :mos8580 }.freeze
|
|
18
|
+
|
|
19
|
+
BANNER = <<~BANNER.freeze
|
|
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,
|
|
25
|
+
a .t64 tape archive, a .tap tape, a .crt cartridge, a .sid tune, or
|
|
26
|
+
a directory to mount as device 8. It opens in the emulator window.
|
|
27
|
+
|
|
28
|
+
--headless plays a .sid tune on the host's audio device without the
|
|
29
|
+
window, and --audio-out renders it to 16-bit PCM instead. The
|
|
30
|
+
container follows the file's extension, .wav or .aiff. Played on a
|
|
31
|
+
terminal, n or → skips to the next song, p or ← to the previous one,
|
|
32
|
+
space pauses and q quits.
|
|
33
|
+
|
|
34
|
+
A .sid file carries no length of its own. Without --seconds the tune is
|
|
35
|
+
looked up by MD5 in HVSC's Songlengths.md5, taken from --songlengths, from
|
|
36
|
+
the DOCUMENTS directory of an HVSC collection above the tune, or from
|
|
37
|
+
$HVSC_BASE. Failing all of those it runs for #{FALLBACK_SECONDS.to_i} seconds.
|
|
38
|
+
|
|
39
|
+
Without the window, PSID tunes run on a bare CPU and SID, faster than
|
|
40
|
+
real time. RSID tunes boot the whole machine and run at about half
|
|
41
|
+
real time, so they stutter when played. --filter-chunk 1 runs the
|
|
42
|
+
filter cycle by cycle, which is exact but takes about twice as long.
|
|
43
|
+
|
|
44
|
+
BANNER
|
|
45
|
+
|
|
46
|
+
attr_reader :media_path, :audio_out, :song, :seconds, :songlengths, :sid_model, :filter_chunk
|
|
47
|
+
|
|
48
|
+
alias tune_path media_path
|
|
49
|
+
|
|
50
|
+
def self.parse(argv) = new.parse(argv)
|
|
51
|
+
|
|
52
|
+
def initialize
|
|
53
|
+
@autostart = true
|
|
54
|
+
@read_only = false
|
|
55
|
+
@sound = false
|
|
56
|
+
@headless = false
|
|
57
|
+
@jit = true
|
|
58
|
+
@quiet = false
|
|
59
|
+
@tui = true
|
|
60
|
+
@help = false
|
|
61
|
+
@window_only = []
|
|
62
|
+
@headless_only = []
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse(argv)
|
|
66
|
+
@media_path, *extra = parser.parse(argv)
|
|
67
|
+
raise Error, "unexpected argument: #{extra.first}" if extra.any?
|
|
68
|
+
|
|
69
|
+
validate unless help?
|
|
70
|
+
self
|
|
71
|
+
rescue OptionParser::ParseError => e
|
|
72
|
+
raise Error, e.message
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Plays or renders a .sid tune without the window.
|
|
76
|
+
def headless? = @headless || render?
|
|
77
|
+
|
|
78
|
+
def window? = !headless?
|
|
79
|
+
|
|
80
|
+
def render? = !audio_out.nil?
|
|
81
|
+
|
|
82
|
+
def rate = @rate || DEFAULT_RATE
|
|
83
|
+
|
|
84
|
+
# How long to play a tune whose length nothing gives.
|
|
85
|
+
def fallback_seconds = FALLBACK_SECONDS
|
|
86
|
+
|
|
87
|
+
def rate_given? = !@rate.nil?
|
|
88
|
+
|
|
89
|
+
def help = parser.help
|
|
90
|
+
|
|
91
|
+
def help? = @help
|
|
92
|
+
|
|
93
|
+
def autostart? = @autostart
|
|
94
|
+
|
|
95
|
+
def read_only? = @read_only
|
|
96
|
+
|
|
97
|
+
def sound? = @sound
|
|
98
|
+
|
|
99
|
+
def quiet? = @quiet
|
|
100
|
+
|
|
101
|
+
def tui? = @tui
|
|
102
|
+
|
|
103
|
+
def jit? = @jit
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def validate
|
|
108
|
+
validate_mode
|
|
109
|
+
validate_numbers
|
|
110
|
+
validate_paths
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def validate_mode
|
|
114
|
+
if headless?
|
|
115
|
+
raise Error, "#{@window_only.first} needs the window" if @window_only.any?
|
|
116
|
+
raise Error, "no tune given" unless media_path
|
|
117
|
+
raise Error, "not a .sid tune: #{media_path}" unless File.extname(media_path).casecmp?(".sid")
|
|
118
|
+
elsif @headless_only.any?
|
|
119
|
+
raise Error, "#{@headless_only.first} needs --headless or --audio-out"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def validate_numbers
|
|
124
|
+
raise Error, "invalid argument: --song #{song}" if song&.< 1
|
|
125
|
+
raise Error, "invalid argument: --seconds #{seconds}" unless seconds.nil? || seconds.positive?
|
|
126
|
+
raise Error, "invalid argument: --rate #{rate}" unless rate.positive?
|
|
127
|
+
raise Error, "invalid argument: --filter-chunk #{filter_chunk}" if filter_chunk&.< 1
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def validate_paths
|
|
131
|
+
[media_path, songlengths].compact.each do |path|
|
|
132
|
+
raise Error, "no such file or directory: #{path}" unless File.exist?(path)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def parser
|
|
137
|
+
@parser ||= OptionParser.new do |opts|
|
|
138
|
+
opts.banner = BANNER
|
|
139
|
+
opts.separator "Options:"
|
|
140
|
+
define_media_options(opts)
|
|
141
|
+
opts.separator ""
|
|
142
|
+
opts.separator "Window options:"
|
|
143
|
+
define_window_options(opts)
|
|
144
|
+
opts.separator ""
|
|
145
|
+
opts.separator "Options without the window:"
|
|
146
|
+
define_headless_options(opts)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def define_media_options(opts)
|
|
151
|
+
opts.on("-s", "--song N", Integer, "Subtune of a .sid, from 1 (default: the tune's own)") { |n| @song = n }
|
|
152
|
+
opts.on("--sid MODEL", SID_MODELS,
|
|
153
|
+
"SID to fit: 6581 or 8580 (default: a .sid tune's own, else 6581)") { |model| @sid_model = model }
|
|
154
|
+
opts.on("--disable-jit", "Run without enabling YJIT") { @jit = false }
|
|
155
|
+
opts.on("-h", "--help", "Show this help") { @help = true }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def define_window_options(opts)
|
|
159
|
+
opts.on("--no-autostart", "Boot to READY. instead of running the program") do
|
|
160
|
+
window_only("--no-autostart") { @autostart = false }
|
|
161
|
+
end
|
|
162
|
+
opts.on("--read-only", "Mount a disk image write-protected, leaving its file unchanged") do
|
|
163
|
+
window_only("--read-only") { @read_only = true }
|
|
164
|
+
end
|
|
165
|
+
opts.on("--sound", "Play the SID through the host's audio device (F10 mutes)") do
|
|
166
|
+
window_only("--sound") { @sound = true }
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def define_headless_options(opts)
|
|
171
|
+
opts.on("--headless", "Play a .sid tune in the terminal instead of the window") { @headless = true }
|
|
172
|
+
opts.on("--audio-out FILE", "Render a .sid tune to a .wav or .aiff file") { |path| @audio_out = path }
|
|
173
|
+
define_length_options(opts)
|
|
174
|
+
define_rendering_options(opts)
|
|
175
|
+
opts.on("--quiet", "Don't report progress") { headless_only("--quiet") { @quiet = true } }
|
|
176
|
+
opts.on("--no-tui", "Play without the interactive display, even on a terminal") do
|
|
177
|
+
headless_only("--no-tui") { @tui = false }
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def define_length_options(opts)
|
|
182
|
+
opts.on("--seconds N", Float, "Length to play (default: the tune's own)") do |n|
|
|
183
|
+
headless_only("--seconds") { @seconds = n }
|
|
184
|
+
end
|
|
185
|
+
opts.on("--songlengths PATH", "HVSC Songlengths.md5 to take the length from") do |path|
|
|
186
|
+
headless_only("--songlengths") { @songlengths = path }
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def define_rendering_options(opts)
|
|
191
|
+
opts.on("--rate HZ", Integer, "Sample rate (default: #{DEFAULT_RATE}, or the audio device's own)") do |n|
|
|
192
|
+
headless_only("--rate") { @rate = n }
|
|
193
|
+
end
|
|
194
|
+
opts.on("--filter-chunk N", Integer, "Filter step in cycles, 1 is exact (default: 4)") do |n|
|
|
195
|
+
headless_only("--filter-chunk") { @filter_chunk = n }
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def window_only(flag)
|
|
200
|
+
@window_only << flag
|
|
201
|
+
yield
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def headless_only(flag)
|
|
205
|
+
@headless_only << flag
|
|
206
|
+
yield
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module RAMExpansion
|
|
5
|
+
# The register and page mapping the expansions share. #map lays the
|
|
6
|
+
# selected banks out in the address bus's page tables, and #map_io puts
|
|
7
|
+
# the register over the VIC's I/O page at $D100.
|
|
8
|
+
module Banking
|
|
9
|
+
attr_reader :low_ram, :high_ram, :video_ram
|
|
10
|
+
|
|
11
|
+
# The block runs whenever the register moves a bank.
|
|
12
|
+
def on_change(&block)
|
|
13
|
+
@on_change = block
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def peek(_addr) = 0xff
|
|
17
|
+
|
|
18
|
+
def map(read_pages, write_pages)
|
|
19
|
+
[read_pages, write_pages].each do |pages|
|
|
20
|
+
pages.fill(@low_ram, 0x00, 0x10)
|
|
21
|
+
pages.fill(@high_ram, 0x10, 0xf0)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def map_io(read_pages, write_pages)
|
|
26
|
+
read_pages[0xd1] = write_pages[0xd1] = self
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module RAMExpansion
|
|
5
|
+
# The +256K: four 64K banks, the first of them the machine's own RAM.
|
|
6
|
+
# $D100 picks one for $0000-$0FFF (bits 0-1), one for the VIC (bits 2-3)
|
|
7
|
+
# and one for $1000-$FFFF (bits 6-7). Setting bit 4 locks the register
|
|
8
|
+
# until the next reset.
|
|
9
|
+
class Plus256k
|
|
10
|
+
include Banking
|
|
11
|
+
|
|
12
|
+
def initialize(ram)
|
|
13
|
+
@banks = [ram, Memory.new, Memory.new, Memory.new]
|
|
14
|
+
@on_change = nil
|
|
15
|
+
reset!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def poke(_addr, value)
|
|
19
|
+
return if @locked
|
|
20
|
+
|
|
21
|
+
@low_ram = @banks[value & 0b11]
|
|
22
|
+
@video_ram = @banks[(value >> 2) & 0b11]
|
|
23
|
+
@high_ram = @banks[value >> 6]
|
|
24
|
+
@locked = value.anybits?(0x10)
|
|
25
|
+
@on_change&.call
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reset!
|
|
29
|
+
@low_ram = @video_ram = @high_ram = @banks[0]
|
|
30
|
+
@locked = false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def power_on!
|
|
34
|
+
@banks.drop(1).each(&:clear!)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module RAMExpansion
|
|
5
|
+
# The +60K: a second bank of RAM behind $1000-$FFFF, selected by bit 7
|
|
6
|
+
# of $D100. $0000-$0FFF stays the machine's own RAM, and so does all of
|
|
7
|
+
# what the VIC sees.
|
|
8
|
+
class Plus60k
|
|
9
|
+
include Banking
|
|
10
|
+
|
|
11
|
+
def initialize(ram)
|
|
12
|
+
@low_ram = @video_ram = @high_ram = ram
|
|
13
|
+
@banks = [ram, Memory.new]
|
|
14
|
+
@on_change = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def poke(_addr, value)
|
|
18
|
+
@high_ram = @banks[value >> 7]
|
|
19
|
+
@on_change&.call
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset!
|
|
23
|
+
@high_ram = @banks[0]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def power_on!
|
|
27
|
+
@banks[1].clear!
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module RAMExpansion
|
|
5
|
+
# The machine with no expansion fitted: its own RAM everywhere, and
|
|
6
|
+
# $D100 left to the VIC.
|
|
7
|
+
class Unexpanded
|
|
8
|
+
include Banking
|
|
9
|
+
|
|
10
|
+
def initialize(ram)
|
|
11
|
+
@low_ram = @video_ram = @high_ram = ram
|
|
12
|
+
@on_change = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def map_io(_read_pages, _write_pages); end
|
|
16
|
+
|
|
17
|
+
def reset!; end
|
|
18
|
+
|
|
19
|
+
def power_on!; end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/ram_expansion/banking"
|
|
4
|
+
require "badline/ram_expansion/unexpanded"
|
|
5
|
+
require "badline/ram_expansion/plus60k"
|
|
6
|
+
require "badline/ram_expansion/plus256k"
|
|
7
|
+
|
|
8
|
+
module Badline
|
|
9
|
+
# RAM expansions that bank extra memory in under the CPU through a
|
|
10
|
+
# register at $D100, in the VIC's I/O page. Any write to $D100-$D1FF
|
|
11
|
+
# sets the register, which reads back as $FF.
|
|
12
|
+
module RAMExpansion
|
|
13
|
+
TYPES = { plus60k: Plus60k, plus256k: Plus256k }.freeze
|
|
14
|
+
|
|
15
|
+
def self.build(type, ram, &)
|
|
16
|
+
expansion = type ? TYPES.fetch(type).new(ram) : Unexpanded.new(ram)
|
|
17
|
+
expansion.on_change(&)
|
|
18
|
+
expansion
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
# = Region
|
|
5
|
+
#
|
|
6
|
+
# The timing and geometry a video standard gives the machine.
|
|
7
|
+
module Region
|
|
8
|
+
# The CPU clock, the VIC's raster, where the beam is blanked, the display
|
|
9
|
+
# window's edges, the part of the raster a front end shows, and the
|
|
10
|
+
# mains frequency that feeds the CIAs' TOD pins.
|
|
11
|
+
#
|
|
12
|
+
# Columns are the VIC's cycles within a line, counted from 0, and a
|
|
13
|
+
# column draws 8 pixels, so the raster is `cycles_per_line * 8` pixels
|
|
14
|
+
# wide. `hblank` and `vblank` are the [first, last] blanked column and
|
|
15
|
+
# line, inclusive, and wrap past the end of the line or frame when first
|
|
16
|
+
# is the larger. `display_x_bounds` holds the display window's
|
|
17
|
+
# [left, right] pixels per CSEL state, 38 columns then 40, and `crop`
|
|
18
|
+
# the [left, top, width, height] a front end shows.
|
|
19
|
+
Profile = Data.define(:name, :clock_hz, :cycles_per_line, :lines_per_frame, :hblank, :vblank,
|
|
20
|
+
:display_x_bounds, :crop, :mains_hz)
|
|
21
|
+
|
|
22
|
+
# The 6569 of a PAL C64: 63 cycles by 312 lines at 985,248 Hz, with
|
|
23
|
+
# 50 Hz mains.
|
|
24
|
+
PAL = Profile.new(
|
|
25
|
+
name: :pal, clock_hz: 985_248, cycles_per_line: 63, lines_per_frame: 312,
|
|
26
|
+
hblank: [61, 9].freeze, vblank: [300, 15].freeze,
|
|
27
|
+
display_x_bounds: [[135, 438].freeze, [128, 447].freeze].freeze,
|
|
28
|
+
crop: [96, 20, 384, 272].freeze, mains_hz: 50
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# The 6567R8 of an NTSC C64: 65 cycles by 263 lines at 1,022,727 Hz,
|
|
32
|
+
# with 60 Hz mains, and lines 13-40 blanked (Bauer). The machine does
|
|
33
|
+
# not run it yet: the blanked columns and the display window are PAL's
|
|
34
|
+
# stand-ins, and the crop leaves out lines 0-12, which show below the
|
|
35
|
+
# last line.
|
|
36
|
+
NTSC = Profile.new(
|
|
37
|
+
name: :ntsc, clock_hz: 1_022_727, cycles_per_line: 65, lines_per_frame: 263,
|
|
38
|
+
hblank: [61, 9].freeze, vblank: [13, 40].freeze,
|
|
39
|
+
display_x_bounds: [[135, 438].freeze, [128, 447].freeze].freeze,
|
|
40
|
+
crop: [96, 41, 384, 222].freeze, mains_hz: 60
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Whether a column or line lies in a [first, last] blanking span.
|
|
44
|
+
def self.blanked?(position, span)
|
|
45
|
+
first, last = span
|
|
46
|
+
return position.between?(first, last) if first <= last
|
|
47
|
+
|
|
48
|
+
position >= first || position <= last
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module SDL
|
|
5
|
+
KEY_TAB = 0x09
|
|
6
|
+
KEY_F10 = 0x4000_0043
|
|
7
|
+
KMOD_SHIFT = 0x0003
|
|
8
|
+
|
|
9
|
+
# The events the front end handles. SDL_Event is a 56-byte union, read
|
|
10
|
+
# here at the offsets its keyboard, mouse and controller members use.
|
|
11
|
+
Quit = Data.define
|
|
12
|
+
# A held key sends repeats, flagged at byte 13 of the event.
|
|
13
|
+
KeyDown = Data.define(:sym, :mod, :repeat) do
|
|
14
|
+
def initialize(sym:, mod:, repeat: false) = super
|
|
15
|
+
end
|
|
16
|
+
KeyUp = Data.define(:sym, :mod)
|
|
17
|
+
MouseMotion = Data.define(:xrel, :yrel)
|
|
18
|
+
MouseButton = Data.define(:button, :pressed)
|
|
19
|
+
ControllerDevice = Data.define
|
|
20
|
+
|
|
21
|
+
EVENT_SIZE = 56
|
|
22
|
+
|
|
23
|
+
# The next event the front end handles, skipping the others, or nil once
|
|
24
|
+
# the queue is empty.
|
|
25
|
+
def self.poll_event
|
|
26
|
+
buffer = (@event ||= Fiddle::Pointer.malloc(EVENT_SIZE, Fiddle::RUBY_FREE))
|
|
27
|
+
while PollEvent.call(buffer) == 1
|
|
28
|
+
event = decode(buffer.to_str(EVENT_SIZE))
|
|
29
|
+
return event if event
|
|
30
|
+
end
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.decode(bytes)
|
|
35
|
+
case bytes.unpack1("L")
|
|
36
|
+
when 0x100 then Quit.new
|
|
37
|
+
when 0x300 then KeyDown.new(*bytes.unpack("lS", offset: 20), bytes.getbyte(13) != 0)
|
|
38
|
+
when 0x301 then KeyUp.new(*bytes.unpack("lS", offset: 20))
|
|
39
|
+
when 0x400 then MouseMotion.new(*bytes.unpack("l2", offset: 28))
|
|
40
|
+
when 0x401, 0x402 then MouseButton.new(bytes.getbyte(16), bytes.getbyte(17) == 1)
|
|
41
|
+
when 0x653..0x655 then ControllerDevice.new
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.key_name(sym) = GetKeyName.call(sym)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module SDL
|
|
5
|
+
INIT_AUDIO = 0x10
|
|
6
|
+
INIT_VIDEO = 0x20
|
|
7
|
+
INIT_GAMECONTROLLER = 0x2000
|
|
8
|
+
INIT_EVENTS = 0x4000
|
|
9
|
+
|
|
10
|
+
WINDOWPOS_CENTERED = 0x2fff_0000
|
|
11
|
+
WINDOW_RESIZABLE = 0x20
|
|
12
|
+
RENDERER_ACCELERATED = 0x02
|
|
13
|
+
RENDERER_PRESENTVSYNC = 0x04
|
|
14
|
+
|
|
15
|
+
AUDIO_S16SYS = [1].pack("s").getbyte(0) == 1 ? 0x8010 : 0x9010
|
|
16
|
+
AUDIO_ALLOW_FREQUENCY_CHANGE = 0x01
|
|
17
|
+
|
|
18
|
+
CONTROLLER_AXIS_LEFTX = 0
|
|
19
|
+
CONTROLLER_AXIS_LEFTY = 1
|
|
20
|
+
CONTROLLER_BUTTON_A = 0
|
|
21
|
+
CONTROLLER_BUTTON_B = 1
|
|
22
|
+
CONTROLLER_BUTTON_X = 2
|
|
23
|
+
CONTROLLER_BUTTON_Y = 3
|
|
24
|
+
CONTROLLER_BUTTON_LEFTSHOULDER = 9
|
|
25
|
+
CONTROLLER_BUTTON_RIGHTSHOULDER = 10
|
|
26
|
+
CONTROLLER_BUTTON_DPAD_UP = 11
|
|
27
|
+
CONTROLLER_BUTTON_DPAD_DOWN = 12
|
|
28
|
+
CONTROLLER_BUTTON_DPAD_LEFT = 13
|
|
29
|
+
CONTROLLER_BUTTON_DPAD_RIGHT = 14
|
|
30
|
+
|
|
31
|
+
InitSubSystem = function("SDL_InitSubSystem", [UINT32], INT)
|
|
32
|
+
QuitSubSystem = function("SDL_QuitSubSystem", [UINT32], VOID)
|
|
33
|
+
GetError = function("SDL_GetError", [], STRING)
|
|
34
|
+
SetHint = function("SDL_SetHint", [STRING, STRING], INT)
|
|
35
|
+
|
|
36
|
+
CreateWindow = function("SDL_CreateWindow", [STRING, INT, INT, INT, INT, UINT32], POINTER)
|
|
37
|
+
SetWindowTitle = function("SDL_SetWindowTitle", [POINTER, STRING], VOID)
|
|
38
|
+
DestroyWindow = function("SDL_DestroyWindow", [POINTER], VOID)
|
|
39
|
+
GetCurrentDisplayMode = function("SDL_GetCurrentDisplayMode", [INT, POINTER], INT)
|
|
40
|
+
CreateRenderer = function("SDL_CreateRenderer", [POINTER, INT, UINT32], POINTER)
|
|
41
|
+
DestroyRenderer = function("SDL_DestroyRenderer", [POINTER], VOID)
|
|
42
|
+
RenderSetLogicalSize = function("SDL_RenderSetLogicalSize", [POINTER, INT, INT], INT)
|
|
43
|
+
SetRenderDrawColor = function("SDL_SetRenderDrawColor", [POINTER, UINT8, UINT8, UINT8, UINT8], INT)
|
|
44
|
+
RenderClear = function("SDL_RenderClear", [POINTER], INT)
|
|
45
|
+
RenderCopy = function("SDL_RenderCopy", [POINTER, POINTER, POINTER, POINTER], INT)
|
|
46
|
+
RenderPresent = function("SDL_RenderPresent", [POINTER], VOID)
|
|
47
|
+
CreateRGBSurfaceFrom = function("SDL_CreateRGBSurfaceFrom",
|
|
48
|
+
[POINTER, INT, INT, INT, INT, UINT32, UINT32, UINT32, UINT32], POINTER)
|
|
49
|
+
FreeSurface = function("SDL_FreeSurface", [POINTER], VOID)
|
|
50
|
+
CreateTextureFromSurface = function("SDL_CreateTextureFromSurface", [POINTER, POINTER], POINTER)
|
|
51
|
+
DestroyTexture = function("SDL_DestroyTexture", [POINTER], VOID)
|
|
52
|
+
|
|
53
|
+
PollEvent = function("SDL_PollEvent", [POINTER], INT)
|
|
54
|
+
PushEvent = function("SDL_PushEvent", [POINTER], INT)
|
|
55
|
+
GetKeyName = function("SDL_GetKeyName", [INT], STRING)
|
|
56
|
+
SetRelativeMouseMode = function("SDL_SetRelativeMouseMode", [INT], INT)
|
|
57
|
+
|
|
58
|
+
NumJoysticks = function("SDL_NumJoysticks", [], INT)
|
|
59
|
+
IsGameController = function("SDL_IsGameController", [INT], INT)
|
|
60
|
+
GameControllerOpen = function("SDL_GameControllerOpen", [INT], POINTER)
|
|
61
|
+
GameControllerName = function("SDL_GameControllerName", [POINTER], STRING)
|
|
62
|
+
GameControllerGetButton = function("SDL_GameControllerGetButton", [POINTER, INT], UINT8)
|
|
63
|
+
GameControllerGetAxis = function("SDL_GameControllerGetAxis", [POINTER, INT], INT16)
|
|
64
|
+
GameControllerClose = function("SDL_GameControllerClose", [POINTER], VOID)
|
|
65
|
+
|
|
66
|
+
OpenAudioDevice = function("SDL_OpenAudioDevice", [STRING, INT, POINTER, POINTER, INT], UINT32)
|
|
67
|
+
CloseAudioDevice = function("SDL_CloseAudioDevice", [UINT32], VOID)
|
|
68
|
+
PauseAudioDevice = function("SDL_PauseAudioDevice", [UINT32, INT], VOID)
|
|
69
|
+
QueueAudio = function("SDL_QueueAudio", [UINT32, POINTER, UINT32], INT)
|
|
70
|
+
GetQueuedAudioSize = function("SDL_GetQueuedAudioSize", [UINT32], UINT32)
|
|
71
|
+
ClearQueuedAudio = function("SDL_ClearQueuedAudio", [UINT32], VOID)
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/badline/sdl.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
# The libSDL2 calls the window, the gamepads and the audio sink make,
|
|
7
|
+
# bound through Fiddle, so nothing has to compile against SDL at install
|
|
8
|
+
# time. Only the library itself has to be present.
|
|
9
|
+
#
|
|
10
|
+
# Each function is a constant named after its C name without the prefix:
|
|
11
|
+
# SDL::RenderClear.call(renderer) calls SDL_RenderClear. They're built
|
|
12
|
+
# with need_gvl, which skips releasing Ruby's lock around the call: none
|
|
13
|
+
# of these block for long, and releasing it costs more than most of them.
|
|
14
|
+
module SDL
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
|
|
17
|
+
# Where package managers put the shared library. dlopen searches the
|
|
18
|
+
# system paths for the bare names, but not Homebrew's.
|
|
19
|
+
LIBRARIES = %w[
|
|
20
|
+
libSDL2-2.0.so.0 libSDL2.so
|
|
21
|
+
libSDL2-2.0.0.dylib /opt/homebrew/lib/libSDL2-2.0.0.dylib /usr/local/lib/libSDL2-2.0.0.dylib
|
|
22
|
+
SDL2.dll
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def self.open_library
|
|
26
|
+
LIBRARIES.each do |name|
|
|
27
|
+
return Fiddle.dlopen(name)
|
|
28
|
+
rescue Fiddle::DLError
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
raise Error, "can't find libSDL2 (tried #{LIBRARIES.join(', ')})"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
LIBRARY = open_library
|
|
35
|
+
|
|
36
|
+
VOID = Fiddle::TYPE_VOID
|
|
37
|
+
INT = Fiddle::TYPE_INT
|
|
38
|
+
UINT8 = Fiddle::TYPE_UINT8_T
|
|
39
|
+
INT16 = Fiddle::TYPE_INT16_T
|
|
40
|
+
UINT32 = Fiddle::TYPE_UINT32_T
|
|
41
|
+
POINTER = Fiddle::TYPE_VOIDP
|
|
42
|
+
STRING = Fiddle::TYPE_CONST_STRING
|
|
43
|
+
|
|
44
|
+
def self.function(name, arguments, result)
|
|
45
|
+
Fiddle::Function.new(LIBRARY[name], arguments, result, name:, need_gvl: true)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.error = GetError.call
|
|
49
|
+
|
|
50
|
+
# A negative result from SDL is an error, and SDL_GetError says which.
|
|
51
|
+
def self.check(result)
|
|
52
|
+
raise Error, error if result.negative?
|
|
53
|
+
|
|
54
|
+
result
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# So is a null handle.
|
|
58
|
+
def self.check_pointer(pointer)
|
|
59
|
+
raise Error, error if pointer.null?
|
|
60
|
+
|
|
61
|
+
pointer
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
require "badline/sdl/functions"
|
|
67
|
+
require "badline/sdl/events"
|
|
@@ -49,29 +49,47 @@ module Badline
|
|
|
49
49
|
NOISE_TAPS.each { |bit, line| @shift_register &= ~bit if value.nobits?(line) }
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# Setting the test bit starts the bleed, and writes the output of a
|
|
53
|
+
# noise combination back first, from the accumulator it had. Only
|
|
54
|
+
# SID/noiselfsrinit sets the bit onto such a combination from outside
|
|
55
|
+
# it: its $f8/$80 pairs go from noise alone to all four.
|
|
56
|
+
def raise_test
|
|
57
|
+
@shift_register_reset = @shift_register_reset_delay
|
|
58
|
+
return unless @selected.anybits?(0x8) && combined?(@selected) && @shift_pipeline != 1
|
|
59
|
+
|
|
60
|
+
write_shift_register(write_back(@selected, shape(@selected)))
|
|
61
|
+
end
|
|
62
|
+
|
|
52
63
|
# Releasing the test bit finishes the shift it interrupted: the old
|
|
53
64
|
# waveform's output may be written back first, then a bit clocks in
|
|
54
|
-
# over the forced-high bit 22.
|
|
55
|
-
# pulse+noise of the old waveform writes the lowest noise lines low.
|
|
65
|
+
# over the forced-high bit 22.
|
|
56
66
|
def release_test(previous)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
elsif release_writes_back?(previous, @selected)
|
|
60
|
-
write_shift_register(write_back(previous, shape(previous)))
|
|
61
|
-
end
|
|
67
|
+
value = release_write(previous)
|
|
68
|
+
write_shift_register(value) if value
|
|
62
69
|
shift_noise(1)
|
|
63
70
|
end
|
|
64
71
|
|
|
72
|
+
# What the release writes over the taps, or nil. On the 6581, a change
|
|
73
|
+
# that keeps only pulse+noise of the old waveform writes the lowest
|
|
74
|
+
# noise lines low. On the 8580, a change from pulse+noise to all four
|
|
75
|
+
# writes every line low.
|
|
76
|
+
def release_write(previous)
|
|
77
|
+
if @topbit_feedback
|
|
78
|
+
return noise & PULSE_NOISE_RELEASE if previous != 0xc && (previous & @selected) == 0xc
|
|
79
|
+
elsif previous == 0xc && @selected == 0xf
|
|
80
|
+
return 0x000
|
|
81
|
+
end
|
|
82
|
+
write_back(previous, shape(previous)) if release_writes_back?(previous, @selected)
|
|
83
|
+
end
|
|
84
|
+
|
|
65
85
|
# Which waveform changes write the old output back as the test bit
|
|
66
86
|
# falls (SID/wb_testsuite, after libresidfp's do_writeback). Noise has
|
|
67
|
-
# to have been combined before and still be
|
|
68
|
-
#
|
|
69
|
-
# nor
|
|
70
|
-
#
|
|
71
|
-
# back.
|
|
87
|
+
# to have been combined before and still be combined after. Nothing
|
|
88
|
+
# is written back on changing to pulse+noise or from pulse+noise to
|
|
89
|
+
# sawtooth+noise, nor, on the 6581, on trading triangle for sawtooth
|
|
90
|
+
# or back.
|
|
72
91
|
def release_writes_back?(previous, selected)
|
|
73
|
-
return false if previous <= 0x8 || selected
|
|
74
|
-
return previous == 0xf if selected == 0x8
|
|
92
|
+
return false if previous <= 0x8 || selected <= 0x8
|
|
75
93
|
return false if selected == 0xc || (previous == 0xc && selected == 0xa)
|
|
76
94
|
|
|
77
95
|
!(@topbit_feedback && [previous & 0x3, selected & 0x3].sort == [0x1, 0x2])
|
data/lib/badline/sid/waveform.rb
CHANGED
|
@@ -114,10 +114,10 @@ module Badline
|
|
|
114
114
|
@sync = value.anybits?(0x02)
|
|
115
115
|
test = value.anybits?(0x08)
|
|
116
116
|
if test
|
|
117
|
+
raise_test unless @test
|
|
117
118
|
@accumulator = 0x000000
|
|
118
119
|
@pulse = 0xfff
|
|
119
120
|
@shift_pipeline = 0
|
|
120
|
-
@shift_register_reset = @shift_register_reset_delay unless @test
|
|
121
121
|
elsif @test
|
|
122
122
|
release_test(previous)
|
|
123
123
|
end
|
data/lib/badline/sid.rb
CHANGED
|
@@ -39,7 +39,9 @@ module Badline
|
|
|
39
39
|
# Cycles the data bus holds a value, measured through SID/bitfade.
|
|
40
40
|
BUS_TTL = { mos6581: 0x1d00, mos8580: 0xa2000 }.freeze
|
|
41
41
|
|
|
42
|
-
# Scales the filter's 20-bit mix down to a signed 16-bit sample
|
|
42
|
+
# Scales the filter's 20-bit mix down to a signed 16-bit sample, as
|
|
43
|
+
# reSID 0.16 does: three voices at full swing and volume 15 span the
|
|
44
|
+
# whole range, on either chip.
|
|
43
45
|
SAMPLE_DIVISOR = ((0xfff * 0xff) >> 7) * 3 * 15 * 2 / (2**16)
|
|
44
46
|
|
|
45
47
|
# Writes the DSP queues between catch-ups. A full queue catches up
|