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,27 +1,103 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/vic/sprite/internal_bus"
4
+ require "badline/vic/sprite/shifter"
5
+
3
6
  module Badline
4
7
  class VIC < Cycleable
8
+ # = Sprite
9
+ #
10
+ # The DMA/display state machine and the pixel sequencer for one sprite.
11
+ # The sequencer is a 24-bit shift register clocked once per pixel: the X
12
+ # comparator starts it, the expansion flip-flop gates the shift, and the
13
+ # multicolor flip-flop gates a two-bit output latch that feeds the color
14
+ # mux. Registers written mid-sprite therefore take hold from the pixel
15
+ # they reach, rather than re-decoding the row.
5
16
  class Sprite
6
17
  X_OFFSET = 104
7
- ROWS = 21
8
18
 
9
- attr_reader :index, :line_pixels
19
+ # MCBASE and MC are six-bit byte counters into the sprite's 64-byte
20
+ # block. A sprite ends when MCBASE reaches exactly 63, which is why a
21
+ # mid-line $d017 write that steps it past 63 keeps it running.
22
+ LAST_MCBASE = 63
23
+ MC_MASK = 0x3f
24
+
25
+ # The X comparator matches the pixel before the sprite's first one.
26
+ COMPARE_OFFSET = X_OFFSET - 1
27
+
28
+ # 24 bits at two pixels each, plus slack for a mid-sprite expansion
29
+ # change stretching the tail.
30
+ MAX_SPAN = 64
31
+
32
+ # Latch codes are the multicolor bit pairs: 0 is transparent, 1 and 3
33
+ # are the shared colors, 2 the sprite's own.
34
+ SHARED0 = 1
35
+ OWN_COLOR = 2
36
+ SHARED1 = 3
37
+
38
+ # The s-accesses reload the shift register partway through the line,
39
+ # at raster pixel 459 for sprite 0 and 16 later for each sprite after
40
+ # it. A sprite still shifting there loses the rest of its row, and the
41
+ # comparator is ignored for the twelve pixels from it (the spritescan
42
+ # dump). Its output holds the last pixel for seven, through K+6
43
+ # (spritefetchbug).
44
+ RELOAD_X = 459
45
+ RELOAD_STEP = 16
46
+ RELOAD_DEAD = 12
47
+ RELOAD_HOLD = 7
48
+
49
+ # On the line of its last row, a sprite whose DMA ended at cycle 16
50
+ # loses its display at cycle 58, so no hit from this pixel on starts
51
+ # it (spritegap3). One already shifting runs on.
52
+ DISPLAY_OFF_X = 460
53
+
54
+ include Shifter
10
55
 
11
- def initialize(index, registers, bank, width)
56
+ attr_reader :index, :leftmost, :span, :codes,
57
+ :reload_leftmost, :reload_span, :reload_codes
58
+
59
+ def initialize(index, registers, bank, width, bus = InternalBus.new(bank, width / 8))
12
60
  @index = index
13
61
  @registers = registers
14
62
  @bank = bank
63
+ @bus = bus
15
64
  @width = width
16
65
  @bit = 1 << index
17
- @displaying = false
18
- @counter = 0
19
- @bits = nil
20
- @line_pixels = nil
21
- @pixel_buffer = Array.new(48)
66
+ @dma = false
67
+ @display_on = false
68
+ @mcbase = 0
69
+ @mc = 0
70
+ @exp_ff = true
71
+ @crunched_mc = nil
72
+ @bits = 0
73
+ @row_ready = false
74
+ @codes = Array.new(MAX_SPAN + RELOAD_HOLD, 0)
75
+ @leftmost = 0
76
+ @span = 0
77
+ @reload_codes = Array.new(MAX_SPAN + RELOAD_HOLD, 0)
78
+ @reload_leftmost = 0
79
+ @reload_span = 0
80
+ @reload_x = (RELOAD_X + (RELOAD_STEP * index)) % width
81
+ @reload_next_line = RELOAD_X + (RELOAD_STEP * index) < width
82
+ @sr = 0
83
+ @latch = 0
84
+ @mc_flop = false
85
+ @xe_flop = false
86
+ @first_byte_lost = false
87
+ @blind = false
88
+ @show_from = 0
22
89
  end
23
90
 
24
- def displaying? = @displaying
91
+ def displaying? = @dma
92
+
93
+ # True when this line has a row for the sequencer: its own, the next
94
+ # one for a sprite that reloads late in the line, or the last one for
95
+ # a sprite that reloads at its start.
96
+ def rendering?
97
+ return true if @row_ready
98
+
99
+ @reload_next_line ? @dma && @display_on : !@prev_bits.nil?
100
+ end
25
101
 
26
102
  def enabled? = @registers[0x15].anybits?(@bit)
27
103
  def multicolor? = @registers[0x1c].anybits?(@bit)
@@ -34,85 +110,219 @@ module Badline
34
110
  msb | @registers[index * 2]
35
111
  end
36
112
 
37
- def y = @registers[(index * 2) + 1]
113
+ # The Y comparator is eight bits wide, so the PAL lines above 255
114
+ # match the coordinates 0-55 a second time (spritey).
115
+ def y_match?(line) = (line & 0xff) == @registers[(index * 2) + 1]
116
+
38
117
  def color = @registers[0x27 + index] & 0x0f
39
118
 
40
- def leftmost = (x + X_OFFSET) % @width
41
- def pixel_width = x_expanded? ? 48 : 24
119
+ # Cycle 16: MCBASE takes MC, which the three s-accesses stepped past
120
+ # the row, but only while the expansion flip-flop is set.
121
+ def advance_mcbase
122
+ mc = @crunched_mc || ((@mc + 3) & MC_MASK)
123
+ @crunched_mc = nil
124
+ @mcbase = mc if @exp_ff && @dma
125
+ end
42
126
 
43
- def start_line(line)
44
- if !@displaying && enabled? && line == y
45
- @displaying = true
46
- @counter = 0
47
- end
127
+ # The end-of-sprite compare, a column after MCBASE moves. MCBASE has
128
+ # to land on 63 exactly, so a crunched sprite steps over it and runs
129
+ # on through the rest of its block. Only the DMA stops here; the
130
+ # display waits for cycle 58.
131
+ def finish_mcbase
132
+ @dma = false if @mcbase == LAST_MCBASE
133
+ end
134
+
135
+ # Clearing MxYE sets the expansion flip-flop at once. Cleared in
136
+ # cycle 15, while the flip-flop is still reset, it also crunches MC:
137
+ # the bits step to a mix of MC and MCBASE (Bauer §3.8, VICE
138
+ # `d017_store`) that cycle 16 then hands to MCBASE.
139
+ def clear_y_expansion(crunch)
140
+ return if @exp_ff
141
+
142
+ @exp_ff = true
143
+ return unless crunch && @dma
144
+
145
+ mc = (@mc + 3) & MC_MASK
146
+ @crunched_mc = (0x2a & @mcbase & mc) | (0x15 & (@mcbase | mc))
147
+ end
48
148
 
49
- return @line_pixels = nil unless @displaying
149
+ # Cycle 56, after the second Y compare: MxYE inverts the expansion
150
+ # flip-flop of a sprite with DMA running.
151
+ def toggle_expansion
152
+ @exp_ff = !@exp_ff if @dma && y_expanded?
153
+ end
154
+
155
+ # Cycles 55 and 56: a Y/enable match starts the DMA and rewinds
156
+ # MCBASE, and returns true. Display is enabled separately in cycle 58,
157
+ # so the rows render from the following line on.
158
+ #
159
+ # On the VIC's side, BA falls at the sprite's own column — 55 for
160
+ # sprite 0, two later for each sprite after it, a column behind the
161
+ # window the CPU sees in VIC::SPRITE_BA_WINDOWS — or two columns after
162
+ # this compare, whichever is later. AEC follows three columns on, and
163
+ # the first of the three s-accesses runs in the column it arrives in:
164
+ # a DMA starting on the second compare therefore loses that access for
165
+ # sprite 0, alone among the eight in following the compares
166
+ # immediately (spriteenable2).
167
+ def check_dma(line, column)
168
+ return false if @dma || !enabled? || !y_match?(line)
169
+
170
+ @first_byte_lost = column + 2 > 55 + (2 * index)
171
+ @dma = true
172
+ @mcbase = 0
173
+ @exp_ff = true
174
+ end
50
175
 
51
- row = y_expanded? ? @counter / 2 : @counter
52
- if row >= ROWS
53
- @displaying = false
54
- return @line_pixels = nil
176
+ # Cycle 58: MC is reloaded from MCBASE for the coming row, and a
177
+ # sprite with DMA running starts displaying only while MxE and Y still
178
+ # match — a write to either between the compares and here keeps the
179
+ # data fetch running invisibly. The display goes off only here, and
180
+ # only once the DMA has, so a DMA restarted on the line of its last
181
+ # row keeps a display that was already on (spriterestart).
182
+ def check_display(line)
183
+ @mc = @mcbase
184
+ if @dma
185
+ @display_on = true if enabled? && y_match?(line)
186
+ else
187
+ @display_on = false
55
188
  end
189
+ blind_fetch if @blind && @display_on
190
+ end
56
191
 
57
- @counter += 1
58
- fetch(row)
59
- decode_line
192
+ # The address of the s-access that falls in phi1, the middle of the
193
+ # three, or nil when the sprite's DMA is off and the VIC idles there.
194
+ def phi1_address
195
+ (pointer * 64) + ((@mc + 1) & MC_MASK) if @dma
60
196
  end
61
197
 
198
+ # The row fetched at the end of the previous line renders on this one.
199
+ # A lost first s-access reads back the $ff the CPU is still driving.
200
+ def start_line
201
+ @span = 0
202
+ @reload_span = 0
203
+ @prev_bits = (@bits if @row_ready && @dma)
204
+ @row_ready = false
205
+ @blind = !@dma && !@reload_next_line
206
+ @show_from = 0
207
+ lost = @first_byte_lost
208
+ @first_byte_lost = false
209
+ return unless @dma && @display_on
210
+
211
+ @bits = row_bits(@mc)
212
+ @bits |= 0xff << 16 if lost
213
+ @row_ready = true
214
+ end
215
+
216
+ # Replay the line through the pixel sequencer, filling the code buffer
217
+ # from the X match on. A log of mid-line register writes makes the
218
+ # comparator and the shift register see the values each pixel was
219
+ # drawn with.
220
+ #
221
+ # The row can be shown once before the reload and once after it, so a
222
+ # sprite moved past the beam can fire a second time on the same line
223
+ # (spritex). A match inside the dead pixels shows nothing.
224
+ def sequence(log = nil)
225
+ @span = 0
226
+ @reload_span = 0
227
+ reload_bits = reload_row
228
+ return unless @row_ready || @prev_bits || reload_bits
229
+
230
+ log = nil if log.nil? || log.empty?
231
+ @stop_x = @dma ? nil : DISPLAY_OFF_X
232
+ comparator_hits(log).each { |start| sequence_hit(log, start, reload_bits) }
233
+ end
234
+
235
+ # The color at a raster position, for a sprite sequenced with the
236
+ # registers as they stand.
62
237
  def pixel(raster_x)
63
- return nil unless @line_pixels
238
+ code = code_at(raster_x, @leftmost, @span, @codes) ||
239
+ code_at(raster_x, @reload_leftmost, @reload_span, @reload_codes)
240
+ return nil if code.nil? || code.zero?
64
241
 
242
+ palette(@registers, Array.new(4, 0))[code]
243
+ end
244
+
245
+ # Fill a four-entry lookup from latch code to color, against a view of
246
+ # the color registers.
247
+ def palette(view, into)
248
+ into[SHARED0] = view[0x25] & 0x0f
249
+ into[OWN_COLOR] = view[0x27 + index] & 0x0f
250
+ into[SHARED1] = view[0x26] & 0x0f
251
+ into
252
+ end
253
+
254
+ private
255
+
256
+ # On the line whose compare started the DMA, sprites 3-7 ran their
257
+ # s-accesses before it was on, so the shift register holds what the
258
+ # VIC saw on its bus instead of a row, and a hit from the display
259
+ # turning on shows it (sbsprf24).
260
+ def blind_fetch
261
+ @bits = @bus.row(index)
262
+ @row_ready = true
263
+ @show_from = DISPLAY_OFF_X
264
+ end
265
+
266
+ def code_at(raster_x, leftmost, span, codes)
65
267
  dist = raster_x - leftmost
66
268
  dist += @width if dist.negative?
67
- dist < pixel_width ? @line_pixels[dist] : nil
269
+ codes[dist] if dist < span
68
270
  end
69
271
 
70
- private
272
+ def sequence_hit(log, start, reload_bits)
273
+ return if (@stop_x && start >= @stop_x) || start < @show_from
71
274
 
72
- # Decode the fetched 24 data bits into a buffer of pixel colors (nil is
73
- # transparent), so compositing can read pixels without re-deriving them.
74
- def decode_line(pixels = @pixel_buffer)
75
- @line_pixels = pixels
76
- if multicolor?
77
- decode_multicolor(pixels, x_expanded?)
78
- else
79
- decode_hires(pixels, x_expanded?)
275
+ if start < @reload_x
276
+ sequence_early(log, start)
277
+ elsif start >= @reload_x + RELOAD_DEAD
278
+ sequence_reloaded(log, start, reload_bits)
80
279
  end
81
280
  end
82
281
 
83
- def decode_hires(pixels, expanded)
84
- own = color
85
- last = expanded ? 48 : 24
86
- i = 0
87
- while i < last
88
- offset = expanded ? i >> 1 : i
89
- pixels[i] = (@bits >> (23 - offset)).anybits?(1) ? own : nil
90
- i += 1
91
- end
282
+ def sequence_early(log, start)
283
+ bits = early_row
284
+ return unless bits && @span.zero?
285
+
286
+ @leftmost = start
287
+ cut = reload_cut(start)
288
+ @span = cut_at_reload(cut, run(log, start, @codes, bits, cut), @codes)
92
289
  end
93
290
 
94
- def decode_multicolor(pixels, expanded)
95
- shared1 = @registers[0x25] & 0x0f
96
- shared2 = @registers[0x26] & 0x0f
97
- own = color
98
- last = expanded ? 48 : 24
99
- i = 0
100
- while i < last
101
- offset = expanded ? i >> 1 : i
102
- pixels[i] = case (@bits >> (22 - (offset & ~1))) & 0b11
103
- when 0b01 then shared1
104
- when 0b10 then own
105
- when 0b11 then shared2
106
- end
107
- i += 1
108
- end
291
+ def sequence_reloaded(log, start, bits)
292
+ return unless bits && @reload_span.zero?
293
+
294
+ @reload_leftmost = start
295
+ cut = reload_cut(start)
296
+ @reload_span = cut_at_reload(cut, run(log, start, @reload_codes, bits, cut), @reload_codes)
297
+ end
298
+
299
+ # The row in the shift register ahead of the reload: this line's for
300
+ # sprites 0-2, and for the others the one their reload at the start of
301
+ # the last line brought in.
302
+ def early_row = @reload_next_line ? current_row : @prev_bits
303
+
304
+ def current_row = (@bits if @row_ready)
305
+
306
+ # The row the reload brings in. Sprites 0-2 reload late in the line
307
+ # with the next line's row, so one that fires after it shows that row
308
+ # a line early (spritegap); the others reload at the start of the line
309
+ # with the row it already has.
310
+ def reload_row
311
+ return current_row unless @reload_next_line
312
+ return unless @dma && @display_on
313
+
314
+ bits = row_bits(@mc)
315
+ bits |= 0xff << 16 if @first_byte_lost
316
+ bits
109
317
  end
110
318
 
111
- def fetch(row)
112
- base = (pointer * 64) + (row * 3)
113
- @bits = (@bank.peek(base) << 16) |
114
- (@bank.peek(base + 1) << 8) |
115
- @bank.peek(base + 2)
319
+ # The three s-accesses step MC through the sprite's block, wrapping
320
+ # within it.
321
+ def row_bits(counter)
322
+ base = pointer * 64
323
+ (@bank.peek(base + counter) << 16) |
324
+ (@bank.peek(base + ((counter + 1) & MC_MASK)) << 8) |
325
+ @bank.peek(base + ((counter + 2) & MC_MASK))
116
326
  end
117
327
 
118
328
  def pointer = @bank.peek(@registers.screen_base + 0x3f8 + index)