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
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ # One open channel on the virtual drive, holding a file, a block buffer
6
+ # or the command channel's status message. It gives out one byte at a
7
+ # time and flags the last one, so the bus can raise EOI with it.
8
+ class Channel
9
+ attr_accessor :pointer
10
+
11
+ def initialize(bytes = [])
12
+ replace(bytes)
13
+ end
14
+
15
+ def replace(bytes)
16
+ @bytes = bytes
17
+ @pointer = 0
18
+ end
19
+
20
+ def exhausted?
21
+ @pointer >= @bytes.length
22
+ end
23
+
24
+ def read
25
+ return if exhausted?
26
+
27
+ byte = @bytes[@pointer]
28
+ @pointer += 1
29
+ [byte, exhausted?]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ # The CBM DOS side of the serial bus. Keeps the open channels, the
6
+ # command channel and its status message. `U1` reads a raw block into a
7
+ # buffer channel, which is how block-access loaders bypass the KERNAL's
8
+ # LOAD.
9
+ class Drive
10
+ COMMAND_CHANNEL = 15
11
+ BLOCK_SIZE = 256
12
+
13
+ OK = 0
14
+ WRITE_PROTECT_ON = 26
15
+ SYNTAX_ERROR = 30
16
+ FILE_NOT_FOUND = 62
17
+ ILLEGAL_TRACK_OR_SECTOR = 66
18
+ NO_CHANNEL = 70
19
+ DRIVE_NOT_READY = 74
20
+
21
+ MESSAGES = {
22
+ OK => " OK",
23
+ 20 => "READ ERROR",
24
+ 21 => "READ ERROR",
25
+ 22 => "READ ERROR",
26
+ 23 => "READ ERROR",
27
+ 24 => "READ ERROR",
28
+ 25 => "WRITE ERROR",
29
+ WRITE_PROTECT_ON => "WRITE PROTECT ON",
30
+ 27 => "READ ERROR",
31
+ 28 => "WRITE ERROR",
32
+ 29 => "DISK ID MISMATCH",
33
+ SYNTAX_ERROR => "SYNTAX ERROR",
34
+ FILE_NOT_FOUND => "FILE NOT FOUND",
35
+ ILLEGAL_TRACK_OR_SECTOR => "ILLEGAL TRACK OR SECTOR",
36
+ NO_CHANNEL => "NO CHANNEL",
37
+ DRIVE_NOT_READY => "DRIVE NOT READY"
38
+ }.freeze
39
+
40
+ MEMORY_COMMANDS = { "M-W" => :memory_write, "M-R" => :memory_read }.freeze
41
+
42
+ RAM_SIZE = 0x800
43
+
44
+ # The job queue: a job code for each of five buffers at $00, their
45
+ # track and sector pairs from $06, and the buffers from $0300. A job
46
+ # code is replaced by its result, 1 for success or an error table
47
+ # code: the DOS error less 18, or 15 for DRIVE NOT READY.
48
+ JOBS = 5
49
+ READ_JOB = 0x80
50
+ JOB_OK = 1
51
+ HEADER_NOT_FOUND = 20
52
+
53
+ COMMANDS = {
54
+ /\AU[1A]\s*:?\s*(.*)/i => :block_read,
55
+ /\AB-R\s*:?\s*(.*)/i => :counted_block_read,
56
+ /\AB-P\s*:?\s*(.*)/i => :buffer_pointer,
57
+ /\A[IV]/i => :initialized,
58
+ /\A(?:U[2B]|B-[WAF])/i => :write_protected
59
+ }.freeze
60
+
61
+ def initialize(storage)
62
+ @storage = storage
63
+ @channels = {}
64
+ @status = Channel.new
65
+ @ram = Array.new(RAM_SIZE, 0)
66
+ report(OK)
67
+ end
68
+
69
+ # A name on the command channel is a command, "#" opens a block
70
+ # buffer, anything else opens a file for reading. The name comes as
71
+ # the bytes sent, because a memory command's arguments are binary.
72
+ def open(secondary, name)
73
+ if secondary == COMMAND_CHANNEL
74
+ command(name.bytes) unless name.empty?
75
+ elsif name.start_with?("#")
76
+ @channels[secondary] = Channel.new(Array.new(BLOCK_SIZE, 0))
77
+ report(OK)
78
+ else
79
+ open_file(secondary, Storage.ascii(name.bytes))
80
+ end
81
+ end
82
+
83
+ # Closing the command channel closes every other channel with it.
84
+ def close(secondary)
85
+ secondary == COMMAND_CHANNEL ? @channels.clear : @channels.delete(secondary)
86
+ end
87
+
88
+ def write(secondary, bytes)
89
+ return report(WRITE_PROTECT_ON) unless secondary == COMMAND_CHANNEL
90
+
91
+ command(bytes)
92
+ end
93
+
94
+ # Returns the next byte and whether it is the channel's last, or nil
95
+ # when there is nothing left to send.
96
+ def read(secondary)
97
+ secondary == COMMAND_CHANNEL ? read_status : @channels[secondary]&.read
98
+ end
99
+
100
+ private
101
+
102
+ def read_status
103
+ result = @status.read
104
+ report(OK) if result&.last
105
+
106
+ result
107
+ end
108
+
109
+ # Secondary addresses 0 and 1 are LOAD and SAVE, which look for a PRG
110
+ # file unless the name asks for another type. Other channels take any
111
+ # type the name doesn't pin down.
112
+ def open_file(secondary, name)
113
+ file, type = Storage.parse_name(name)
114
+ type ||= :prg if secondary < 2
115
+ data = @storage.read_file(file, type:)
116
+ @channels[secondary] = Channel.new(data || [])
117
+ report(data ? OK : FILE_NOT_FOUND)
118
+ end
119
+
120
+ def execute(text)
121
+ command = text.strip
122
+ pattern, action = COMMANDS.find { |candidate, _| candidate.match?(command) }
123
+ return report(SYNTAX_ERROR) unless action
124
+
125
+ send(action, arguments(pattern.match(command)[1]))
126
+ end
127
+
128
+ def arguments(text)
129
+ text.to_s.scan(/\d+/).map(&:to_i)
130
+ end
131
+
132
+ def command(bytes)
133
+ action = MEMORY_COMMANDS[bytes[0, 3].pack("C*")]
134
+ action ? send(action, bytes[3..]) : execute(Storage.ascii(bytes))
135
+ end
136
+
137
+ def block_read(arguments, counted: false)
138
+ channel, _drive, track, sector = arguments
139
+ buffer = @channels[channel]
140
+ return report(NO_CHANNEL) unless buffer
141
+ return report(DRIVE_NOT_READY) unless @storage.respond_to?(:read_block)
142
+
143
+ data = sector && @storage.read_block(track, sector)
144
+ return report(ILLEGAL_TRACK_OR_SECTOR, track, sector) unless data
145
+
146
+ buffer.replace(counted ? data[0, data[0] + 1] : data)
147
+ buffer.pointer = 1 if counted
148
+ report_block_error(track, sector)
149
+ end
150
+
151
+ # A block the image's error table marks bad still fills the buffer,
152
+ # but the read reports its error, which copy protection checks for.
153
+ def report_block_error(track, sector)
154
+ error = @storage.block_error(track, sector)
155
+ error ? report(error, track, sector) : report(OK)
156
+ end
157
+
158
+ # B-R takes the block's first byte as the index of its last one, and
159
+ # hands out the bytes from the second up to there.
160
+ def counted_block_read(arguments)
161
+ block_read(arguments, counted: true)
162
+ end
163
+
164
+ def buffer_pointer(arguments)
165
+ channel, position = arguments
166
+ buffer = @channels[channel]
167
+ return report(NO_CHANNEL) unless buffer
168
+
169
+ buffer.pointer = position.to_i
170
+ report(OK)
171
+ end
172
+
173
+ # M-W takes an address, a count and that many bytes. Writes past the
174
+ # RAM are dropped, since the rest of the address space is I/O and ROM.
175
+ def memory_write(arguments)
176
+ address = word(arguments)
177
+ arguments[3, arguments[2].to_i].to_a.each.with_index(address) do |byte, target|
178
+ @ram[target] = byte if target < RAM_SIZE
179
+ end
180
+ run_jobs
181
+ report(OK)
182
+ end
183
+
184
+ # M-R takes an address and a count, and answers on the command
185
+ # channel. Without a count, or with only the carriage return PRINT#
186
+ # ends a line with, it reads one byte. Only the RAM reads back.
187
+ def memory_read(arguments)
188
+ address = word(arguments)
189
+ count = arguments[2..] == ["\r".ord] ? 1 : arguments.fetch(2, 1)
190
+ count = BLOCK_SIZE if count.zero?
191
+ @status.replace(Array.new(count) { |i| @ram.fetch(address + i, 0) })
192
+ nil
193
+ end
194
+
195
+ def word(arguments)
196
+ arguments[0].to_i | (arguments[1].to_i << 8)
197
+ end
198
+
199
+ def run_jobs
200
+ JOBS.times do |job|
201
+ next unless @ram[job] == READ_JOB
202
+
203
+ track, sector = @ram[6 + (job * 2), 2]
204
+ @ram[job] = read_job(0x300 + (job * BLOCK_SIZE), track, sector)
205
+ end
206
+ end
207
+
208
+ def read_job(buffer, track, sector)
209
+ data = @storage.respond_to?(:read_block) && @storage.read_block(track, sector)
210
+ return job_result(HEADER_NOT_FOUND) unless data
211
+
212
+ @ram[buffer, BLOCK_SIZE] = data
213
+ job_result(@storage.block_error(track, sector))
214
+ end
215
+
216
+ def job_result(error)
217
+ return JOB_OK unless error
218
+
219
+ error == DRIVE_NOT_READY ? 15 : error - 18
220
+ end
221
+
222
+ def initialized(_arguments)
223
+ report(OK)
224
+ end
225
+
226
+ def write_protected(_arguments)
227
+ report(WRITE_PROTECT_ON)
228
+ end
229
+
230
+ def report(code, track = 0, sector = 0)
231
+ message = format("%<code>02d,%<message>s,%<track>02d,%<sector>02d\r",
232
+ code:, message: MESSAGES.fetch(code),
233
+ track: track.to_i, sector: sector.to_i)
234
+ @status.replace(message.bytes)
235
+ nil
236
+ end
237
+ end
238
+ end
239
+ end
@@ -2,23 +2,18 @@
2
2
 
3
3
  module Badline
4
4
  module KernalTrap
5
- class File
6
- include IntegerHelper
7
-
8
- DEVICE = 8
9
-
5
+ class File < Routine
10
6
  MISSING_FILENAME = 0x08
11
7
 
12
8
  def initialize(cpu:, bus:, storage:)
13
- @cpu = cpu
14
- @bus = bus
9
+ super(cpu:, bus:)
15
10
  @storage = storage
16
11
  end
17
12
 
18
13
  private
19
14
 
20
15
  def active?
21
- @bus.io_port.kernal? && @bus.peek(0xba) == DEVICE
16
+ kernal? && @bus.peek(0xba) == DEVICE
22
17
  end
23
18
 
24
19
  # Filename pointer at $BB/$BC, length at $B7
@@ -27,28 +22,13 @@ module Badline
27
22
  bytes = Array.new(@bus.peek(0xb7)) do |i|
28
23
  @bus.peek((pointer + i) & 0xffff)
29
24
  end
30
- strip_drive_prefix(Storage.ascii(bytes))
31
- end
32
-
33
- # CBM DOS drive prefix — "0:NAME" selects a drive, "@0:NAME" is
34
- # save-with-replace
35
- def strip_drive_prefix(name)
36
- name.sub(/\A@?\d*:/, "")
25
+ Storage.strip_drive_prefix(Storage.ascii(bytes))
37
26
  end
38
27
 
39
28
  def error(code)
40
29
  @cpu.a = code
41
30
  @cpu.status.carry = true
42
31
  end
43
-
44
- def return_to_caller
45
- @cpu.program_counter = (uint16(pull_byte, pull_byte) + 1) & 0xffff
46
- end
47
-
48
- def pull_byte
49
- @cpu.stack_pointer = (@cpu.stack_pointer + 1) & 0xff
50
- @bus.peek(0x0100 + @cpu.stack_pointer)
51
- end
52
32
  end
53
33
  end
54
34
  end
@@ -14,10 +14,10 @@ module Badline
14
14
  def call
15
15
  return unless active?
16
16
 
17
- name = filename
17
+ name, type = Storage.parse_name(filename)
18
18
  if name.empty?
19
19
  error(MISSING_FILENAME)
20
- elsif (data = @storage.read_file(name))
20
+ elsif (data = @storage.read_file(name, type: type || :prg))
21
21
  deliver(data)
22
22
  else
23
23
  error(FILE_NOT_FOUND)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ # Base class for the PC traps on KERNAL routines. Holds the device
6
+ # number they answer for and the return that stands in for the trapped
7
+ # routine's RTS.
8
+ class Routine
9
+ include IntegerHelper
10
+
11
+ DEVICE = 8
12
+
13
+ def initialize(cpu:, bus:)
14
+ @cpu = cpu
15
+ @bus = bus
16
+ end
17
+
18
+ private
19
+
20
+ def kernal?
21
+ @bus.io_port.kernal?
22
+ end
23
+
24
+ def return_to_caller
25
+ @cpu.program_counter = (uint16(pull_byte, pull_byte) + 1) & 0xffff
26
+ end
27
+
28
+ def pull_byte
29
+ @cpu.stack_pointer = (@cpu.stack_pointer + 1) & 0xff
30
+ @bus.peek(0x0100 + @cpu.stack_pointer)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -12,7 +12,7 @@ module Badline
12
12
  def call
13
13
  return unless active?
14
14
 
15
- name = filename
15
+ name = Storage.parse_name(filename).first
16
16
  if name.empty?
17
17
  error(MISSING_FILENAME)
18
18
  else
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ # PC traps on the KERNAL's serial bus primitives. Frames addressed to
6
+ # device 8 go to a virtual drive instead of out over the IEC lines, so
7
+ # OPEN/CHKIN/CHRIN reach a CBM DOS, and so do the block reads that
8
+ # loaders make through them. Other devices fall through to the ROM.
9
+ class Serial < Routine
10
+ ROUTINES = {
11
+ 0xed09 => :talk,
12
+ 0xed0c => :listen,
13
+ 0xedb9 => :second,
14
+ 0xedc7 => :tksa,
15
+ 0xeddd => :ciout,
16
+ 0xedef => :untalk,
17
+ 0xedfe => :unlisten,
18
+ 0xee13 => :acptr
19
+ }.freeze
20
+
21
+ # Frame type in the high nibble of the secondary address
22
+ OPEN = 0xf0
23
+ CLOSE = 0xe0
24
+
25
+ # ST bits at $90
26
+ EOI = 0x40
27
+ READ_TIMEOUT = 0x02
28
+
29
+ NO_DATA = [0x0d, EOI | READ_TIMEOUT].freeze
30
+
31
+ def initialize(cpu:, bus:, drive:)
32
+ super(cpu:, bus:)
33
+ @drive = drive
34
+ @listening = false
35
+ @talking = false
36
+ @listen_channel = nil
37
+ @talk_channel = nil
38
+ @frame = OPEN
39
+ @buffer = []
40
+ end
41
+
42
+ def install
43
+ ROUTINES.each do |address, routine|
44
+ @cpu.install_trap(address) { send(routine) if kernal? }
45
+ end
46
+ self
47
+ end
48
+
49
+ private
50
+
51
+ # Addressing the bus starts a new frame. The channel number arrives
52
+ # in the secondary address that follows.
53
+ def talk
54
+ @talk_channel = nil
55
+ @talking = @cpu.a == DEVICE
56
+ return_to_caller if @talking
57
+ end
58
+
59
+ def listen
60
+ @listen_channel = nil
61
+ @listening = @cpu.a == DEVICE
62
+ return_to_caller if @listening
63
+ end
64
+
65
+ def second
66
+ return unless @listening
67
+
68
+ @frame = @cpu.a & 0xf0
69
+ @listen_channel = @cpu.a & 0x0f
70
+ @buffer = []
71
+ return_to_caller
72
+ end
73
+
74
+ def tksa
75
+ return unless @talking
76
+
77
+ @talk_channel = @cpu.a & 0x0f
78
+ return_to_caller
79
+ end
80
+
81
+ def ciout
82
+ return unless @listening
83
+
84
+ @buffer << @cpu.a
85
+ @cpu.status.carry = false
86
+ return_to_caller
87
+ end
88
+
89
+ def unlisten
90
+ return unless @listening
91
+
92
+ @listening = false
93
+ deliver_frame
94
+ return_to_caller
95
+ end
96
+
97
+ def untalk
98
+ return unless @talking
99
+
100
+ @talking = false
101
+ return_to_caller
102
+ end
103
+
104
+ def acptr
105
+ return unless @talking
106
+
107
+ byte, status = received
108
+ @cpu.a = byte
109
+ update_status(status)
110
+ @cpu.status.carry = false
111
+ return_to_caller
112
+ end
113
+
114
+ def deliver_frame
115
+ return unless @listen_channel
116
+
117
+ case @frame
118
+ when OPEN then @drive.open(@listen_channel, @buffer.pack("C*"))
119
+ when CLOSE then @drive.close(@listen_channel)
120
+ else @drive.write(@listen_channel, @buffer)
121
+ end
122
+ @buffer = []
123
+ end
124
+
125
+ def received
126
+ byte, eoi = @drive.read(@talk_channel)
127
+ return NO_DATA unless byte
128
+
129
+ [byte, eoi ? EOI : 0]
130
+ end
131
+
132
+ def update_status(bits)
133
+ return if bits.zero?
134
+
135
+ @bus.poke(0x90, @bus.peek(0x90) | bits)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/kernal_trap/routine"
4
+ require "badline/kernal_trap/channel"
5
+ require "badline/kernal_trap/drive"
3
6
  require "badline/kernal_trap/file"
4
7
  require "badline/kernal_trap/load"
5
8
  require "badline/kernal_trap/save"
9
+ require "badline/kernal_trap/serial"
@@ -1,6 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
+ # The 8x8 key matrix spanning CIA 1's two ports: rows on port A, columns on
5
+ # port B.
6
+ #
7
+ # A pressed key shorts its row line to its column line, so whichever side is
8
+ # pulled low drags the other down with it. Scanning works in either
9
+ # direction, and chains of keys sharing a line propagate the pulldown -- the
10
+ # ghost keys real hardware reports.
4
11
  class Keyboard
5
12
  include IntegerHelper
6
13
 
@@ -8,9 +15,10 @@ module Badline
8
15
 
9
16
  def initialize
10
17
  @keys = []
18
+ @row_masks = nil
11
19
 
12
20
  @matrix = [
13
- %i[delete return cursor_h f7 f1 f3 cursor_v],
21
+ %i[delete return cursor_h f7 f1 f3 f5 cursor_v],
14
22
  %i[3 w a 4 z s e lshift],
15
23
  %i[5 r d 6 c f t x],
16
24
  %i[7 y g 8 b h u v],
@@ -22,35 +30,51 @@ module Badline
22
30
  end
23
31
 
24
32
  def press(key)
25
- @keys << key if valid_key?(key)
33
+ return unless valid_key?(key)
34
+
35
+ @keys << key
36
+ @row_masks = nil
26
37
  end
27
38
 
28
- def read_a(_port_a, _port_b)
29
- 0xff
39
+ def release(key)
40
+ @keys.reject! { |k| k == key }
41
+ @row_masks = nil
30
42
  end
31
43
 
32
- def read_b(port_a, _port_b)
33
- return 0xff unless keys.any?
44
+ # Settles the row (port A) and column (port B) lines against each other
45
+ # until no key contact pulls a line low any more, and returns both.
46
+ def scan(rows, cols)
47
+ return [rows, cols] if keys.empty?
34
48
 
35
- output = 0xff
49
+ loop do
50
+ settled_rows = rows
51
+ settled_cols = cols
36
52
 
37
- matrix.each_with_index do |row, a|
38
- next unless port_a[a].zero?
53
+ row_masks.each_with_index do |mask, row|
54
+ bit = 1 << row
55
+ next if mask.zero? || (rows.allbits?(bit) && cols.allbits?(mask))
39
56
 
40
- row.each_with_index do |key, b|
41
- output -= (1 << b) if keys.include?(key)
57
+ rows &= ~bit
58
+ cols &= ~mask
42
59
  end
43
- end
44
60
 
45
- output
61
+ return [rows, cols] if rows == settled_rows && cols == settled_cols
62
+ end
46
63
  end
47
64
 
48
- def release(key)
49
- @keys.reject! { |k| k == key }
50
- end
65
+ def read_a(port_a, port_b) = scan(port_a, port_b).first
66
+
67
+ def read_b(port_a, port_b) = scan(port_a, port_b).last
51
68
 
52
69
  private
53
70
 
71
+ # Columns held down per row, as a bit mask of port B lines.
72
+ def row_masks
73
+ @row_masks ||= matrix.map do |row|
74
+ row.each_with_index.sum { |key, column| keys.include?(key) ? 1 << column : 0 }
75
+ end
76
+ end
77
+
54
78
  def valid_key?(key)
55
79
  matrix.flatten.include?(key)
56
80
  end