badline 0.1.0 → 0.2.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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +185 -0
  3. data/CLAUDE.md +217 -0
  4. data/CODE_OF_CONDUCT.md +92 -0
  5. data/CONTRIBUTING.md +51 -0
  6. data/README.md +224 -31
  7. data/doc/pinned-behaviour.md +1117 -0
  8. data/exe/badline +22 -2
  9. data/exe/badline-sid +34 -0
  10. data/lib/badline/address_bus.rb +96 -25
  11. data/lib/badline/audio/aiff.rb +36 -0
  12. data/lib/badline/audio/bare_player.rb +129 -0
  13. data/lib/badline/audio/cli.rb +133 -0
  14. data/lib/badline/audio/console.rb +75 -0
  15. data/lib/badline/audio/jukebox.rb +82 -0
  16. data/lib/badline/audio/machine_player.rb +57 -0
  17. data/lib/badline/audio/options.rb +137 -0
  18. data/lib/badline/audio/pcm_writer.rb +57 -0
  19. data/lib/badline/audio/playback.rb +108 -0
  20. data/lib/badline/audio/renderer.rb +85 -0
  21. data/lib/badline/audio/sdl_sink.rb +102 -0
  22. data/lib/badline/audio/wav.rb +23 -0
  23. data/lib/badline/audio.rb +14 -0
  24. data/lib/badline/cartridge/action_replay.rb +111 -0
  25. data/lib/badline/cartridge/atomic_power.rb +24 -0
  26. data/lib/badline/cartridge/bank.rb +81 -0
  27. data/lib/badline/cartridge/comal80.rb +48 -0
  28. data/lib/badline/cartridge/dinamic.rb +34 -0
  29. data/lib/badline/cartridge/easy_flash.rb +122 -0
  30. data/lib/badline/cartridge/epyx_fastload.rb +71 -0
  31. data/lib/badline/cartridge/final_cartridge3.rb +77 -0
  32. data/lib/badline/cartridge/flash.rb +265 -0
  33. data/lib/badline/cartridge/freezer.rb +31 -0
  34. data/lib/badline/cartridge/fun_play.rb +38 -0
  35. data/lib/badline/cartridge/g_mod2.rb +62 -0
  36. data/lib/badline/cartridge/game_system.rb +40 -0
  37. data/lib/badline/cartridge/kcs_power.rb +72 -0
  38. data/lib/badline/cartridge/mach5.rb +34 -0
  39. data/lib/badline/cartridge/magic_desk.rb +6 -3
  40. data/lib/badline/cartridge/ocean.rb +11 -12
  41. data/lib/badline/cartridge/pagefox.rb +56 -0
  42. data/lib/badline/cartridge/retro_replay.rb +276 -0
  43. data/lib/badline/cartridge/rex_utility.rb +33 -0
  44. data/lib/badline/cartridge/rgcd.rb +48 -0
  45. data/lib/badline/cartridge/simons_basic.rb +40 -0
  46. data/lib/badline/cartridge/standard.rb +3 -19
  47. data/lib/badline/cartridge/super_games.rb +36 -0
  48. data/lib/badline/cartridge/westermann.rb +34 -0
  49. data/lib/badline/cartridge/zaxxon.rb +50 -0
  50. data/lib/badline/cartridge.rb +132 -16
  51. data/lib/badline/cia/interrupt_register.rb +102 -0
  52. data/lib/badline/cia/serial.rb +219 -0
  53. data/lib/badline/cia/timer.rb +43 -31
  54. data/lib/badline/cia.rb +138 -88
  55. data/lib/badline/color_memory.rb +14 -5
  56. data/lib/badline/computer.rb +97 -12
  57. data/lib/badline/control_ports.rb +54 -9
  58. data/lib/badline/cpu/addressing.rb +131 -0
  59. data/lib/badline/cpu/microcode.rb +169 -0
  60. data/lib/badline/cpu/operations.rb +88 -0
  61. data/lib/badline/cpu/stack_operations.rb +82 -0
  62. data/lib/badline/cpu.rb +109 -168
  63. data/lib/badline/cycleable.rb +3 -0
  64. data/lib/badline/datasette.rb +81 -0
  65. data/lib/badline/debug_register.rb +28 -0
  66. data/lib/badline/gui/application.rb +79 -18
  67. data/lib/badline/gui/gamepads.rb +97 -0
  68. data/lib/badline/gui/joy_map.rb +15 -7
  69. data/lib/badline/gui.rb +1 -0
  70. data/lib/badline/input/mouse1351.rb +41 -0
  71. data/lib/badline/input/paddles.rb +36 -0
  72. data/lib/badline/input.rb +4 -0
  73. data/lib/badline/instruction_set/arithmetic.rb +16 -21
  74. data/lib/badline/instruction_set/bitwise.rb +17 -22
  75. data/lib/badline/instruction_set/branch.rb +20 -19
  76. data/lib/badline/instruction_set/flag.rb +7 -7
  77. data/lib/badline/instruction_set/illegal.rb +14 -22
  78. data/lib/badline/instruction_set/inc_dec.rb +8 -10
  79. data/lib/badline/instruction_set/stack.rb +5 -57
  80. data/lib/badline/instruction_set/transfer.rb +9 -9
  81. data/lib/badline/instruction_set.rb +9 -31
  82. data/lib/badline/interrupts.rb +104 -0
  83. data/lib/badline/joystick.rb +9 -9
  84. data/lib/badline/kernal_trap/channel.rb +51 -0
  85. data/lib/badline/kernal_trap/drive/status.rb +49 -0
  86. data/lib/badline/kernal_trap/drive.rb +277 -0
  87. data/lib/badline/kernal_trap/file.rb +5 -25
  88. data/lib/badline/kernal_trap/load.rb +86 -24
  89. data/lib/badline/kernal_trap/routine.rb +51 -0
  90. data/lib/badline/kernal_trap/save.rb +46 -14
  91. data/lib/badline/kernal_trap/serial.rb +171 -0
  92. data/lib/badline/kernal_trap.rb +4 -0
  93. data/lib/badline/keyboard.rb +40 -16
  94. data/lib/badline/media.rb +48 -10
  95. data/lib/badline/memory.rb +5 -0
  96. data/lib/badline/roms/eapi/LICENSE.md +15 -0
  97. data/lib/badline/roms/eapi/README +41 -0
  98. data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
  99. data/lib/badline/sid/decimator.rb +46 -0
  100. data/lib/badline/sid/envelope.rb +232 -0
  101. data/lib/badline/sid/filter.rb +213 -0
  102. data/lib/badline/sid/voice.rb +61 -0
  103. data/lib/badline/sid/waveform/combined.rb +126 -0
  104. data/lib/badline/sid/waveform/fast_forward.rb +105 -0
  105. data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
  106. data/lib/badline/sid/waveform.rb +289 -0
  107. data/lib/badline/sid.rb +274 -7
  108. data/lib/badline/status.rb +251 -23
  109. data/lib/badline/storage/crt_file.rb +18 -2
  110. data/lib/badline/storage/d64_image.rb +6 -0
  111. data/lib/badline/storage/d71_image.rb +4 -0
  112. data/lib/badline/storage/d81_image.rb +4 -0
  113. data/lib/badline/storage/disk_image.rb +99 -13
  114. data/lib/badline/storage/host_directory.rb +30 -4
  115. data/lib/badline/storage/sid_file.rb +274 -0
  116. data/lib/badline/storage/song_lengths.rb +65 -0
  117. data/lib/badline/storage/t64.rb +67 -0
  118. data/lib/badline/storage/tap.rb +68 -0
  119. data/lib/badline/storage.rb +19 -0
  120. data/lib/badline/time_of_day.rb +105 -52
  121. data/lib/badline/version.rb +1 -1
  122. data/lib/badline/vic/bank.rb +14 -6
  123. data/lib/badline/vic/border_mask.rb +76 -0
  124. data/lib/badline/vic/collisions.rb +104 -0
  125. data/lib/badline/vic/color_patches.rb +33 -0
  126. data/lib/badline/vic/display_state.rb +92 -29
  127. data/lib/badline/vic/graphics_mode.rb +78 -53
  128. data/lib/badline/vic/graphics_shifter.rb +140 -0
  129. data/lib/badline/vic/register_log.rb +63 -0
  130. data/lib/badline/vic/registers.rb +3 -0
  131. data/lib/badline/vic/sequencer.rb +163 -137
  132. data/lib/badline/vic/sequencer_output.rb +116 -0
  133. data/lib/badline/vic/sprite/internal_bus.rb +49 -0
  134. data/lib/badline/vic/sprite/shifter.rb +129 -0
  135. data/lib/badline/vic/sprite.rb +275 -65
  136. data/lib/badline/vic/sprites.rb +220 -58
  137. data/lib/badline/vic.rb +389 -55
  138. data/lib/badline.rb +5 -0
  139. data/vendor/.gitkeep +0 -0
  140. metadata +104 -4
@@ -1,111 +1,273 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/vic/collisions"
4
+ require "badline/vic/register_log"
3
5
  require "badline/vic/sprite"
4
6
 
5
7
  module Badline
6
8
  class VIC < Cycleable
7
9
  class Sprites
8
- # $D019 latch bits raised by the collision registers.
9
- SPRITE_COLLISION_IRQ = 0x04 # IMMC, mirrors $D01E
10
- DATA_COLLISION_IRQ = 0x02 # IMBC, mirrors $D01F
10
+ # Pixels between the start of the cycle following a write and the
11
+ # point the new value shows. Each signal reaches the output on its own
12
+ # path: the colors feed the final mux, the sequencer inputs — X
13
+ # position, multicolor and expansion — sit furthest upstream, and the
14
+ # priority mux lands one pixel ahead of them.
15
+ COLOR_DELAY = 1
16
+ PRIORITY_DELAY = 6
17
+ SEQUENCER_DELAY = 7
18
+
19
+ # The beam position a CPU write in Bauer cycle 15 sees: the VIC has
20
+ # run column 13 and runs column 14, where MCBASE moves, next.
21
+ CRUNCH_X = 14 * 8
22
+
23
+ WRITE_DELAY = Array.new(2**6).tap do |delays|
24
+ (0x00..0x0e).step(2) { |reg| delays[reg] = SEQUENCER_DELAY }
25
+ [0x10, 0x1c, 0x1d].each { |reg| delays[reg] = SEQUENCER_DELAY }
26
+ delays[0x1b] = PRIORITY_DELAY
27
+ [0x25, 0x26, *0x27..0x2e].each { |reg| delays[reg] = COLOR_DELAY }
28
+ end.freeze
11
29
 
12
30
  def initialize(registers, bank, width)
13
31
  @registers = registers
14
32
  @bank = bank
15
33
  @width = width
16
- @sprites = Array.new(8) { |i| Sprite.new(i, registers, bank, width) }
17
- @hits = Array.new(width, 0)
34
+ @bus = Sprite::InternalBus.new(bank, width / 8)
35
+ @sprites = Array.new(8) { |i| Sprite.new(i, registers, bank, width, @bus) }
36
+ @collisions = Collisions.new(registers, width)
37
+ @hits = @collisions.hits
18
38
  @win_color = Array.new(width, 0)
19
39
  @win_priority = Array.new(width, false)
40
+ @palette = Array.new(4, 0)
41
+ @log = RegisterLog.new(registers)
42
+ @any_dma = false
43
+ @stopped_dma = false
44
+ @carry = Array.new(8) { [] }
45
+ @carry_next = Array.new(8) { [] }
46
+ @carried = false
47
+ @lo = width
48
+ @hi = 0
49
+ @sequenced = -1
20
50
  end
21
51
 
22
52
  def [](index) = @sprites[index]
23
53
 
24
- def start_line(line)
25
- @sprites.each { |sprite| sprite.start_line(line) }
54
+ # Pixels a sprite drew past the end of the last line land at the start
55
+ # of this one, which is when the beam reaches them.
56
+ def start_line
57
+ @bus.start_line(@registers[0x15].nonzero?)
58
+ @log.clear
59
+ @sprites.each(&:start_line)
60
+ @collisions.start_line
61
+ @sequenced = -1
62
+ @carry, @carry_next = @carry_next, @carry
63
+ @carry_next.each(&:clear)
64
+ @carried = @carry.any?(&:any?)
65
+ end
66
+
67
+ # Bauer cycle 16, two columns ahead like the compares: MCBASE takes
68
+ # MC for every sprite whose expansion flip-flop is set.
69
+ def advance_mcbase
70
+ @sprites.each(&:advance_mcbase) if @any_dma
71
+ end
72
+
73
+ # The end-of-sprite compare, a column after MCBASE moves. It is the
74
+ # only point a sprite's DMA stops, so the cached flag is settled here
75
+ # and set again by the compare that starts one.
76
+ def finish_mcbase
77
+ @stopped_dma = false
78
+ return unless @any_dma
79
+
80
+ running = @sprites.count(&:displaying?)
81
+ @sprites.each(&:finish_mcbase)
82
+ still = @sprites.count(&:displaying?)
83
+ @any_dma = still.positive?
84
+ @stopped_dma = still < running
85
+ end
86
+
87
+ # True when the last #finish_mcbase ended a DMA, so the BA columns
88
+ # have to be rebuilt without it.
89
+ def stopped_dma? = @stopped_dma
90
+
91
+ # Bauer cycle 56, after the second Y compare.
92
+ def toggle_expansion
93
+ @sprites.each(&:toggle_expansion) if @any_dma
94
+ end
95
+
96
+ # Run the Y/enable compare (Bauer cycles 55/56) for every sprite;
97
+ # returns true when any sprite turned its DMA on, so the BA columns
98
+ # can be rebuilt mid-line.
99
+ def check_dma(line, column)
100
+ return false if @registers[0x15].zero?
101
+
102
+ hit = @sprites.map { |sprite| sprite.check_dma(line, column) }.any?
103
+ @any_dma ||= hit
104
+ hit
105
+ end
106
+
107
+ # Not gated on the DMA flag, since this is where a display outlives its
108
+ # DMA. MC now points at the next row, which a sprite firing after its
109
+ # reload shows, so a line sequenced before this has to be redone.
110
+ def check_display(line)
111
+ @sprites.each { |sprite| sprite.check_display(line) }
112
+ @sequenced = -1
113
+ end
114
+
115
+ # A CPU access to a VIC register, in the cycle after the VIC's
116
+ # `column`.
117
+ def bus_data(column, value) = @bus.access(column, value)
118
+
119
+ # Not gated on the DMA flag: a row fetched at the start of the line
120
+ # still renders when cycle 16 ends the sprite.
121
+ def active? = @carried || @sprites.any?(&:rendering?)
122
+
123
+ # Record a mid-line write at the pixel where it becomes visible.
124
+ def log_change(reg, old, value, beam_x)
125
+ clear_y_expansion(old & ~value, beam_x) if reg == 0x17
126
+ delay = WRITE_DELAY[reg]
127
+ return unless delay && active?
128
+
129
+ @log.log(beam_x + delay, reg, old, value)
130
+ end
131
+
132
+ # Latch the collisions for every sprite pixel the beam has passed
133
+ # since the last fold.
134
+ def collide_upto(beam_x, mask)
135
+ upto = [beam_x, @width].min
136
+ return if upto <= @collisions.folded
137
+
138
+ sequence_line if active?
139
+ @collisions.fold(mask, upto, @lo, @hi)
26
140
  end
27
141
 
28
- def active? = @sprites.any?(&:displaying?)
142
+ def clear_collision(reg, beam_x) = @collisions.clear(reg, beam_x)
29
143
 
30
- # Merge each displaying sprite's line into the scratch buffers, then
31
- # apply the winners over the background in a single pass over the
32
- # touched span.
33
- def composite(colors, mask)
34
- @sprite_clash = 0
35
- @data_clash = 0
36
- lo = @width
37
- hi = 0
144
+ # End of line: fold in the pixels the beam reached since the last
145
+ # read, then paint the winners over the background. A line with no
146
+ # background to paint over — one in the vertical blank — still
147
+ # collides, so `colors` is nil there rather than absent.
148
+ def finish_line(colors, mask)
149
+ collide_upto(@width, mask)
150
+ colors ? apply(colors, mask) : clear_coverage
151
+ end
38
152
 
153
+ private
154
+
155
+ # A $d017 write that clears MxYE acts on the flip-flop at once, rather
156
+ # than from a column hook, and crunches in Bauer cycle 15.
157
+ def clear_y_expansion(cleared, beam_x)
158
+ crunch = beam_x == CRUNCH_X
39
159
  @sprites.each do |sprite|
40
- next unless sprite.displaying?
160
+ sprite.clear_y_expansion(crunch) if cleared.anybits?(1 << sprite.index)
161
+ end
162
+ end
163
+
164
+ # Replay the line for every sprite, filling the per-pixel coverage
165
+ # that both the fold and the composite read. A write logged later in
166
+ # the line can only move pixels past it, so re-running after the log
167
+ # grows leaves everything already folded where it was.
168
+ def sequence_line
169
+ return if @sequenced == @log.length
41
170
 
42
- seg_lo, seg_hi = merge(sprite, mask)
43
- lo = seg_lo if seg_lo < lo
44
- hi = seg_hi if seg_hi > hi
171
+ clear_coverage
172
+ @log.prepare
173
+ @sprites.each do |sprite|
174
+ sprite.sequence(@log)
175
+ merge(sprite)
45
176
  end
177
+ @sequenced = @log.length
178
+ end
46
179
 
47
- apply(colors, mask, lo, hi)
48
- register_collisions
180
+ # Write one sprite's pixels into the coverage, picking up the color and
181
+ # priority registers as they stood at each pixel. The first sprite to
182
+ # claim a pixel wins (lowest index has priority); later ones only add
183
+ # their bit to it.
184
+ def merge(sprite)
185
+ bit = 1 << sprite.index
186
+ @carry_next[sprite.index].clear
187
+ @carry[sprite.index].each_slice(3) do |x, color, priority|
188
+ track(x, 1)
189
+ merge_pixel(x, color, bit, priority)
190
+ end
191
+ merge_run(sprite, sprite.leftmost, sprite.span, sprite.codes)
192
+ merge_run(sprite, sprite.reload_leftmost, sprite.reload_span, sprite.reload_codes)
49
193
  end
50
194
 
51
- private
195
+ def merge_run(sprite, leftmost, span, codes)
196
+ return if span.zero?
197
+
198
+ track(leftmost, span)
199
+ log = @log.empty? ? nil : @log
200
+ log&.rewind
201
+ merge_span(sprite, log, leftmost, span, codes)
202
+ end
52
203
 
53
- # Write one sprite's pixels into the scratch line. The first sprite to
54
- # claim a pixel wins (lowest index has priority); later hits only
55
- # accumulate collision bits.
56
- def merge(sprite, mask)
57
- left = sprite.leftmost
58
- pixels = sprite.line_pixels
59
- last = sprite.pixel_width
204
+ def merge_span(sprite, log, leftmost, span, codes)
60
205
  bit = 1 << sprite.index
61
- priority = sprite.priority?
62
-
63
- i = 0
64
- while i < last
65
- color = pixels[i]
66
- if color
67
- x = left + i
68
- x -= @width if x >= @width
69
- merge_pixel(x, color, bit, priority, mask)
206
+ palette = sprite.palette(log || @registers, @palette)
207
+ priority = (log || @registers)[0x1b].anybits?(bit)
208
+ pos = leftmost
209
+ boundary = log ? 0 : Float::INFINITY
210
+ index = 0
211
+ while index < span
212
+ if pos >= boundary
213
+ log.advance(pos)
214
+ sprite.palette(log, palette)
215
+ priority = log[0x1b].anybits?(bit)
216
+ boundary = log.next_x
70
217
  end
71
- i += 1
218
+ code = codes[index]
219
+ place(pos, palette[code], bit, priority) unless code.zero?
220
+ index += 1
221
+ pos += 1
72
222
  end
223
+ end
73
224
 
74
- right = left + last
75
- right > @width ? [0, @width] : [left, right]
225
+ # A pixel past the end of the line belongs to the start of the next.
226
+ def place(pos, color, bit, priority)
227
+ if pos < @width
228
+ merge_pixel(pos, color, bit, priority)
229
+ else
230
+ @carry_next[bit.bit_length - 1].push(pos - @width, color, priority)
231
+ end
76
232
  end
77
233
 
78
- def merge_pixel(pos, color, bit, priority, mask)
234
+ def track(left, span)
235
+ right = left + span
236
+ if right > @width
237
+ @lo = 0
238
+ @hi = @width
239
+ else
240
+ @lo = left if left < @lo
241
+ @hi = right if right > @hi
242
+ end
243
+ end
244
+
245
+ def merge_pixel(pos, color, bit, priority)
79
246
  bits = @hits[pos]
80
247
  if bits.zero?
81
248
  @win_color[pos] = color
82
249
  @win_priority[pos] = priority
83
- else
84
- @sprite_clash |= bits | bit
85
250
  end
86
251
  @hits[pos] = bits | bit
87
- @data_clash |= bit if mask[pos]
88
252
  end
89
253
 
90
- def apply(colors, mask, from, upto)
91
- pos = from
92
- while pos < upto
93
- if @hits[pos].nonzero?
254
+ def apply(colors, mask)
255
+ pos = @lo
256
+ while pos < @hi
257
+ unless @hits[pos].zero?
94
258
  colors[pos] = @win_color[pos] unless @win_priority[pos] && mask[pos]
95
259
  @hits[pos] = 0
96
260
  end
97
261
  pos += 1
98
262
  end
263
+ @lo = @width
264
+ @hi = 0
99
265
  end
100
266
 
101
- def register_collisions
102
- if @sprite_clash.nonzero? && @registers.collide!(0x1e, @sprite_clash)
103
- @registers.latch_irq!(SPRITE_COLLISION_IRQ)
104
- end
105
- return if @data_clash.zero?
106
- return unless @registers.collide!(0x1f, @data_clash)
107
-
108
- @registers.latch_irq!(DATA_COLLISION_IRQ)
267
+ def clear_coverage
268
+ @hits.fill(0, @lo, @hi - @lo) if @hi > @lo
269
+ @lo = @width
270
+ @hi = 0
109
271
  end
110
272
  end
111
273
  end