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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/CLAUDE.md +10 -9
  4. data/README.md +135 -42
  5. data/doc/pinned-behaviour.md +131 -30
  6. data/exe/badline-ruby +28 -24
  7. data/lib/badline/address_bus/extra_sids.rb +30 -0
  8. data/lib/badline/address_bus/sid_slots.rb +42 -0
  9. data/lib/badline/address_bus.rb +15 -1
  10. data/lib/badline/audio/aiff.rb +2 -2
  11. data/lib/badline/audio/bare_player.rb +23 -12
  12. data/lib/badline/audio/cli.rb +167 -59
  13. data/lib/badline/audio/console.rb +2 -1
  14. data/lib/badline/audio/jukebox.rb +121 -36
  15. data/lib/badline/audio/machine_player.rb +25 -10
  16. data/lib/badline/audio/pcm_writer.rb +10 -6
  17. data/lib/badline/audio/playback.rb +6 -5
  18. data/lib/badline/audio/queued_tune.rb +89 -0
  19. data/lib/badline/audio/renderer.rb +57 -10
  20. data/lib/badline/audio/stereo.rb +97 -0
  21. data/lib/badline/audio/terminal.rb +39 -6
  22. data/lib/badline/audio/wav.rb +1 -1
  23. data/lib/badline/audio.rb +2 -3
  24. data/lib/badline/computer.rb +6 -3
  25. data/lib/badline/cpu/boundary.rb +54 -0
  26. data/lib/badline/cpu/core.rb +165 -0
  27. data/lib/badline/cpu.rb +3 -214
  28. data/lib/badline/drive1541/cpu.rb +29 -0
  29. data/lib/badline/drive1541.rb +1 -0
  30. data/lib/badline/ffi.rb +157 -0
  31. data/lib/badline/frontend/app.rb +225 -0
  32. data/lib/badline/frontend/audio_sink.rb +73 -0
  33. data/lib/badline/frontend/boot.rb +52 -0
  34. data/lib/badline/frontend/buttons.rb +90 -0
  35. data/lib/badline/frontend/controls.rb +163 -0
  36. data/lib/badline/frontend/drive_led.rb +51 -0
  37. data/lib/badline/frontend/filter_view.rb +96 -0
  38. data/lib/badline/frontend/frame_rate.rb +83 -0
  39. data/lib/badline/frontend/gamepads.rb +76 -0
  40. data/lib/badline/frontend/info_view.rb +120 -0
  41. data/lib/badline/frontend/keys.rb +48 -0
  42. data/lib/badline/frontend/pacer.rb +100 -0
  43. data/lib/badline/frontend/pad_port.rb +61 -0
  44. data/lib/badline/frontend/painter.rb +175 -0
  45. data/lib/badline/frontend/player_commands.rb +33 -0
  46. data/lib/badline/frontend/player_footer.rb +86 -0
  47. data/lib/badline/frontend/player_header.rb +60 -0
  48. data/lib/badline/frontend/player_screen.rb +124 -0
  49. data/lib/badline/frontend/player_state.rb +67 -0
  50. data/lib/badline/frontend/player_window.rb +228 -0
  51. data/lib/badline/frontend/scope.rb +42 -0
  52. data/lib/badline/frontend/screen.rb +65 -0
  53. data/lib/badline/frontend/screenshot.rb +22 -0
  54. data/lib/badline/frontend/sid_history.rb +175 -0
  55. data/lib/badline/frontend/sid_view.rb +140 -0
  56. data/lib/badline/frontend/snapshots.rb +60 -0
  57. data/lib/badline/frontend/sound.rb +153 -0
  58. data/lib/badline/frontend/timeline.rb +171 -0
  59. data/lib/badline/frontend/visualizer_view.rb +101 -0
  60. data/lib/badline/frontend.rb +32 -0
  61. data/lib/badline/media/queue.rb +158 -0
  62. data/lib/badline/media.rb +13 -8
  63. data/lib/badline/options/event.rb +94 -0
  64. data/lib/badline/options/help.rb +107 -0
  65. data/lib/badline/options/option.rb +29 -0
  66. data/lib/badline/options/table.rb +64 -0
  67. data/lib/badline/options/validation.rb +54 -0
  68. data/lib/badline/options.rb +204 -149
  69. data/lib/badline/reu/dma/saved_state.rb +50 -0
  70. data/lib/badline/reu/dma.rb +56 -47
  71. data/lib/badline/reu/saved_state.rb +2 -1
  72. data/lib/badline/reu.rb +19 -10
  73. data/lib/badline/sdl.rb +133 -51
  74. data/lib/badline/sid/catch_up.rb +11 -1
  75. data/lib/badline/sid/filter.rb +9 -0
  76. data/lib/badline/sid/voice.rb +6 -0
  77. data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
  78. data/lib/badline/sid/waveform.rb +22 -8
  79. data/lib/badline/sid.rb +35 -3
  80. data/lib/badline/snapshot/state.rb +1 -1
  81. data/lib/badline/storage/hvsc.rb +41 -0
  82. data/lib/badline/storage/sid_file.rb +45 -25
  83. data/lib/badline/storage/song_lengths.rb +6 -29
  84. data/lib/badline/storage/stil/credit.rb +50 -0
  85. data/lib/badline/storage/stil.rb +126 -0
  86. data/lib/badline/storage.rb +8 -0
  87. data/lib/badline/version.rb +1 -1
  88. data/lib/badline/vic/bank.rb +21 -0
  89. data/lib/badline/vic.rb +15 -6
  90. metadata +49 -19
  91. data/.claude/hooks/session-start.sh +0 -57
  92. data/.claude/settings.json +0 -14
  93. data/lib/badline/audio/sdl_sink.rb +0 -70
  94. data/lib/badline/audio/stream.rb +0 -123
  95. data/lib/badline/gui/application.rb +0 -243
  96. data/lib/badline/gui/drive_led_pane.rb +0 -29
  97. data/lib/badline/gui/gamepads.rb +0 -104
  98. data/lib/badline/gui/joy_map.rb +0 -27
  99. data/lib/badline/gui/key_map.rb +0 -39
  100. data/lib/badline/gui/palette.rb +0 -35
  101. data/lib/badline/gui/pane.rb +0 -36
  102. data/lib/badline/gui/screen_pane.rb +0 -46
  103. data/lib/badline/gui/snapshots.rb +0 -67
  104. data/lib/badline/gui/window.rb +0 -61
  105. data/lib/badline/gui.rb +0 -14
  106. data/lib/badline/sdl/events.rb +0 -49
  107. data/lib/badline/sdl/functions.rb +0 -74
  108. data/vendor/.gitkeep +0 -0
@@ -16,7 +16,7 @@ module Badline
16
16
  # BASIC's RUN handler, where a BASIC tune starts.
17
17
  BASIC_RUN = 0xa871
18
18
 
19
- # The 6502 stub that starts a tune: call init with the song number,
19
+ # The 6502 stub that starts a tune: call init with the subtune number,
20
20
  # point the KERNAL's IRQ vector at a raster handler that calls play,
21
21
  # and spin. Both calls are wrapped in the `$01` save, bank and restore
22
22
  # that puts the tune's own RAM under the CPU.
@@ -41,7 +41,7 @@ module Badline
41
41
  # pha
42
42
  # lda #iomap
43
43
  # sta $01
44
- # lda #song
44
+ # lda #subtune
45
45
  # jsr init
46
46
  # pla
47
47
  # sta $01
@@ -70,9 +70,9 @@ module Badline
70
70
  CIA1_ICR = 0xdc0d
71
71
  RASTER_IRQ_STATUS = 0xd019
72
72
 
73
- def initialize(tune, song:, base:)
73
+ def initialize(tune, subtune:, base:)
74
74
  @tune = tune
75
- @song = song
75
+ @subtune = subtune
76
76
  @base = base
77
77
  end
78
78
 
@@ -96,7 +96,7 @@ module Badline
96
96
 
97
97
  def boot
98
98
  start = [0x78] +
99
- banked(@tune.init_address, lda_imm(@song) + jsr(@tune.init_address)) +
99
+ banked(@tune.init_address, lda_imm(@subtune) + jsr(@tune.init_address)) +
100
100
  raster_irq + [0x58]
101
101
  start + jmp(boot_address + start.length)
102
102
  end
@@ -153,14 +153,13 @@ module Badline
153
153
  raise FormatError, "Truncated header" if truncated?
154
154
  raise FormatError, "No tune data" if data.empty?
155
155
  raise FormatError, "Sidplayer MUS data, which needs a MUS player" if mus?
156
- raise FormatError, "Written for #{sids} SIDs; only one is emulated" if sids > 1
157
156
  end
158
157
 
159
158
  def version = word(0x04)
160
159
  def data_offset = word(0x06)
161
160
  def play_address = word(0x0c)
162
- def songs = [word(0x0e), 1].max
163
- def start_song = word(0x10).clamp(1, songs)
161
+ def subtunes = [word(0x0e), 1].max
162
+ def start_subtune = word(0x10).clamp(1, subtunes)
164
163
  def speed = (word(0x12) << 16) + word(0x14)
165
164
  def name = text(0x16)
166
165
  def author = text(0x36)
@@ -181,22 +180,38 @@ module Badline
181
180
  # A v3 header can place a second SID at `$Dxx0` and a v4 header a
182
181
  # third, each given by its middle byte: even, and in `$d420-$d7e0` or
183
182
  # `$de00-$dfe0`. Anything else means the SID isn't there.
184
- def sids
185
- 1 + [0x7a, 0x7b].first([version - 2, 0].max).count do |offset|
183
+ def sids = 1 + sid_addresses.size
184
+
185
+ # Where the second and third SIDs sit, those the header places.
186
+ def sid_addresses
187
+ addresses = []
188
+ [0x7a, 0x7b].first([version - 2, 0].max).each do |offset|
186
189
  page = @bytes[offset].to_i
187
- page.even? && ((0x42..0x7e).cover?(page) || (0xe0..0xfe).cover?(page))
190
+ next unless page.even? && ((0x42..0x7e).cover?(page) || (0xe0..0xfe).cover?(page))
191
+
192
+ addresses << (0xd000 | (page << 4))
188
193
  end
194
+ addresses
189
195
  end
190
196
 
191
197
  # Flag bits 4-5 name the SID the tune was written for: 01 the 6581,
192
198
  # 10 the 8580, 11 either and 00 unknown. Only an 8580-only tune gets one.
193
199
  def sid_model = flags[4, 2] == 0b10 ? :mos8580 : :mos6581
194
200
 
201
+ # Each SID's model, SID 1's first. Bits 6-7 name the second SID's and
202
+ # bits 8-9 the third's the same way, and one the header leaves unknown
203
+ # or open takes SID 1's.
204
+ def sid_models
205
+ models = [sid_model]
206
+ sid_addresses.size.times { |extra| models << model_named(flags[6 + (extra * 2), 2]) }
207
+ models
208
+ end
209
+
195
210
  # Flag bits 2-3 name the video standard the same way: 01 PAL, 10 NTSC,
196
211
  # 11 either and 00 unknown. Only an NTSC-only tune gets NTSC.
197
212
  def ntsc? = flags[2, 2] == 0b10
198
213
 
199
- # HVSC keys its song length database on the whole file, header and all.
214
+ # HVSC keys its subtune length database on the whole file, header and all.
200
215
  def md5 = @md5 ||= Digest::MD5.hexdigest(@bytes.pack("C*"))
201
216
 
202
217
  # The `$01` value a routine at `address` has to run under, following
@@ -227,8 +242,8 @@ module Badline
227
242
  # A set speed bit asks for CIA timer pacing instead of a raster IRQ.
228
243
  # The bare player honours it; the machine driver paces everything off
229
244
  # the raster either way.
230
- def cia_timed?(song = start_song)
231
- speed[[song - 1, 31].min] == 1
245
+ def cia_timed?(subtune = start_subtune)
246
+ speed[[subtune - 1, 31].min] == 1
232
247
  end
233
248
 
234
249
  def driver_address
@@ -236,20 +251,20 @@ module Badline
236
251
  raise(FormatError, "No free RAM for the player")
237
252
  end
238
253
 
239
- def driver(song: start_song)
240
- Driver.new(self, song: song.clamp(1, songs) - 1, base: driver_address).bytes
254
+ def driver(subtune: start_subtune)
255
+ Driver.new(self, subtune: subtune.clamp(1, subtunes) - 1, base: driver_address).bytes
241
256
  end
242
257
 
243
258
  # What a booted machine needs in RAM besides the image before
244
259
  # `boot_command` starts the tune, as address => bytes. A BASIC tune
245
260
  # gets the end of its program in VARTAB, as LOAD leaves it, and the
246
- # song in the A, X and Y that SYS loads from `$030c-$030e`, where the
261
+ # subtune in the A, X and Y that SYS loads from `$030c-$030e`, where the
247
262
  # tune reads it with PEEK(780).
248
- def boot_memory(song: start_song)
249
- return { driver_address => driver(song:) } unless basic?
263
+ def boot_memory(subtune: start_subtune)
264
+ return { driver_address => driver(subtune:) } unless basic?
250
265
 
251
266
  { 0x2d => [low_byte(end_address), high_byte(end_address)],
252
- 0x030c => [song.clamp(1, songs) - 1] * 3 }
267
+ 0x030c => [subtune.clamp(1, subtunes) - 1] * 3 }
253
268
  end
254
269
 
255
270
  def boot_command = basic? ? "run\r" : "sys#{driver_address}\r"
@@ -289,14 +304,19 @@ module Badline
289
304
  end
290
305
 
291
306
  def driver_size
292
- @driver_size ||= Driver.new(self, song: 0, base: 0).bytes.length
307
+ @driver_size ||= Driver.new(self, subtune: 0, base: 0).bytes.length
308
+ end
309
+
310
+ def model_named(bits)
311
+ case bits
312
+ when 0b01 then :mos6581
313
+ when 0b10 then :mos8580
314
+ else sid_model
315
+ end
293
316
  end
294
317
 
295
318
  def text(offset)
296
- @bytes[offset, 32].take_while(&:positive?)
297
- .pack("C*")
298
- .force_encoding(Encoding::ISO_8859_1)
299
- .encode(Encoding::UTF_8)
319
+ Storage.latin1(@bytes[offset, 32].take_while(&:positive?))
300
320
  end
301
321
 
302
322
  # Header fields are big-endian, unlike everything else the 6502 sees.
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Badline
4
4
  module Storage
5
- # HVSC's song length database. Each tune gets a comment naming its path
5
+ # HVSC's subtune length database. Each tune gets a comment naming its path
6
6
  # and a line keyed by the MD5 of the whole `.sid` file, holding one
7
- # `m:ss` time per song, optionally down to milliseconds:
7
+ # `m:ss` time per subtune, optionally down to milliseconds:
8
8
  #
9
9
  # ; /DEMOS/0-9/12th_Sector_Music.sid
10
10
  # c7c299ce06ec5ccffb2261fb11b42a73=4:33.108
@@ -12,38 +12,15 @@ module Badline
12
12
  # The file runs to well over a hundred thousand entries, so a lookup
13
13
  # scans for its one key rather than building a table.
14
14
  class SongLengths
15
- class << self
16
- # The database lives in the DOCUMENTS directory of an HVSC
17
- # collection, so look for one above the tune, then wherever
18
- # `$HVSC_BASE` points.
19
- def locate(tune_path)
20
- candidates(tune_path).find { |path| File.file?(path) }
21
- end
22
-
23
- private
24
-
25
- def candidates(tune_path)
26
- ancestors(File.dirname(File.expand_path(tune_path)))
27
- .push(ENV.fetch("HVSC_BASE", nil))
28
- .compact
29
- .map { |dir| File.join(dir, "DOCUMENTS", "Songlengths.md5") }
30
- end
31
-
32
- def ancestors(dir)
33
- dirs = []
34
- until dirs.last == dir
35
- dirs << dir
36
- dir = File.dirname(dir)
37
- end
38
- dirs
39
- end
40
- end
15
+ # The database lives in the DOCUMENTS directory of an HVSC
16
+ # collection.
17
+ def self.locate(tune_path) = HVSC.document(tune_path, "Songlengths.md5")
41
18
 
42
19
  def initialize(path)
43
20
  @path = path
44
21
  end
45
22
 
46
- # The tune's per-song lengths in seconds, or nil when it isn't listed.
23
+ # The tune's per-subtune lengths in seconds, or nil when it isn't listed.
47
24
  def lengths(md5)
48
25
  prefix = "#{md5.downcase}="
49
26
  File.foreach(@path) do |line|
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ class STIL
6
+ # A TITLE in a STIL entry, the tune a subtune covers, with the ARTIST
7
+ # after it, and the stretch of the subtune it covers when the title
8
+ # ends in one, such as `(0:53)` or `(1:16-1:32)`. A credit without an
9
+ # end lasts until the next one takes over.
10
+ class Credit
11
+ TIMES = /\s*\((\d+):(\d\d)(?:\.\d+)?(?:-(\d+):(\d\d)(?:\.\d+)?)?\)\s*\z/
12
+
13
+ attr_reader :text, :start, :finish
14
+
15
+ def initialize(title, artist)
16
+ match = TIMES.match(title)
17
+ @start = match ? (match[1].to_i * 60) + match[2].to_i : 0
18
+ @finish = match && match[3] ? (match[3].to_i * 60) + match[4].to_i : -1
19
+ name = match ? title[0, match.begin(0)] : title
20
+ @text = artist.empty? ? name : "#{name} - #{artist}"
21
+ end
22
+
23
+ # The credits in the fields, in the order STIL lists them.
24
+ def self.list(fields)
25
+ credits = []
26
+ fields.each_with_index do |field, index|
27
+ next unless field.name == "TITLE"
28
+
29
+ following = fields[index + 1]
30
+ artist = following && following.name == "ARTIST" ? following.text : ""
31
+ credits << new(field.text.tr("\n", " "), artist.tr("\n", " "))
32
+ end
33
+ credits
34
+ end
35
+
36
+ # The text of the credit covering `seconds` into the subtune, or ""
37
+ # when none does: the last one started by then that hasn't ended.
38
+ def self.at(credits, seconds)
39
+ text = ""
40
+ credits.each do |credit|
41
+ text = credit.text if credit.covers?(seconds)
42
+ end
43
+ text
44
+ end
45
+
46
+ def covers?(seconds) = seconds >= @start && (@finish.negative? || seconds <= @finish)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/storage/stil/credit"
4
+
5
+ module Badline
6
+ module Storage
7
+ # HVSC's SID Tune Information List, `DOCUMENTS/STIL.txt`: covers,
8
+ # subtune names and composers' comments, keyed by each tune's HVSC path.
9
+ # An entry holds fields for the whole file, then a block per subtune
10
+ # that has any:
11
+ #
12
+ # /MUSICIANS/C/Crowther_Anthony/From_Ratt_To_You.sid
13
+ # COMMENT: The tune names are listed, if they were given a name, the
14
+ # remaining tunes were just called Tune 35, Tune 27 etc.
15
+ # (#16)
16
+ # NAME: G-Bust
17
+ # TITLE: Ghostbusters [from the movie]
18
+ # ARTIST: Ray Parker, Jr.
19
+ #
20
+ # A comment runs on over lines indented by nine spaces, and a blank line
21
+ # ends the entry. The file is ISO-8859-1.
22
+ class STIL
23
+ # One of NAME, AUTHOR, TITLE, ARTIST or COMMENT, with its lines of
24
+ # text joined by newlines.
25
+ class Field
26
+ attr_reader :name, :text
27
+
28
+ def initialize(name, text)
29
+ @name = name
30
+ @text = text
31
+ end
32
+
33
+ def continue(line)
34
+ @text = "#{@text}\n#{line}"
35
+ end
36
+
37
+ # The field laid out as STIL.txt does.
38
+ def lines
39
+ @text.split("\n").each_with_index.map do |line, index|
40
+ label = index.zero? ? "#{@name}:" : ""
41
+ "#{label.rjust(8)} #{line}"
42
+ end
43
+ end
44
+ end
45
+
46
+ # A tune's fields for the whole file, and for each subtune by number.
47
+ class Entry
48
+ attr_reader :fields
49
+
50
+ def initialize
51
+ @fields = []
52
+ @subtunes = {}
53
+ @subtune = 0
54
+ end
55
+
56
+ def subtune(number) = @subtunes.fetch(number, [])
57
+
58
+ # Starts the block for a subtune, which the fields read after it
59
+ # go to.
60
+ def open_subtune(subtune)
61
+ @subtune = subtune
62
+ @subtunes[subtune] = []
63
+ end
64
+
65
+ def add(field)
66
+ if @subtune.zero?
67
+ @fields << field
68
+ else
69
+ @subtunes[@subtune] << field
70
+ end
71
+ end
72
+
73
+ # Adds a line to the last field read.
74
+ def continue(line)
75
+ last = @subtune.zero? ? @fields.last : @subtunes[@subtune].last
76
+ last&.continue(line)
77
+ end
78
+ end
79
+
80
+ # The list lives in the DOCUMENTS directory of an HVSC collection.
81
+ def self.locate(tune_path) = HVSC.document(tune_path, "STIL.txt")
82
+
83
+ def initialize(path)
84
+ @path = path
85
+ end
86
+
87
+ # The entry for a tune's HVSC path, or nil when the list has none. The
88
+ # first lookup reads the whole list.
89
+ def entry(hvsc_path) = entries[hvsc_path]
90
+
91
+ def entries
92
+ @entries ||= parse(Storage.latin1(File.binread(@path).bytes))
93
+ end
94
+
95
+ private
96
+
97
+ def parse(text)
98
+ entries = {}
99
+ entry = nil
100
+ text.split("\n").each do |line|
101
+ line = line.chomp
102
+ if line.start_with?("/")
103
+ entry = Entry.new
104
+ entries[line] = entry
105
+ elsif entry.nil? || line.empty? || line.start_with?("#")
106
+ entry = nil
107
+ else
108
+ read(entry, line)
109
+ end
110
+ end
111
+ entries
112
+ end
113
+
114
+ def read(entry, line)
115
+ if line.start_with?("(#")
116
+ entry.open_subtune(line[2..].to_i)
117
+ elsif line.start_with?(" ")
118
+ entry.continue(line[9..])
119
+ else
120
+ name, text = line.lstrip.split(": ", 2)
121
+ entry.add(Field.new(name, text.to_s)) if %w[NAME AUTHOR TITLE ARTIST COMMENT].include?(name)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -12,7 +12,9 @@ require "badline/storage/t64"
12
12
  require "badline/storage/tap"
13
13
  require "badline/storage/crt_file"
14
14
  require "badline/storage/sid_file"
15
+ require "badline/storage/hvsc"
15
16
  require "badline/storage/song_lengths"
17
+ require "badline/storage/stil"
16
18
  require "badline/storage/unavailable"
17
19
 
18
20
  module Badline
@@ -62,6 +64,12 @@ module Badline
62
64
  bytes.map { |b| b.between?(0xc1, 0xda) ? b - 0x80 : b }.pack("C*")
63
65
  end
64
66
 
67
+ # Decodes ISO-8859-1 bytes, the text of .sid headers and HVSC's
68
+ # documents, to UTF-8.
69
+ def latin1(bytes)
70
+ bytes.pack("U*").force_encoding(Encoding::UTF_8)
71
+ end
72
+
65
73
  # CBM DOS drive prefix: "0:NAME" selects a drive, "@0:NAME" is
66
74
  # save-with-replace
67
75
  def strip_drive_prefix(name)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
@@ -13,9 +13,26 @@ module Badline
13
13
 
14
14
  attr_reader :address_bus
15
15
 
16
+ # The bank lines as #sample_lines last saw them.
17
+ attr_accessor :lines
18
+
16
19
  def initialize(address_bus = nil)
17
20
  addressable_at(0x0000, length: 2**14)
18
21
  @address_bus = address_bus || AddressBus.new
22
+ @lines = 0b11
23
+ @swapped = false
24
+ end
25
+
26
+ # On the 8565, a port A write that swaps the two bank lines, driving
27
+ # one high as the other goes low, shows the VIC both lines low for the
28
+ # cycle after it: every access in that cycle reads bank 3. A swap that
29
+ # the DDR makes, releasing a line to float high, shows nothing.
30
+ # Called once a cycle, ahead of the cycle's accesses.
31
+ def sample_lines
32
+ cia2 = @address_bus.cia2
33
+ lines = cia2.port_a_lines & 0b11
34
+ @swapped = swap?(@lines, lines) && cia2.port_registers[2].allbits?(0b11)
35
+ @lines = lines
19
36
  end
20
37
 
21
38
  def peek(offset)
@@ -66,7 +83,11 @@ module Badline
66
83
  end
67
84
  end
68
85
 
86
+ def swap?(from, to) = (from ^ to) == 0b11 && from.anybits?(0b11) && to.anybits?(0b11)
87
+
69
88
  def bank_switch_register
89
+ return 0 if @swapped
90
+
70
91
  @address_bus.cia2.port_a_lines & 0b11
71
92
  end
72
93
  end
data/lib/badline/vic.rb CHANGED
@@ -86,6 +86,7 @@ module Badline
86
86
  @grey_dots = model == :mos8565
87
87
  @delayed_fetch = model == :mos8565
88
88
  @dma_delay_idle = model == :mos6569
89
+ @bank_swaps = model == :mos8565
89
90
  @address_bus = address_bus || AddressBus.new
90
91
  @vic_bank = VIC::Bank.new(@address_bus)
91
92
  @debug = debug
@@ -153,6 +154,7 @@ module Badline
153
154
  out.int(@column).int(@rasterline).int(@cycles).boolean(@pending_write)
154
155
  out.int(@g_tick).ints(@g_kind).int(@g_kept_char).int(@g_kept_color).boolean(@g_display)
155
156
  out.int(@fetch_d011).boolean(@lp_triggered).boolean(@lp_low).boolean(@raster_match)
157
+ out.int(@vic_bank.lines)
156
158
  out.blob(@character_buffer).blob(@color_buffer).booleans(@sprite_ba)
157
159
  @lines.each { |line| out.blob(line) }
158
160
  @registers.save_state(out)
@@ -260,14 +262,19 @@ module Badline
260
262
  @column > DisplayState::DMA_FIRST && @column <= DisplayState::DMA_LAST + 1
261
263
  end
262
264
 
263
- # BA as an REU's DMA sees it. On the line whose raster matches sprite
264
- # 0's Y, where the sprite's DMA starts, the REU doesn't see BA fall on
265
- # the first cycle of the sprite's window. Pinned by
265
+ # Whether this is the first cycle of sprite 0's BA window on the line
266
+ # whose raster matches its Y, where the sprite's DMA starts. An REU
267
+ # that moved a byte on the cycle before doesn't see BA fall here. Pinned by
266
268
  # REU/bonzai/spritetiming.
267
- def reu_ba_low?
268
- return false if @column == SPRITE_BA_FIRST + @sprite_shift && sprite_zero_starting?
269
+ def reu_ba_late?
270
+ @column == SPRITE_BA_FIRST + @sprite_shift && sprite_zero_starting?
271
+ end
269
272
 
270
- ba_low?
273
+ # Whether BA passes straight from a bad line's window to sprite 0's on
274
+ # this cycle, the first of sprite 0's. An REU sees the bad line's DMA
275
+ # end here. Pinned by REU/reutiming2/e4-m2 and e6-m2.
276
+ def reu_ba_handed_on?
277
+ @column == DisplayState::DMA_LAST + 2 && @sprite_ba[@column] && @display_state.bad_line_condition?
271
278
  end
272
279
 
273
280
  # Light pen input level (CIA1 PB4). A falling edge triggers the latch.
@@ -372,6 +379,7 @@ module Badline
372
379
  @lp_triggered = input.boolean?
373
380
  @lp_low = input.boolean?
374
381
  @raster_match = input.boolean?
382
+ @vic_bank.lines = input.int
375
383
  end
376
384
 
377
385
  def load_buffers(input)
@@ -536,6 +544,7 @@ module Badline
536
544
  # on zero data with the screen byte and colour the last g-access
537
545
  # latched, which an idle g-access clears.
538
546
  def draw!
547
+ @vic_bank.sample_lines if @bank_swaps
539
548
  slot = @g_tick = (@g_tick + 1) & 3
540
549
  display_state = @display_state
541
550
  access = display_state.graphics_column?(@column) && !@sequencer.vertical_closed?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: badline
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Inge Jørgensen
@@ -34,8 +34,6 @@ executables:
34
34
  extensions: []
35
35
  extra_rdoc_files: []
36
36
  files:
37
- - ".claude/hooks/session-start.sh"
38
- - ".claude/settings.json"
39
37
  - CHANGELOG.md
40
38
  - CLAUDE.md
41
39
  - CODE_OF_CONDUCT.md
@@ -47,7 +45,9 @@ files:
47
45
  - exe/badline-ruby
48
46
  - lib/badline.rb
49
47
  - lib/badline/address_bus.rb
48
+ - lib/badline/address_bus/extra_sids.rb
50
49
  - lib/badline/address_bus/saved_state.rb
50
+ - lib/badline/address_bus/sid_slots.rb
51
51
  - lib/badline/address_bus/ultimax_pages.rb
52
52
  - lib/badline/addressable.rb
53
53
  - lib/badline/audio.rb
@@ -59,9 +59,9 @@ files:
59
59
  - lib/badline/audio/machine_player.rb
60
60
  - lib/badline/audio/pcm_writer.rb
61
61
  - lib/badline/audio/playback.rb
62
+ - lib/badline/audio/queued_tune.rb
62
63
  - lib/badline/audio/renderer.rb
63
- - lib/badline/audio/sdl_sink.rb
64
- - lib/badline/audio/stream.rb
64
+ - lib/badline/audio/stereo.rb
65
65
  - lib/badline/audio/terminal.rb
66
66
  - lib/badline/audio/wav.rb
67
67
  - lib/badline/cartridge.rb
@@ -106,6 +106,8 @@ files:
106
106
  - lib/badline/control_ports.rb
107
107
  - lib/badline/cpu.rb
108
108
  - lib/badline/cpu/addressing.rb
109
+ - lib/badline/cpu/boundary.rb
110
+ - lib/badline/cpu/core.rb
109
111
  - lib/badline/cpu/microcode.rb
110
112
  - lib/badline/cpu/operations.rb
111
113
  - lib/badline/cpu/saved_state.rb
@@ -115,6 +117,7 @@ files:
115
117
  - lib/badline/debug_register.rb
116
118
  - lib/badline/drive1541.rb
117
119
  - lib/badline/drive1541/bus.rb
120
+ - lib/badline/drive1541/cpu.rb
118
121
  - lib/badline/drive1541/disk.rb
119
122
  - lib/badline/drive1541/disk_state.rb
120
123
  - lib/badline/drive1541/disk_via.rb
@@ -128,17 +131,37 @@ files:
128
131
  - lib/badline/drive1541/serial_port.rb
129
132
  - lib/badline/drive1541/sleep.rb
130
133
  - lib/badline/drive1541/track.rb
131
- - lib/badline/gui.rb
132
- - lib/badline/gui/application.rb
133
- - lib/badline/gui/drive_led_pane.rb
134
- - lib/badline/gui/gamepads.rb
135
- - lib/badline/gui/joy_map.rb
136
- - lib/badline/gui/key_map.rb
137
- - lib/badline/gui/palette.rb
138
- - lib/badline/gui/pane.rb
139
- - lib/badline/gui/screen_pane.rb
140
- - lib/badline/gui/snapshots.rb
141
- - lib/badline/gui/window.rb
134
+ - lib/badline/ffi.rb
135
+ - lib/badline/frontend.rb
136
+ - lib/badline/frontend/app.rb
137
+ - lib/badline/frontend/audio_sink.rb
138
+ - lib/badline/frontend/boot.rb
139
+ - lib/badline/frontend/buttons.rb
140
+ - lib/badline/frontend/controls.rb
141
+ - lib/badline/frontend/drive_led.rb
142
+ - lib/badline/frontend/filter_view.rb
143
+ - lib/badline/frontend/frame_rate.rb
144
+ - lib/badline/frontend/gamepads.rb
145
+ - lib/badline/frontend/info_view.rb
146
+ - lib/badline/frontend/keys.rb
147
+ - lib/badline/frontend/pacer.rb
148
+ - lib/badline/frontend/pad_port.rb
149
+ - lib/badline/frontend/painter.rb
150
+ - lib/badline/frontend/player_commands.rb
151
+ - lib/badline/frontend/player_footer.rb
152
+ - lib/badline/frontend/player_header.rb
153
+ - lib/badline/frontend/player_screen.rb
154
+ - lib/badline/frontend/player_state.rb
155
+ - lib/badline/frontend/player_window.rb
156
+ - lib/badline/frontend/scope.rb
157
+ - lib/badline/frontend/screen.rb
158
+ - lib/badline/frontend/screenshot.rb
159
+ - lib/badline/frontend/sid_history.rb
160
+ - lib/badline/frontend/sid_view.rb
161
+ - lib/badline/frontend/snapshots.rb
162
+ - lib/badline/frontend/sound.rb
163
+ - lib/badline/frontend/timeline.rb
164
+ - lib/badline/frontend/visualizer_view.rb
142
165
  - lib/badline/iec_bus.rb
143
166
  - lib/badline/input.rb
144
167
  - lib/badline/input/mouse1351.rb
@@ -174,9 +197,15 @@ files:
174
197
  - lib/badline/keyboard.rb
175
198
  - lib/badline/keyboard_buffer.rb
176
199
  - lib/badline/media.rb
200
+ - lib/badline/media/queue.rb
177
201
  - lib/badline/media/true_drive.rb
178
202
  - lib/badline/memory.rb
179
203
  - lib/badline/options.rb
204
+ - lib/badline/options/event.rb
205
+ - lib/badline/options/help.rb
206
+ - lib/badline/options/option.rb
207
+ - lib/badline/options/table.rb
208
+ - lib/badline/options/validation.rb
180
209
  - lib/badline/ram_expansion.rb
181
210
  - lib/badline/ram_expansion/banking.rb
182
211
  - lib/badline/ram_expansion/plus256k.rb
@@ -185,6 +214,7 @@ files:
185
214
  - lib/badline/region.rb
186
215
  - lib/badline/reu.rb
187
216
  - lib/badline/reu/dma.rb
217
+ - lib/badline/reu/dma/saved_state.rb
188
218
  - lib/badline/reu/ram.rb
189
219
  - lib/badline/reu/saved_state.rb
190
220
  - lib/badline/reu/trigger.rb
@@ -198,8 +228,6 @@ files:
198
228
  - lib/badline/roms/eapi/eapi-am29f040-14
199
229
  - lib/badline/roms/kernal.rom
200
230
  - lib/badline/sdl.rb
201
- - lib/badline/sdl/events.rb
202
- - lib/badline/sdl/functions.rb
203
231
  - lib/badline/sid.rb
204
232
  - lib/badline/sid/catch_up.rb
205
233
  - lib/badline/sid/decimator.rb
@@ -243,10 +271,13 @@ files:
243
271
  - lib/badline/storage/disk_image/writing.rb
244
272
  - lib/badline/storage/g64_image.rb
245
273
  - lib/badline/storage/host_directory.rb
274
+ - lib/badline/storage/hvsc.rb
246
275
  - lib/badline/storage/listing.rb
247
276
  - lib/badline/storage/p00.rb
248
277
  - lib/badline/storage/sid_file.rb
249
278
  - lib/badline/storage/song_lengths.rb
279
+ - lib/badline/storage/stil.rb
280
+ - lib/badline/storage/stil/credit.rb
250
281
  - lib/badline/storage/t64.rb
251
282
  - lib/badline/storage/tap.rb
252
283
  - lib/badline/storage/unavailable.rb
@@ -282,7 +313,6 @@ files:
282
313
  - lib/badline/vic/sprites.rb
283
314
  - lib/badline/vic/sprites_state.rb
284
315
  - packaging/homebrew/badline.rb
285
- - vendor/.gitkeep
286
316
  homepage: https://github.com/elektronaut/badline
287
317
  licenses:
288
318
  - MIT