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,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/vic/border_mask"
4
+ require "badline/vic/color_patches"
3
5
  require "badline/vic/graphics_mode"
6
+ require "badline/vic/graphics_shifter"
7
+ require "badline/vic/sequencer_output"
4
8
 
5
9
  module Badline
6
10
  class VIC < Cycleable
@@ -8,6 +12,8 @@ module Badline
8
12
  #
9
13
  # Turns fetched graphics data into output pixels.
10
14
  class Sequencer
15
+ include Output
16
+
11
17
  DISPLAY_X_BOUNDS = [
12
18
  [135, 438].freeze,
13
19
  [128, 447].freeze
@@ -28,28 +34,16 @@ module Badline
28
34
  [51, 251].freeze
29
35
  ].freeze
30
36
 
31
- # Border coverage per 8-pixel group, tracked so apply_border can skip
32
- # window groups and bulk-fill full border groups.
33
- BORDER_FULL = 0
34
- BORDER_NONE = 1
35
- BORDER_MIXED = 2
36
-
37
- NULL_MODE = GraphicsMode::Null.new
38
- MODES = [
39
- GraphicsMode::Text.new, # 000 standard text
40
- GraphicsMode::MulticolorText.new, # 001 multicolour text
41
- GraphicsMode::Bitmap.new, # 010 standard bitmap
42
- GraphicsMode::MulticolorBitmap.new, # 011 multicolour bitmap
43
- GraphicsMode::ExtendedBackgroundText.new, # 100 ECM text
44
- NULL_MODE, # 101 invalid
45
- NULL_MODE, # 110 invalid
46
- NULL_MODE # 111 invalid
47
- ].freeze
37
+ MODES = GraphicsMode::MODES
48
38
 
49
- attr_reader :colors, :fg, :border, :registers, :bank, :cur_colors
39
+ attr_reader :colors, :fg, :registers, :bank, :cur_colors,
40
+ :color_patches
50
41
  # The fg masks are shared frozen patterns assigned by reference, never
51
42
  # mutated in place.
52
43
  attr_accessor :cur_fg
44
+ # False leaves the line's colours unpainted and keeps only the
45
+ # foreground mask the sprite collisions read.
46
+ attr_writer :render
53
47
 
54
48
  def initialize(width, registers, bank)
55
49
  @width = width
@@ -57,180 +51,212 @@ module Badline
57
51
  @bank = bank
58
52
  @colors = Array.new(width, 0)
59
53
  @fg = Array.new(width, false)
60
- @border = Array.new(width, true)
61
- @border_groups = Array.new(width / 8, BORDER_FULL)
54
+ @border_mask = BorderMask.new(width)
55
+ # Hot-path aliases: the pixel loops write coverage straight into the
56
+ # mask's arrays.
57
+ @border = @border_mask.mask
58
+ @border_groups = @border_mask.groups
62
59
  @cur_colors = Array.new(8, 0)
63
60
  @cur_fg = GraphicsMode::NO_FG
64
61
  @prev_colors = Array.new(8, 0)
65
62
  @prev_fg = GraphicsMode::NO_FG
66
63
  @vertical_border = true
64
+ @vertical_armed = true
67
65
  @main_border = true
66
+ @color_patches = ColorPatches.new(self)
67
+ @shifter = GraphicsShifter.new(registers)
68
+ @xscroll = 0
69
+ @last_shift = 0
70
+ @last_mode = 0
71
+ @settling = false
72
+ @ring_data = Array.new(4, 0)
73
+ @ring_char = Array.new(4, 0)
74
+ @ring_color = Array.new(4, 1)
75
+ @prev_fresh = false
76
+ @check = true
77
+ @render = true
68
78
  new_line(0)
69
79
  end
70
80
 
71
- # Reset the line buffers at the start of a rasterline and re-evaluate the
72
- # vertical border flip-flop.
81
+ # Reset the line buffers at the start of a rasterline.
73
82
  def new_line(line)
74
- update_vertical_border(line)
83
+ @line = line
84
+ @left_vertical_border = nil
85
+ @first_csel = @registers.csel?
75
86
  @colors.fill(@registers.border)
76
87
  @fg.fill(false)
77
- @border_groups.fill(BORDER_FULL)
88
+ @border_mask.reset
78
89
  @prev_colors.fill(@registers.background)
79
90
  @prev_fg = GraphicsMode::NO_FG
91
+ @prev_fresh = false
92
+ @check = true
93
+ @color_patches.clear
80
94
  end
81
95
 
82
- # Repaint the border over the composited line, hiding the sprites.
83
- def apply_border
84
- color = @registers.border
85
- group = 0
86
- while group < @border_groups.length
87
- case @border_groups[group]
88
- when BORDER_FULL then @colors.fill(color, group * 8, 8)
89
- when BORDER_MIXED then paint_mixed_border(color, group * 8)
90
- end
91
- group += 1
92
- end
96
+ # The load point for the next group's byte: XSCROLL as a g-access
97
+ # column with the border open sees it, a column before the group that
98
+ # uses it (VICE x64sc `xscroll_pipe`).
99
+ def latch_xscroll(xscroll = @registers.xscroll)
100
+ return if xscroll == @xscroll
101
+
102
+ @xscroll = xscroll
103
+ @check = true
93
104
  end
94
105
 
95
- def emit(screencode, color, col, cell, row)
96
- MODES[@registers.mode].decode(screencode, color, cell, row, self)
97
- output(col)
98
- roll
106
+ # A write to a register the painted colours depend on: the byte kept
107
+ # from the previous group is painted again before its pixels are shown.
108
+ def colors_changed!
109
+ @prev_fresh = false
110
+ @check = true
99
111
  end
100
112
 
101
- def emit_idle(col)
102
- @cur_colors.fill(@registers.background)
103
- @cur_fg = GraphicsMode::NO_FG
104
- output(col)
105
- roll
113
+ def apply_color_patches
114
+ @color_patches.apply(@colors, @fg)
106
115
  end
107
116
 
108
- private
117
+ # The vertical border compares run in every cycle of their line (VICE
118
+ # x64sc). The top one resets the flip-flop at once when DEN is set. The
119
+ # bottom one only arms it, and the armed state takes hold at the line's
120
+ # first cycle and at the left window edge (Bauer §3.9 rules 2-5). The
121
+ # VIC calls this after each $d011 write.
122
+ def compare_vertical_border(line)
123
+ top, bottom = BORDER_Y_BOUNDS[@registers.rsel? ? 1 : 0]
124
+ @vertical_armed = true if line == bottom
125
+ @vertical_armed = @vertical_border = false if line == top && @registers.display_enabled?
126
+ end
109
127
 
110
- # Write the 8-pixel group for a column into the line buffers. The main
111
- # border flip-flop only changes state in the groups containing the
112
- # window edge compares, so all other groups take a branch-free bulk
113
- # path: fully border or fully window.
114
- def output(col)
115
- x_pos = (col + 16) * 8
116
- win_lo, right_compare = WINDOW_COMPARES[@registers.csel? ? 1 : 0]
117
-
118
- if boundary_group?(x_pos, win_lo, right_compare)
119
- output_boundary(col, x_pos, win_lo, right_compare)
120
- elsif @main_border || @vertical_border
121
- output_border(x_pos)
122
- else
123
- output_window(col, x_pos)
124
- end
128
+ # The line's first cycle, Bauer's cycle 1, which is the last column of
129
+ # the line before it.
130
+ def start_vertical_border(line)
131
+ compare_vertical_border(line)
132
+ @vertical_border = @vertical_armed
125
133
  end
126
134
 
127
- # True if the group contains a window edge, where the main border
128
- # flip-flop can change state.
129
- def boundary_group?(x_pos, win_lo, right_compare)
130
- lo_delta = win_lo - x_pos
131
- hi_delta = right_compare - x_pos
132
- (lo_delta >= 0 && lo_delta < 8) || (hi_delta >= 0 && hi_delta < 8)
135
+ # The 40-column left compare, a column before the column that draws its
136
+ # pixel. The 38-column one runs in the drawing column.
137
+ def left_compare_vertical_border
138
+ @left_vertical_border = start_vertical_border(@line) if @registers.csel?
133
139
  end
134
140
 
135
- def output_border(x_pos)
136
- @colors.fill(@registers.border, x_pos, 8)
137
- @border_groups[x_pos >> 3] = BORDER_FULL
138
- @fg.fill(false, x_pos, 8)
141
+ def border_at?(pixel_x) = @border_mask.at?(pixel_x)
142
+
143
+ # True while the vertical border flip-flop is set and stays set for the
144
+ # rest of the line: it can only clear mid-line on a top compare line
145
+ # with DEN set.
146
+ def vertical_closed?
147
+ return false unless @vertical_border
148
+ return true unless @line == 51 || @line == 55
149
+
150
+ top, = BORDER_Y_BOUNDS[@registers.rsel? ? 1 : 0]
151
+ !(@line == top && @registers.display_enabled?)
139
152
  end
140
153
 
141
- def output_window(col, x_pos)
142
- in_gfx = x_pos >= GFX_X_START && x_pos < GFX_X_END
143
- @border_groups[x_pos >> 3] = BORDER_NONE
154
+ # Snapshot the finished line so apply_border can restore the border
155
+ # pixels sprites were composited over, keeping mid-line border splits.
156
+ def snapshot_line = @border_mask.snapshot(@colors)
157
+
158
+ # Repaint the border over the composited line, hiding the sprites.
159
+ def apply_border = @border_mask.restore(@colors)
160
+
161
+ # The g-accesses the VIC hands over, as a ring the column's slot
162
+ # indexes: the byte read, the screen byte and the colour nibble. The
163
+ # slot before is the byte the previous group loaded.
164
+ attr_reader :ring_data, :ring_char, :ring_color
165
+
166
+ # Paints the byte in the slot through the current mode. An idle access
167
+ # latches 0/0 and one that made no access passes on zero data with the
168
+ # kept values; under a closed border neither has foreground. A group
169
+ # where the mode or the load point changes, and the one after it, run
170
+ # the shift register pixel by pixel.
171
+ def emit(slot, col, display)
172
+ mode = @registers.mode
173
+ return emit_checked(slot, col, display, mode) if @check || mode != @last_mode
144
174
 
145
- if @registers.xscroll.zero?
146
- @colors[x_pos, 8] = @cur_colors
147
- if in_gfx
148
- @fg[x_pos, 8] = @cur_fg
149
- else
150
- @fg.fill(false, x_pos, 8)
151
- end
175
+ paint_slot(slot, mode, display)
176
+ output(col, @last_shift)
177
+ roll
178
+ end
179
+
180
+ private
181
+
182
+ # After a mode, XSCROLL or colour change: groups paint whole bytes
183
+ # again once the change has settled, and run pixel by pixel until then.
184
+ def emit_checked(slot, col, display, mode)
185
+ shift = @xscroll
186
+ if mode == @last_mode && shift == @last_shift && !@settling
187
+ emit_settled(slot, col, display, mode, shift)
152
188
  else
153
- output_window_shifted(col, x_pos, in_gfx)
189
+ emit_pixels(slot, col, mode, shift, !display && border_hidden?)
190
+ @settling = mode != @last_mode || shift != @last_shift
191
+ @last_mode = mode
192
+ @last_shift = shift
154
193
  end
155
194
  end
156
195
 
157
- def output_window_shifted(col, x_pos, in_gfx)
158
- shift = @registers.xscroll
159
- keep = 8 - shift
196
+ def emit_settled(slot, col, display, mode, shift)
197
+ repaint_previous(mode, (slot - 1) & 3) unless @prev_fresh || shift.zero?
198
+ paint_slot(slot, mode, display)
199
+ output(col, shift)
200
+ roll
201
+ @prev_fresh = true
202
+ @check = false
203
+ end
160
204
 
161
- @colors[x_pos + shift, keep] = @cur_colors[0, keep]
162
- if col.positive? # the left column only exists from column 1 on
163
- @colors[x_pos, shift] = @prev_colors[keep, shift]
205
+ def paint_slot(slot, mode, display)
206
+ if !display && border_hidden?
207
+ @cur_fg = GraphicsMode::NO_FG
164
208
  else
165
- @colors.fill(@registers.background, x_pos, shift)
209
+ paint(slot, mode)
166
210
  end
167
- output_shifted_fg(col, x_pos, in_gfx, shift, keep)
168
211
  end
169
212
 
170
- def output_shifted_fg(col, x_pos, in_gfx, shift, keep)
171
- return @fg.fill(false, x_pos, 8) unless in_gfx
213
+ def repaint_previous(mode, slot)
214
+ roll
215
+ paint(slot, mode)
216
+ roll
217
+ end
172
218
 
173
- @fg[x_pos + shift, keep] = @cur_fg[0, keep]
174
- if col.positive?
175
- @fg[x_pos, shift] = @prev_fg[keep, shift]
219
+ def paint(slot, mode)
220
+ if @render
221
+ MODES[mode].paint(@ring_data[slot], @ring_char[slot], @ring_color[slot], self)
176
222
  else
177
- @fg.fill(false, x_pos, shift)
223
+ @cur_fg = MODES[mode].fg(@ring_data[slot], @ring_char[slot], @ring_color[slot])
178
224
  end
179
225
  end
180
226
 
181
- # Slow path for the groups where the border flip-flop can change state.
182
- def output_boundary(col, x_pos, win_lo, right_compare)
183
- shift = @registers.xscroll
184
- bg = @registers.background
185
- bleed = col.positive?
186
- border = @registers.border
187
- @border_groups[x_pos >> 3] = BORDER_MIXED
188
-
189
- i = 0
190
- while i < 8
191
- src = i - shift
192
- if src >= 0
193
- pixel = @cur_colors[src]
194
- mask = @cur_fg[src]
195
- elsif bleed
196
- pixel = @prev_colors[8 + src]
197
- mask = @prev_fg[8 + src]
198
- else
199
- pixel = bg
200
- mask = false
201
- end
202
- x = x_pos + i
203
- shown = pixel_shown?(x, win_lo, right_compare)
204
- @colors[x] = shown ? pixel : border
205
- @border[x] = !shown
206
- @fg[x] = x >= GFX_X_START && x < GFX_X_END ? mask : false
207
- i += 1
208
- end
227
+ def emit_pixels(slot, col, mode, shift, hidden)
228
+ prime_shifter((slot - 1) & 3) unless @settling
229
+ @shifter.draw(hidden ? 0 : @ring_data[slot], @ring_char[slot], @ring_color[slot], shift, mode)
230
+ @cur_colors[0, 8] = @shifter.colors
231
+ @cur_fg = @shifter.fg
232
+ output(col, 0)
233
+ @prev_fresh = false
234
+ @check = true
209
235
  end
210
236
 
211
- def paint_mixed_border(color, x_pos)
212
- i = 0
213
- while i < 8
214
- @colors[x_pos + i] = color if @border[x_pos + i]
215
- i += 1
216
- end
237
+ # Picks up from a group that painted whole bytes.
238
+ def prime_shifter(slot)
239
+ @shifter.prime(@ring_data[slot], @ring_char[slot], @ring_color[slot], @last_shift, @last_mode)
217
240
  end
218
241
 
242
+ # The group is border throughout when the main flip-flop is set and the
243
+ # vertical one keeps it from clearing.
244
+ def border_hidden? = @main_border && vertical_closed?
245
+
219
246
  def pixel_shown?(pixel_x, left_compare, right_compare)
220
247
  @main_border = true if pixel_x == right_compare
221
- @main_border = false if pixel_x == left_compare && !@vertical_border
222
- !(@main_border || @vertical_border)
248
+ @main_border = false if pixel_x == left_compare && !left_vertical_border
249
+ !@main_border
250
+ end
251
+
252
+ def left_vertical_border
253
+ @left_vertical_border.nil? ? start_vertical_border(@line) : @left_vertical_border
223
254
  end
224
255
 
225
256
  def roll
226
257
  @prev_colors, @cur_colors = @cur_colors, @prev_colors
227
258
  @prev_fg, @cur_fg = @cur_fg, @prev_fg
228
- end
229
-
230
- def update_vertical_border(line)
231
- top, bottom = BORDER_Y_BOUNDS[@registers.rsel? ? 1 : 0]
232
- @vertical_border = true if line == bottom
233
- @vertical_border = false if line == top && @registers.display_enabled?
259
+ nil
234
260
  end
235
261
  end
236
262
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class VIC < Cycleable
5
+ class Sequencer
6
+ # Writes a column's 8-pixel group into the line buffers, through the
7
+ # main border flip-flop and XSCROLL.
8
+ module Output
9
+ private
10
+
11
+ # Write the 8-pixel group for a column into the line buffers. The main
12
+ # border flip-flop only changes state in the groups containing the
13
+ # window edge compares, so all other groups take a branch-free bulk
14
+ # path: fully border or fully window. A group's first pixel, where the
15
+ # 40-column compares fall, still sees CSEL as the column before had it.
16
+ def output(col, shift)
17
+ x_pos = (col + 16) * 8
18
+ csel = @registers.csel?
19
+ first = WINDOW_COMPARES[@first_csel ? 1 : 0]
20
+ @first_csel = csel
21
+ compares = WINDOW_COMPARES[csel ? 1 : 0]
22
+ win_lo, right_compare = compares
23
+
24
+ if !first.equal?(compares) || boundary_group?(x_pos, win_lo, right_compare)
25
+ output_boundary(x_pos, first, compares, shift)
26
+ elsif @main_border
27
+ output_border(x_pos)
28
+ else
29
+ output_window(x_pos, shift)
30
+ end
31
+ end
32
+
33
+ # True if the group contains a window edge, where the main border
34
+ # flip-flop can change state.
35
+ def boundary_group?(x_pos, win_lo, right_compare)
36
+ lo_delta = win_lo - x_pos
37
+ hi_delta = right_compare - x_pos
38
+ (lo_delta >= 0 && lo_delta < 8) || (hi_delta >= 0 && hi_delta < 8)
39
+ end
40
+
41
+ def output_border(x_pos)
42
+ @colors.fill(@registers.border, x_pos, 8) if @render
43
+ @border_groups[x_pos >> 3] = BorderMask::FULL
44
+ @fg.fill(false, x_pos, 8)
45
+ end
46
+
47
+ def output_window(x_pos, shift)
48
+ in_gfx = x_pos >= GFX_X_START && x_pos < GFX_X_END
49
+ @border_groups[x_pos >> 3] = BorderMask::NONE
50
+
51
+ if shift.zero?
52
+ @colors[x_pos, 8] = @cur_colors if @render
53
+ if in_gfx
54
+ @fg[x_pos, 8] = @cur_fg
55
+ else
56
+ @fg.fill(false, x_pos, 8)
57
+ end
58
+ else
59
+ output_window_shifted(x_pos, in_gfx, shift)
60
+ end
61
+ end
62
+
63
+ def output_window_shifted(x_pos, in_gfx, shift)
64
+ keep = 8 - shift
65
+
66
+ if @render
67
+ copy(@cur_colors, 0, @colors, x_pos + shift, keep)
68
+ copy(@prev_colors, keep, @colors, x_pos, shift)
69
+ end
70
+ output_shifted_fg(x_pos, in_gfx, shift, keep)
71
+ end
72
+
73
+ def output_shifted_fg(x_pos, in_gfx, shift, keep)
74
+ return @fg.fill(false, x_pos, 8) unless in_gfx
75
+
76
+ copy(@cur_fg, 0, @fg, x_pos + shift, keep)
77
+ copy(@prev_fg, keep, @fg, x_pos, shift)
78
+ end
79
+
80
+ def copy(src, from, dest, to, count)
81
+ i = 0
82
+ while i < count
83
+ dest[to + i] = src[from + i]
84
+ i += 1
85
+ end
86
+ end
87
+
88
+ # Slow path for the groups where the border flip-flop can change state.
89
+ def output_boundary(x_pos, first, compares, shift)
90
+ border = @registers.border
91
+ win_lo, right_compare = first
92
+ @border_groups[x_pos >> 3] = BorderMask::MIXED
93
+
94
+ i = 0
95
+ while i < 8
96
+ src = i - shift
97
+ if src >= 0
98
+ pixel = @cur_colors[src]
99
+ mask = @cur_fg[src]
100
+ else
101
+ pixel = @prev_colors[8 + src]
102
+ mask = @prev_fg[8 + src]
103
+ end
104
+ x = x_pos + i
105
+ shown = pixel_shown?(x, win_lo, right_compare)
106
+ @colors[x] = shown ? pixel : border
107
+ @border[x] = !shown
108
+ @fg[x] = x >= GFX_X_START && x < GFX_X_END ? mask : false
109
+ win_lo, right_compare = compares if i.zero?
110
+ i += 1
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class VIC < Cycleable
5
+ class Sprite
6
+ # = InternalBus
7
+ #
8
+ # What a sprite's s-accesses read while its DMA is off. The first and
9
+ # third fall in phi2, where the VIC's internal bus floats to $ff unless
10
+ # the CPU reads or writes a VIC register in that cycle, and the middle
11
+ # one is the idle fetch at $3fff (sbsprf24's readme).
12
+ class InternalBus
13
+ def initialize(bank, columns)
14
+ @bank = bank
15
+ @lines = 0
16
+ @ghost = 0xff
17
+ @value = Array.new(columns, 0xff)
18
+ @line = Array.new(columns, -1)
19
+ end
20
+
21
+ # Sprites 3-7 fetch in Bauer cycles 1-10, so the idle byte is taken
22
+ # as the line starts.
23
+ def start_line(sprites_enabled)
24
+ @lines += 1
25
+ @ghost = @bank.peek(0x3fff) if sprites_enabled
26
+ end
27
+
28
+ # The VIC has advanced to `column`, so the access runs in Bauer cycle
29
+ # `column + 1`, and column 0 is the first cycle of the line about to
30
+ # start.
31
+ def access(column, value)
32
+ @value[column] = value
33
+ @line[column] = column.zero? ? @lines + 1 : @lines
34
+ end
35
+
36
+ # The row sprite `index` (3-7) fetched this line, in Bauer cycles
37
+ # 2n-5 and 2n-4.
38
+ def row(index)
39
+ column = (2 * index) - 6
40
+ (phi2(column) << 16) | (@ghost << 8) | phi2(column + 1)
41
+ end
42
+
43
+ private
44
+
45
+ def phi2(column) = @line[column] == @lines ? @value[column] : 0xff
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class VIC < Cycleable
5
+ class Sprite
6
+ # The X comparator and the 24-bit shift register behind it: where a
7
+ # line fires the sprite, and the pixels each firing shifts out.
8
+ module Shifter
9
+ EMPTY = [].freeze
10
+
11
+ private
12
+
13
+ # The first pixel of every comparator match along the line. Writes to
14
+ # $d000/$d010 move the compare value mid-line, so a sprite can miss
15
+ # its match entirely and pick up a later one, a second one, or none.
16
+ def comparator_hits(log)
17
+ unless log
18
+ match = compare_x(@registers)
19
+ return match ? [(match + 1) % @width] : EMPTY
20
+ end
21
+
22
+ hits = []
23
+ log.rewind
24
+ log.advance(0)
25
+ from = 0
26
+ while from < @width
27
+ upto = [log.next_x, @width].min
28
+ match = compare_x(log)
29
+ hits << ((match + 1) % @width) if match && match >= from && match < upto
30
+ break if upto >= @width
31
+
32
+ from = upto
33
+ log.advance(from)
34
+ end
35
+ hits
36
+ end
37
+
38
+ # The VIC's X counter only runs to 503, so the eight coordinates above
39
+ # it never match and the sprite stays dark — the gap the spritegap
40
+ # tests find at $1f8.
41
+ def compare_x(view)
42
+ msb = view[0x10].anybits?(@bit) ? 0x100 : 0
43
+ xpos = msb | view[index * 2]
44
+ return if xpos >= @width
45
+
46
+ (xpos + COMPARE_OFFSET) % @width
47
+ end
48
+
49
+ # Pixels from the X match to the next reload.
50
+ def reload_cut(start) = (start < @reload_x ? @reload_x : @reload_x + @width) - start
51
+
52
+ # A run that reaches the next reload is cut there, holding its last
53
+ # pixel through K+6. Returns the span that is left.
54
+ def cut_at_reload(cut, span, codes)
55
+ return span if cut > span || cut >= MAX_SPAN
56
+
57
+ RELOAD_HOLD.times { |i| codes[cut + i] = codes[cut - 1] }
58
+ cut + RELOAD_HOLD
59
+ end
60
+
61
+ # Shift one firing out into `codes`, up to the sprite's end, `cut`
62
+ # pixels from the reload.
63
+ def run(log, start, codes, bits, cut)
64
+ @sr = bits
65
+ log&.rewind
66
+ pos = start
67
+ count = 0
68
+ boundary = log ? 0 : Float::INFINITY
69
+ mc = multicolor?
70
+ expanded = x_expanded?
71
+ while count < MAX_SPAN
72
+ if pos >= boundary
73
+ log.advance(pos)
74
+ mc = log[0x1c].anybits?(@bit)
75
+ expanded = log[0x1d].anybits?(@bit)
76
+ boundary = log.next_x
77
+ end
78
+ count.zero? ? prime(mc) : step(mc, expanded, count == cut - 1)
79
+ break if @latch.zero? && @sr.zero?
80
+
81
+ codes[count] = @latch
82
+ count += 1
83
+ pos += 1
84
+ end
85
+ count
86
+ end
87
+
88
+ # The X match loads the shift register and arms both flip-flops, so
89
+ # the sprite's first pixel comes straight off the fetched row.
90
+ def prime(multicolor)
91
+ @mc_flop = false
92
+ @xe_flop = false
93
+ @latch = (@sr >> 22) & (multicolor ? 3 : 2)
94
+ end
95
+
96
+ # One pixel of the sequencer. An unexpanded sprite shifts every pixel;
97
+ # an expanded one shifts every other, and the multicolor latch reloads
98
+ # every other shift, so its pairs stretch with the expansion. Both
99
+ # flip-flops sit idle while their register bit is clear, which is why
100
+ # the first pixel after a mid-sprite change repeats the last one.
101
+ def step(multicolor, expanded, last)
102
+ shift = true
103
+ if expanded
104
+ shift = @xe_flop
105
+ @xe_flop = !@xe_flop
106
+ else
107
+ @xe_flop = false
108
+ end
109
+ @mc_flop = false unless multicolor
110
+ return unless shift
111
+
112
+ @sr = (@sr << 1) & 0xffffff
113
+ load_latch(multicolor, last)
114
+ end
115
+
116
+ # A multicolor pair loaded on the last pixel before the reload keeps
117
+ # only its high bit, as a hi-res pixel does (spritefetchbug).
118
+ def load_latch(multicolor, last)
119
+ if multicolor
120
+ @latch = (@sr >> 22) & (last ? 2 : 3) if @mc_flop
121
+ @mc_flop = !@mc_flop
122
+ else
123
+ @latch = (@sr >> 22) & 2
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end