badline 0.4.1 → 0.5.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/CLAUDE.md +10 -5
  4. data/README.md +59 -24
  5. data/doc/pinned-behaviour.md +221 -0
  6. data/exe/badline-ruby +8 -4
  7. data/lib/badline/address_bus/ultimax_pages.rb +38 -0
  8. data/lib/badline/address_bus.rb +19 -29
  9. data/lib/badline/audio/cli.rb +2 -1
  10. data/lib/badline/audio/jukebox.rb +6 -4
  11. data/lib/badline/audio/machine_player.rb +10 -6
  12. data/lib/badline/cartridge/kcs_power.rb +3 -3
  13. data/lib/badline/cia.rb +5 -1
  14. data/lib/badline/computer.rb +86 -7
  15. data/lib/badline/cpu.rb +39 -0
  16. data/lib/badline/drive1541/bus.rb +134 -0
  17. data/lib/badline/drive1541/disk.rb +256 -0
  18. data/lib/badline/drive1541/disk_via.rb +33 -0
  19. data/lib/badline/drive1541/gcr.rb +57 -0
  20. data/lib/badline/drive1541/idle.rb +184 -0
  21. data/lib/badline/drive1541/mechanism.rb +277 -0
  22. data/lib/badline/drive1541/rotation.rb +111 -0
  23. data/lib/badline/drive1541/sector_reader.rb +76 -0
  24. data/lib/badline/drive1541/serial_port.rb +67 -0
  25. data/lib/badline/drive1541/track.rb +98 -0
  26. data/lib/badline/drive1541.rb +218 -0
  27. data/lib/badline/gui/application.rb +24 -7
  28. data/lib/badline/gui/drive_led_pane.rb +29 -0
  29. data/lib/badline/gui.rb +1 -0
  30. data/lib/badline/iec_bus.rb +93 -0
  31. data/lib/badline/kernal_trap/channel.rb +9 -1
  32. data/lib/badline/kernal_trap/serial.rb +9 -3
  33. data/lib/badline/media/true_drive.rb +54 -0
  34. data/lib/badline/media.rb +22 -1
  35. data/lib/badline/options.rb +30 -4
  36. data/lib/badline/region.rb +35 -10
  37. data/lib/badline/reu/dma.rb +228 -0
  38. data/lib/badline/reu/ram.rb +65 -0
  39. data/lib/badline/reu/trigger.rb +32 -0
  40. data/lib/badline/reu.rb +276 -0
  41. data/lib/badline/roms/README +10 -0
  42. data/lib/badline/roms/dos1541.rom +0 -0
  43. data/lib/badline/sdl/functions.rb +1 -0
  44. data/lib/badline/storage/d64_image.rb +18 -9
  45. data/lib/badline/storage/d81_image.rb +1 -0
  46. data/lib/badline/storage/disk_image/catalog.rb +52 -0
  47. data/lib/badline/storage/disk_image/writing.rb +16 -0
  48. data/lib/badline/storage/disk_image.rb +2 -0
  49. data/lib/badline/storage/g64_image.rb +222 -0
  50. data/lib/badline/storage/host_directory.rb +21 -1
  51. data/lib/badline/storage/listing.rb +118 -0
  52. data/lib/badline/storage/t64.rb +14 -0
  53. data/lib/badline/storage.rb +2 -0
  54. data/lib/badline/traps.rb +4 -0
  55. data/lib/badline/version.rb +1 -1
  56. data/lib/badline/via/control_lines.rb +3 -0
  57. data/lib/badline/via/fast_forward.rb +38 -0
  58. data/lib/badline/via/shift_register.rb +8 -3
  59. data/lib/badline/via/timer1.rb +30 -0
  60. data/lib/badline/via/timer2.rb +25 -0
  61. data/lib/badline/via.rb +2 -0
  62. data/lib/badline/vic/display_state.rb +7 -7
  63. data/lib/badline/vic/sprite/internal_bus.rb +11 -7
  64. data/lib/badline/vic/sprite/shifter.rb +10 -15
  65. data/lib/badline/vic/sprite/timing.rb +60 -0
  66. data/lib/badline/vic/sprite.rb +14 -12
  67. data/lib/badline/vic/sprites.rb +2 -2
  68. data/lib/badline/vic.rb +86 -39
  69. data/lib/badline.rb +3 -0
  70. metadata +26 -1
@@ -66,6 +66,36 @@ module Badline
66
66
  @counter == 0xffff && timeout(free_run)
67
67
  end
68
68
 
69
+ # How many cycles the timer can run without setting its flag: until
70
+ # an armed timer's timeout, and forever for an unarmed one, whose
71
+ # timeouts only reload the counter. None while an armed timer loads
72
+ # or reloads.
73
+ def quiet_cycles
74
+ return FastForward::QUIET unless @armed
75
+ return 0 if @hold || @reload
76
+
77
+ @counter
78
+ end
79
+
80
+ # Runs +cycles+ cycles at once, as many as quiet_cycles allows. An
81
+ # unarmed timer goes round its period of latch + 2 cycles: the
82
+ # timeout at $FFFF, the reload, and latch down to 0.
83
+ def fast_forward(cycles)
84
+ if @hold || @reload
85
+ cycle!(false)
86
+ cycles -= 1
87
+ end
88
+ return @counter -= cycles if cycles <= @counter
89
+
90
+ phase = (cycles - @counter - 1) % (@latch + 2)
91
+ @reload = phase.zero?
92
+ @counter = @reload ? 0xffff : @latch - phase + 1
93
+ end
94
+
95
+ # What the counter and PB7 don't hold, for comparing the timer at two
96
+ # points.
97
+ def state = [@latch, @armed]
98
+
69
99
  private
70
100
 
71
101
  # The counter reloads either way. Armed, PB7 inverts and the flag
@@ -63,6 +63,31 @@ module Badline
63
63
  # A falling edge on PB6. Returns true when it sets the flag.
64
64
  def pulse! = @counting_pulses && decrement
65
65
 
66
+ # How many cycles the timer can run without setting its flag, taken
67
+ # without the shift register clocking off it: until an armed timer
68
+ # underflows, and forever for one counting pulses or unarmed, which
69
+ # only rolls on. None while a load or a switch of mode is under way.
70
+ def quiet_cycles
71
+ return 0 if @hold || @counting_pulses != @count_pulses
72
+ return FastForward::QUIET if @counting_pulses || !@armed
73
+
74
+ @counter
75
+ end
76
+
77
+ # Runs +cycles+ cycles at once, as many as quiet_cycles allows. The
78
+ # low byte's underflow is set when the last cycle took it from $00.
79
+ def fast_forward(cycles)
80
+ @low_underflowed = false
81
+ return if @counting_pulses
82
+
83
+ @counter = (@counter - cycles) & 0xffff
84
+ @low_underflowed = @counter.allbits?(0xff)
85
+ end
86
+
87
+ # What the counter and the low byte's underflow don't hold, for
88
+ # comparing the timer at two points.
89
+ def state = [@latch_low, @armed, @hold, @low_reload, @count_pulses, @counting_pulses]
90
+
66
91
  private
67
92
 
68
93
  # A cycle while counting pulses, or held after a load or a low byte
data/lib/badline/via.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/via/control_lines"
4
+ require "badline/via/fast_forward"
4
5
  require "badline/via/interrupt_register"
5
6
  require "badline/via/shift_register"
6
7
  require "badline/via/timer1"
@@ -16,6 +17,7 @@ module Badline
16
17
  # feed the counter.
17
18
  class VIA
18
19
  include Addressable
20
+ include FastForward
19
21
 
20
22
  attr_reader :start, :peripheral, :shift_register, :acr, :pcr
21
23
 
@@ -12,10 +12,6 @@ module Badline
12
12
  LAST_LINE = 0xf7 # 247
13
13
  COLUMNS_PER_ROW = 40
14
14
 
15
- # The last column of a rasterline already compares against the line
16
- # about to start, the same wrap the raster IRQ latch uses.
17
- WRAP_COLUMN = 62
18
-
19
15
  # AEC follows BA three cycles later; the c-accesses before it read
20
16
  # the bus the CPU still drives.
21
17
  BA_DELAY = 3
@@ -32,12 +28,16 @@ module Badline
32
28
  LOAD_COLUMN = 12
33
29
  RC_COLUMN = 56
34
30
 
35
- GRAPHICS_COLUMNS = Array.new(63) { |column| column.between?(GRAPHICS_FIRST, GRAPHICS_LAST) }.freeze
31
+ GRAPHICS_COLUMNS = Array.new(65) { |column| column.between?(GRAPHICS_FIRST, GRAPHICS_LAST) }.freeze
36
32
 
37
33
  attr_reader :vc_base, :vc, :vmli, :rc
38
34
 
39
- def initialize(registers)
35
+ # The wrap column is the last column of a rasterline, 62 on the 6569,
36
+ # which already compares against the line about to start, the same
37
+ # wrap the raster IRQ latch uses.
38
+ def initialize(registers, wrap_column = 62)
40
39
  @registers = registers
40
+ @wrap_column = wrap_column
41
41
  @vc_base = 0
42
42
  @vc = 0
43
43
  @vmli = 0
@@ -77,7 +77,7 @@ module Badline
77
77
 
78
78
  # Returns true when this compare opened display state.
79
79
  def cycle(rasterline, column)
80
- match = column == WRAP_COLUMN ? wrap_match(rasterline + 1) : line_match
80
+ match = column == @wrap_column ? wrap_match(rasterline + 1) : line_match
81
81
  @matched = match
82
82
  opened = false
83
83
  if match
@@ -10,12 +10,15 @@ 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
13
+ # The columns of a raster line, and where the region puts the
14
+ # sprites in it.
15
+ attr_reader :columns, :timing
15
16
 
16
- def initialize(bank, columns)
17
+ def initialize(bank, timing)
17
18
  @bank = bank
18
- @columns = columns
19
+ @timing = timing
20
+ @columns = timing.columns
21
+ @sprite_cycle = timing.sprite_cycle
19
22
  @lines = 0
20
23
  @ghost = 0xff
21
24
  @value = Array.new(columns, 0xff)
@@ -37,10 +40,11 @@ module Badline
37
40
  @line[column] = column.zero? ? @lines + 1 : @lines
38
41
  end
39
42
 
40
- # The row sprite `index` (3-7) fetched this line, in Bauer cycles
41
- # 2n-5 and 2n-4.
43
+ # The row sprite `index` (3-7) fetched this line, in the phi2 halves
44
+ # of the two cycles from its p-access: Bauer cycles 2n-5 and 2n-4 on
45
+ # the 6569.
42
46
  def row(index)
43
- column = (2 * index) - 6
47
+ column = (@sprite_cycle + (2 * index) - 1) % @columns
44
48
  (phi2(column) << 16) | (@ghost << 8) | phi2(column + 1)
45
49
  end
46
50
 
@@ -6,18 +6,13 @@ module Badline
6
6
  # The X comparator and the 24-bit shift register behind it: where a
7
7
  # line fires the sprite, and the pixels each firing shifts out.
8
8
  module Shifter
9
- EMPTY = [].freeze
10
-
11
9
  private
12
10
 
13
11
  # The first pixel of every comparator match along the line. Writes to
14
12
  # $d000/$d010 move the compare value mid-line, so a sprite can miss
15
13
  # its match entirely and pick up a later one, a second one, or none.
16
14
  def comparator_hits(log)
17
- unless log
18
- match = compare_x(@registers)
19
- return match ? [(match + 1) % @width] : EMPTY
20
- end
15
+ return compare_x(@registers) unless log
21
16
 
22
17
  hits = []
23
18
  log.rewind
@@ -25,8 +20,10 @@ module Badline
25
20
  from = 0
26
21
  while from < @width
27
22
  upto = [log.next_x, @width].min
28
- match = compare_x(log)
29
- hits << ((match + 1) % @width) if match && match >= from && match < upto
23
+ compare_x(log).each do |start|
24
+ match = (start - 1) % @width
25
+ hits << start if match >= from && match < upto
26
+ end
30
27
  break if upto >= @width
31
28
 
32
29
  from = upto
@@ -35,15 +32,13 @@ module Badline
35
32
  hits
36
33
  end
37
34
 
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.
35
+ # The pixels the sprite starts at, one past each comparator match.
36
+ # The 6569's X counter only runs to 503, so the eight coordinates
37
+ # above it never match and the sprite stays dark — the gap the
38
+ # spritegap tests find at $1f8.
41
39
  def compare_x(view)
42
40
  msb = view[0x10].anybits?(@bit) ? 0x100 : 0
43
- xpos = msb | view[index * 2]
44
- return if xpos >= @width
45
-
46
- (xpos + COMPARE_OFFSET) % @width
41
+ @timing.x_pixels[msb | view[index * 2]]
47
42
  end
48
43
 
49
44
  # Pixels from the X match to the next reload.
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class VIC < Cycleable
5
+ class Sprite
6
+ # = Timing
7
+ #
8
+ # Where a region puts the sprites in the line: the cycles of their
9
+ # fetches and of the display compare, the pixels those land on, and
10
+ # the raster pixels the X counter reads each coordinate at. Where the
11
+ # fetches or the compare run a cycle later than on the 6569, what
12
+ # hangs off them moves 8 pixels with them.
13
+ class Timing
14
+ # The X counter's value where it holds on a raster of more than 512
15
+ # pixels, and the four values it repeats from there.
16
+ HOLD_AT = 0x188
17
+ HOLD_BASE = 0x184
18
+
19
+ attr_reader :columns, :sprite_cycle, :display_off_x, :x_pixels
20
+
21
+ def initialize(region)
22
+ @columns = region.cycles_per_line
23
+ @sprite_cycle = region.sprite_cycle
24
+ @shift = region.sprite_cycle - Region::PAL.sprite_cycle
25
+ @display_off_x = DISPLAY_OFF_X + (8 * (region.sprite_display_cycle - Region::PAL.sprite_display_cycle))
26
+ @x_pixels = Timing.x_pixels(@columns * 8, region.x_hold)
27
+ end
28
+
29
+ # The column BA falls in for a sprite's own fetch: 55 for sprite 0 on
30
+ # the 6569, and two later for each sprite after it.
31
+ def ba_column(index) = 55 + @shift + (2 * index)
32
+
33
+ # The raster pixel of a sprite's reload, counted from the start of
34
+ # the line it is fetched on, so past the end of it for sprites 3-7
35
+ # on the 6569.
36
+ def reload_x(index) = RELOAD_X + (8 * @shift) + (RELOAD_STEP * index)
37
+
38
+ # The X counter at a raster pixel: X_OFFSET behind it, wrapping at
39
+ # the end of the line, less the pixels it held for past HOLD_AT,
40
+ # where it ran over $184-$187 again instead.
41
+ def self.xpos(pixel, width, hold)
42
+ count = (pixel - X_OFFSET) % width
43
+ return count if count < HOLD_AT
44
+ return count - hold if count >= HOLD_AT + hold
45
+
46
+ HOLD_BASE + ((count - HOLD_AT) % 4)
47
+ end
48
+
49
+ # The raster pixels at which the X counter reads each coordinate, 512
50
+ # lists. A sprite starts at one where its X matches. On the 6569 the
51
+ # coordinates past 503 have none.
52
+ def self.x_pixels(width, hold)
53
+ pixels = Array.new(512) { [] }
54
+ width.times { |pixel| pixels[xpos(pixel, width, hold)] << pixel }
55
+ pixels.each(&:freeze).freeze
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "badline/vic/sprite/internal_bus"
4
4
  require "badline/vic/sprite/shifter"
5
+ require "badline/vic/sprite/timing"
5
6
 
6
7
  module Badline
7
8
  class VIC < Cycleable
@@ -22,9 +23,6 @@ module Badline
22
23
  LAST_MCBASE = 63
23
24
  MC_MASK = 0x3f
24
25
 
25
- # The X comparator matches the pixel before the sprite's first one.
26
- COMPARE_OFFSET = X_OFFSET - 1
27
-
28
26
  # 24 bits at two pixels each, plus slack for a mid-sprite expansion
29
27
  # change stretching the tail.
30
28
  MAX_SPAN = 64
@@ -48,7 +46,8 @@ module Badline
48
46
 
49
47
  # On the line of its last row, a sprite whose DMA ended at cycle 16
50
48
  # loses its display at cycle 58, so no hit from this pixel on starts
51
- # it (spritegap3). One already shifting runs on.
49
+ # it (spritegap3). One already shifting runs on. A region whose
50
+ # display compare falls later moves it with the compare.
52
51
  DISPLAY_OFF_X = 460
53
52
 
54
53
  include Shifter
@@ -63,6 +62,7 @@ module Badline
63
62
  @bank = bank
64
63
  @bus = bus
65
64
  @width = bus.columns * 8
65
+ @timing = bus.timing
66
66
  @bit = 1 << index
67
67
  @dma = false
68
68
  @display_on = false
@@ -78,8 +78,9 @@ module Badline
78
78
  @reload_codes = Array.new(MAX_SPAN + RELOAD_HOLD, 0)
79
79
  @reload_leftmost = 0
80
80
  @reload_span = 0
81
- @reload_x = (RELOAD_X + (RELOAD_STEP * index)) % @width
82
- @reload_next_line = RELOAD_X + (RELOAD_STEP * index) < @width
81
+ reload = @timing.reload_x(index)
82
+ @reload_x = reload % @width
83
+ @reload_next_line = reload < @width
83
84
  @sr = 0
84
85
  @latch = 0
85
86
  @mc_flop = false
@@ -157,9 +158,10 @@ module Badline
157
158
  # MCBASE, and returns true. Display is enabled separately in cycle 58,
158
159
  # so the rows render from the following line on.
159
160
  #
160
- # On the VIC's side, BA falls at the sprite's own column — 55 for
161
- # sprite 0, two later for each sprite after it, a column behind the
162
- # window the CPU sees in VIC::SPRITE_BA_WINDOWS — or two columns after
161
+ # On the VIC's side, BA falls at the sprite's own column
162
+ # (Timing#ba_column) — 55 for sprite 0 on the 6569, two later for each
163
+ # sprite after it, a column behind the window the CPU sees
164
+ # (VIC#layout_columns) — or two columns after
163
165
  # this compare, whichever is later. AEC follows three columns on, and
164
166
  # the first of the three s-accesses runs in the column it arrives in:
165
167
  # a DMA starting on the second compare therefore loses that access for
@@ -168,7 +170,7 @@ module Badline
168
170
  def check_dma(line, column)
169
171
  return false if @dma || !enabled? || !y_match?(line)
170
172
 
171
- @first_byte_lost = column + 2 > 55 + (2 * index)
173
+ @first_byte_lost = column + 2 > @timing.ba_column(index)
172
174
  @dma = true
173
175
  @mcbase = 0
174
176
  @exp_ff = true
@@ -229,7 +231,7 @@ module Badline
229
231
  return unless @row_ready || @prev_bits || reload_bits
230
232
 
231
233
  log = nil if log.nil? || log.empty?
232
- @stop_x = @dma ? nil : DISPLAY_OFF_X
234
+ @stop_x = @dma ? nil : @timing.display_off_x
233
235
  comparator_hits(log).each { |start| sequence_hit(log, start, reload_bits) }
234
236
  end
235
237
 
@@ -261,7 +263,7 @@ module Badline
261
263
  def blind_fetch
262
264
  @bits = @bus.row(index)
263
265
  @row_ready = true
264
- @show_from = DISPLAY_OFF_X
266
+ @show_from = @timing.display_off_x
265
267
  end
266
268
 
267
269
  def code_at(raster_x, leftmost, span, codes)
@@ -31,12 +31,12 @@ module Badline
31
31
  # sooner than the 6569's.
32
32
  WRITE_DELAY_8565 = WRITE_DELAY.dup.tap { |delays| delays[0x1c] = SEQUENCER_DELAY - 1 }.freeze
33
33
 
34
- def initialize(registers, bank, width, model: :mos6569)
34
+ def initialize(registers, bank, width, model: :mos6569, region: Region::PAL)
35
35
  @registers = registers
36
36
  @write_delay = model == :mos8565 ? WRITE_DELAY_8565 : WRITE_DELAY
37
37
  @bank = bank
38
38
  @width = width
39
- @bus = Sprite::InternalBus.new(bank, width / 8)
39
+ @bus = Sprite::InternalBus.new(bank, Sprite::Timing.new(region))
40
40
  @sprites = Array.new(8) { |i| Sprite.new(i, registers, bank, @bus, model:) }
41
41
  @collisions = Collisions.new(registers, width)
42
42
  @hits = @collisions.hits
data/lib/badline/vic.rb CHANGED
@@ -46,16 +46,31 @@ module Badline
46
46
  FETCH_HOLD = 0x20
47
47
  FETCH_HOLD_ROM = 0x60
48
48
 
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
49
+ # The hooks a column can carry, as bits: MCBASE, the left border
50
+ # compare with the end of the sprite, the sprite DMA compare, the
51
+ # expansion flip-flop, the sprite display compare, and the start of the
52
+ # vertical border for the line after the last column.
53
+ HOOK_MCBASE = 1
54
+ HOOK_LEFT = 2
55
+ HOOK_DMA = 4
56
+ HOOK_EXPANSION = 8
57
+ HOOK_DISPLAY = 16
58
+ HOOK_LAST = 32
59
+
60
+ # On the 6569 the DMA compares fall in columns 53 and 54 and the
61
+ # display compare in column 57. A region whose sprite fetches run later
62
+ # moves them with the fetches (Region::Profile#sprite_cycle).
63
+ DMA_COLUMN = 53
64
+ EXPANSION_COLUMN = 54
52
65
 
53
66
  # BA falls three columns ahead of each sprite's pair of s-accesses, and
54
67
  # the five-column windows step two columns apart from sprite 0 at column
55
- # 54. From sprite 3 on they reach past the end of the line, so each is
56
- # split: the tail columns fall on the line whose cycle 55/56 compare
57
- # started the fetch, the head columns on the line after it.
58
- SPRITE_BA_WINDOWS = Array.new(8) { |n| (54 + (2 * n))..(58 + (2 * n)) }.freeze
68
+ # 54 on the 6569, a column later where the fetches are. From sprite 3 on
69
+ # they reach past the end of the line, so each is split: the tail
70
+ # columns fall on the line whose DMA compare started the fetch, the head
71
+ # columns on the line after it.
72
+ SPRITE_BA_FIRST = 54
73
+ SPRITE_BA_LENGTH = 5
59
74
 
60
75
  def initialize(address_bus = nil, debug: false, model: :mos6569, region: Region::PAL)
61
76
  raise ArgumentError, "unknown VIC-II model #{model}" unless MODELS.include?(model)
@@ -72,6 +87,8 @@ module Badline
72
87
  @debug = debug
73
88
 
74
89
  @columns_per_line = region.cycles_per_line
90
+ @sprite_cycle = region.sprite_cycle
91
+ @sprite_shift = region.sprite_cycle - Region::PAL.sprite_cycle
75
92
  @width = @columns_per_line * 8
76
93
  @height = region.lines_per_frame
77
94
  @last_column = @columns_per_line - 1
@@ -94,10 +111,10 @@ module Badline
94
111
  def power_on!
95
112
  @registers = VIC::Registers.new
96
113
  @register_bytes = @registers.bytes
97
- @display_state = VIC::DisplayState.new(@registers)
114
+ @display_state = VIC::DisplayState.new(@registers, @last_column)
98
115
  @sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank, model: @model, region: @region)
99
116
  @sequencer.render = @render
100
- @sprites = VIC::Sprites.new(@registers, @vic_bank, @width, model: @model)
117
+ @sprites = VIC::Sprites.new(@registers, @vic_bank, @width, model: @model, region: @region)
101
118
  @display.fill(0)
102
119
  @lines.each { |line| line.fill(0) }
103
120
  @dirty_lines.fill(true)
@@ -132,7 +149,8 @@ module Badline
132
149
 
133
150
  fetch_character_data! if dma_active?
134
151
 
135
- column_hooks if @hook_columns[@column]
152
+ hooks = @hook_columns[@column]
153
+ column_hooks(hooks) if hooks
136
154
 
137
155
  @column += 1
138
156
  if @column == @columns_per_line
@@ -149,19 +167,18 @@ module Badline
149
167
  # ahead of Bauer's numbering — two for MCBASE, the DMA compares and the
150
168
  # expansion flip-flop, which the `spriteenable` and `spritecrunch`
151
169
  # references place a column earlier still. Guarded in
152
- # #cycle! so an ordinary column pays two compares rather than the
170
+ # #cycle! so an ordinary column pays an array read rather than the
153
171
  # dispatch.
154
- def column_hooks
155
- case @column
156
- when 14 then @sprites.advance_mcbase
157
- when 15
172
+ def column_hooks(hooks)
173
+ @sprites.advance_mcbase if hooks.anybits?(HOOK_MCBASE)
174
+ if hooks.anybits?(HOOK_LEFT)
158
175
  @sequencer.left_compare_vertical_border
159
176
  finish_sprite_mcbase
160
- when 53 then check_sprite_dma
161
- when 54 then check_dma_and_toggle_expansion
162
- when 57 then @sprites.check_display(@rasterline)
163
- when @last_column then @sequencer.start_vertical_border(@rasterline == @last_line ? 0 : @rasterline + 1)
164
177
  end
178
+ check_sprite_dma if hooks.anybits?(HOOK_DMA)
179
+ @sprites.toggle_expansion if hooks.anybits?(HOOK_EXPANSION)
180
+ @sprites.check_display(@rasterline) if hooks.anybits?(HOOK_DISPLAY)
181
+ @sequencer.start_vertical_border(@rasterline == @last_line ? 0 : @rasterline + 1) if hooks.anybits?(HOOK_LAST)
165
182
  end
166
183
 
167
184
  # The IRQ line is held asserted while any enabled latch bit is set in
@@ -210,6 +227,16 @@ module Badline
210
227
  @column > DisplayState::DMA_FIRST && @column <= DisplayState::DMA_LAST + 1
211
228
  end
212
229
 
230
+ # BA as an REU's DMA sees it. On the line whose raster matches sprite
231
+ # 0's Y, where the sprite's DMA starts, the REU doesn't see BA fall on
232
+ # the first cycle of the sprite's window. Pinned by
233
+ # REU/bonzai/spritetiming.
234
+ def reu_ba_low?
235
+ return false if @column == SPRITE_BA_FIRST + @sprite_shift && sprite_zero_starting?
236
+
237
+ ba_low?
238
+ end
239
+
213
240
  # Light pen input level (CIA1 PB4). A falling edge triggers the latch.
214
241
  def lightpen_level(high)
215
242
  @lp_low = !high
@@ -233,7 +260,7 @@ module Badline
233
260
  end
234
261
  return if line == @last_line && column.positive?
235
262
 
236
- vic_x = ((column * 8) - Sprite::X_OFFSET) % @width
263
+ vic_x = Sprite::Timing.xpos(column * 8, @width, @region.x_hold)
237
264
  latch_lightpen((vic_x >> 1) + @lightpen_extra, line)
238
265
  end
239
266
 
@@ -302,24 +329,27 @@ module Badline
302
329
  @sprites.log_change(reg, old, value, beam_x)
303
330
  end
304
331
 
305
- # Bauer cycles 1-10 and 58-63 are sprite p- and s-accesses, 11-15
306
- # refresh, 16-55 g-accesses and 56-57 idle. @column has already
307
- # advanced, so it is the Bauer cycle less one.
332
+ # On the 6569 Bauer cycles 1-10 and 58-63 are sprite p- and s-accesses,
333
+ # 11-15 refresh, 16-55 g-accesses and 56-57 idle. Where the sprite
334
+ # fetches start later, the idle cycles before them grow, and on the
335
+ # 6567R8 cycle 10 idles too. @column has already advanced, so it is the
336
+ # Bauer cycle less one.
308
337
  def phi1_address
309
338
  cycle = @column + 1
310
- if cycle < 11 || cycle > 57
311
- sprite_phi1_address((cycle - 58) % 63)
312
- elsif cycle < 16
339
+ slot = (cycle - @sprite_cycle) % @columns_per_line
340
+ if slot < 16
341
+ sprite_phi1_address(slot)
342
+ elsif cycle.between?(11, 15)
313
343
  0x3f00 | ((0xff - (5 * @rasterline) - (cycle - 11)) & 0xff)
314
- elsif cycle < 56
344
+ elsif cycle.between?(16, 55)
315
345
  graphics_phi1_address
316
346
  else
317
347
  0x3fff
318
348
  end
319
349
  end
320
350
 
321
- # Each sprite takes two cycles from Bauer 58, the p-access and then the
322
- # middle s-access.
351
+ # Each sprite takes two cycles from sprite 0's, the p-access and then
352
+ # the middle s-access.
323
353
  def sprite_phi1_address(slot)
324
354
  n = slot >> 1
325
355
  return @registers.screen_base + 0x3f8 + n if slot.even?
@@ -357,11 +387,6 @@ module Badline
357
387
  rebuild_sprite_ba if @sprites.stopped_dma?
358
388
  end
359
389
 
360
- def check_dma_and_toggle_expansion
361
- check_sprite_dma
362
- @sprites.toggle_expansion
363
- end
364
-
365
390
  # The trigger re-arms at the start of each frame; if the pen line is
366
391
  # still low, the latch retriggers immediately with a fixed LPX of $d1.
367
392
  def start_lightpen_frame
@@ -378,6 +403,10 @@ module Badline
378
403
  @registers.latch_irq!(LIGHTPEN_IRQ)
379
404
  end
380
405
 
406
+ def sprite_zero_starting?
407
+ @registers[0x15].anybits?(0x01) && @registers[0x01] == (@rasterline & 0xff)
408
+ end
409
+
381
410
  def rebuild_sprite_ba
382
411
  @sprite_ba.fill(false)
383
412
  8.times do |n|
@@ -389,13 +418,31 @@ module Badline
389
418
  end
390
419
 
391
420
  # 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.
421
+ # columns, for a line of the region's length: 14, 15, 53, 54 and 57, and
422
+ # the line's last column, which starts the vertical border for the line
423
+ # after it. The marks let an ordinary column cost one array read instead
424
+ # of the dispatch.
394
425
  def layout_columns
395
426
  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
427
+ windows = Array.new(8) do |n|
428
+ first = SPRITE_BA_FIRST + @sprite_shift + (2 * n)
429
+ first...(first + SPRITE_BA_LENGTH)
430
+ end
431
+ @sprite_ba_tail = windows.map { |w| w.select { |c| c <= last } }.freeze
432
+ @sprite_ba_head = windows.map { |w| w.filter_map { |c| c - last - 1 if c > last } }.freeze
433
+ @hook_columns = layout_hooks.map { |hooks| hooks unless hooks.zero? }.freeze
434
+ end
435
+
436
+ def layout_hooks
437
+ hooks = Array.new(@columns_per_line, 0)
438
+ hooks[14] |= HOOK_MCBASE
439
+ hooks[15] |= HOOK_LEFT
440
+ hooks[DMA_COLUMN + @sprite_shift] |= HOOK_DMA
441
+ hooks[DMA_COLUMN + @sprite_shift + 1] |= HOOK_DMA
442
+ hooks[EXPANSION_COLUMN] |= HOOK_EXPANSION
443
+ hooks[@region.sprite_display_cycle - 1] |= HOOK_DISPLAY
444
+ hooks[@last_column] |= HOOK_LAST
445
+ hooks
399
446
  end
400
447
 
401
448
  # Runs this column's g-access and draws the one from GRAPHICS_DELAY
data/lib/badline.rb CHANGED
@@ -24,11 +24,14 @@ require "badline/cia"
24
24
  require "badline/via"
25
25
  require "badline/sid"
26
26
  require "badline/debug_register"
27
+ require "badline/reu"
27
28
  require "badline/interrupts"
28
29
  require "badline/traps"
29
30
  require "badline/cpu"
30
31
  require "badline/vic"
31
32
  require "badline/keyboard_buffer"
33
+ require "badline/iec_bus"
34
+ require "badline/drive1541"
32
35
  require "badline/computer"
33
36
  require "badline/checkpoint"
34
37
  require "badline/storage"