badline 0.2.1 → 0.4.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/hooks/session-start.sh +57 -0
  3. data/.claude/settings.json +14 -0
  4. data/CHANGELOG.md +107 -0
  5. data/CLAUDE.md +51 -16
  6. data/CONTRIBUTING.md +95 -3
  7. data/README.md +119 -51
  8. data/SECURITY.md +9 -0
  9. data/doc/pinned-behaviour.md +293 -39
  10. data/exe/badline-ruby +59 -0
  11. data/lib/badline/address_bus.rb +28 -14
  12. data/lib/badline/addressable.rb +1 -1
  13. data/lib/badline/audio/aiff.rb +8 -3
  14. data/lib/badline/audio/bare_player.rb +20 -3
  15. data/lib/badline/audio/cli.rb +17 -12
  16. data/lib/badline/audio/console.rb +12 -58
  17. data/lib/badline/audio/machine_player.rb +9 -6
  18. data/lib/badline/audio/playback.rb +3 -0
  19. data/lib/badline/audio/renderer.rb +3 -3
  20. data/lib/badline/audio/sdl_sink.rb +20 -52
  21. data/lib/badline/audio/stream.rb +117 -0
  22. data/lib/badline/audio/terminal.rb +90 -0
  23. data/lib/badline/audio.rb +3 -2
  24. data/lib/badline/cartridge/easy_flash.rb +6 -2
  25. data/lib/badline/cartridge/epyx_fastload.rb +1 -1
  26. data/lib/badline/cartridge/geo_ram.rb +63 -0
  27. data/lib/badline/cartridge/mach5.rb +1 -1
  28. data/lib/badline/cartridge/magic_desk.rb +5 -1
  29. data/lib/badline/cartridge/ocean.rb +5 -2
  30. data/lib/badline/cartridge/rex_utility.rb +1 -1
  31. data/lib/badline/cartridge/rgcd.rb +1 -1
  32. data/lib/badline/cartridge/simons_basic.rb +2 -2
  33. data/lib/badline/cartridge/westermann.rb +2 -2
  34. data/lib/badline/cartridge.rb +9 -0
  35. data/lib/badline/checkpoint.rb +98 -0
  36. data/lib/badline/cia/interrupt_register.rb +34 -10
  37. data/lib/badline/cia.rb +19 -10
  38. data/lib/badline/color_memory.rb +2 -2
  39. data/lib/badline/computer.rb +34 -14
  40. data/lib/badline/cpu.rb +2 -2
  41. data/lib/badline/gui/application.rb +70 -26
  42. data/lib/badline/gui/gamepads.rb +22 -22
  43. data/lib/badline/gui/joy_map.rb +1 -1
  44. data/lib/badline/gui/key_map.rb +6 -1
  45. data/lib/badline/gui/pane.rb +7 -6
  46. data/lib/badline/gui/screen_pane.rb +9 -13
  47. data/lib/badline/gui/window.rb +35 -20
  48. data/lib/badline/gui.rb +1 -1
  49. data/lib/badline/input/mouse1351.rb +21 -6
  50. data/lib/badline/input/paddles.rb +11 -6
  51. data/lib/badline/kernal_trap/channel.rb +102 -7
  52. data/lib/badline/kernal_trap/drive/block_commands.rb +108 -0
  53. data/lib/badline/kernal_trap/drive/channels.rb +53 -0
  54. data/lib/badline/kernal_trap/drive/memory.rb +109 -0
  55. data/lib/badline/kernal_trap/drive/parameters.rb +28 -0
  56. data/lib/badline/kernal_trap/drive/status.rb +3 -0
  57. data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
  58. data/lib/badline/kernal_trap/drive/writes.rb +92 -0
  59. data/lib/badline/kernal_trap/drive.rb +109 -131
  60. data/lib/badline/kernal_trap/file.rb +6 -14
  61. data/lib/badline/kernal_trap/load.rb +6 -0
  62. data/lib/badline/kernal_trap/save.rb +13 -7
  63. data/lib/badline/keyboard.rb +19 -3
  64. data/lib/badline/media.rb +44 -13
  65. data/lib/badline/memory.rb +2 -2
  66. data/lib/badline/options.rb +209 -0
  67. data/lib/badline/ram_expansion/banking.rb +30 -0
  68. data/lib/badline/ram_expansion/plus256k.rb +38 -0
  69. data/lib/badline/ram_expansion/plus60k.rb +31 -0
  70. data/lib/badline/ram_expansion/unexpanded.rb +22 -0
  71. data/lib/badline/ram_expansion.rb +21 -0
  72. data/lib/badline/region.rb +51 -0
  73. data/lib/badline/rom.rb +14 -5
  74. data/lib/badline/sdl/events.rb +47 -0
  75. data/lib/badline/sdl/functions.rb +73 -0
  76. data/lib/badline/sdl.rb +67 -0
  77. data/lib/badline/sid/catch_up.rb +110 -0
  78. data/lib/badline/sid/decimator.rb +6 -1
  79. data/lib/badline/sid/filter.rb +5 -5
  80. data/lib/badline/sid/waveform/fast_forward.rb +10 -10
  81. data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
  82. data/lib/badline/sid/waveform.rb +8 -8
  83. data/lib/badline/sid.rb +19 -93
  84. data/lib/badline/storage/d64_image.rb +10 -0
  85. data/lib/badline/storage/d71_image.rb +12 -0
  86. data/lib/badline/storage/d81_image.rb +9 -0
  87. data/lib/badline/storage/disk_image/bam.rb +111 -0
  88. data/lib/badline/storage/disk_image/directory.rb +79 -0
  89. data/lib/badline/storage/disk_image/writing.rb +110 -0
  90. data/lib/badline/storage/disk_image.rb +49 -4
  91. data/lib/badline/storage/host_directory.rb +17 -4
  92. data/lib/badline/storage/sid_file.rb +36 -2
  93. data/lib/badline/storage.rb +18 -1
  94. data/lib/badline/time_of_day.rb +11 -2
  95. data/lib/badline/version.rb +1 -1
  96. data/lib/badline/via/control_lines.rb +104 -0
  97. data/lib/badline/via/interrupt_register.rb +58 -0
  98. data/lib/badline/via/shift_register.rb +135 -0
  99. data/lib/badline/via/timer1.rb +82 -0
  100. data/lib/badline/via/timer2.rb +84 -0
  101. data/lib/badline/via.rb +262 -0
  102. data/lib/badline/vic/bank.rb +4 -3
  103. data/lib/badline/vic/color_patches.rb +6 -3
  104. data/lib/badline/vic/display_state.rb +4 -0
  105. data/lib/badline/vic/graphics_shifter.rb +49 -8
  106. data/lib/badline/vic/registers.rb +19 -27
  107. data/lib/badline/vic/sequencer.rb +18 -18
  108. data/lib/badline/vic/sequencer_output.rb +4 -4
  109. data/lib/badline/vic/sprite/internal_bus.rb +4 -0
  110. data/lib/badline/vic/sprite/shifter.rb +25 -3
  111. data/lib/badline/vic/sprite.rb +5 -4
  112. data/lib/badline/vic/sprites.rb +8 -3
  113. data/lib/badline/vic.rb +98 -46
  114. data/lib/badline.rb +5 -0
  115. data/packaging/homebrew/badline.rb +34 -0
  116. metadata +37 -20
  117. data/exe/badline +0 -75
  118. data/exe/badline-sid +0 -34
  119. data/lib/badline/audio/options.rb +0 -137
@@ -9,22 +9,34 @@ module Badline
9
9
  #
10
10
  # The byte a group draws loads at pixel XSCROLL. ECM and BMM take hold at
11
11
  # pixel 4 as they rise and at pixel 6 as they fall (the 6569's colour
12
- # latency). MCM takes hold at pixel 4 for the colour lookup but only at
12
+ # latency), except that BMM falls at pixel 5 out of any mode but hi-res
13
+ # bitmap. MCM takes hold at pixel 4 for the colour lookup but only at
13
14
  # pixel 7 for how the register is read, where a rising MCM also resets
14
15
  # the multicolour flip-flop. An MCM that falls out of the invalid
15
16
  # ECM+MCM mode is a pixel later on both counts: the lookup changes at
16
17
  # pixel 5 and the read at the next group's pixel 0. The mode bits are
17
18
  # the sequencer's: ECM 4, BMM 2, MCM 1.
19
+ #
20
+ # The 8565 has no colour latency: ECM and BMM take hold, rising or
21
+ # falling, at the next group's pixel 0, and an MCM that falls out of
22
+ # ECM+MCM does so on time. Where that pixel 0 takes the group out of an
23
+ # invalid mode, it is still black into hi-res text, and so is a
24
+ # background pixel into multicolour bitmap.
18
25
  class GraphicsShifter
26
+ ECM = 0b100
19
27
  ECM_BMM = 0b110
28
+ MULTICOLOR_BITMAP = 0b011
20
29
  ECM_MCM = 0b101
21
30
  BMM = 0b010
22
31
 
23
32
  # The group #draw painted, and which of its pixels are foreground.
24
33
  attr_reader :colors, :fg
25
34
 
26
- def initialize(registers)
35
+ def initialize(registers, model: :mos6569)
27
36
  @registers = registers
37
+ @latency = model != :mos8565
38
+ @next_ecm_bmm = 0
39
+ @black_below = 0
28
40
  @colors = Array.new(8, 0)
29
41
  @fg = Array.new(8, false)
30
42
  @gbuf = 0
@@ -42,6 +54,7 @@ module Badline
42
54
  # the byte loaded at pixel shift under an unchanging mode.
43
55
  def prime(data, screencode, color, shift, mode)
44
56
  @mode_lookup = mode
57
+ @next_ecm_bmm = mode & ECM_BMM
45
58
  @mode_read = mode & 1
46
59
  @late_read = false
47
60
  load(data, screencode, color)
@@ -55,27 +68,55 @@ module Badline
55
68
  step_mode(i, mode)
56
69
  load(data, screencode, color) if i == shift
57
70
  pixel = read_pixel
58
- @colors[i] = lookup(pixel)
71
+ @colors[i] = pixel < @black_below ? 0 : lookup(pixel)
72
+ @black_below = 0
59
73
  @fg[i] = pixel >= 2
60
74
  i += 1
61
75
  end
76
+ @next_ecm_bmm = mode & ECM_BMM
62
77
  end
63
78
 
64
79
  private
65
80
 
66
81
  def step_mode(pixel, mode)
67
82
  case pixel
68
- when 0 then end_late_read
83
+ when 0 then start_group
69
84
  when 4 then step_lookup(mode)
70
- when 5 then @mode_lookup &= ~1 if @late_mcm
71
- when 6 then @mode_lookup &= mode | 1
85
+ when 5 then step_early_fall(mode)
86
+ when 6 then @mode_lookup &= mode | 1 if @latency
72
87
  when 7 then step_read
73
88
  end
74
89
  end
75
90
 
91
+ def start_group
92
+ end_late_read
93
+ return if @latency
94
+
95
+ invalid = @mode_lookup > ECM
96
+ @mode_lookup = (@mode_lookup & ~ECM_BMM) | @next_ecm_bmm
97
+ @black_below = black_pixels_out_of_invalid if invalid
98
+ end
99
+
100
+ # The pixel values below which pixel 0 stays black as the group leaves
101
+ # an invalid mode: all four into hi-res text, the background ones into
102
+ # multicolour bitmap.
103
+ def black_pixels_out_of_invalid
104
+ case @mode_lookup
105
+ when 0 then 4
106
+ when MULTICOLOR_BITMAP then 2
107
+ else 0
108
+ end
109
+ end
110
+
76
111
  def step_lookup(mode)
77
- @late_mcm = @mode_lookup.allbits?(ECM_MCM) && mode.nobits?(1)
78
- @mode_lookup = (@mode_lookup & ~1) | (mode & 1) | (@late_mcm ? 1 : 0) | (mode & ECM_BMM)
112
+ @late_mcm = @latency && @mode_lookup.allbits?(ECM_MCM) && mode.nobits?(1)
113
+ rising = @latency ? mode & ECM_BMM : 0
114
+ @mode_lookup = (@mode_lookup & ~1) | (mode & 1) | (@late_mcm ? 1 : 0) | rising
115
+ end
116
+
117
+ def step_early_fall(mode)
118
+ @mode_lookup &= ~1 if @late_mcm
119
+ @mode_lookup &= mode | ~BMM if @latency && @mode_lookup != BMM
79
120
  end
80
121
 
81
122
  def step_read
@@ -53,21 +53,21 @@ module Badline
53
53
  #
54
54
  # All color registers are 4bit, bits 4-7 always read 1.
55
55
  #
56
- # $D020: Border color - Default: 14, light blue
57
- # $D021: Background color 0 - Default: 6, blue
58
- # $D022: Background color 1 - Default: 1, white
59
- # $D023: Background color 2 - Default: 2, red
60
- # $D024: Background color 3 - Default: 3, cyan
61
- # $D025: Sprite multicolor 0 - Default: 4, purple
62
- # $D026: Sprite multicolor 1 - Default: 0, black
63
- # $D027: Sprite 0 color - Default: 1, white
64
- # $D028: Sprite 1 color - Default: 2, red
65
- # $D029: Sprite 2 color - Default: 3, cyan
66
- # $D02A: Sprite 3 color - Default: 4, purple
67
- # $D02B: Sprite 4 color - Default: 5, green
68
- # $D02C: Sprite 5 color - Default: 6, blue
69
- # $D02D: Sprite 6 color - Default: 7, yellow
70
- # $D02E: Sprite 7 color - Default: 12, medium gray
56
+ # $D020: Border color
57
+ # $D021: Background color 0
58
+ # $D022: Background color 1
59
+ # $D023: Background color 2
60
+ # $D024: Background color 3
61
+ # $D025: Sprite multicolor 0
62
+ # $D026: Sprite multicolor 1
63
+ # $D027: Sprite 0 color
64
+ # $D028: Sprite 1 color
65
+ # $D029: Sprite 2 color
66
+ # $D02A: Sprite 3 color
67
+ # $D02B: Sprite 4 color
68
+ # $D02C: Sprite 5 color
69
+ # $D02D: Sprite 6 color
70
+ # $D02E: Sprite 7 color
71
71
  #
72
72
  # $D02F-$D03F: Not in use, always reads 0xff
73
73
  # $D040-$D3FF: Repeat $D0000 to $D03F every 64 bytes
@@ -77,10 +77,10 @@ module Badline
77
77
  # The raw register bytes, for the per-column reads on the hot path.
78
78
  attr_reader :bytes
79
79
 
80
+ # Every register powers on as zero, as in VICE.
80
81
  def initialize
81
82
  @bytes = Array.new(2**6, 0)
82
83
  @irq_line = false
83
- write_defaults
84
84
  end
85
85
 
86
86
  # True while any enabled latch bit is set in $D019/$D01A. Cached and
@@ -91,6 +91,8 @@ module Badline
91
91
 
92
92
  def read(reg)
93
93
  case reg
94
+ when 0x16 then @bytes[reg] | 0xc0 # bits 7-6 unused, read 1
95
+ when 0x18 then @bytes[reg] | 0x01 # bit 0 unused, reads 1
94
96
  when 0x1a, 0x20..0x2e then @bytes[reg] | 0xf0
95
97
  when 0x1e, 0x1f then read_clear(reg) # collision registers clear on read
96
98
  when 0x2f..0x3f then 0xff
@@ -106,6 +108,7 @@ module Badline
106
108
  when 0x1a # only the four latch bits
107
109
  @bytes[reg] = value & 0x0f
108
110
  update_irq_line
111
+ when 0x1e, 0x1f then nil # the collision registers are read-only
109
112
  else @bytes[reg] = value
110
113
  end
111
114
  end
@@ -157,17 +160,6 @@ module Badline
157
160
  def read_clear(reg)
158
161
  @bytes[reg].tap { @bytes[reg] = 0 }
159
162
  end
160
-
161
- def write_defaults
162
- write_each(0x20, [14, 6, 1, 2, 3, 4, 0, 1, 2, 3, 4, 5, 6, 7, 12])
163
- write_each(0x11, [0x1b, 0]) # $D011: DEN=1, RSEL=1, YSCROLL=3
164
- write_each(0x16, [0xc8]) # $D016: Text mode, XSCROLL=0
165
- write_each(0x19, [0, 0]) # IRQ flags
166
- end
167
-
168
- def write_each(addr, values)
169
- values.each_with_index { |v, i| @bytes[addr + i] = v }
170
- end
171
163
  end
172
164
  end
173
165
  end
@@ -14,21 +14,6 @@ module Badline
14
14
  class Sequencer
15
15
  include Output
16
16
 
17
- DISPLAY_X_BOUNDS = [
18
- [135, 438].freeze,
19
- [128, 447].freeze
20
- ].freeze
21
-
22
- # The graphics window always spans the full 40 columns, ignoring CSEL.
23
- GFX_X_START = DISPLAY_X_BOUNDS[1][0]
24
- GFX_X_END = DISPLAY_X_BOUNDS[1][1] + 1
25
-
26
- # [left compare, right compare] for the border flip-flop per CSEL state.
27
- WINDOW_COMPARES = [
28
- [DISPLAY_X_BOUNDS[0][0], DISPLAY_X_BOUNDS[0][1] + 1].freeze,
29
- [GFX_X_START, GFX_X_END].freeze
30
- ].freeze
31
-
32
17
  BORDER_Y_BOUNDS = [
33
18
  [55, 247].freeze,
34
19
  [51, 251].freeze
@@ -45,8 +30,9 @@ module Badline
45
30
  # foreground mask the sprite collisions read.
46
31
  attr_writer :render
47
32
 
48
- def initialize(width, registers, bank)
33
+ def initialize(width, registers, bank, model: :mos6569, region: Region::PAL)
49
34
  @width = width
35
+ window_bounds(region.display_x_bounds)
50
36
  @registers = registers
51
37
  @bank = bank
52
38
  @colors = Array.new(width, 0)
@@ -63,8 +49,8 @@ module Badline
63
49
  @vertical_border = true
64
50
  @vertical_armed = true
65
51
  @main_border = true
66
- @color_patches = ColorPatches.new(self)
67
- @shifter = GraphicsShifter.new(registers)
52
+ @color_patches = ColorPatches.new(self, grey_dots: model == :mos8565)
53
+ @shifter = GraphicsShifter.new(registers, model:)
68
54
  @xscroll = 0
69
55
  @last_shift = 0
70
56
  @last_mode = 0
@@ -179,6 +165,20 @@ module Badline
179
165
 
180
166
  private
181
167
 
168
+ # The display window's edges per CSEL state, as the region gives them.
169
+ # The graphics window always spans the full 40 columns, ignoring CSEL,
170
+ # and the border flip-flop takes [left compare, right compare] per
171
+ # CSEL state.
172
+ def window_bounds(bounds)
173
+ narrow, wide = bounds
174
+ @gfx_x_start = wide[0]
175
+ @gfx_x_end = wide[1] + 1
176
+ @window_compares = [
177
+ [narrow[0], narrow[1] + 1].freeze,
178
+ [@gfx_x_start, @gfx_x_end].freeze
179
+ ].freeze
180
+ end
181
+
182
182
  # After a mode, XSCROLL or colour change: groups paint whole bytes
183
183
  # again once the change has settled, and run pixel by pixel until then.
184
184
  def emit_checked(slot, col, display, mode)
@@ -16,9 +16,9 @@ module Badline
16
16
  def output(col, shift)
17
17
  x_pos = (col + 16) * 8
18
18
  csel = @registers.csel?
19
- first = WINDOW_COMPARES[@first_csel ? 1 : 0]
19
+ first = @window_compares[@first_csel ? 1 : 0]
20
20
  @first_csel = csel
21
- compares = WINDOW_COMPARES[csel ? 1 : 0]
21
+ compares = @window_compares[csel ? 1 : 0]
22
22
  win_lo, right_compare = compares
23
23
 
24
24
  if !first.equal?(compares) || boundary_group?(x_pos, win_lo, right_compare)
@@ -45,7 +45,7 @@ module Badline
45
45
  end
46
46
 
47
47
  def output_window(x_pos, shift)
48
- in_gfx = x_pos >= GFX_X_START && x_pos < GFX_X_END
48
+ in_gfx = x_pos >= @gfx_x_start && x_pos < @gfx_x_end
49
49
  @border_groups[x_pos >> 3] = BorderMask::NONE
50
50
 
51
51
  if shift.zero?
@@ -105,7 +105,7 @@ module Badline
105
105
  shown = pixel_shown?(x, win_lo, right_compare)
106
106
  @colors[x] = shown ? pixel : border
107
107
  @border[x] = !shown
108
- @fg[x] = x >= GFX_X_START && x < GFX_X_END ? mask : false
108
+ @fg[x] = x >= @gfx_x_start && x < @gfx_x_end ? mask : false
109
109
  win_lo, right_compare = compares if i.zero?
110
110
  i += 1
111
111
  end
@@ -10,8 +10,12 @@ module Badline
10
10
  # the CPU reads or writes a VIC register in that cycle, and the middle
11
11
  # one is the idle fetch at $3fff (sbsprf24's readme).
12
12
  class InternalBus
13
+ # The columns of a raster line.
14
+ attr_reader :columns
15
+
13
16
  def initialize(bank, columns)
14
17
  @bank = bank
18
+ @columns = columns
15
19
  @lines = 0
16
20
  @ghost = 0xff
17
21
  @value = Array.new(columns, 0xff)
@@ -71,8 +71,8 @@ module Badline
71
71
  while count < MAX_SPAN
72
72
  if pos >= boundary
73
73
  log.advance(pos)
74
- mc = log[0x1c].anybits?(@bit)
75
74
  expanded = log[0x1d].anybits?(@bit)
75
+ mc = logged_multicolor(log, mc, expanded, count)
76
76
  boundary = log.next_x
77
77
  end
78
78
  count.zero? ? prime(mc) : step(mc, expanded, count == cut - 1)
@@ -88,11 +88,29 @@ module Badline
88
88
  # The X match loads the shift register and arms both flip-flops, so
89
89
  # the sprite's first pixel comes straight off the fetched row.
90
90
  def prime(multicolor)
91
- @mc_flop = false
91
+ @mc_flop = @new_chip && !multicolor
92
92
  @xe_flop = false
93
93
  @latch = (@sr >> 22) & (multicolor ? 3 : 2)
94
94
  end
95
95
 
96
+ # The multicolor bit as the log has it at the pixel. A change after
97
+ # the first pixel reaches the 8565's flip-flop.
98
+ def logged_multicolor(log, was, expanded, count)
99
+ multicolor = log[0x1c].anybits?(@bit)
100
+ switch_multicolor(multicolor, expanded) if @new_chip && multicolor != was && count.positive?
101
+ multicolor
102
+ end
103
+
104
+ # On the 8565 a multicolor change works on the flip-flop instead of
105
+ # the next pixels. It stays as it is unless the change lands between
106
+ # the two halves of an expanded pixel, where it flips, or is set by a
107
+ # switch to hi-res.
108
+ def switch_multicolor(multicolor, expanded)
109
+ return unless expanded && !@xe_flop
110
+
111
+ @mc_flop = multicolor ? !@mc_flop : true
112
+ end
113
+
96
114
  # One pixel of the sequencer. An unexpanded sprite shifts every pixel;
97
115
  # an expanded one shifts every other, and the multicolor latch reloads
98
116
  # every other shift, so its pairs stretch with the expansion. Both
@@ -106,7 +124,7 @@ module Badline
106
124
  else
107
125
  @xe_flop = false
108
126
  end
109
- @mc_flop = false unless multicolor
127
+ @mc_flop = false unless multicolor || @new_chip
110
128
  return unless shift
111
129
 
112
130
  @sr = (@sr << 1) & 0xffffff
@@ -115,10 +133,14 @@ module Badline
115
133
 
116
134
  # A multicolor pair loaded on the last pixel before the reload keeps
117
135
  # only its high bit, as a hi-res pixel does (spritefetchbug).
136
+ # On the 8565 a hi-res pixel with the multicolor flip-flop clear
137
+ # sets it and loads nothing, so the latch repeats its last pixel.
118
138
  def load_latch(multicolor, last)
119
139
  if multicolor
120
140
  @latch = (@sr >> 22) & (last ? 2 : 3) if @mc_flop
121
141
  @mc_flop = !@mc_flop
142
+ elsif @new_chip && !@mc_flop
143
+ @mc_flop = true
122
144
  else
123
145
  @latch = (@sr >> 22) & 2
124
146
  end
@@ -56,12 +56,13 @@ module Badline
56
56
  attr_reader :index, :leftmost, :span, :codes,
57
57
  :reload_leftmost, :reload_span, :reload_codes
58
58
 
59
- def initialize(index, registers, bank, width, bus = InternalBus.new(bank, width / 8))
59
+ def initialize(index, registers, bank, bus, model: :mos6569)
60
60
  @index = index
61
+ @new_chip = model == :mos8565
61
62
  @registers = registers
62
63
  @bank = bank
63
64
  @bus = bus
64
- @width = width
65
+ @width = bus.columns * 8
65
66
  @bit = 1 << index
66
67
  @dma = false
67
68
  @display_on = false
@@ -77,8 +78,8 @@ module Badline
77
78
  @reload_codes = Array.new(MAX_SPAN + RELOAD_HOLD, 0)
78
79
  @reload_leftmost = 0
79
80
  @reload_span = 0
80
- @reload_x = (RELOAD_X + (RELOAD_STEP * index)) % width
81
- @reload_next_line = RELOAD_X + (RELOAD_STEP * index) < width
81
+ @reload_x = (RELOAD_X + (RELOAD_STEP * index)) % @width
82
+ @reload_next_line = RELOAD_X + (RELOAD_STEP * index) < @width
82
83
  @sr = 0
83
84
  @latch = 0
84
85
  @mc_flop = false
@@ -27,12 +27,17 @@ module Badline
27
27
  [0x25, 0x26, *0x27..0x2e].each { |reg| delays[reg] = COLOR_DELAY }
28
28
  end.freeze
29
29
 
30
- def initialize(registers, bank, width)
30
+ # The 8565's sprite sequencers take a multicolor change a pixel
31
+ # sooner than the 6569's.
32
+ WRITE_DELAY_8565 = WRITE_DELAY.dup.tap { |delays| delays[0x1c] = SEQUENCER_DELAY - 1 }.freeze
33
+
34
+ def initialize(registers, bank, width, model: :mos6569)
31
35
  @registers = registers
36
+ @write_delay = model == :mos8565 ? WRITE_DELAY_8565 : WRITE_DELAY
32
37
  @bank = bank
33
38
  @width = width
34
39
  @bus = Sprite::InternalBus.new(bank, width / 8)
35
- @sprites = Array.new(8) { |i| Sprite.new(i, registers, bank, width, @bus) }
40
+ @sprites = Array.new(8) { |i| Sprite.new(i, registers, bank, @bus, model:) }
36
41
  @collisions = Collisions.new(registers, width)
37
42
  @hits = @collisions.hits
38
43
  @win_color = Array.new(width, 0)
@@ -123,7 +128,7 @@ module Badline
123
128
  # Record a mid-line write at the pixel where it becomes visible.
124
129
  def log_change(reg, old, value, beam_x)
125
130
  clear_y_expansion(old & ~value, beam_x) if reg == 0x17
126
- delay = WRITE_DELAY[reg]
131
+ delay = @write_delay[reg]
127
132
  return unless delay && active?
128
133
 
129
134
  @log.log(beam_x + delay, reg, old, value)