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