badline 0.3.0 → 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 (91) 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 +58 -0
  5. data/CLAUDE.md +45 -16
  6. data/CONTRIBUTING.md +95 -3
  7. data/README.md +111 -61
  8. data/SECURITY.md +9 -0
  9. data/doc/pinned-behaviour.md +275 -33
  10. data/exe/badline-ruby +59 -0
  11. data/lib/badline/address_bus.rb +22 -10
  12. data/lib/badline/audio/aiff.rb +8 -3
  13. data/lib/badline/audio/cli.rb +17 -12
  14. data/lib/badline/audio/console.rb +12 -58
  15. data/lib/badline/audio/playback.rb +3 -0
  16. data/lib/badline/audio/sdl_sink.rb +20 -52
  17. data/lib/badline/audio/terminal.rb +90 -0
  18. data/lib/badline/audio.rb +2 -2
  19. data/lib/badline/cartridge/geo_ram.rb +63 -0
  20. data/lib/badline/cartridge.rb +1 -0
  21. data/lib/badline/cia/interrupt_register.rb +34 -10
  22. data/lib/badline/cia.rb +15 -8
  23. data/lib/badline/computer.rb +28 -13
  24. data/lib/badline/gui/application.rb +33 -21
  25. data/lib/badline/gui/gamepads.rb +22 -22
  26. data/lib/badline/gui/joy_map.rb +1 -1
  27. data/lib/badline/gui/key_map.rb +6 -1
  28. data/lib/badline/gui/pane.rb +7 -6
  29. data/lib/badline/gui/screen_pane.rb +9 -13
  30. data/lib/badline/gui/window.rb +35 -20
  31. data/lib/badline/gui.rb +1 -1
  32. data/lib/badline/input/mouse1351.rb +21 -6
  33. data/lib/badline/input/paddles.rb +11 -6
  34. data/lib/badline/kernal_trap/channel.rb +3 -0
  35. data/lib/badline/kernal_trap/drive/block_commands.rb +33 -4
  36. data/lib/badline/kernal_trap/drive/status.rb +3 -0
  37. data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
  38. data/lib/badline/kernal_trap/drive/writes.rb +92 -0
  39. data/lib/badline/kernal_trap/drive.rb +51 -51
  40. data/lib/badline/kernal_trap/file.rb +6 -9
  41. data/lib/badline/kernal_trap/save.rb +9 -4
  42. data/lib/badline/keyboard.rb +19 -3
  43. data/lib/badline/media.rb +42 -11
  44. data/lib/badline/options.rb +209 -0
  45. data/lib/badline/ram_expansion/banking.rb +30 -0
  46. data/lib/badline/ram_expansion/plus256k.rb +38 -0
  47. data/lib/badline/ram_expansion/plus60k.rb +31 -0
  48. data/lib/badline/ram_expansion/unexpanded.rb +22 -0
  49. data/lib/badline/ram_expansion.rb +21 -0
  50. data/lib/badline/region.rb +51 -0
  51. data/lib/badline/sdl/events.rb +47 -0
  52. data/lib/badline/sdl/functions.rb +73 -0
  53. data/lib/badline/sdl.rb +67 -0
  54. data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
  55. data/lib/badline/sid/waveform.rb +1 -1
  56. data/lib/badline/sid.rb +3 -1
  57. data/lib/badline/storage/d64_image.rb +10 -0
  58. data/lib/badline/storage/d71_image.rb +12 -0
  59. data/lib/badline/storage/d81_image.rb +9 -0
  60. data/lib/badline/storage/disk_image/bam.rb +111 -0
  61. data/lib/badline/storage/disk_image/directory.rb +79 -0
  62. data/lib/badline/storage/disk_image/writing.rb +110 -0
  63. data/lib/badline/storage/disk_image.rb +27 -4
  64. data/lib/badline/storage/host_directory.rb +2 -1
  65. data/lib/badline/storage.rb +15 -0
  66. data/lib/badline/time_of_day.rb +4 -2
  67. data/lib/badline/version.rb +1 -1
  68. data/lib/badline/via/control_lines.rb +104 -0
  69. data/lib/badline/via/interrupt_register.rb +58 -0
  70. data/lib/badline/via/shift_register.rb +135 -0
  71. data/lib/badline/via/timer1.rb +82 -0
  72. data/lib/badline/via/timer2.rb +84 -0
  73. data/lib/badline/via.rb +262 -0
  74. data/lib/badline/vic/bank.rb +4 -3
  75. data/lib/badline/vic/color_patches.rb +6 -3
  76. data/lib/badline/vic/display_state.rb +4 -0
  77. data/lib/badline/vic/graphics_shifter.rb +42 -7
  78. data/lib/badline/vic/registers.rb +3 -0
  79. data/lib/badline/vic/sequencer.rb +18 -18
  80. data/lib/badline/vic/sequencer_output.rb +4 -4
  81. data/lib/badline/vic/sprite/internal_bus.rb +4 -0
  82. data/lib/badline/vic/sprite/shifter.rb +25 -3
  83. data/lib/badline/vic/sprite.rb +5 -4
  84. data/lib/badline/vic/sprites.rb +8 -3
  85. data/lib/badline/vic.rb +93 -44
  86. data/lib/badline.rb +4 -0
  87. data/packaging/homebrew/badline.rb +34 -0
  88. metadata +30 -20
  89. data/exe/badline +0 -78
  90. data/exe/badline-sid +0 -34
  91. data/lib/badline/audio/options.rb +0 -137
@@ -16,16 +16,27 @@ module Badline
16
16
  # ECM+MCM mode is a pixel later on both counts: the lookup changes at
17
17
  # pixel 5 and the read at the next group's pixel 0. The mode bits are
18
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.
19
25
  class GraphicsShifter
26
+ ECM = 0b100
20
27
  ECM_BMM = 0b110
28
+ MULTICOLOR_BITMAP = 0b011
21
29
  ECM_MCM = 0b101
22
30
  BMM = 0b010
23
31
 
24
32
  # The group #draw painted, and which of its pixels are foreground.
25
33
  attr_reader :colors, :fg
26
34
 
27
- def initialize(registers)
35
+ def initialize(registers, model: :mos6569)
28
36
  @registers = registers
37
+ @latency = model != :mos8565
38
+ @next_ecm_bmm = 0
39
+ @black_below = 0
29
40
  @colors = Array.new(8, 0)
30
41
  @fg = Array.new(8, false)
31
42
  @gbuf = 0
@@ -43,6 +54,7 @@ module Badline
43
54
  # the byte loaded at pixel shift under an unchanging mode.
44
55
  def prime(data, screencode, color, shift, mode)
45
56
  @mode_lookup = mode
57
+ @next_ecm_bmm = mode & ECM_BMM
46
58
  @mode_read = mode & 1
47
59
  @late_read = false
48
60
  load(data, screencode, color)
@@ -56,32 +68,55 @@ module Badline
56
68
  step_mode(i, mode)
57
69
  load(data, screencode, color) if i == shift
58
70
  pixel = read_pixel
59
- @colors[i] = lookup(pixel)
71
+ @colors[i] = pixel < @black_below ? 0 : lookup(pixel)
72
+ @black_below = 0
60
73
  @fg[i] = pixel >= 2
61
74
  i += 1
62
75
  end
76
+ @next_ecm_bmm = mode & ECM_BMM
63
77
  end
64
78
 
65
79
  private
66
80
 
67
81
  def step_mode(pixel, mode)
68
82
  case pixel
69
- when 0 then end_late_read
83
+ when 0 then start_group
70
84
  when 4 then step_lookup(mode)
71
85
  when 5 then step_early_fall(mode)
72
- when 6 then @mode_lookup &= mode | 1
86
+ when 6 then @mode_lookup &= mode | 1 if @latency
73
87
  when 7 then step_read
74
88
  end
75
89
  end
76
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
+
77
111
  def step_lookup(mode)
78
- @late_mcm = @mode_lookup.allbits?(ECM_MCM) && mode.nobits?(1)
79
- @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
80
115
  end
81
116
 
82
117
  def step_early_fall(mode)
83
118
  @mode_lookup &= ~1 if @late_mcm
84
- @mode_lookup &= mode | ~BMM unless @mode_lookup == BMM
119
+ @mode_lookup &= mode | ~BMM if @latency && @mode_lookup != BMM
85
120
  end
86
121
 
87
122
  def step_read
@@ -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
@@ -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)
data/lib/badline/vic.rb CHANGED
@@ -11,14 +11,22 @@ module Badline
11
11
  include Addressable
12
12
  include IntegerHelper
13
13
 
14
+ # The 6569 of the breadbin C64, and the HMOS 8565 of the C64C.
15
+ MODELS = %i[mos6569 mos8565].freeze
16
+
14
17
  attr_reader :address_bus, :display, :width, :height, :vic_bank, :column,
15
- :rasterline, :dirty_lines
18
+ :rasterline, :dirty_lines, :model, :region
16
19
 
17
20
  # Returns the byte the CPU's halted read would see, for the c-accesses
18
21
  # that run before AEC. Without it they read colour RAM.
19
22
  attr_writer :open_bus
20
23
 
21
24
  LIGHTPEN_IRQ = 0x08 # $D019 latch bit
25
+ COLOR_REGISTERS = 0x20..0x2e
26
+
27
+ # What the 8565 shows on the pixel before a color register write takes
28
+ # hold, where the 6569 shows the old color: the grey dot.
29
+ GREY_DOT = 0x0f
22
30
 
23
31
  # A g-access reaches the pixel output this many columns after it runs.
24
32
  GRAPHICS_DELAY = 2
@@ -28,17 +36,19 @@ module Badline
28
36
  G_IDLE = 1
29
37
  G_DISPLAY = 2
30
38
 
39
+ # The idle g-access in the column a mid-row bad line opens display
40
+ # state reads here on a 6569.
41
+ DMA_DELAY_IDLE_ADDRESS = 0x38ff
42
+
31
43
  # The $d011 mode bits a g-access still sees for a cycle after they fall:
32
44
  # BMM always, and ECM too when the access it leaves read the character
33
45
  # ROM.
34
46
  FETCH_HOLD = 0x20
35
47
  FETCH_HOLD_ROM = 0x60
36
48
 
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
49
+ # The columns carrying a hook, besides the line's last column, which
50
+ # starts the vertical border for the line after it.
51
+ HOOKS = [14, 15, 53, 54, 57].freeze
42
52
 
43
53
  # BA falls three columns ahead of each sprite's pair of s-accesses, and
44
54
  # the five-column windows step two columns apart from sprite 0 at column
@@ -46,19 +56,29 @@ module Badline
46
56
  # split: the tail columns fall on the line whose cycle 55/56 compare
47
57
  # started the fetch, the head columns on the line after it.
48
58
  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
51
59
 
52
- def initialize(address_bus = nil, debug: false)
60
+ def initialize(address_bus = nil, debug: false, model: :mos6569, region: Region::PAL)
61
+ raise ArgumentError, "unknown VIC-II model #{model}" unless MODELS.include?(model)
62
+
53
63
  addressable_at(0xd000, length: 2**10)
64
+ @model = model
65
+ @region = region
66
+ @lightpen_extra = model == :mos8565 ? 1 : 2
67
+ @grey_dots = model == :mos8565
68
+ @delayed_fetch = model == :mos8565
69
+ @dma_delay_idle = model == :mos6569
54
70
  @address_bus = address_bus || AddressBus.new
55
71
  @vic_bank = VIC::Bank.new(@address_bus)
56
72
  @debug = debug
57
73
 
58
- @width = 504
59
- @height = 312
60
- @columns_per_line = @width / 8
74
+ @columns_per_line = region.cycles_per_line
75
+ @width = @columns_per_line * 8
76
+ @height = region.lines_per_frame
77
+ @last_column = @columns_per_line - 1
61
78
  @last_line = @height - 1
79
+ layout_columns
80
+ @blank_columns = Array.new(@columns_per_line) { |column| Region.blanked?(column, region.hblank) }.freeze
81
+ @blank_lines = Array.new(@height) { |line| Region.blanked?(line, region.vblank) }.freeze
62
82
  @display = Array.new(@width * @height, 0)
63
83
  @lines = Array.new(@height) { Array.new(@width, 0) }
64
84
  @dirty_lines = Array.new(@height, true)
@@ -75,9 +95,9 @@ module Badline
75
95
  @registers = VIC::Registers.new
76
96
  @register_bytes = @registers.bytes
77
97
  @display_state = VIC::DisplayState.new(@registers)
78
- @sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank)
98
+ @sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank, model: @model, region: @region)
79
99
  @sequencer.render = @render
80
- @sprites = VIC::Sprites.new(@registers, @vic_bank, @width)
100
+ @sprites = VIC::Sprites.new(@registers, @vic_bank, @width, model: @model)
81
101
  @display.fill(0)
82
102
  @lines.each { |line| line.fill(0) }
83
103
  @dirty_lines.fill(true)
@@ -108,11 +128,11 @@ module Badline
108
128
  # The g-access runs in the first half of the cycle, ahead of the bad
109
129
  # line compare, and the c-access in the second half, after it.
110
130
  draw!
111
- @display_state.cycle(@rasterline, @column)
131
+ dma_delay_idle_access if @display_state.cycle(@rasterline, @column)
112
132
 
113
133
  fetch_character_data! if dma_active?
114
134
 
115
- column_hooks if HOOK_COLUMNS[@column]
135
+ column_hooks if @hook_columns[@column]
116
136
 
117
137
  @column += 1
118
138
  if @column == @columns_per_line
@@ -140,7 +160,7 @@ module Badline
140
160
  when 53 then check_sprite_dma
141
161
  when 54 then check_dma_and_toggle_expansion
142
162
  when 57 then @sprites.check_display(@rasterline)
143
- when 62 then @sequencer.start_vertical_border(@rasterline == @last_line ? 0 : @rasterline + 1)
163
+ when @last_column then @sequencer.start_vertical_border(@rasterline == @last_line ? 0 : @rasterline + 1)
144
164
  end
145
165
  end
146
166
 
@@ -197,9 +217,10 @@ module Badline
197
217
  end
198
218
 
199
219
  # The first trigger per frame latches the beam position into LPX/LPY and
200
- # raises the LP IRQ. The latch happens one cycle after the edge, with the
201
- # 6569's two extra half-pixels; a trigger on the last line is consumed
202
- # without latching unless it lands on the line's first cycle.
220
+ # raises the LP IRQ. The latch happens one cycle after the edge, with
221
+ # the model's extra half-pixels, two on the 6569 and one on the 8565; a
222
+ # trigger on the last line is consumed without latching unless it lands
223
+ # on the line's first cycle.
203
224
  def trigger_lightpen
204
225
  return if @lp_triggered
205
226
 
@@ -213,7 +234,7 @@ module Badline
213
234
  return if line == @last_line && column.positive?
214
235
 
215
236
  vic_x = ((column * 8) - Sprite::X_OFFSET) % @width
216
- latch_lightpen((vic_x >> 1) + 2, line)
237
+ latch_lightpen((vic_x >> 1) + @lightpen_extra, line)
217
238
  end
218
239
 
219
240
  # The byte the VIC fetched in the phi1 half of the cycle the CPU is
@@ -224,17 +245,9 @@ module Badline
224
245
  vic_bank.peek(phi1_address)
225
246
  end
226
247
 
227
- def hblank?
228
- @column < 10 || @column > 60
229
- end
230
-
231
- def vblank?
232
- @rasterline < 16 || @rasterline > 299
233
- end
234
-
235
- def blanking?
236
- @rasterline < 16 || @rasterline > 299 || @column < 10 || @column > 60
237
- end
248
+ def hblank? = @blank_columns[@column]
249
+ def vblank? = @blank_lines[@rasterline]
250
+ def blanking? = @blank_lines[@rasterline] || @blank_columns[@column]
238
251
 
239
252
  def render? = @render
240
253
 
@@ -265,19 +278,30 @@ module Badline
265
278
  # Mid-line writes to the color and sprite registers are logged against
266
279
  # the cycle after the write (the CPU runs after the VIC within a machine
267
280
  # cycle, so @column already points there); each register adds its own
268
- # pixel delay on top.
281
+ # pixel delay on top. On the 8565 a color register write that leaves
282
+ # the value as it was still shows its grey dot.
269
283
  def log_register_change(reg, value)
270
284
  old = @registers[reg]
271
- return if old == value
285
+ return if old == value && !grey_dot?(reg)
272
286
 
273
287
  if (0x20..0x24).cover?(reg)
274
288
  @sequencer.colors_changed! unless reg == 0x20
275
289
  @sequencer.color_patches.log(reg, old, value, @column * 8) if @render && !blanking?
276
290
  else
277
- @sprites.log_change(reg, old, value, @column * 8)
291
+ log_sprite_change(reg, old, value)
278
292
  end
279
293
  end
280
294
 
295
+ def grey_dot?(reg) = @grey_dots && COLOR_REGISTERS.cover?(reg)
296
+
297
+ # A sprite color shows its grey dot as a write of light grey on the
298
+ # pixel before the new color.
299
+ def log_sprite_change(reg, old, value)
300
+ beam_x = @column * 8
301
+ @sprites.log_change(reg, old, GREY_DOT, beam_x - 1) if grey_dot?(reg)
302
+ @sprites.log_change(reg, old, value, beam_x)
303
+ end
304
+
281
305
  # Bauer cycles 1-10 and 58-63 are sprite p- and s-accesses, 11-15
282
306
  # refresh, 16-55 g-accesses and 56-57 idle. @column has already
283
307
  # advanced, so it is the Bauer cycle less one.
@@ -311,11 +335,12 @@ module Badline
311
335
  graphics_address(@registers[0x11], @display_state.vmli - 1, (@display_state.vc - 1) & 0x3ff)
312
336
  end
313
337
 
314
- # A $d011/$d012 write is compared in the next column. A write after
315
- # column 61 is left to column 62, which compares the next line.
338
+ # A $d011/$d012 write is compared in the next column. A write in the
339
+ # line's second last column is left to its last, which compares the
340
+ # next line.
316
341
  def compare_raster_writes(reg)
317
342
  return unless (0x11..0x12).cover?(reg)
318
- return if @column == @columns_per_line - 1
343
+ return if @column == @last_column
319
344
 
320
345
  check_raster_irq!
321
346
  @sequencer.compare_vertical_border(@rasterline) if reg == 0x11
@@ -358,11 +383,21 @@ module Badline
358
383
  8.times do |n|
359
384
  next unless @sprites[n].displaying?
360
385
 
361
- SPRITE_BA_TAIL[n].each { |c| @sprite_ba[c] = true }
362
- SPRITE_BA_HEAD[n].each { |c| @sprite_ba[c] = true }
386
+ @sprite_ba_tail[n].each { |c| @sprite_ba[c] = true }
387
+ @sprite_ba_head[n].each { |c| @sprite_ba[c] = true }
363
388
  end
364
389
  end
365
390
 
391
+ # Splits the sprite BA windows at the end of the line and marks the hook
392
+ # columns, for a line of the region's length. The marks let an ordinary
393
+ # column cost one array read instead of the dispatch.
394
+ def layout_columns
395
+ last = @last_column
396
+ @sprite_ba_tail = SPRITE_BA_WINDOWS.map { |w| w.select { |c| c <= last } }.freeze
397
+ @sprite_ba_head = SPRITE_BA_WINDOWS.map { |w| w.filter_map { |c| c - last - 1 if c > last } }.freeze
398
+ @hook_columns = Array.new(@columns_per_line) { |c| c == last || HOOKS.include?(c) }.freeze
399
+ end
400
+
366
401
  # Runs this column's g-access and draws the one from GRAPHICS_DELAY
367
402
  # columns earlier. The access reads its byte now, through the mode,
368
403
  # $d018 and bank of this cycle. A column with no g-access, or one made
@@ -403,15 +438,29 @@ module Badline
403
438
  @g_color[slot] = @g_kept_color = @color_buffer[vmli] || 1
404
439
  end
405
440
 
406
- # The g-access sees a mode bit that falls a cycle late: it addresses
407
- # with $d011 as this column has it, OR-ed with the held bits the column
408
- # before had. When BMM changes and the access moves from RAM onto the
409
- # character ROM, the low address byte still comes from the old mode
441
+ # On the 6569 a bad line that opens display state mid-row (DMA delay)
442
+ # moves the idle g-access of the column that triggers it from $3fff (or
443
+ # $39ff) to DMA_DELAY_IDLE_ADDRESS, unless YSCROLL is 0 (vsp-tester,
444
+ # colorfetchbug/main).
445
+ def dma_delay_idle_access
446
+ slot = @g_tick
447
+ return unless @dma_delay_idle && @g_kind[slot] == G_IDLE
448
+ return if @registers.yscroll.zero?
449
+
450
+ @g_data[slot] = vic_bank.peek(DMA_DELAY_IDLE_ADDRESS)
451
+ end
452
+
453
+ # The 6569's g-access sees a mode bit that falls a cycle late: it
454
+ # addresses with $d011 as this column has it, OR-ed with the held bits
455
+ # the column before had. When BMM changes and the access moves from RAM
456
+ # onto the character ROM, the low address byte still comes from the old
457
+ # mode. The 8565's addresses with $d011 as the column before had it
410
458
  # (VICE x64sc `vicii_fetch_graphics`).
411
459
  def fetch_graphics(vmli, counter)
412
460
  d011 = @register_bytes[0x11]
413
461
  last = @fetch_d011
414
462
  return vic_bank.peek(graphics_address(d011, vmli, counter)) if d011 == last
463
+ return vic_bank.peek(graphics_address(last, vmli, counter)) if @delayed_fetch
415
464
 
416
465
  from = graphics_address(last, vmli, counter)
417
466
  from_rom = vic_bank.character_rom?(from)
data/lib/badline.rb CHANGED
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/version"
4
+ require "badline/options"
4
5
  require "badline/integer_helper"
6
+ require "badline/region"
5
7
  require "badline/addressable"
6
8
  require "badline/memory"
7
9
  require "badline/color_memory"
8
10
  require "badline/rom"
11
+ require "badline/ram_expansion"
9
12
  require "badline/address_bus"
10
13
  require "badline/instruction"
11
14
  require "badline/instruction_set"
@@ -18,6 +21,7 @@ require "badline/cycleable"
18
21
  require "badline/datasette"
19
22
  require "badline/time_of_day"
20
23
  require "badline/cia"
24
+ require "badline/via"
21
25
  require "badline/sid"
22
26
  require "badline/debug_register"
23
27
  require "badline/interrupts"
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The native badline, built from the `rake native:pack` tarball attached to
4
+ # each release. The release workflow copies this file into the tap with the
5
+ # release's url and sha256 filled in.
6
+ class Badline < Formula
7
+ desc "Cycle-accurate Commodore 64 emulator"
8
+ homepage "https://github.com/elektronaut/badline"
9
+ url "https://github.com/elektronaut/badline/releases/download/v0.3.0/badline-0.3.0-spinel-15f037af.tar.gz"
10
+ sha256 "0000000000000000000000000000000000000000000000000000000000000000"
11
+ license "MIT"
12
+
13
+ depends_on "sdl2"
14
+
15
+ uses_from_macos "libxcrypt"
16
+
17
+ def install
18
+ system "make"
19
+ libexec.install "badline"
20
+ pkgshare.install "roms"
21
+ (bin / "badline").write_env_script libexec / "badline",
22
+ BADLINE_ROM_PATH: "${BADLINE_ROM_PATH:-#{pkgshare}/roms}"
23
+ end
24
+
25
+ test do
26
+ assert_match "badline #{version}", shell_output("#{bin}/badline --version")
27
+
28
+ ENV["SDL_VIDEODRIVER"] = "dummy"
29
+ ENV["SDL_RENDER_DRIVER"] = "software"
30
+ ENV["SDL_AUDIODRIVER"] = "dummy"
31
+ system bin / "badline", "--unpaced", "--no-sound", "--frames", "10", "--screenshot", testpath / "frame.bmp"
32
+ assert_path_exists testpath / "frame.bmp"
33
+ end
34
+ end