badline 0.1.0 → 0.2.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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +167 -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 +88 -23
  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 +131 -15
  51. data/lib/badline/cia/interrupt_register.rb +88 -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 +109 -74
  55. data/lib/badline/color_memory.rb +14 -5
  56. data/lib/badline/computer.rb +68 -8
  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 +171 -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 +108 -167
  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 +33 -0
  85. data/lib/badline/kernal_trap/drive.rb +239 -0
  86. data/lib/badline/kernal_trap/file.rb +4 -24
  87. data/lib/badline/kernal_trap/load.rb +2 -2
  88. data/lib/badline/kernal_trap/routine.rb +34 -0
  89. data/lib/badline/kernal_trap/save.rb +1 -1
  90. data/lib/badline/kernal_trap/serial.rb +139 -0
  91. data/lib/badline/kernal_trap.rb +4 -0
  92. data/lib/badline/keyboard.rb +40 -16
  93. data/lib/badline/media.rb +48 -10
  94. data/lib/badline/roms/eapi/LICENSE.md +15 -0
  95. data/lib/badline/roms/eapi/README +41 -0
  96. data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
  97. data/lib/badline/sid/decimator.rb +46 -0
  98. data/lib/badline/sid/envelope.rb +232 -0
  99. data/lib/badline/sid/filter.rb +213 -0
  100. data/lib/badline/sid/voice.rb +61 -0
  101. data/lib/badline/sid/waveform/combined.rb +126 -0
  102. data/lib/badline/sid/waveform/fast_forward.rb +105 -0
  103. data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
  104. data/lib/badline/sid/waveform.rb +289 -0
  105. data/lib/badline/sid.rb +274 -7
  106. data/lib/badline/status.rb +32 -27
  107. data/lib/badline/storage/crt_file.rb +18 -2
  108. data/lib/badline/storage/disk_image.rb +52 -5
  109. data/lib/badline/storage/host_directory.rb +18 -4
  110. data/lib/badline/storage/sid_file.rb +274 -0
  111. data/lib/badline/storage/song_lengths.rb +65 -0
  112. data/lib/badline/storage/t64.rb +67 -0
  113. data/lib/badline/storage/tap.rb +68 -0
  114. data/lib/badline/storage.rb +19 -0
  115. data/lib/badline/time_of_day.rb +105 -52
  116. data/lib/badline/version.rb +1 -1
  117. data/lib/badline/vic/bank.rb +14 -6
  118. data/lib/badline/vic/border_mask.rb +76 -0
  119. data/lib/badline/vic/collisions.rb +104 -0
  120. data/lib/badline/vic/color_patches.rb +33 -0
  121. data/lib/badline/vic/display_state.rb +92 -29
  122. data/lib/badline/vic/graphics_mode.rb +78 -53
  123. data/lib/badline/vic/graphics_shifter.rb +140 -0
  124. data/lib/badline/vic/register_log.rb +63 -0
  125. data/lib/badline/vic/registers.rb +3 -0
  126. data/lib/badline/vic/sequencer.rb +163 -137
  127. data/lib/badline/vic/sequencer_output.rb +116 -0
  128. data/lib/badline/vic/sprite/internal_bus.rb +49 -0
  129. data/lib/badline/vic/sprite/shifter.rb +129 -0
  130. data/lib/badline/vic/sprite.rb +275 -65
  131. data/lib/badline/vic/sprites.rb +220 -58
  132. data/lib/badline/vic.rb +373 -49
  133. data/lib/badline.rb +5 -0
  134. data/vendor/.gitkeep +0 -0
  135. metadata +103 -4
data/lib/badline/vic.rb CHANGED
@@ -14,10 +14,40 @@ module Badline
14
14
  attr_reader :address_bus, :display, :width, :height, :vic_bank, :column,
15
15
  :rasterline, :dirty_lines
16
16
 
17
- SPRITE_BA_RANGES = [
18
- 55..59, 57..61, 59..62,
19
- 0..2, 0..4, 2..6, 4..8, 6..10
20
- ].freeze
17
+ # Returns the byte the CPU's halted read would see, for the c-accesses
18
+ # that run before AEC. Without it they read colour RAM.
19
+ attr_writer :open_bus
20
+
21
+ LIGHTPEN_IRQ = 0x08 # $D019 latch bit
22
+
23
+ # A g-access reaches the pixel output this many columns after it runs.
24
+ GRAPHICS_DELAY = 2
25
+
26
+ # What a column's g-access passed to the sequencer.
27
+ G_BLANK = 0
28
+ G_IDLE = 1
29
+ G_DISPLAY = 2
30
+
31
+ # The $d011 mode bits a g-access still sees for a cycle after they fall:
32
+ # BMM always, and ECM too when the access it leaves read the character
33
+ # ROM.
34
+ FETCH_HOLD = 0x20
35
+ FETCH_HOLD_ROM = 0x60
36
+
37
+ # Columns carrying a per-cycle hook, so an ordinary column costs one
38
+ # array read instead of the dispatch.
39
+ HOOK_COLUMNS = Array.new(63) do |column|
40
+ [14, 15, 53, 54, 57, 62].include?(column)
41
+ end.freeze
42
+
43
+ # BA falls three columns ahead of each sprite's pair of s-accesses, and
44
+ # the five-column windows step two columns apart from sprite 0 at column
45
+ # 54. From sprite 3 on they reach past the end of the line, so each is
46
+ # split: the tail columns fall on the line whose cycle 55/56 compare
47
+ # started the fetch, the head columns on the line after it.
48
+ SPRITE_BA_WINDOWS = Array.new(8) { |n| (54 + (2 * n))..(58 + (2 * n)) }.freeze
49
+ SPRITE_BA_TAIL = SPRITE_BA_WINDOWS.map { |w| w.select { |c| c < 63 } }.freeze
50
+ SPRITE_BA_HEAD = SPRITE_BA_WINDOWS.map { |w| w.filter_map { |c| c - 63 if c >= 63 } }.freeze
21
51
 
22
52
  def initialize(address_bus = nil, debug: false)
23
53
  addressable_at(0xd000, length: 2**10)
@@ -29,10 +59,12 @@ module Badline
29
59
  @height = 312
30
60
 
31
61
  @registers = VIC::Registers.new
62
+ @register_bytes = @registers.bytes
32
63
  @display_state = VIC::DisplayState.new(@registers)
33
64
  @sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank)
34
65
  @sprites = VIC::Sprites.new(@registers, @vic_bank, @width)
35
66
  @display = Array.new(@width * @height, 0)
67
+ @lines = Array.new(@height) { Array.new(@width, 0) }
36
68
  @dirty_lines = Array.new(@height, true)
37
69
 
38
70
  @column = 0
@@ -43,35 +75,65 @@ module Badline
43
75
  @character_buffer = Array.new(40, 0)
44
76
  @color_buffer = Array.new(40, 0)
45
77
  @sprite_ba = Array.new(@width / 8, false)
78
+ @g_tick = 0
79
+ @g_kind = Array.new(4, G_BLANK)
80
+ @g_kept_char = 0
81
+ @g_kept_color = 0
82
+ @g_data = @sequencer.ring_data
83
+ @g_char = @sequencer.ring_char
84
+ @g_color = @sequencer.ring_color
85
+ @g_display = false
86
+ @fetch_d011 = @register_bytes[0x11]
87
+ @lp_triggered = false
88
+ @lp_low = false
89
+ @raster_match = false
90
+ @render = true
46
91
 
47
92
  super()
48
93
  end
49
94
 
50
95
  def cycle!
51
- if @column.zero?
52
- @display_state.new_frame if @rasterline.zero?
53
- check_raster_irq!
54
- @sequencer.new_line(@rasterline)
55
- @sprites.start_line(@rasterline)
56
- rebuild_sprite_ba
57
- @display_state.new_line
58
- end
96
+ start_line! if @column.zero?
59
97
 
98
+ # The g-access runs in the first half of the cycle, ahead of the bad
99
+ # line compare, and the c-access in the second half, after it.
100
+ draw!
60
101
  @display_state.cycle(@rasterline, @column)
61
102
 
62
103
  fetch_character_data! if dma_active?
63
104
 
64
- draw!
105
+ column_hooks if HOOK_COLUMNS[@column]
65
106
 
66
107
  @column += 1
67
108
  if @column == @columns_per_line
68
109
  finish_line!
69
110
  @column = 0
70
111
  @rasterline = @rasterline == @last_line ? 0 : @rasterline + 1
112
+ check_raster_irq!(@rasterline.zero? ? @last_line : @rasterline)
71
113
  end
114
+ watch_collisions
72
115
  nil
73
116
  end
74
117
 
118
+ # The sprite and border hooks that fall on named cycles, one column
119
+ # ahead of Bauer's numbering — two for MCBASE, the DMA compares and the
120
+ # expansion flip-flop, which the `spriteenable` and `spritecrunch`
121
+ # references place a column earlier still. Guarded in
122
+ # #cycle! so an ordinary column pays two compares rather than the
123
+ # dispatch.
124
+ def column_hooks
125
+ case @column
126
+ when 14 then @sprites.advance_mcbase
127
+ when 15
128
+ @sequencer.left_compare_vertical_border
129
+ finish_sprite_mcbase
130
+ when 53 then check_sprite_dma
131
+ when 54 then check_dma_and_toggle_expansion
132
+ when 57 then @sprites.check_display(@rasterline)
133
+ when 62 then @sequencer.start_vertical_border(@rasterline == @last_line ? 0 : @rasterline + 1)
134
+ end
135
+ end
136
+
75
137
  # The IRQ line is held asserted while any enabled latch bit is set in
76
138
  # $D019/$D01A, until the program acknowledges it by writing to $D019.
77
139
  def interrupted?
@@ -81,15 +143,20 @@ module Badline
81
143
  def peek(addr)
82
144
  i = index(addr) % (2**6)
83
145
  case i
84
- when 0x11 then (@registers[0x11] & 0x7f) | ((rasterline & 0x100) >> 1)
85
- when 0x12 then rasterline & 0xff
146
+ when 0x11 then (@registers[0x11] & 0x7f) | ((raster_counter & 0x100) >> 1)
147
+ when 0x12 then raster_counter & 0xff
86
148
  when 0x19 then irq_status # Latch + master IRQ bit, unused bits read 1
149
+ when 0x1e, 0x1f then read_collision(i)
87
150
  else @registers.read(i)
88
- end
151
+ end.tap { |value| @sprites.bus_data(@column, value) }
89
152
  end
90
153
 
91
154
  def poke(addr, value)
92
- @registers.write(index(addr) % (2**6), value)
155
+ reg = index(addr) % (2**6)
156
+ @sprites.bus_data(@column, value)
157
+ log_register_change(reg, value)
158
+ @registers.write(reg, value)
159
+ compare_raster_writes(reg)
93
160
  end
94
161
 
95
162
  def position
@@ -97,13 +164,51 @@ module Badline
97
164
  end
98
165
 
99
166
  def dma_active?
100
- @display_state.bad_line? && @column >= 15 && @column < 55
167
+ @display_state.fetching?(@column)
101
168
  end
102
169
 
170
+ # Asked once the VIC has advanced, so @column is one ahead of the cycle
171
+ # the CPU is about to run. A bad line holds BA on the condition as it
172
+ # stands rather than the latched match.
103
173
  def ba_low?
104
174
  return true if @sprite_ba[@column]
105
175
 
106
- @display_state.bad_line? && @column >= 13 && @column < 56
176
+ @display_state.bad_line_condition? &&
177
+ @column > DisplayState::DMA_FIRST && @column <= DisplayState::DMA_LAST + 1
178
+ end
179
+
180
+ # Light pen input level (CIA1 PB4). A falling edge triggers the latch.
181
+ def lightpen_level(high)
182
+ @lp_low = !high
183
+ trigger_lightpen if @lp_low
184
+ end
185
+
186
+ # The first trigger per frame latches the beam position into LPX/LPY and
187
+ # raises the LP IRQ. The latch happens one cycle after the edge, with the
188
+ # 6569's two extra half-pixels; a trigger on the last line is consumed
189
+ # without latching unless it lands on the line's first cycle.
190
+ def trigger_lightpen
191
+ return if @lp_triggered
192
+
193
+ @lp_triggered = true
194
+ column = @column + 1
195
+ line = @rasterline
196
+ if column == @columns_per_line
197
+ column = 0
198
+ line = line == @last_line ? 0 : line + 1
199
+ end
200
+ return if line == @last_line && column.positive?
201
+
202
+ vic_x = ((column * 8) - Sprite::X_OFFSET) % @width
203
+ latch_lightpen((vic_x >> 1) + 2, line)
204
+ end
205
+
206
+ # The byte the VIC fetched in the phi1 half of the cycle the CPU is
207
+ # running, worked out from the VIC's state. The CPU cycle after column
208
+ # c is Bauer cycle c + 2, and each Bauer cycle has a fixed access (VICE
209
+ # `cycle_phi1_fetch`).
210
+ def phi1_data
211
+ vic_bank.peek(phi1_address)
107
212
  end
108
213
 
109
214
  def hblank?
@@ -118,6 +223,18 @@ module Badline
118
223
  @rasterline < 16 || @rasterline > 299 || @column < 10 || @column > 60
119
224
  end
120
225
 
226
+ def render? = @render
227
+
228
+ # Whether finished lines are painted into #display. A headless run that
229
+ # only reads the machine's state can turn it off: the sprite
230
+ # collisions, lightpen and every register still behave the same, but
231
+ # #display keeps the last lines painted. Turned back on, it is whole
232
+ # again from the start of the next frame.
233
+ def render=(on)
234
+ @render = on
235
+ @sequencer.render = on
236
+ end
237
+
121
238
  # Reset the dirty flags once the frontend has consumed them.
122
239
  def clear_dirty_lines!
123
240
  @dirty_lines.fill(false)
@@ -125,68 +242,275 @@ module Badline
125
242
 
126
243
  private
127
244
 
245
+ # The raster counter as $D011/$D012 read it. Every line advances on the
246
+ # CPU cycle paired with column 62 except line 0, which the counter
247
+ # reaches a cycle later, together with the line 0 raster compare.
248
+ def raster_counter
249
+ @column.zero? && @rasterline.zero? ? @last_line : @rasterline
250
+ end
251
+
252
+ # Mid-line writes to the color and sprite registers are logged against
253
+ # the cycle after the write (the CPU runs after the VIC within a machine
254
+ # cycle, so @column already points there); each register adds its own
255
+ # pixel delay on top.
256
+ def log_register_change(reg, value)
257
+ old = @registers[reg]
258
+ return if old == value
259
+
260
+ if (0x20..0x24).cover?(reg)
261
+ @sequencer.colors_changed! unless reg == 0x20
262
+ @sequencer.color_patches.log(reg, old, value, @column * 8) if @render && !blanking?
263
+ else
264
+ @sprites.log_change(reg, old, value, @column * 8)
265
+ end
266
+ end
267
+
268
+ # Bauer cycles 1-10 and 58-63 are sprite p- and s-accesses, 11-15
269
+ # refresh, 16-55 g-accesses and 56-57 idle. @column has already
270
+ # advanced, so it is the Bauer cycle less one.
271
+ def phi1_address
272
+ cycle = @column + 1
273
+ if cycle < 11 || cycle > 57
274
+ sprite_phi1_address((cycle - 58) % 63)
275
+ elsif cycle < 16
276
+ 0x3f00 | ((0xff - (5 * @rasterline) - (cycle - 11)) & 0xff)
277
+ elsif cycle < 56
278
+ graphics_phi1_address
279
+ else
280
+ 0x3fff
281
+ end
282
+ end
283
+
284
+ # Each sprite takes two cycles from Bauer 58, the p-access and then the
285
+ # middle s-access.
286
+ def sprite_phi1_address(slot)
287
+ n = slot >> 1
288
+ return @registers.screen_base + 0x3f8 + n if slot.even?
289
+
290
+ @sprites[n].phi1_address || 0x3fff
291
+ end
292
+
293
+ # The g-access of the column just run. In display state it used the
294
+ # VC and VMLI it then stepped past.
295
+ def graphics_phi1_address
296
+ return @registers.ecm.nonzero? ? 0x39ff : 0x3fff unless @g_display
297
+
298
+ graphics_address(@registers[0x11], @display_state.vmli - 1, (@display_state.vc - 1) & 0x3ff)
299
+ end
300
+
301
+ # A $d011/$d012 write is compared in the next column. A write after
302
+ # column 61 is left to column 62, which compares the next line.
303
+ def compare_raster_writes(reg)
304
+ return unless (0x11..0x12).cover?(reg)
305
+ return if @column == @columns_per_line - 1
306
+
307
+ check_raster_irq!
308
+ @sequencer.compare_vertical_border(@rasterline) if reg == 0x11
309
+ end
310
+
311
+ def check_sprite_dma
312
+ rebuild_sprite_ba if @sprites.check_dma(@rasterline, @column)
313
+ end
314
+
315
+ # A sprite whose DMA ends here fetches nothing at the end of this line,
316
+ # so its BA tail goes too.
317
+ def finish_sprite_mcbase
318
+ @sprites.finish_mcbase
319
+ rebuild_sprite_ba if @sprites.stopped_dma?
320
+ end
321
+
322
+ def check_dma_and_toggle_expansion
323
+ check_sprite_dma
324
+ @sprites.toggle_expansion
325
+ end
326
+
327
+ # The trigger re-arms at the start of each frame; if the pen line is
328
+ # still low, the latch retriggers immediately with a fixed LPX of $d1.
329
+ def start_lightpen_frame
330
+ @lp_triggered = false
331
+ return unless @lp_low
332
+
333
+ @lp_triggered = true
334
+ latch_lightpen(0xd1, 0)
335
+ end
336
+
337
+ def latch_lightpen(lpx, line)
338
+ @registers.write(0x13, lpx & 0xff)
339
+ @registers.write(0x14, line & 0xff)
340
+ @registers.latch_irq!(LIGHTPEN_IRQ)
341
+ end
342
+
128
343
  def rebuild_sprite_ba
129
344
  @sprite_ba.fill(false)
130
- SPRITE_BA_RANGES.each_with_index do |range, n|
345
+ 8.times do |n|
131
346
  next unless @sprites[n].displaying?
132
347
 
133
- range.each { |c| @sprite_ba[c] = true }
348
+ SPRITE_BA_TAIL[n].each { |c| @sprite_ba[c] = true }
349
+ SPRITE_BA_HEAD[n].each { |c| @sprite_ba[c] = true }
134
350
  end
135
351
  end
136
352
 
353
+ # Runs this column's g-access and draws the one from GRAPHICS_DELAY
354
+ # columns earlier. The access reads its byte now, through the mode,
355
+ # $d018 and bank of this cycle. A column with no g-access, or one made
356
+ # while the vertical border flip-flop is set, latches nothing: it passes
357
+ # on zero data with the screen byte and colour the last g-access
358
+ # latched, which an idle g-access clears.
137
359
  def draw!
138
- return if blanking?
360
+ slot = @g_tick = (@g_tick + 1) & 3
361
+ display_state = @display_state
362
+ access = display_state.graphics_column?(@column) && !@sequencer.vertical_closed?
363
+ if access
364
+ latch_graphics(slot, display_state)
365
+ else
366
+ @g_kind[slot] = G_BLANK
367
+ @g_data[slot] = 0
368
+ @g_char[slot] = @g_kept_char
369
+ @g_color[slot] = @g_kept_color
370
+ end
371
+ @g_display = display_state.display?
372
+ display_state.graphics_access(@column) if @g_display
373
+ emit_graphics((slot - GRAPHICS_DELAY) & 3, @column - 16) unless blanking?
374
+ @sequencer.latch_xscroll(@register_bytes[0x16] & 0b111) if access
375
+ @fetch_d011 = @register_bytes[0x11]
376
+ end
139
377
 
140
- col = @column - 16
141
- unless @display_state.display?
142
- @sequencer.emit_idle(col)
378
+ def latch_graphics(slot, display_state)
379
+ unless display_state.display?
380
+ @g_kind[slot] = G_IDLE
381
+ @g_data[slot] = vic_bank.peek(@registers.ecm.zero? ? 0x3fff : 0x39ff)
382
+ @g_char[slot] = @g_color[slot] = @g_kept_char = @g_kept_color = 0
143
383
  return
144
384
  end
145
385
 
146
- cell = (@display_state.vc_base + col) & 0x3ff
147
- screencode = @character_buffer[col] || 0
148
- @sequencer.emit(screencode, @color_buffer[col] || 1, col,
149
- cell, @display_state.rc)
386
+ vmli = display_state.vmli
387
+ @g_kind[slot] = G_DISPLAY
388
+ @g_data[slot] = fetch_graphics(vmli, display_state.vc)
389
+ @g_char[slot] = @g_kept_char = @character_buffer[vmli] || 0
390
+ @g_color[slot] = @g_kept_color = @color_buffer[vmli] || 1
150
391
  end
151
392
 
152
- def finish_line!
153
- return if vblank?
393
+ # The g-access sees a mode bit that falls a cycle late: it addresses
394
+ # with $d011 as this column has it, OR-ed with the held bits the column
395
+ # before had. When BMM changes and the access moves from RAM onto the
396
+ # character ROM, the low address byte still comes from the old mode
397
+ # (VICE x64sc `vicii_fetch_graphics`).
398
+ def fetch_graphics(vmli, counter)
399
+ d011 = @register_bytes[0x11]
400
+ last = @fetch_d011
401
+ return vic_bank.peek(graphics_address(d011, vmli, counter)) if d011 == last
402
+
403
+ from = graphics_address(last, vmli, counter)
404
+ from_rom = vic_bank.character_rom?(from)
405
+ address = graphics_address(d011 | (last & (from_rom ? FETCH_HOLD_ROM : FETCH_HOLD)), vmli, counter)
406
+ if (d011 ^ last).anybits?(0x20) && !from_rom
407
+ to = graphics_address(d011, vmli, counter)
408
+ address = (from & 0xff) | (to & 0x3f00) if vic_bank.character_rom?(to)
409
+ end
410
+ vic_bank.peek(address)
411
+ end
154
412
 
155
- # Composite the active sprites over the finished background line
156
- # and copy the line into the frame display.
157
- if @sprites.active?
158
- @sprites.composite(@sequencer.colors, @sequencer.fg)
159
- @sequencer.apply_border
413
+ def graphics_address(d011, vmli, counter)
414
+ rc = @display_state.rc
415
+ case d011 & 0x60
416
+ when 0x00 then @registers.char_base | ((@character_buffer[vmli] || 0) << 3) | rc
417
+ when 0x20 then @registers.bitmap_base | (counter << 3) | rc
418
+ when 0x40 then (@registers.char_base | ((@character_buffer[vmli] || 0) << 3) | rc) & 0x39ff
419
+ else (@registers.bitmap_base | (counter << 3) | rc) & 0x39ff
160
420
  end
161
- base = @rasterline * @width
421
+ end
422
+
423
+ def emit_graphics(slot, col)
424
+ @sequencer.emit(slot, col, @g_kind[slot] == G_DISPLAY)
425
+ end
426
+
427
+ # Fold the rest of the line's sprite collisions in — they latch whether
428
+ # or not there is a line to draw — then composite the active sprites
429
+ # over the finished background and copy the line into the frame display.
430
+ def finish_line!
431
+ return @sprites.finish_line(nil, @sequencer.fg) if vblank? || !@render
432
+
433
+ composite = @sprites.active?
434
+ @sequencer.apply_color_patches
435
+ @sequencer.snapshot_line if composite
436
+ @sprites.finish_line(composite ? @sequencer.colors : nil, @sequencer.fg)
437
+ @sequencer.apply_border if composite
438
+
162
439
  colors = @sequencer.colors
163
- return if @display[base, @width] == colors
440
+ line = @lines[@rasterline]
441
+ return if line == colors
164
442
 
165
- @display[base, @width] = colors
443
+ line[0, @width] = colors
444
+ @display[@rasterline * @width, @width] = colors
166
445
  @dirty_lines[@rasterline] = true
167
446
  end
168
447
 
169
448
  def video_matrix(index)
170
- vic_bank.peek(@registers.screen_base + index)
449
+ vic_bank.peek_phi2(@registers.screen_base + index)
171
450
  end
172
451
 
173
- def check_raster_irq!
174
- return unless @rasterline == @registers.raster_target
452
+ def start_line!
453
+ if @rasterline.zero?
454
+ @display_state.new_frame
455
+ start_lightpen_frame
456
+ # The line 0 raster compare happens one cycle later than on all
457
+ # other lines (Bauer 3.12).
458
+ check_raster_irq!
459
+ end
460
+ @sequencer.new_line(@rasterline)
461
+ @sprites.start_line
462
+ rebuild_sprite_ba
463
+ @display_state.new_line(@rasterline)
464
+ end
465
+
466
+ # The raster compare latches the flag only as the line and the target
467
+ # come to match, so a target moved along with the raster line holds the
468
+ # match without latching again (VICE x64sc). It runs at each line step
469
+ # and after each $d011/$d012 write. The line asserts via #interrupted?
470
+ # when the mask bit in $D01A is set.
471
+ def check_raster_irq!(line = @rasterline)
472
+ match = line == @registers.raster_target
473
+ @registers.latch_raster_irq! if match && !@raster_match
474
+ @raster_match = match
475
+ end
175
476
 
176
- # Latch the raster IRQ flag. The line asserts via #interrupted? when the
177
- # matching mask bit in $D01A is set.
178
- @registers.latch_raster_irq!
477
+ # A collision register carries the pixels drawn up to the cycle before
478
+ # the read — the VIC runs ahead of the CPU within a machine cycle, so
479
+ # the column the read lands on has not latched yet — and the reset the
480
+ # read asserts outlives it, swallowing the pixels drawn under it.
481
+ def read_collision(reg)
482
+ beam_x = @column * 8
483
+ @sprites.collide_upto(beam_x, @sequencer.fg)
484
+ @registers.read(reg).tap { @sprites.clear_collision(reg, beam_x) }
485
+ end
486
+
487
+ # A collision raises its $D019 bit as the beam crosses the pixel rather
488
+ # than when the line is folded at its end, so while an enabled
489
+ # collision IRQ is still unlatched the fold keeps up with the beam.
490
+ def watch_collisions
491
+ return if (@registers[0x1a] & ~@registers[0x19]).nobits?(0x06)
492
+
493
+ @sprites.collide_upto(@column * 8, @sequencer.fg)
179
494
  end
180
495
 
181
496
  def irq_status
497
+ @sprites.collide_upto(@column * 8, @sequencer.fg)
182
498
  (@registers[0x19] & 0x0f) | 0x70 | (interrupted? ? 0x80 : 0)
183
499
  end
184
500
 
501
+ # A c-access that falls between BA and AEC reads a bus the CPU still
502
+ # drives: the video matrix byte comes back as $ff, and the colour
503
+ # nibble is the low nibble of the byte at the halted CPU's PC.
185
504
  def fetch_character_data!
186
- vmli = @column - 15
187
- vc = (@display_state.vc_base + vmli) & 0x3ff
188
- @character_buffer[vmli] = video_matrix(vc)
189
- @color_buffer[vmli] = vic_bank.peek_color(vc)
505
+ vmli = @display_state.vmli
506
+ vc = @display_state.vc
507
+ if @display_state.bus_taken?(@column)
508
+ @character_buffer[vmli] = video_matrix(vc)
509
+ @color_buffer[vmli] = vic_bank.peek_color(vc)
510
+ else
511
+ @character_buffer[vmli] = 0xff
512
+ @color_buffer[vmli] = @open_bus ? @open_bus.call & 0x0f : vic_bank.peek_color(vc)
513
+ end
190
514
  end
191
515
  end
192
516
  end
data/lib/badline.rb CHANGED
@@ -13,10 +13,14 @@ require "badline/status"
13
13
  require "badline/keyboard"
14
14
  require "badline/joystick"
15
15
  require "badline/control_ports"
16
+ require "badline/input"
16
17
  require "badline/cycleable"
18
+ require "badline/datasette"
17
19
  require "badline/time_of_day"
18
20
  require "badline/cia"
19
21
  require "badline/sid"
22
+ require "badline/debug_register"
23
+ require "badline/interrupts"
20
24
  require "badline/traps"
21
25
  require "badline/cpu"
22
26
  require "badline/vic"
@@ -27,3 +31,4 @@ require "badline/cartridge"
27
31
  require "badline/kernal_trap"
28
32
  require "badline/chrout_trap"
29
33
  require "badline/media"
34
+ require "badline/audio"
data/vendor/.gitkeep ADDED
File without changes