badline 0.3.0 → 0.4.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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/hooks/session-start.sh +57 -0
  3. data/.claude/settings.json +14 -0
  4. data/CHANGELOG.md +65 -0
  5. data/CLAUDE.md +45 -16
  6. data/CONTRIBUTING.md +95 -3
  7. data/README.md +111 -61
  8. data/SECURITY.md +9 -0
  9. data/doc/pinned-behaviour.md +275 -33
  10. data/exe/badline-ruby +59 -0
  11. data/lib/badline/address_bus.rb +22 -10
  12. data/lib/badline/audio/aiff.rb +8 -3
  13. data/lib/badline/audio/cli.rb +17 -12
  14. data/lib/badline/audio/console.rb +12 -58
  15. data/lib/badline/audio/playback.rb +3 -0
  16. data/lib/badline/audio/sdl_sink.rb +20 -52
  17. data/lib/badline/audio/terminal.rb +90 -0
  18. data/lib/badline/audio.rb +2 -2
  19. data/lib/badline/cartridge/geo_ram.rb +63 -0
  20. data/lib/badline/cartridge.rb +1 -0
  21. data/lib/badline/cia/interrupt_register.rb +34 -10
  22. data/lib/badline/cia.rb +15 -8
  23. data/lib/badline/computer.rb +28 -13
  24. data/lib/badline/gui/application.rb +33 -21
  25. data/lib/badline/gui/gamepads.rb +22 -22
  26. data/lib/badline/gui/joy_map.rb +1 -1
  27. data/lib/badline/gui/key_map.rb +6 -1
  28. data/lib/badline/gui/pane.rb +7 -6
  29. data/lib/badline/gui/screen_pane.rb +9 -13
  30. data/lib/badline/gui/window.rb +35 -20
  31. data/lib/badline/gui.rb +1 -1
  32. data/lib/badline/input/mouse1351.rb +21 -6
  33. data/lib/badline/input/paddles.rb +11 -6
  34. data/lib/badline/kernal_trap/channel.rb +3 -0
  35. data/lib/badline/kernal_trap/drive/block_commands.rb +33 -4
  36. data/lib/badline/kernal_trap/drive/status.rb +3 -0
  37. data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
  38. data/lib/badline/kernal_trap/drive/writes.rb +92 -0
  39. data/lib/badline/kernal_trap/drive.rb +51 -51
  40. data/lib/badline/kernal_trap/file.rb +6 -9
  41. data/lib/badline/kernal_trap/save.rb +9 -4
  42. data/lib/badline/keyboard.rb +19 -3
  43. data/lib/badline/media.rb +42 -11
  44. data/lib/badline/options.rb +209 -0
  45. data/lib/badline/ram_expansion/banking.rb +30 -0
  46. data/lib/badline/ram_expansion/plus256k.rb +38 -0
  47. data/lib/badline/ram_expansion/plus60k.rb +31 -0
  48. data/lib/badline/ram_expansion/unexpanded.rb +22 -0
  49. data/lib/badline/ram_expansion.rb +21 -0
  50. data/lib/badline/region.rb +51 -0
  51. data/lib/badline/sdl/events.rb +47 -0
  52. data/lib/badline/sdl/functions.rb +73 -0
  53. data/lib/badline/sdl.rb +67 -0
  54. data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
  55. data/lib/badline/sid/waveform.rb +1 -1
  56. data/lib/badline/sid.rb +3 -1
  57. data/lib/badline/storage/d64_image.rb +10 -0
  58. data/lib/badline/storage/d71_image.rb +12 -0
  59. data/lib/badline/storage/d81_image.rb +9 -0
  60. data/lib/badline/storage/disk_image/bam.rb +111 -0
  61. data/lib/badline/storage/disk_image/directory.rb +79 -0
  62. data/lib/badline/storage/disk_image/writing.rb +110 -0
  63. data/lib/badline/storage/disk_image.rb +27 -4
  64. data/lib/badline/storage/host_directory.rb +2 -1
  65. data/lib/badline/storage.rb +15 -0
  66. data/lib/badline/time_of_day.rb +4 -2
  67. data/lib/badline/version.rb +1 -1
  68. data/lib/badline/via/control_lines.rb +104 -0
  69. data/lib/badline/via/interrupt_register.rb +58 -0
  70. data/lib/badline/via/shift_register.rb +135 -0
  71. data/lib/badline/via/timer1.rb +82 -0
  72. data/lib/badline/via/timer2.rb +84 -0
  73. data/lib/badline/via.rb +262 -0
  74. data/lib/badline/vic/bank.rb +4 -3
  75. data/lib/badline/vic/color_patches.rb +6 -3
  76. data/lib/badline/vic/display_state.rb +4 -0
  77. data/lib/badline/vic/graphics_shifter.rb +42 -7
  78. data/lib/badline/vic/registers.rb +3 -0
  79. data/lib/badline/vic/sequencer.rb +18 -18
  80. data/lib/badline/vic/sequencer_output.rb +4 -4
  81. data/lib/badline/vic/sprite/internal_bus.rb +4 -0
  82. data/lib/badline/vic/sprite/shifter.rb +25 -3
  83. data/lib/badline/vic/sprite.rb +5 -4
  84. data/lib/badline/vic/sprites.rb +8 -3
  85. data/lib/badline/vic.rb +93 -44
  86. data/lib/badline.rb +4 -0
  87. data/packaging/homebrew/badline.rb +34 -0
  88. metadata +30 -20
  89. data/exe/badline +0 -78
  90. data/exe/badline-sid +0 -34
  91. 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
@@ -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. On the 6581, a change that keeps only
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
- if @topbit_feedback && previous != 0xc && (previous & @selected) == 0xc
58
- write_shift_register(noise & PULSE_NOISE_RELEASE)
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 selected after. Dropping
68
- # to noise alone writes nothing back unless all four were selected,
69
- # nor does changing to pulse+noise or from pulse+noise to
70
- # sawtooth+noise, nor, on the 6581, trading triangle for sawtooth or
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 < 0x8
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])
@@ -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