badline 0.1.0 → 0.2.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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +167 -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 +88 -23
  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 +131 -15
  51. data/lib/badline/cia/interrupt_register.rb +88 -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 +109 -74
  55. data/lib/badline/color_memory.rb +14 -5
  56. data/lib/badline/computer.rb +68 -8
  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 +171 -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 +108 -167
  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 +33 -0
  85. data/lib/badline/kernal_trap/drive.rb +239 -0
  86. data/lib/badline/kernal_trap/file.rb +4 -24
  87. data/lib/badline/kernal_trap/load.rb +2 -2
  88. data/lib/badline/kernal_trap/routine.rb +34 -0
  89. data/lib/badline/kernal_trap/save.rb +1 -1
  90. data/lib/badline/kernal_trap/serial.rb +139 -0
  91. data/lib/badline/kernal_trap.rb +4 -0
  92. data/lib/badline/keyboard.rb +40 -16
  93. data/lib/badline/media.rb +48 -10
  94. data/lib/badline/roms/eapi/LICENSE.md +15 -0
  95. data/lib/badline/roms/eapi/README +41 -0
  96. data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
  97. data/lib/badline/sid/decimator.rb +46 -0
  98. data/lib/badline/sid/envelope.rb +232 -0
  99. data/lib/badline/sid/filter.rb +213 -0
  100. data/lib/badline/sid/voice.rb +61 -0
  101. data/lib/badline/sid/waveform/combined.rb +126 -0
  102. data/lib/badline/sid/waveform/fast_forward.rb +105 -0
  103. data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
  104. data/lib/badline/sid/waveform.rb +289 -0
  105. data/lib/badline/sid.rb +274 -7
  106. data/lib/badline/status.rb +32 -27
  107. data/lib/badline/storage/crt_file.rb +18 -2
  108. data/lib/badline/storage/disk_image.rb +52 -5
  109. data/lib/badline/storage/host_directory.rb +18 -4
  110. data/lib/badline/storage/sid_file.rb +274 -0
  111. data/lib/badline/storage/song_lengths.rb +65 -0
  112. data/lib/badline/storage/t64.rb +67 -0
  113. data/lib/badline/storage/tap.rb +68 -0
  114. data/lib/badline/storage.rb +19 -0
  115. data/lib/badline/time_of_day.rb +105 -52
  116. data/lib/badline/version.rb +1 -1
  117. data/lib/badline/vic/bank.rb +14 -6
  118. data/lib/badline/vic/border_mask.rb +76 -0
  119. data/lib/badline/vic/collisions.rb +104 -0
  120. data/lib/badline/vic/color_patches.rb +33 -0
  121. data/lib/badline/vic/display_state.rb +92 -29
  122. data/lib/badline/vic/graphics_mode.rb +78 -53
  123. data/lib/badline/vic/graphics_shifter.rb +140 -0
  124. data/lib/badline/vic/register_log.rb +63 -0
  125. data/lib/badline/vic/registers.rb +3 -0
  126. data/lib/badline/vic/sequencer.rb +163 -137
  127. data/lib/badline/vic/sequencer_output.rb +116 -0
  128. data/lib/badline/vic/sprite/internal_bus.rb +49 -0
  129. data/lib/badline/vic/sprite/shifter.rb +129 -0
  130. data/lib/badline/vic/sprite.rb +275 -65
  131. data/lib/badline/vic/sprites.rb +220 -58
  132. data/lib/badline/vic.rb +373 -49
  133. data/lib/badline.rb +5 -0
  134. data/vendor/.gitkeep +0 -0
  135. metadata +103 -4
@@ -2,100 +2,153 @@
2
2
 
3
3
  module Badline
4
4
  class TimeOfDay
5
- include IntegerHelper
6
-
7
5
  CLOCK_HZ = 985_248 # PAL only for now.
6
+ MAINS_HZ = 50 # The TOD pin is fed from the AC supply.
8
7
 
9
- def initialize(clock_hz: CLOCK_HZ)
10
- # The accumulator advances 10 per cycle, so a tenth of a second has
11
- # passed when it reaches clock_hz. Integer math keeps it exact.
12
- @cycles_per_tenth = clock_hz
8
+ def initialize(clock_hz: CLOCK_HZ, mains_hz: MAINS_HZ)
9
+ # The accumulator advances mains_hz per cycle, so a TOD pin pulse has
10
+ # arrived when it reaches clock_hz. Integer math keeps it exact.
11
+ @cycles_per_pulse = clock_hz
12
+ @mains_hz = mains_hz
13
13
  @accumulator = 0
14
- @clock = { tenths: 0, seconds: 0, minutes: 0, hours: 12, pm: false }
15
- @alarm = { tenths: 0, seconds: 0, minutes: 0, hours: 0, pm: false }
14
+ @pulses = 0
15
+ @clock = { tenths: 0x00, seconds: 0x00, minutes: 0x00, hours: 0x01 }
16
+ @alarm = { tenths: 0x00, seconds: 0x00, minutes: 0x00, hours: 0x00 }
16
17
  @latch = nil
17
- @stopped = false
18
+ @stopped = true
19
+ @alarm_pending = false
20
+ self.fifty_hz = false
18
21
  end
19
22
 
20
- def cycle!
21
- return if @stopped
23
+ # CRA bit 7 divides the TOD pin by 5 instead of 6. The divider has to
24
+ # match the pin frequency to keep time, so PAL software selects 50 Hz.
25
+ def fifty_hz=(enabled)
26
+ @match = enabled ? 4 : 5
27
+ end
22
28
 
23
- @accumulator += 10
24
- return if @accumulator < @cycles_per_tenth
29
+ def cycle!
30
+ @accumulator += @mains_hz
31
+ pulse! if @accumulator >= @cycles_per_pulse
32
+ return unless @alarm_pending
25
33
 
26
- @accumulator -= @cycles_per_tenth
27
- advance
28
- yield if block_given? && alarm?
34
+ @alarm_pending = false
35
+ yield if block_given?
29
36
  end
30
37
 
31
38
  def tenths
32
39
  value = (@latch || @clock)[:tenths]
33
40
  @latch = nil
34
- bcd(value)
35
- end
36
-
37
- def seconds
38
- bcd((@latch || @clock)[:seconds])
41
+ value
39
42
  end
40
43
 
41
- def minutes
42
- bcd((@latch || @clock)[:minutes])
43
- end
44
+ def seconds = (@latch || @clock)[:seconds]
45
+ def minutes = (@latch || @clock)[:minutes]
44
46
 
45
47
  def hours
46
48
  @latch ||= @clock.dup
47
- bcd(@latch[:hours]) | (@latch[:pm] ? 0x80 : 0)
49
+ @latch[:hours]
48
50
  end
49
51
 
50
52
  def write(field, value, alarm:)
51
- (alarm ? @alarm : @clock)[field] = bcd_to_i(value)
52
- # Start the clock again when writing tenths.
53
- resume if field == :tenths && !alarm
53
+ # The frequency counter is held clear while the clock is stopped, and
54
+ # starts counting again from the write to tenths that restarts it.
55
+ if field == :tenths && !alarm && @stopped
56
+ @pulses = 0
57
+ @stopped = false
58
+ end
59
+ store(field, value & (field == :tenths ? 0x0f : 0x7f), alarm: alarm)
54
60
  end
55
61
 
56
62
  # 12-hour BCD value, bit 7 is the AM/PM flag.
57
63
  def write_hours(value, alarm:)
58
- target = alarm ? @alarm : @clock
59
- target[:hours] = bcd_to_i(value & 0x7f)
60
- target[:pm] = value.anybits?(0x80)
64
+ value &= 0x9f
65
+ # Writing 12 to the hour register flips the AM/PM bit, the same way a
66
+ # carry into 12 does. Writing the alarm hours does not.
67
+ value ^= 0x80 if !alarm && (value & 0x1f) == 0x12
61
68
  # Halt the clock when writing hours, so that it doesn't
62
69
  # advance mid-update.
63
70
  @stopped = true unless alarm
71
+ store(:hours, value, alarm: alarm)
64
72
  end
65
73
 
66
74
  private
67
75
 
68
- def resume
69
- @stopped = false
70
- @accumulator = 0
76
+ def store(field, value, alarm:)
77
+ target = alarm ? @alarm : @clock
78
+ return if target[field] == value
79
+
80
+ target[field] = value
81
+ @alarm_pending = true if alarm?
71
82
  end
72
83
 
73
- def advance
74
- @clock[:tenths] += 1
75
- return if @clock[:tenths] < 10
84
+ def pulse!
85
+ @accumulator -= @cycles_per_pulse
86
+ return if @stopped
87
+
88
+ # A three-bit ring counter with six states, compared against the
89
+ # divider on each pin pulse rather than on its way past it.
90
+ if @pulses == @match
91
+ @pulses = 0
92
+ advance
93
+ else
94
+ @pulses += 1
95
+ @pulses = 0 if @pulses > 5
96
+ end
97
+ end
76
98
 
77
- @clock[:tenths] = 0
78
- @clock[:seconds] += 1
79
- return if @clock[:seconds] < 60
99
+ def advance
100
+ tenths = (@clock[:tenths] + 1) & 0x0f
101
+ @clock[:tenths] = tenths == 10 ? 0 : tenths
102
+ carry_seconds if tenths == 10
103
+ @alarm_pending = true if alarm?
104
+ end
80
105
 
81
- @clock[:seconds] = 0
82
- @clock[:minutes] += 1
83
- return if @clock[:minutes] < 60
106
+ def carry_seconds
107
+ return unless advance_pair(:seconds)
108
+ return unless advance_pair(:minutes)
84
109
 
85
- @clock[:minutes] = 0
86
- advance_hour
110
+ advance_hours
87
111
  end
88
112
 
89
- def advance_hour
90
- @clock[:hours] += 1
91
- @clock[:pm] = !@clock[:pm] if @clock[:hours] == 12
92
- @clock[:hours] = 1 if @clock[:hours] > 12
113
+ # Seconds and minutes hold a four-bit low digit that carries at ten into
114
+ # a three-bit high digit that carries out at six. Both are plain binary
115
+ # counters, so out-of-range BCD counts up from where it was written.
116
+ def advance_pair(field)
117
+ low = ((@clock[field] & 0x0f) + 1) & 0x0f
118
+ high = (@clock[field] >> 4) & 0x07
119
+ carry = false
120
+
121
+ if low == 10
122
+ low = 0
123
+ high = (high + 1) & 0x07
124
+ if high == 6
125
+ high = 0
126
+ carry = true
127
+ end
128
+ end
129
+
130
+ @clock[field] = (high << 4) | low
131
+ carry
93
132
  end
94
133
 
95
- def alarm?
96
- %i[tenths seconds minutes hours pm].all? do |field|
97
- @clock[field] == @alarm[field]
134
+ # The hour runs 9 -> 10 and 12 -> 1 by swapping its two digits, and the
135
+ # AM/PM bit flips as it passes through 12.
136
+ def advance_hours
137
+ pm = @clock[:hours] & 0x80
138
+ high = (@clock[:hours] >> 4) & 0x01
139
+ low = @clock[:hours] & 0x0f
140
+
141
+ if (high == 1 && low == 2) || (high.zero? && low == 9)
142
+ low = high
143
+ high ^= 1
144
+ else
145
+ low = (low + 1) & 0x0f
146
+ pm ^= 0x80 if high == 1 && low == 2
98
147
  end
148
+
149
+ @clock[:hours] = pm | (high << 4) | low
99
150
  end
151
+
152
+ def alarm? = @clock == @alarm
100
153
  end
101
154
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -18,7 +18,7 @@ module Badline
18
18
  end
19
19
 
20
20
  def peek(offset)
21
- return ultimax_peek(offset) if @address_bus.ultimax
21
+ return ultimax_peek(offset) if @address_bus.phi1_ultimax
22
22
 
23
23
  bits = bank_switch_register
24
24
  if bits.allbits?(0b01) && (offset & 0xf000) == 0x1000
@@ -28,14 +28,26 @@ module Badline
28
28
  end
29
29
  end
30
30
 
31
+ # A fetch in the second half of the cycle, the c-access, sees the
32
+ # memory configuration the CPU does in that half. No cartridge drives
33
+ # Ultimax mode in the first half alone, so otherwise it reads as #peek.
34
+ def peek_phi2(offset)
35
+ @address_bus.ultimax ? ultimax_peek(offset) : peek(offset)
36
+ end
37
+
31
38
  def peek_color(offset)
32
- address_bus.color_ram.peek(0xd800 + offset) & 0x0f
39
+ address_bus.color_ram.nibble(0xd800 + offset)
33
40
  end
34
41
 
35
42
  def poke(_addr, _value)
36
43
  raise ReadOnlyMemoryError
37
44
  end
38
45
 
46
+ # True if the VIC reads the character ROM at this offset.
47
+ def character_rom?(offset)
48
+ !@address_bus.phi1_ultimax && bank_switch_register.allbits?(0b01) && (offset & 0xf000) == 0x1000
49
+ end
50
+
39
51
  def start
40
52
  BANK_STARTS[bank_switch_register]
41
53
  end
@@ -56,10 +68,6 @@ module Badline
56
68
  def bank_switch_register
57
69
  @address_bus.cia2.port_a_lines & 0b11
58
70
  end
59
-
60
- def character_rom?
61
- bank_switch_register.allbits?(0b01)
62
- end
63
71
  end
64
72
  end
65
73
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class VIC < Cycleable
5
+ # Border coverage per 8-pixel group, tracked as the sequencer outputs
6
+ # a line.
7
+ #
8
+ # The border is repainted over composited sprites from a snapshot of
9
+ # the finished line, which keeps mid-line border color splits intact.
10
+ class BorderMask
11
+ FULL = 0
12
+ NONE = 1
13
+ MIXED = 2
14
+
15
+ # The group table and per-pixel mask are exposed so the sequencer's
16
+ # pixel loops can write them without a method call per column.
17
+ attr_reader :groups, :mask
18
+
19
+ def initialize(width)
20
+ @width = width
21
+ @groups = Array.new(width / 8, FULL)
22
+ @mask = Array.new(width, true)
23
+ @snapshot = Array.new(width, 0)
24
+ end
25
+
26
+ def reset
27
+ @groups.fill(FULL)
28
+ end
29
+
30
+ def at?(pixel_x)
31
+ case @groups[pixel_x >> 3]
32
+ when FULL then true
33
+ when MIXED then @mask[pixel_x]
34
+ else false
35
+ end
36
+ end
37
+
38
+ def snapshot(colors)
39
+ @snapshot[0, @width] = colors
40
+ end
41
+
42
+ def restore(colors)
43
+ group = 0
44
+ while group < @groups.length
45
+ case @groups[group]
46
+ when FULL then group = restore_full(colors, group)
47
+ when MIXED then restore_mixed(colors, group * 8)
48
+ end
49
+ group += 1
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # Copies a run of full-border groups in one splice and returns its
56
+ # last group.
57
+ def restore_full(colors, first)
58
+ last = first
59
+ last += 1 while @groups[last + 1] == FULL
60
+ x_pos = first * 8
61
+ length = (last - first + 1) * 8
62
+ colors[x_pos, length] = @snapshot[x_pos, length]
63
+ last
64
+ end
65
+
66
+ def restore_mixed(colors, x_pos)
67
+ i = 0
68
+ while i < 8
69
+ x = x_pos + i
70
+ colors[x] = @snapshot[x] if @mask[x]
71
+ i += 1
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class VIC < Cycleable
5
+ # = Sprite collisions
6
+ #
7
+ # The $D01E/$D01F latches and the per-pixel sprite coverage that feeds
8
+ # them. Collisions latch as the beam passes each covered pixel, so a
9
+ # read mid-line sees the pixels drawn before the reading cycle and none
10
+ # of the ones after it.
11
+ class Collisions
12
+ # $D019 latch bits raised by the collision registers.
13
+ SPRITE_COLLISION_IRQ = 0x04 # IMMC, mirrors $D01E
14
+ DATA_COLLISION_IRQ = 0x02 # IMBC, mirrors $D01F
15
+
16
+ # A read holds the register's reset asserted past the cycle that
17
+ # issued it, so the pixels drawn under it never reach the register.
18
+ # The read still reports only the pixels drawn before its own cycle,
19
+ # which is why the two numbers do not cancel out.
20
+ CLEAR_OVERRUN = 12
21
+
22
+ # The pixel the last fold reached, so the sprites know whether a read
23
+ # has anything left to latch.
24
+ attr_reader :folded
25
+
26
+ # One bitmask per pixel of the line, written straight by the sprite
27
+ # merge and read back here and by the composite.
28
+ attr_reader :hits
29
+
30
+ def initialize(registers, width)
31
+ @registers = registers
32
+ @width = width
33
+ @hits = Array.new(width, 0)
34
+ @folded = 0
35
+ @cleared_sprite = 0
36
+ @cleared_data = 0
37
+ end
38
+
39
+ def start_line
40
+ @folded = 0
41
+ @cleared_sprite = carry(@cleared_sprite)
42
+ @cleared_data = carry(@cleared_data)
43
+ end
44
+
45
+ # Latch every covered pixel between the last fold and the beam. The
46
+ # sprites' touched span bounds the scan; the coverage is zero outside
47
+ # it either way.
48
+ def fold(mask, upto, low, high)
49
+ from = [@folded, low].max
50
+ @folded = upto
51
+ scan(mask, from, [upto, high].min)
52
+ end
53
+
54
+ # A read resets its register for the pixels that follow the reading
55
+ # cycle as well as for everything before it.
56
+ def clear(reg, beam_x)
57
+ if reg == 0x1e
58
+ @cleared_sprite = beam_x + CLEAR_OVERRUN
59
+ else
60
+ @cleared_data = beam_x + CLEAR_OVERRUN
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ # Two or more sprites on a pixel collide with each other; any sprite
67
+ # over a foreground pixel collides with the graphics. Pixels still
68
+ # under a read's reset are dropped rather than latched.
69
+ def scan(mask, from, upto)
70
+ sprite_clash = 0
71
+ data_clash = 0
72
+ pos = from
73
+ while pos < upto
74
+ bits = @hits[pos]
75
+ unless bits.zero?
76
+ sprite_clash |= bits if pos >= @cleared_sprite && bits.anybits?(bits - 1)
77
+ data_clash |= bits if pos >= @cleared_data && mask[pos]
78
+ end
79
+ pos += 1
80
+ end
81
+ latch(sprite_clash, data_clash)
82
+ end
83
+
84
+ def latch(sprite_clash, data_clash)
85
+ latch_one(0x1e, sprite_clash, SPRITE_COLLISION_IRQ)
86
+ latch_one(0x1f, data_clash, DATA_COLLISION_IRQ)
87
+ end
88
+
89
+ # The IRQ is raised on the edge out of an empty register, so a
90
+ # collision folded on top of an unread one raises nothing.
91
+ def latch_one(reg, clash, irq)
92
+ return if clash.zero?
93
+ return unless @registers.collide!(reg, clash)
94
+
95
+ @registers.latch_irq!(irq)
96
+ end
97
+
98
+ # A reset still asserted at the line wrap carries into the new line.
99
+ def carry(cleared)
100
+ cleared > @width ? cleared - @width : 0
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class VIC < Cycleable
5
+ # A mid-line color register write becomes visible one pixel into the
6
+ # column emitted the cycle after the write. That emit paints the whole
7
+ # column with the new color, so the boundary pixel is patched back to
8
+ # the old one before the line is finished.
9
+ class ColorPatches
10
+ def initialize(sequencer)
11
+ @sequencer = sequencer
12
+ @patches = []
13
+ end
14
+
15
+ def clear = @patches.clear
16
+
17
+ def log(reg, old, value, boundary_x)
18
+ @patches << [boundary_x, reg, old & 0x0f, value & 0x0f]
19
+ end
20
+
21
+ def apply(colors, fg_mask)
22
+ @patches.each do |(x, reg, old, value)|
23
+ border = @sequencer.border_at?(x)
24
+ if reg == 0x20
25
+ colors[x] = old if border
26
+ elsif !border && !fg_mask[x] && colors[x] == value
27
+ colors[x] = old
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -2,76 +2,139 @@
2
2
 
3
3
  module Badline
4
4
  class VIC < Cycleable
5
+ # = VIC-II display state
6
+ #
7
+ # The bad line condition, the video counters it drives, and the DMA it
8
+ # starts. A condition that arrives mid-line opens the row late, which is
9
+ # what makes the screen scroll sideways or lose a column of cells.
5
10
  class DisplayState
6
11
  FIRST_LINE = 0x30 # 48: bad lines and the DEN-at-$30 latch begin here
7
12
  LAST_LINE = 0xf7 # 247
8
13
  COLUMNS_PER_ROW = 40
9
14
 
10
- attr_reader :vc_base, :rc
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
+ # AEC follows BA three cycles later; the c-accesses before it read
20
+ # the bus the CPU still drives.
21
+ BA_DELAY = 3
22
+
23
+ # Columns run two ahead of Bauer's cycle numbers: column 10 is his
24
+ # cycle 12. BA falls on a match in columns 10-52, the c-accesses run
25
+ # in 13-52 and the g-accesses in 14-53. VC and VMLI reload in column
26
+ # 12 and the row counter steps in column 56.
27
+ DMA_FIRST = 10
28
+ DMA_LAST = 52
29
+ FETCH_FIRST = 13
30
+ GRAPHICS_FIRST = 14
31
+ GRAPHICS_LAST = 53
32
+ LOAD_COLUMN = 12
33
+ RC_COLUMN = 56
34
+
35
+ GRAPHICS_COLUMNS = Array.new(63) { |column| column.between?(GRAPHICS_FIRST, GRAPHICS_LAST) }.freeze
36
+
37
+ attr_reader :vc_base, :vc, :vmli, :rc
11
38
 
12
39
  def initialize(registers)
13
40
  @registers = registers
14
41
  @vc_base = 0
42
+ @vc = 0
43
+ @vmli = 0
15
44
  @rc = 0
16
45
  @display = false
17
46
  @bad_lines_enabled = false
18
- @bad_line = false
47
+ @matched = false
48
+ @ba = nil
49
+ new_line(0)
19
50
  end
20
51
 
21
52
  def display? = @display
22
53
  def idle? = !@display
23
- def bad_line? = @bad_line
54
+ def bad_line? = !@ba.nil?
55
+
56
+ # The bad line condition as the last column compared it.
57
+ def bad_line_condition? = @matched
58
+
59
+ # True once AEC has followed BA down and the VIC owns the bus.
60
+ def bus_taken?(column) = @ba ? column >= @ba + BA_DELAY : false
61
+
62
+ def fetching?(column)
63
+ @matched && column >= FETCH_FIRST && column <= DMA_LAST
64
+ end
24
65
 
25
66
  def new_frame
26
67
  @vc_base = 0
27
68
  @bad_lines_enabled = false
28
69
  end
29
70
 
30
- def new_line
31
- @bad_line = false
71
+ def new_line(line)
72
+ @ba = nil
73
+ @line_bits = line & 0b111
74
+ @den_line = line == FIRST_LINE
75
+ @in_window = line.between?(FIRST_LINE, LAST_LINE)
32
76
  end
33
77
 
34
78
  def cycle(rasterline, column)
35
- @bad_lines_enabled = true if rasterline == FIRST_LINE && @registers.display_enabled?
79
+ match = column == WRAP_COLUMN ? wrap_match(rasterline + 1) : line_match
80
+ @matched = match
81
+ if match
82
+ @display = true
83
+ @ba = column if @ba.nil? && column >= DMA_FIRST && column <= DMA_LAST
84
+ end
85
+ load_counters(match) if column == LOAD_COLUMN
86
+ check_row_counter(match) if column == RC_COLUMN
87
+ end
88
+
89
+ # A g-access in display state consumes one buffer cell and one video
90
+ # matrix address. A row that opens mid-line makes fewer of them, and
91
+ # the shortfall carries into VCBASE.
92
+ def graphics_column?(column) = GRAPHICS_COLUMNS[column]
93
+
94
+ def graphics_access(column)
95
+ return unless graphics_column?(column)
36
96
 
37
- load_vc(rasterline) if column == 14
38
- trigger_bad_line(rasterline) if column.between?(12, 54)
39
- check_row_counter if column == 58
97
+ @vc = (@vc + 1) & 0x3ff
98
+ @vmli += 1
40
99
  end
41
100
 
42
101
  private
43
102
 
44
- def load_vc(rasterline)
45
- return unless bad_line_condition?(rasterline)
46
-
47
- @bad_line = true
48
- @display = true
49
- @rc = 0
103
+ def line_match
104
+ latch_den if @den_line
105
+ @bad_lines_enabled && @in_window && @line_bits == @registers.yscroll
50
106
  end
51
107
 
52
- def trigger_bad_line(rasterline)
53
- return if @bad_line
54
- return unless bad_line_condition?(rasterline)
108
+ # The wrap column compares against the line about to start. The DEN
109
+ # latch is level-sensitive across the counter's increment, so this one
110
+ # column sees both the line ending and the line starting.
111
+ def wrap_match(line)
112
+ latch_den if @den_line || line == FIRST_LINE
113
+ return false unless line.between?(FIRST_LINE, LAST_LINE)
55
114
 
56
- @bad_line = true
57
- @display = true
115
+ @bad_lines_enabled && (line & 0b111) == @registers.yscroll
58
116
  end
59
117
 
60
- def bad_line_condition?(rasterline)
61
- @bad_lines_enabled &&
62
- rasterline.between?(FIRST_LINE, LAST_LINE) &&
63
- (rasterline & 0b111) == @registers.yscroll
118
+ def latch_den
119
+ @bad_lines_enabled = true if @registers.display_enabled?
64
120
  end
65
121
 
66
- def check_row_counter
67
- return unless @display
122
+ def load_counters(match)
123
+ @vc = @vc_base
124
+ @vmli = 0
125
+ @rc = 0 if match
126
+ end
68
127
 
128
+ # A condition still standing when the row counter wraps puts the logic
129
+ # straight back into display state, so a forced row rolls into the
130
+ # next one instead of leaving the line idle (Bauer 3.7.2 step 5).
131
+ def check_row_counter(match)
69
132
  if @rc == 7
70
133
  @display = false
71
- @vc_base = (@vc_base + COLUMNS_PER_ROW) & 0x3ff
72
- else
73
- @rc = (@rc + 1) & 0b111
134
+ @vc_base = @vc
74
135
  end
136
+ @display ||= match
137
+ @rc = (@rc + 1) & 0b111 if @display
75
138
  end
76
139
  end
77
140
  end