badline 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +185 -0
  3. data/CLAUDE.md +217 -0
  4. data/CODE_OF_CONDUCT.md +92 -0
  5. data/CONTRIBUTING.md +51 -0
  6. data/README.md +224 -31
  7. data/doc/pinned-behaviour.md +1117 -0
  8. data/exe/badline +22 -2
  9. data/exe/badline-sid +34 -0
  10. data/lib/badline/address_bus.rb +96 -25
  11. data/lib/badline/audio/aiff.rb +36 -0
  12. data/lib/badline/audio/bare_player.rb +129 -0
  13. data/lib/badline/audio/cli.rb +133 -0
  14. data/lib/badline/audio/console.rb +75 -0
  15. data/lib/badline/audio/jukebox.rb +82 -0
  16. data/lib/badline/audio/machine_player.rb +57 -0
  17. data/lib/badline/audio/options.rb +137 -0
  18. data/lib/badline/audio/pcm_writer.rb +57 -0
  19. data/lib/badline/audio/playback.rb +108 -0
  20. data/lib/badline/audio/renderer.rb +85 -0
  21. data/lib/badline/audio/sdl_sink.rb +102 -0
  22. data/lib/badline/audio/wav.rb +23 -0
  23. data/lib/badline/audio.rb +14 -0
  24. data/lib/badline/cartridge/action_replay.rb +111 -0
  25. data/lib/badline/cartridge/atomic_power.rb +24 -0
  26. data/lib/badline/cartridge/bank.rb +81 -0
  27. data/lib/badline/cartridge/comal80.rb +48 -0
  28. data/lib/badline/cartridge/dinamic.rb +34 -0
  29. data/lib/badline/cartridge/easy_flash.rb +122 -0
  30. data/lib/badline/cartridge/epyx_fastload.rb +71 -0
  31. data/lib/badline/cartridge/final_cartridge3.rb +77 -0
  32. data/lib/badline/cartridge/flash.rb +265 -0
  33. data/lib/badline/cartridge/freezer.rb +31 -0
  34. data/lib/badline/cartridge/fun_play.rb +38 -0
  35. data/lib/badline/cartridge/g_mod2.rb +62 -0
  36. data/lib/badline/cartridge/game_system.rb +40 -0
  37. data/lib/badline/cartridge/kcs_power.rb +72 -0
  38. data/lib/badline/cartridge/mach5.rb +34 -0
  39. data/lib/badline/cartridge/magic_desk.rb +6 -3
  40. data/lib/badline/cartridge/ocean.rb +11 -12
  41. data/lib/badline/cartridge/pagefox.rb +56 -0
  42. data/lib/badline/cartridge/retro_replay.rb +276 -0
  43. data/lib/badline/cartridge/rex_utility.rb +33 -0
  44. data/lib/badline/cartridge/rgcd.rb +48 -0
  45. data/lib/badline/cartridge/simons_basic.rb +40 -0
  46. data/lib/badline/cartridge/standard.rb +3 -19
  47. data/lib/badline/cartridge/super_games.rb +36 -0
  48. data/lib/badline/cartridge/westermann.rb +34 -0
  49. data/lib/badline/cartridge/zaxxon.rb +50 -0
  50. data/lib/badline/cartridge.rb +132 -16
  51. data/lib/badline/cia/interrupt_register.rb +102 -0
  52. data/lib/badline/cia/serial.rb +219 -0
  53. data/lib/badline/cia/timer.rb +43 -31
  54. data/lib/badline/cia.rb +138 -88
  55. data/lib/badline/color_memory.rb +14 -5
  56. data/lib/badline/computer.rb +97 -12
  57. data/lib/badline/control_ports.rb +54 -9
  58. data/lib/badline/cpu/addressing.rb +131 -0
  59. data/lib/badline/cpu/microcode.rb +169 -0
  60. data/lib/badline/cpu/operations.rb +88 -0
  61. data/lib/badline/cpu/stack_operations.rb +82 -0
  62. data/lib/badline/cpu.rb +109 -168
  63. data/lib/badline/cycleable.rb +3 -0
  64. data/lib/badline/datasette.rb +81 -0
  65. data/lib/badline/debug_register.rb +28 -0
  66. data/lib/badline/gui/application.rb +79 -18
  67. data/lib/badline/gui/gamepads.rb +97 -0
  68. data/lib/badline/gui/joy_map.rb +15 -7
  69. data/lib/badline/gui.rb +1 -0
  70. data/lib/badline/input/mouse1351.rb +41 -0
  71. data/lib/badline/input/paddles.rb +36 -0
  72. data/lib/badline/input.rb +4 -0
  73. data/lib/badline/instruction_set/arithmetic.rb +16 -21
  74. data/lib/badline/instruction_set/bitwise.rb +17 -22
  75. data/lib/badline/instruction_set/branch.rb +20 -19
  76. data/lib/badline/instruction_set/flag.rb +7 -7
  77. data/lib/badline/instruction_set/illegal.rb +14 -22
  78. data/lib/badline/instruction_set/inc_dec.rb +8 -10
  79. data/lib/badline/instruction_set/stack.rb +5 -57
  80. data/lib/badline/instruction_set/transfer.rb +9 -9
  81. data/lib/badline/instruction_set.rb +9 -31
  82. data/lib/badline/interrupts.rb +104 -0
  83. data/lib/badline/joystick.rb +9 -9
  84. data/lib/badline/kernal_trap/channel.rb +51 -0
  85. data/lib/badline/kernal_trap/drive/status.rb +49 -0
  86. data/lib/badline/kernal_trap/drive.rb +277 -0
  87. data/lib/badline/kernal_trap/file.rb +5 -25
  88. data/lib/badline/kernal_trap/load.rb +86 -24
  89. data/lib/badline/kernal_trap/routine.rb +51 -0
  90. data/lib/badline/kernal_trap/save.rb +46 -14
  91. data/lib/badline/kernal_trap/serial.rb +171 -0
  92. data/lib/badline/kernal_trap.rb +4 -0
  93. data/lib/badline/keyboard.rb +40 -16
  94. data/lib/badline/media.rb +48 -10
  95. data/lib/badline/memory.rb +5 -0
  96. data/lib/badline/roms/eapi/LICENSE.md +15 -0
  97. data/lib/badline/roms/eapi/README +41 -0
  98. data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
  99. data/lib/badline/sid/decimator.rb +46 -0
  100. data/lib/badline/sid/envelope.rb +232 -0
  101. data/lib/badline/sid/filter.rb +213 -0
  102. data/lib/badline/sid/voice.rb +61 -0
  103. data/lib/badline/sid/waveform/combined.rb +126 -0
  104. data/lib/badline/sid/waveform/fast_forward.rb +105 -0
  105. data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
  106. data/lib/badline/sid/waveform.rb +289 -0
  107. data/lib/badline/sid.rb +274 -7
  108. data/lib/badline/status.rb +251 -23
  109. data/lib/badline/storage/crt_file.rb +18 -2
  110. data/lib/badline/storage/d64_image.rb +6 -0
  111. data/lib/badline/storage/d71_image.rb +4 -0
  112. data/lib/badline/storage/d81_image.rb +4 -0
  113. data/lib/badline/storage/disk_image.rb +99 -13
  114. data/lib/badline/storage/host_directory.rb +30 -4
  115. data/lib/badline/storage/sid_file.rb +274 -0
  116. data/lib/badline/storage/song_lengths.rb +65 -0
  117. data/lib/badline/storage/t64.rb +67 -0
  118. data/lib/badline/storage/tap.rb +68 -0
  119. data/lib/badline/storage.rb +19 -0
  120. data/lib/badline/time_of_day.rb +105 -52
  121. data/lib/badline/version.rb +1 -1
  122. data/lib/badline/vic/bank.rb +14 -6
  123. data/lib/badline/vic/border_mask.rb +76 -0
  124. data/lib/badline/vic/collisions.rb +104 -0
  125. data/lib/badline/vic/color_patches.rb +33 -0
  126. data/lib/badline/vic/display_state.rb +92 -29
  127. data/lib/badline/vic/graphics_mode.rb +78 -53
  128. data/lib/badline/vic/graphics_shifter.rb +140 -0
  129. data/lib/badline/vic/register_log.rb +63 -0
  130. data/lib/badline/vic/registers.rb +3 -0
  131. data/lib/badline/vic/sequencer.rb +163 -137
  132. data/lib/badline/vic/sequencer_output.rb +116 -0
  133. data/lib/badline/vic/sprite/internal_bus.rb +49 -0
  134. data/lib/badline/vic/sprite/shifter.rb +129 -0
  135. data/lib/badline/vic/sprite.rb +275 -65
  136. data/lib/badline/vic/sprites.rb +220 -58
  137. data/lib/badline/vic.rb +389 -55
  138. data/lib/badline.rb +5 -0
  139. data/vendor/.gitkeep +0 -0
  140. metadata +104 -4
data/lib/badline/cia.rb CHANGED
@@ -1,68 +1,105 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
3
+ require "badline/cia/interrupt_register"
4
+ require "badline/cia/serial"
4
5
  require "badline/cia/timer"
5
6
 
6
7
  module Badline
7
8
  # CIA (Complex Interface Adapter) chip
8
9
  class CIA
9
10
  include Addressable
10
- extend Forwardable
11
11
 
12
- attr_reader :start, :control_a, :control_b, :interrupt_status, :interrupt_control, :peripheral
12
+ attr_reader :start, :control_a, :control_b, :peripheral, :serial
13
13
 
14
- def_delegator :@ta, :counter, :timer_a
15
- def_delegator :@ta, :counter=, :timer_a=
16
- def_delegator :@ta, :latch, :timer_a_latch
17
- def_delegator :@ta, :latch=, :timer_a_latch=
18
- def_delegator :@tb, :counter, :timer_b
19
- def_delegator :@tb, :counter=, :timer_b=
20
- def_delegator :@tb, :latch, :timer_b_latch
21
- def_delegator :@tb, :latch=, :timer_b_latch=
14
+ def interrupt_status = @icr.status
15
+
16
+ def interrupt_control = @icr.mask
17
+
18
+ def interrupt!(delay = 1) = @icr.assert!(delay)
19
+
20
+ def timer_a = @ta.counter
21
+
22
+ def timer_a=(value)
23
+ @ta.counter = value
24
+ end
25
+
26
+ def timer_a_latch = @ta.latch
27
+
28
+ def timer_a_latch=(value)
29
+ @ta.latch = value
30
+ end
31
+
32
+ def timer_b = @tb.counter
33
+
34
+ def timer_b=(value)
35
+ @tb.counter = value
36
+ end
37
+
38
+ def timer_b_latch = @tb.latch
39
+
40
+ def timer_b_latch=(value)
41
+ @tb.latch = value
42
+ end
22
43
 
23
44
  def initialize(start: 0, peripheral: nil)
24
45
  addressable_at(start, length: 2**8)
25
46
 
26
47
  @peripheral = peripheral
27
- @data_port_a = 0xff
28
- @data_port_b = 0xff
29
- @data_dir_a = 0xff
30
- @data_dir_b = 0x0
31
- @irq_pending = 0
32
- @serial_data = 0x0
48
+ @port_b4_handler = nil
49
+ @port_b4_high = true
50
+ reset!
51
+ end
52
+
53
+ # The RES line: ports back to inputs, timers stopped with their latches
54
+ # full, interrupts masked and the TOD clock stopped at 1:00:00.0. The
55
+ # PB4 level carries over and is resampled on the next cycle.
56
+ def reset!
57
+ @data_port_a = 0x00
58
+ @data_port_b = 0x00
59
+ @data_dir_a = 0x00
60
+ @data_dir_b = 0x00
61
+ @port_b4_driven_high = true
62
+ @cnt_high = true
63
+ @cnt_rise = false
33
64
  @tod = TimeOfDay.new
34
- @interrupt_control = Status.new([:timer_a, :timer_b, :alarm, :serial,
35
- :flag, 0, 0, 0])
36
- @interrupt_status = Status.new([:timer_a, :timer_b, :alarm, :serial,
37
- :flag, 0, 0, :interrupt])
38
- @control_a = Status.new(%i[start output out_mode run_mode load
39
- in_mode serial_mode clock_frequency])
40
- @control_b = Status.new(%i[start output out_mode run_mode load
41
- in_cnt in_timer_a alarm])
65
+ @icr = InterruptRegister.new
66
+ @icr_status = @icr.status
67
+ @control_a = ControlRegister.new(%i[start output out_mode run_mode load
68
+ in_mode serial_mode clock_frequency])
69
+ @control_b = ControlRegister.new(%i[start output out_mode run_mode load
70
+ in_cnt in_timer_a alarm])
42
71
  @ta = Timer.new(@control_a)
43
72
  @tb = Timer.new(@control_b)
73
+ @serial = Serial.new(@control_a)
44
74
  end
45
75
 
46
- def interrupt!(delay = 1)
47
- @irq_pending = @irq_pending.positive? ? [@irq_pending, delay].min : delay
76
+ # Register a change handler on the PB4 line. On CIA 1, this feeds the
77
+ # light pen input.
78
+ def on_port_b4_change(&handler)
79
+ @port_b4_handler = handler
48
80
  end
49
81
 
50
- def interrupted?
51
- interrupt_status.value.anybits?(0x80)
52
- end
82
+ # A falling edge on the FLAG pin. On CIA 1 the datasette's tape read
83
+ # line drives it, on CIA 2 the serial bus SRQ.
84
+ def flag! = raise_interrupt(:flag)
85
+
86
+ def interrupted? = @icr_status.value >= 0x80
53
87
 
54
88
  def cycle!
55
- if @irq_pending.positive?
56
- @irq_pending -= 1
57
- interrupt_status.interrupt = true if @irq_pending.zero?
58
- end
59
- update_timers
89
+ @icr.cycle! unless @icr.quiet
90
+ refresh_port_b4 if @port_b4_handler
91
+ level = @serial.cnt
92
+ level == @cnt_high ? @cnt_rise = false : cnt_edge(level)
93
+ underflowed = update_timers
94
+ @serial.cycle!(underflowed) { trigger_serial } if underflowed || !@serial.idle
60
95
  @tod.cycle! { trigger_alarm }
61
96
  end
62
97
 
63
98
  def read_port_a
64
- pulldown = peripheral ? peripheral.read_a(@data_port_a, @data_port_b) : 0xff
65
- driven_lines(@data_port_a, @data_dir_a) & pulldown
99
+ lines = driven_lines(@data_port_a, @data_dir_a)
100
+ return lines unless peripheral
101
+
102
+ lines & peripheral.read_a(lines, driven_lines(@data_port_b, @data_dir_b))
66
103
  end
67
104
 
68
105
  # Port A as driven by the data/direction registers alone, without
@@ -72,9 +109,9 @@ module Badline
72
109
  end
73
110
 
74
111
  def read_port_b
75
- pulldown = peripheral ? peripheral.read_b(@data_port_a, @data_port_b) : 0xff
76
- value = driven_lines(@data_port_b, @data_dir_b) & pulldown
77
- apply_timer_output(value)
112
+ lines = driven_lines(@data_port_b, @data_dir_b)
113
+ lines &= peripheral.read_b(driven_lines(@data_port_a, @data_dir_a), lines) if peripheral
114
+ apply_timer_output(lines)
78
115
  end
79
116
 
80
117
  def peek(addr)
@@ -91,12 +128,8 @@ module Badline
91
128
  when 0x09 then @tod.seconds
92
129
  when 0x0a then @tod.minutes
93
130
  when 0x0b then @tod.hours
94
- when 0x0c then @serial_data
95
- when 0x0d
96
- value = interrupt_status.value
97
- interrupt_status.value = 0x0 # Burn after reading
98
- @irq_pending = 0
99
- value
131
+ when 0x0c then @serial.data
132
+ when 0x0d then @icr.read
100
133
  when 0x0e then control_a.value
101
134
  when 0x0f then control_b.value
102
135
  end
@@ -105,9 +138,9 @@ module Badline
105
138
  def poke(addr, value)
106
139
  case index(addr) & 0x0f
107
140
  when 0x00 then @data_port_a = value
108
- when 0x01 then @data_port_b = value
141
+ when 0x01 then update_port_b { @data_port_b = value }
109
142
  when 0x02 then @data_dir_a = value
110
- when 0x03 then @data_dir_b = value
143
+ when 0x03 then update_port_b { @data_dir_b = value }
111
144
  when 0x04 then @ta.write_latch_low(value)
112
145
  when 0x05 then @ta.write_latch_high(value)
113
146
  when 0x06 then @tb.write_latch_low(value)
@@ -116,16 +149,32 @@ module Badline
116
149
  when 0x09 then @tod.write(:seconds, value, alarm: control_b.alarm?)
117
150
  when 0x0a then @tod.write(:minutes, value, alarm: control_b.alarm?)
118
151
  when 0x0b then @tod.write_hours(value, alarm: control_b.alarm?)
119
- when 0x0c
120
- # TODO: Serial
121
- when 0x0d then write_interrupt_control(value)
122
- when 0x0e then @ta.write_control(value)
152
+ when 0x0c then @serial.write(value)
153
+ when 0x0d then @icr.write(value)
154
+ when 0x0e then write_control_a(value)
123
155
  when 0x0f then @tb.write_control(value)
124
156
  end
125
157
  end
126
158
 
127
159
  private
128
160
 
161
+ def update_port_b
162
+ yield
163
+ @port_b4_driven_high = driven_lines(@data_port_b, @data_dir_b).anybits?(0x10)
164
+ refresh_port_b4 if @port_b4_handler
165
+ end
166
+
167
+ # PB4 is also control port 1's fire line, and a peripheral pulls it low
168
+ # without any register write, so the level is resampled every cycle rather
169
+ # than only after a poke.
170
+ def refresh_port_b4
171
+ high = @port_b4_driven_high && (peripheral.nil? || peripheral.port_b4_high?)
172
+ return if high == @port_b4_high
173
+
174
+ @port_b4_high = high
175
+ @port_b4_handler.call(high)
176
+ end
177
+
129
178
  def driven_lines(register, direction)
130
179
  # Output bits are driven from the data register; input bits float high.
131
180
  # External peripherals can still pull any line low (wired-AND).
@@ -133,57 +182,58 @@ module Badline
133
182
  end
134
183
 
135
184
  def apply_timer_output(value)
136
- value = with_bit(value, 6, timer_a_output?) if control_a.output?
137
- value = with_bit(value, 7, timer_b_output?) if control_b.output?
185
+ value = with_bit(value, 6, @ta.output?) if control_a.output?
186
+ value = with_bit(value, 7, @tb.output?) if control_b.output?
138
187
  value
139
188
  end
140
189
 
141
- def timer_a_output?
142
- control_a.out_mode? ? @ta.toggle? : @ta.underflowed
143
- end
144
-
145
- def timer_b_output?
146
- control_b.out_mode? ? @tb.toggle? : @tb.underflowed
147
- end
148
-
149
190
  def with_bit(value, bit, set)
150
191
  set ? value | (1 << bit) : value & ~(1 << bit)
151
192
  end
152
193
 
153
- def trigger_alarm
154
- interrupt_status.alarm = true
155
- interrupt! if interrupt_control.alarm?
194
+ def trigger_alarm = raise_interrupt(:alarm)
195
+ def trigger_serial = raise_interrupt(:serial)
196
+
197
+ def raise_interrupt(source) = @icr.flag(source)
198
+
199
+ # CNT is sampled once a cycle. When the serial port drives it from this
200
+ # cycle's timer A underflow, the new level is picked up on the next one.
201
+ def cnt_edge(level)
202
+ @cnt_high = level
203
+ @cnt_rise = level
204
+ @serial.rising_edge! { trigger_serial } if level
156
205
  end
157
206
 
207
+ # Returns whether timer A underflowed, which clocks the serial port.
208
+ #
209
+ # CRB bits 6-5 pick timer B's source: ø2, CNT edges, timer A
210
+ # underflows, or timer A underflows gated by the CNT level. Every source
211
+ # drives the same count-enable line, so a cascaded underflow goes through
212
+ # the input pipeline exactly as a CNT edge does.
158
213
  def update_timers
159
- @ta.cycle!(@control_a.value.nobits?(0x20), true)
160
- crb = @control_b.value
161
- if crb.anybits?(0x40)
162
- @tb.cycle!(true, @ta.underflowed)
163
- else
164
- @tb.cycle!(crb.nobits?(0x20), true)
165
- end
166
- if @ta.underflowed
214
+ @ta.cycle!(@control_a.value & 0x20 == 0x20 ? @cnt_rise : true)
215
+ underflowed = @ta.underflowed
216
+ @tb.cycle!(
217
+ case @control_b.value & 0x60
218
+ when 0x00 then true
219
+ when 0x20 then @cnt_rise
220
+ when 0x40 then underflowed
221
+ else underflowed && @cnt_high
222
+ end
223
+ )
224
+ if underflowed
167
225
  interrupt_status.timer_a = true
168
226
  interrupt! if interrupt_control.timer_a?
169
227
  end
170
- return unless @tb.underflowed
171
-
172
- interrupt_status.timer_b = true
173
- interrupt! if interrupt_control.timer_b?
228
+ @icr.timer_b_underflow! if @tb.underflowed
229
+ underflowed
174
230
  end
175
231
 
176
- def write_interrupt_control(value)
177
- if value.nobits?(0x80)
178
- # Clear interrupts based on bits 0-4
179
- interrupt_control.value &= ~(value & 0x1f)
180
- else
181
- # Set interrupts based on bits 0-4
182
- interrupt_control.value |= (value & 0x1f)
183
- end
184
- return unless interrupt_control.value.anybits?(interrupt_status.value & 0x1f)
185
-
186
- interrupt!(2) unless interrupted?
232
+ def write_control_a(value)
233
+ was_output = control_a.serial_mode?
234
+ @ta.write_control(value)
235
+ @serial.reset! { trigger_serial } if control_a.serial_mode? != was_output
236
+ @tod.fifty_hz = control_a.clock_frequency?
187
237
  end
188
238
  end
189
239
  end
@@ -1,16 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
+ # Colour RAM is four bits wide. A CPU read takes the upper nibble off the
5
+ # open data bus, which still holds the byte the VIC fetched in phi1.
4
6
  class ColorMemory < Memory
7
+ def initialize(vic, start: 0xd800, length: 2**10)
8
+ super(start:, length:)
9
+ @vic = vic
10
+ end
11
+
12
+ def peek(addr)
13
+ (@vic.phi1_data & 0xf0) | @storage[index(addr)]
14
+ end
15
+
5
16
  def poke(addr, value)
6
- # Only write the lower 4 bits
7
17
  super(addr, value & 0x0f)
8
18
  end
9
19
 
10
- private
11
-
12
- def blank_value
13
- rand(16) << 4
20
+ # The four bits the cell stores, as the VIC's c-access sees them.
21
+ def nibble(addr)
22
+ @storage[index(addr)]
14
23
  end
15
24
  end
16
25
  end
@@ -1,25 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
-
5
3
  module Badline
6
4
  class Computer
7
5
  include IntegerHelper
8
6
  include KeyboardBuffer
9
- extend Forwardable
10
7
 
11
8
  attr_reader :address_bus, :cpu, :cycles
12
9
 
13
- def_delegators :address_bus, :vic, :cia1, :cia2, :sid, :ram, :keyboard, :joystick2
10
+ def vic = address_bus.vic
11
+
12
+ def cia1 = address_bus.cia1
13
+
14
+ def cia2 = address_bus.cia2
15
+
16
+ def sid = address_bus.sid
17
+
18
+ def ram = address_bus.ram
19
+
20
+ def keyboard = address_bus.keyboard
21
+
22
+ def joystick1 = address_bus.joystick1
23
+
24
+ def joystick2 = address_bus.joystick2
14
25
 
15
- def initialize(debug: false)
16
- @address_bus = AddressBus.new
26
+ def control_ports = address_bus.control_ports
27
+
28
+ def datasette = address_bus.datasette
29
+
30
+ def install_debug_register(&) = address_bus.install_debug_register(&)
31
+
32
+ def initialize(debug: false, sid_model: :mos6581)
33
+ @address_bus = AddressBus.new(sid_model:)
17
34
  @cpu = CPU.new(@address_bus, debug:)
18
35
  @vic = @address_bus.vic
36
+ @vic.open_bus = -> { @address_bus.ram.peek(@cpu.program_counter) }
19
37
  @cia1 = @address_bus.cia1
20
38
  @cia2 = @address_bus.cia2
39
+ @sid = @address_bus.sid
40
+ @datasette = @address_bus.datasette
21
41
  @cycles = 0
22
42
  @nmi_asserted = false
43
+ @cartridge_nmi = false
44
+ @freezing = false
45
+ @freeze_writes = 0
23
46
  @init_handlers = []
24
47
  @pending_keys = nil
25
48
  end
@@ -30,17 +53,19 @@ module Badline
30
53
  handle_init if @cycles == INIT_THRESHOLD
31
54
  feed_keyboard if @pending_keys
32
55
 
56
+ # The chips clock ahead of the CPU, so a register write lands on the
57
+ # cycle after the one it was issued on.
33
58
  @vic.cycle!
34
59
  @cia1.cycle!
35
60
  @cia2.cycle!
61
+ @sid.cycle!
62
+ @datasette.cycle!
36
63
 
37
64
  @cpu.irq = @cia1.interrupted? || @vic.interrupted?
38
65
 
39
- nmi = @cia2.interrupted?
40
- @cpu.nmi = true if nmi && !@nmi_asserted
41
- @nmi_asserted = nmi
42
-
43
- @cpu.cycle! if @cpu.pending_write? || !@vic.ba_low?
66
+ drive_nmi
67
+ watch_freeze if @freezing
68
+ @cpu.pending_write? || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
44
69
 
45
70
  @cycles += 1
46
71
  end
@@ -52,13 +77,54 @@ module Badline
52
77
  end
53
78
 
54
79
  def attach_cartridge(cartridge)
80
+ cartridge.clock = -> { @cycles }
81
+ cartridge.on_nmi_change { |level| @cartridge_nmi = level }
55
82
  address_bus.attach_cartridge(cartridge)
83
+ power_cycle!
84
+ end
85
+
86
+ # A cartridge goes in with the power off, so attaching one switches the
87
+ # machine off and on: the VIC and RAM start from their power-on state,
88
+ # and the RES line resets everything else.
89
+ def power_cycle!
90
+ vic.power_on!
91
+ address_bus.power_on!
92
+ reset!
93
+ end
94
+
95
+ # The RES line reaches the CPU and its port, both CIAs, the SID and the
96
+ # cartridge port. The VIC has no reset pin.
97
+ def reset!
98
+ address_bus.reset_port!
99
+ @cia1.reset!
100
+ @cia2.reset!
101
+ @sid.reset!
102
+ address_bus.cartridge&.reset
103
+ @freezing = false
104
+ @nmi_asserted = false
56
105
  cpu.reset!
57
106
  end
58
107
 
108
+ # Presses the cartridge's freeze button. When the press pulls NMI, the
109
+ # cartridge freezes once the CPU takes the interrupt.
110
+ def press_cartridge_button
111
+ cartridge = address_bus.cartridge
112
+ return unless cartridge
113
+
114
+ cartridge.press_button
115
+ @freezing = cartridge.nmi?
116
+ @freeze_writes = 0
117
+ end
118
+
119
+ def release_cartridge_button
120
+ address_bus.cartridge&.release_button
121
+ end
122
+
59
123
  def mount(storage)
60
- load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, storage:)
124
+ drive = KernalTrap::Drive.new(storage)
125
+ load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive:)
61
126
  cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
127
+ KernalTrap::Serial.new(cpu:, bus: address_bus, drive:).install
62
128
  return unless storage.respond_to?(:write_file)
63
129
 
64
130
  save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, storage:)
@@ -85,6 +151,25 @@ module Badline
85
151
 
86
152
  private
87
153
 
154
+ # The NMI line is wired-OR between CIA 2 and the cartridge, and the CPU
155
+ # takes an interrupt on its falling edge.
156
+ def drive_nmi
157
+ nmi = @cia2.interrupted? || @cartridge_nmi
158
+ @cpu.nmi = true if nmi && !@nmi_asserted
159
+ @nmi_asserted = nmi
160
+ end
161
+
162
+ # A freezer counts the CPU's write cycles once it pulls NMI and switches
163
+ # to Ultimax on the third in a row, the last push of the interrupt
164
+ # sequence, so the vector fetch that follows reads the cartridge.
165
+ def watch_freeze
166
+ @freeze_writes = @cpu.pending_write? ? @freeze_writes + 1 : 0
167
+ return if @freeze_writes < 3
168
+
169
+ @freezing = false
170
+ address_bus.cartridge.freeze!
171
+ end
172
+
88
173
  def booting?
89
174
  @cycles < init_threshold
90
175
  end
@@ -1,24 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- # CIA 1 peripheral combining the keyboard matrix with the control ports.
4
+ # CIA 1's control ports and the keyboard matrix that spans them.
5
5
  #
6
- # Joystick switches share the same lines as the keyboard matrix and are
7
- # wired-AND with it (active low). Port 2 sits on Port A.
6
+ # Port A carries the matrix rows together with joystick 2, port B the matrix
7
+ # columns together with joystick 1. Every switch is active low, so the
8
+ # joysticks are wired-AND onto the lines *before* the matrix settles: a stick
9
+ # held in a direction selects rows and columns of its own, which is where
10
+ # hardware's phantom keypresses come from. Paddles and a 1351 mouse have
11
+ # buttons on those same lines.
12
+ #
13
+ # The ports also carry SID's POTX/POTY lines, which reach them through analog
14
+ # switches closed by CIA 1 PA6 (port 1) and PA7 (port 2). Close both and the
15
+ # pots sit in parallel, so SID charges through the lower of the two
16
+ # resistances; close neither and the lines float high. Reading a pot asks the
17
+ # CIA for the current select lines, since a program sets them and then goes
18
+ # straight to SID without touching the ports again.
8
19
  class ControlPorts
9
- attr_reader :keyboard, :joystick2
20
+ PORT1_POTS = 0x40
21
+ PORT2_POTS = 0x80
22
+ FIRE = 0x10
23
+
24
+ attr_reader :keyboard, :joystick1, :joystick2
25
+ attr_accessor :device1, :device2, :port_a_source
10
26
 
11
- def initialize(keyboard:, joystick2:)
27
+ def initialize(keyboard:, joystick1:, joystick2:)
12
28
  @keyboard = keyboard
29
+ @joystick1 = joystick1
13
30
  @joystick2 = joystick2
31
+ @device1 = nil
32
+ @device2 = nil
33
+ @port_a_source = nil
34
+ end
35
+
36
+ def read_a(port_a, port_b) = scan(port_a, port_b).first
37
+
38
+ def read_b(port_a, port_b) = scan(port_a, port_b).last
39
+
40
+ # Control port 1 pin 6 is both the port B fire line and the VIC's light
41
+ # pen input, so joystick 1's button and a 1351's left button latch
42
+ # $D013/$D014. The CIA samples this every cycle, so it skips the matrix and
43
+ # asks only what the port's own devices drive.
44
+ def port_b4_high? = port_bits(@joystick1, @device1).anybits?(FIRE)
45
+
46
+ def pot_x = selected_devices.map(&:pot_x).min || 0xff
47
+
48
+ def pot_y = selected_devices.map(&:pot_y).min || 0xff
49
+
50
+ private
51
+
52
+ def selected_devices
53
+ lines = @port_a_source&.port_a_lines || 0x00
54
+ devices = []
55
+ devices << @device1 if @device1 && lines.anybits?(PORT1_POTS)
56
+ devices << @device2 if @device2 && lines.anybits?(PORT2_POTS)
57
+ devices
14
58
  end
15
59
 
16
- def read_a(port_a, port_b)
17
- keyboard.read_a(port_a, port_b) & joystick2.port_bits
60
+ def scan(port_a, port_b)
61
+ keyboard.scan(port_a & port_bits(@joystick2, @device2),
62
+ port_b & port_bits(@joystick1, @device1))
18
63
  end
19
64
 
20
- def read_b(port_a, port_b)
21
- keyboard.read_b(port_a, port_b)
65
+ def port_bits(joystick, device)
66
+ device ? joystick.port_bits & device.port_bits : joystick.port_bits
22
67
  end
23
68
  end
24
69
  end