badline 0.9.0 → 0.10.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +39 -4
  5. data/doc/pinned-behaviour.md +146 -0
  6. data/doc/snapshots.md +13 -0
  7. data/lib/badline/address_bus/pla.rb +130 -0
  8. data/lib/badline/address_bus.rb +9 -88
  9. data/lib/badline/chrout_trap.rb +4 -3
  10. data/lib/badline/computer/attachments.rb +47 -7
  11. data/lib/badline/computer/saved_state.rb +1 -0
  12. data/lib/badline/computer.rb +44 -29
  13. data/lib/badline/control_ports.rb +10 -3
  14. data/lib/badline/core.rb +3 -0
  15. data/lib/badline/datasette.rb +39 -1
  16. data/lib/badline/drive1541/serial_port.rb +5 -8
  17. data/lib/badline/drive1541/sleep.rb +5 -6
  18. data/lib/badline/drive1541.rb +3 -3
  19. data/lib/badline/frontend/app.rb +62 -64
  20. data/lib/badline/frontend/boot.rb +33 -8
  21. data/lib/badline/frontend/controls.rb +12 -6
  22. data/lib/badline/frontend/drive_led.rb +9 -10
  23. data/lib/badline/frontend/frame_rate.rb +4 -4
  24. data/lib/badline/frontend/machine_window.rb +87 -0
  25. data/lib/badline/frontend/menu_media.rb +49 -24
  26. data/lib/badline/frontend/menu_pages.rb +23 -40
  27. data/lib/badline/frontend/menu_ports.rb +69 -0
  28. data/lib/badline/frontend/pacer.rb +6 -5
  29. data/lib/badline/frontend/pause_menu.rb +30 -33
  30. data/lib/badline/frontend/player_screen.rb +1 -1
  31. data/lib/badline/frontend/player_window.rb +1 -1
  32. data/lib/badline/frontend/screen.rb +39 -37
  33. data/lib/badline/frontend/sid_view.rb +1 -1
  34. data/lib/badline/frontend/sound.rb +11 -9
  35. data/lib/badline/frontend/timeline.rb +36 -5
  36. data/lib/badline/frontend.rb +2 -0
  37. data/lib/badline/iec_bus.rb +16 -28
  38. data/lib/badline/kernal_trap/drive.rb +3 -0
  39. data/lib/badline/kernal_trap/file.rb +1 -1
  40. data/lib/badline/kernal_trap/layout.rb +112 -0
  41. data/lib/badline/kernal_trap/load.rb +16 -27
  42. data/lib/badline/kernal_trap/routine.rb +11 -27
  43. data/lib/badline/kernal_trap/save.rb +13 -23
  44. data/lib/badline/kernal_trap/serial.rb +21 -19
  45. data/lib/badline/kernal_trap.rb +1 -0
  46. data/lib/badline/keyboard.rb +17 -14
  47. data/lib/badline/machine.rb +60 -0
  48. data/lib/badline/media/vic20_basic.rb +54 -0
  49. data/lib/badline/media/vic20_cartridge.rb +74 -0
  50. data/lib/badline/media/vic20_media.rb +132 -0
  51. data/lib/badline/media.rb +32 -18
  52. data/lib/badline/options/event.rb +5 -3
  53. data/lib/badline/options/help.rb +11 -3
  54. data/lib/badline/options/table.rb +11 -1
  55. data/lib/badline/options/validation.rb +55 -4
  56. data/lib/badline/options.rb +33 -33
  57. data/lib/badline/roms/README +27 -0
  58. data/lib/badline/roms/vic20/basic.rom +0 -0
  59. data/lib/badline/roms/vic20/character.rom +0 -0
  60. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  61. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  62. data/lib/badline/snapshot/container.rb +2 -0
  63. data/lib/badline/snapshot/image.rb +15 -0
  64. data/lib/badline/snapshot/state.rb +4 -3
  65. data/lib/badline/snapshot.rb +10 -0
  66. data/lib/badline/storage/crt_file.rb +16 -5
  67. data/lib/badline/storage/disk_image.rb +3 -0
  68. data/lib/badline/storage/host_directory.rb +3 -0
  69. data/lib/badline/storage/t64.rb +3 -0
  70. data/lib/badline/storage/tap.rb +38 -4
  71. data/lib/badline/storage/unavailable.rb +2 -0
  72. data/lib/badline/timing.rb +12 -0
  73. data/lib/badline/version.rb +1 -1
  74. data/lib/badline/via/shift_register.rb +43 -16
  75. data/lib/badline/vic/bank.rb +53 -16
  76. data/lib/badline/vic/registers.rb +20 -4
  77. data/lib/badline/vic.rb +34 -15
  78. data/lib/badline/vic20/attachments.rb +103 -0
  79. data/lib/badline/vic20/bus.rb +308 -0
  80. data/lib/badline/vic20/cpu.rb +10 -0
  81. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  82. data/lib/badline/vic20/port_wiring.rb +50 -0
  83. data/lib/badline/vic20/saved_state.rb +152 -0
  84. data/lib/badline/vic20/sound/output.rb +83 -0
  85. data/lib/badline/vic20/sound.rb +233 -0
  86. data/lib/badline/vic20/user_via_ports.rb +47 -0
  87. data/lib/badline/vic20/vic/painter.rb +239 -0
  88. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  89. data/lib/badline/vic20/vic/text_window.rb +71 -0
  90. data/lib/badline/vic20/vic.rb +267 -0
  91. data/lib/badline/vic20.rb +220 -0
  92. metadata +28 -2
  93. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ # What plugs into the VIC-20 besides its chips: cartridge ROM in the
6
+ # expansion blocks, the disk device 8 serves through the KERNAL traps,
7
+ # and a true 1541 on the serial bus.
8
+ module Attachments
9
+ # The cycle by which a 1541 switched on with the machine has run its
10
+ # DOS's reset, the RAM test and the ROM checksum, and waits on the
11
+ # bus, with a margin: about 1.1 million of the VIC-20's cycles.
12
+ DRIVE_BOOT_CYCLES = 1_300_000
13
+
14
+ # Where BASIC's program starts, from TXTTAB, which the KERNAL sets at
15
+ # boot by the RAM it finds: $1001 unexpanded, $0401 with the 3K
16
+ # expansion and $1201 with 8K or more in BLK1.
17
+ def basic_start = ram.peek16(0x2b)
18
+
19
+ # Puts a cartridge's ROM chips, each a CRTFile::Chip with its address
20
+ # and bytes, into their blocks, and switches the machine off and on,
21
+ # as a cartridge goes in with the power off. The KERNAL then starts
22
+ # the cartridge in BLK5 that signs itself with A0CBM.
23
+ def attach_cartridge(chips)
24
+ chips.each { |chip| @bus.map_rom(chip.address, chip.data) }
25
+ power_cycle!
26
+ end
27
+
28
+ # Puts the storage in device 8. Mounting again swaps the disk at any
29
+ # point while the machine runs: the drive keeps its RAM and its
30
+ # status.
31
+ def mount(storage)
32
+ return @drive.insert(storage) if @drive
33
+
34
+ @drive = KernalTrap::Drive.new(storage)
35
+ layout = KernalTrap::VIC20_LAYOUT
36
+ load_trap = KernalTrap::Load.new(cpu:, bus:, layout:, drive: @drive)
37
+ cpu.install_trap(layout.load) { load_trap.call }
38
+ @serial_trap = KernalTrap::Serial.new(cpu:, bus:, layout:, drive: @drive, device: serial_trap_device).install
39
+ save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus:, layout:, drive: @drive)
40
+ cpu.install_trap(layout.save) { save_trap.call }
41
+ end
42
+
43
+ # Takes device 8's mounted storage out, and with it the LOAD, SAVE and
44
+ # serial traps, so the KERNAL's routines go out over the serial bus.
45
+ # Mounting again starts a new drive, with its RAM cleared.
46
+ def unmount
47
+ return unless @drive
48
+
49
+ cpu.remove_trap(KernalTrap::VIC20_LAYOUT.load)
50
+ cpu.remove_trap(KernalTrap::VIC20_LAYOUT.save)
51
+ @serial_trap.device = nil
52
+ @serial_trap = nil
53
+ @save_trap = nil
54
+ @drive = nil
55
+ end
56
+
57
+ # Whether device 8 serves a disk or directory through the traps.
58
+ def mounted? = !@drive.nil?
59
+
60
+ # Plugs in a Drive1541, which then runs alongside the VIC-20 on its own
61
+ # clock and talks to it over the serial bus. The serial traps stop
62
+ # answering the drive's device number, so the KERNAL's TALK, LISTEN
63
+ # and byte transfers reach the drive. The LOAD and SAVE traps stay,
64
+ # and still serve a mounted image.
65
+ #
66
+ # A drive that powers on with the machine takes longer to boot than
67
+ # the KERNAL, and doesn't answer the bus until it has, so the
68
+ # machine's on_init handlers wait for it too.
69
+ def attach_drive1541(drive)
70
+ @init_threshold = [@init_threshold, DRIVE_BOOT_CYCLES].max if @cycles < @init_threshold
71
+ @iec_bus.detach(@drive1541) if @drive1541
72
+ drive.host_clock_hz = timing.clock_hz
73
+ @drive1541 = drive
74
+ drive.connect(@iec_bus)
75
+ @serial_trap&.device = serial_trap_device
76
+ end
77
+
78
+ # Unplugs the Drive1541, leaving the serial bus with nothing on it.
79
+ def detach_drive1541
80
+ return unless @drive1541
81
+
82
+ @iec_bus.detach(@drive1541)
83
+ @drive1541 = nil
84
+ @serial_trap&.device = serial_trap_device
85
+ end
86
+
87
+ # Records what the KERNAL prints through CHROUT (ChroutTrap).
88
+ def capture_output
89
+ @capture_output ||= ChroutTrap.new(cpu:, bus:, layout: KernalTrap::VIC20_LAYOUT).tap do |trap|
90
+ cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ # The device number the serial traps answer: device 8, unless a true
97
+ # drive is on the bus as device 8.
98
+ def serial_trap_device
99
+ @drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,308 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ # The CPU's bus:
6
+ #
7
+ # $0000-$03FF - 1K of RAM
8
+ # $0400-$0FFF - RAM1-3, 3K of expansion RAM, or nothing
9
+ # $1000-$1FFF - 4K of RAM
10
+ # $2000-$7FFF - BLK1, BLK2 and BLK3, 8K of expansion RAM each, or nothing
11
+ # $8000-$8FFF - the character ROM
12
+ # $9000-$93FF - I/O 0: the VIC and both VIAs (see IO0)
13
+ # $9400-$97FF - colour RAM, 1K by four bits
14
+ # $9800-$9FFF - I/O 2 and I/O 3, for cartridges
15
+ # $A000-$BFFF - BLK5, 8K of expansion RAM, or nothing
16
+ # $C000-$DFFF - the BASIC ROM
17
+ # $E000-$FFFF - the KERNAL ROM
18
+ #
19
+ # The VIC reaches $0000-$1FFF and $8000-$9FFF, the pages where A13 and
20
+ # A14 are both low, over the V-bus, and the CPU reaches them through it
21
+ # too. Elsewhere the CPU is on its own side of the bus.
22
+ #
23
+ # Each side's data lines hold the last byte they carried, so a read
24
+ # from an empty spot finds that byte: the V-bus's in the 3K hole, and
25
+ # the CPU's own in the empty blocks, I/O 2 and I/O 3. Colour RAM drives
26
+ # the low four lines only, and the V-bus's high four come through.
27
+ # Writes to ROM and to empty spots go nowhere.
28
+ class Bus
29
+ include Addressable
30
+
31
+ # The pages each expansion block covers, as [first page, count].
32
+ BLOCKS = {
33
+ ram123: [0x04, 0x0c], blk1: [0x20, 0x20], blk2: [0x40, 0x20], blk3: [0x60, 0x20], blk5: [0xa0, 0x20]
34
+ }.freeze
35
+
36
+ # The blocks each of the usual RAM expansions fills, and every block,
37
+ # which is what xvic's -memory all fits.
38
+ RAM_CONFIGURATIONS = {
39
+ unexpanded: [],
40
+ "3k": %i[ram123],
41
+ "8k": %i[blk1],
42
+ "16k": %i[blk1 blk2],
43
+ "24k": %i[blk1 blk2 blk3],
44
+ "32k": %i[blk1 blk2 blk3 blk5],
45
+ all: %i[ram123 blk1 blk2 blk3 blk5]
46
+ }.freeze
47
+
48
+ # RAM, internal and expansion alike, powers on in alternating $FF
49
+ # and $00 bytes, $FF at the even addresses, the alternation most of
50
+ # the machines in VIC20/raminitpattern/readme.txt show. Colour RAM
51
+ # powers on at zero. See doc/pinned-behaviour.md.
52
+ RAM_POWER_ON = Array.new(0xc000) { |addr| addr.even? ? 0xff : 0x00 }.freeze
53
+
54
+ # The last byte on the V-bus's data lines, which an empty spot on
55
+ # the VIC's side reads back.
56
+ class VideoOpenBus
57
+ def initialize(bus)
58
+ @bus = bus
59
+ end
60
+
61
+ def peek(_addr) = @bus.video_data
62
+ def poke(_addr, _value); end
63
+ end
64
+
65
+ # The last byte the CPU moved, which an empty spot on the CPU's side
66
+ # reads back.
67
+ class OpenBus
68
+ def initialize(bus)
69
+ @bus = bus
70
+ end
71
+
72
+ def peek(_addr) = @bus.data
73
+ def poke(_addr, _value); end
74
+ end
75
+
76
+ # Colour RAM: four bits a cell, under the V-bus's high four lines.
77
+ class ColorRAM
78
+ # The 1,024 cells, which the VIC reads its colour nibbles from.
79
+ attr_reader :cells
80
+
81
+ def initialize(bus)
82
+ @bus = bus
83
+ @cells = Array.new(0x400, 0)
84
+ end
85
+
86
+ def peek(addr) = (@bus.video_data & 0xf0) | @cells[addr & 0x3ff]
87
+
88
+ def poke(addr, value)
89
+ @cells[addr & 0x3ff] = value & 0x0f
90
+ end
91
+
92
+ # The four bits the cell at +addr+ stores.
93
+ def nibble(addr) = @cells[addr & 0x3ff]
94
+ end
95
+
96
+ # I/O 0, $9000-$93FF. The VIC answers in $9000-$90FF, its sixteen
97
+ # registers repeated. The VIAs answer all through I/O 0, on the
98
+ # address lines alone: A4 selects VIA 1 and A5 selects VIA 2. Where
99
+ # more than one chip answers a read, each pulls the lines it drives
100
+ # low, so the CPU reads the AND of them, and a write reaches them
101
+ # all. Where none answers, the CPU reads the V-bus's last byte: the
102
+ # VIC's fetch in a cycle it fetches in, as the real 6561E's dump of
103
+ # VIC20/split-tests/timing shows at $9100 and $9200.
104
+ class IO0
105
+ def initialize(bus, vic:, via1:, via2:)
106
+ @bus = bus
107
+ @vic = vic
108
+ @via1 = via1
109
+ @via2 = via2
110
+ end
111
+
112
+ def peek(addr)
113
+ vic = addr < 0x9100
114
+ return @bus.video_data unless vic || addr.anybits?(0x30)
115
+
116
+ value = vic ? @vic.peek(addr) : 0xff
117
+ value &= @via1.peek(addr) if addr.anybits?(0x10)
118
+ value &= @via2.peek(addr) if addr.anybits?(0x20)
119
+ value
120
+ end
121
+
122
+ def poke(addr, value)
123
+ @vic.poke(addr, value) if addr < 0x9100
124
+ return unless addr.anybits?(0x30)
125
+
126
+ @via1.poke(addr, value) if addr.anybits?(0x10)
127
+ @via2.poke(addr, value) if addr.anybits?(0x20)
128
+ @bus.via_written
129
+ end
130
+ end
131
+
132
+ # VICE's debug cartridge register at $910F, a spot in I/O 0 that
133
+ # nothing answers. A write there calls the handler with the byte, the
134
+ # exit code the VICE testprogs report. Every other access falls
135
+ # through to I/O 0.
136
+ class DebugRegister
137
+ ADDRESS = 0x910f
138
+
139
+ def initialize(io0, &handler)
140
+ @io0 = io0
141
+ @handler = handler
142
+ end
143
+
144
+ def peek(addr) = @io0.peek(addr)
145
+
146
+ def poke(addr, value)
147
+ return @handler.call(value) if addr == ADDRESS
148
+
149
+ @io0.poke(addr, value)
150
+ end
151
+ end
152
+
153
+ attr_reader :ram, :color_ram, :blocks
154
+
155
+ # The last byte the CPU read or wrote.
156
+ attr_reader :data
157
+
158
+ # The last byte on the V-bus.
159
+ attr_reader :video_data
160
+
161
+ # +vic+, +via1+ and +via2+ are the chips in I/O 0, each answering
162
+ # peek and poke with the CPU's address. +blocks+ names the expansion
163
+ # blocks that hold RAM (BLOCKS), and +kernal+ the KERNAL ROM's file.
164
+ def initialize(vic:, via1:, via2:, blocks: [], kernal: "vic20/kernal-pal.rom")
165
+ addressable_at(0, length: 2**16)
166
+ @ram = Memory.new(RAM_POWER_ON, length: 0xc000, start: 0)
167
+ @color_ram = ColorRAM.new(self)
168
+ @character_rom = ROM.load("vic20/character.rom", 0x8000)
169
+ @basic_rom = ROM.load("vic20/basic.rom", 0xc000)
170
+ @kernal_rom = ROM.load(kernal, 0xe000)
171
+ @io0 = IO0.new(self, vic:, via1:, via2:)
172
+ @open_bus = OpenBus.new(self)
173
+ @video_open_bus = VideoOpenBus.new(self)
174
+ @data = @video_data = 0
175
+ @read_pages = Array.new(256, @open_bus)
176
+ @write_pages = Array.new(256, @open_bus)
177
+ @roms = []
178
+ @debug_register = nil
179
+ @via_write_handler = nil
180
+ self.blocks = blocks
181
+ end
182
+
183
+ # Puts a cartridge's ROM chip of +bytes+, whole pages of them, at
184
+ # +address+. It answers reads on the pages it covers in place of
185
+ # whatever was there, and writes to them go nowhere.
186
+ def map_rom(address, bytes)
187
+ @roms << ROM.new(bytes, length: bytes.length, start: address)
188
+ map_pages
189
+ end
190
+
191
+ # Calls the block after each write to a VIA, which may have moved the
192
+ # lines its ports drive.
193
+ def on_via_write(&handler)
194
+ @via_write_handler = handler
195
+ end
196
+
197
+ def via_written
198
+ @via_write_handler&.call
199
+ end
200
+
201
+ # Calls the block with each byte written to $910F (DebugRegister).
202
+ def install_debug_register(&)
203
+ @debug_register = DebugRegister.new(@io0, &)
204
+ map_pages
205
+ end
206
+
207
+ # Fills the expansion blocks named in +names+ with RAM and empties
208
+ # the rest.
209
+ def blocks=(names)
210
+ @blocks = names.dup.freeze
211
+ map_pages
212
+ end
213
+
214
+ def power_on!
215
+ @ram.clear!(RAM_POWER_ON)
216
+ end
217
+
218
+ # The RAM, the colour RAM, the byte each side's data lines hold and the
219
+ # cartridge's ROM chips, for a snapshot. The expansion blocks are how
220
+ # the machine was built.
221
+ def save_state(out)
222
+ out.marker("VIC20 BUS")
223
+ @ram.save_state(out)
224
+ out.blob(@color_ram.cells).int(@data).int(@video_data).int(@roms.length)
225
+ @roms.each do |rom|
226
+ out.int(rom.start)
227
+ rom.save_state(out)
228
+ end
229
+ end
230
+
231
+ def load_state(input)
232
+ input.marker("VIC20 BUS")
233
+ @ram.load_state(input)
234
+ input.blob_into(@color_ram.cells)
235
+ @data = input.int
236
+ @video_data = input.int
237
+ chips = Array.new(input.int) { [input.int, input.blob] }
238
+ @roms = []
239
+ chips.each { |chip| @roms << ROM.new(chip[1], length: chip[1].length, start: chip[0]) }
240
+ map_pages
241
+ end
242
+
243
+ # Whether +block+ (BLOCKS) holds RAM.
244
+ def ram?(block) = @blocks.include?(block)
245
+
246
+ # The VIC's fetch from +address+, its own 14-bit address, which leaves
247
+ # the byte on the V-bus. The VIC's A13 is the CPU's A15 inverted, so
248
+ # $2000-$3FFF are the CPU's $0000-$1FFF and $0000-$1FFF its
249
+ # $8000-$9FFF. It reaches the internal RAM and the character ROM, and
250
+ # no expansion RAM. Elsewhere nothing drives the V-bus's data lines,
251
+ # and the VIC reads the byte they hold, as does a fetch from colour
252
+ # RAM, which drives only the colour bus while the VIC fetches.
253
+ def video_fetch(address)
254
+ value =
255
+ if address >= 0x2000
256
+ internal = address & 0x1fff
257
+ internal < 0x0400 || internal >= 0x1000 ? @ram.peek(internal) : @video_data
258
+ elsif address < 0x1000
259
+ @character_rom.peek(address | 0x8000)
260
+ else
261
+ @video_data
262
+ end
263
+ @video_data = value
264
+ end
265
+
266
+ def peek(addr)
267
+ value = @read_pages[addr >> 8].peek(addr)
268
+ @video_data = value if addr.nobits?(0x6000)
269
+ @data = value
270
+ end
271
+
272
+ def poke(addr, value)
273
+ @data = value
274
+ @video_data = value if addr.nobits?(0x6000)
275
+ @write_pages[addr >> 8].poke(addr, value)
276
+ end
277
+
278
+ private
279
+
280
+ def map_pages
281
+ map(@open_bus, 0x00, 0x100)
282
+ @read_pages.fill(@video_open_bus, 0x04, 0x0c)
283
+ map(@ram, 0x00, 0x04)
284
+ map(@ram, 0x10, 0x10)
285
+ BLOCKS.each { |block, pages| map(@ram, pages[0], pages[1]) if ram?(block) }
286
+ @read_pages.fill(@character_rom, 0x80, 0x10)
287
+ map(@io0, 0x90, 0x04)
288
+ map(@color_ram, 0x94, 0x04)
289
+ @read_pages.fill(@basic_rom, 0xc0, 0x20)
290
+ @read_pages.fill(@kernal_rom, 0xe0, 0x20)
291
+ @roms.each { |rom| map_rom_pages(rom) }
292
+ @write_pages[0x91] = @debug_register if @debug_register
293
+ end
294
+
295
+ def map_rom_pages(rom)
296
+ first = rom.start >> 8
297
+ count = rom.length >> 8
298
+ @read_pages.fill(rom, first, count)
299
+ @write_pages.fill(@open_bus, first, count)
300
+ end
301
+
302
+ def map(chip, first, count)
303
+ @read_pages.fill(chip, first, count)
304
+ @write_pages.fill(chip, first, count)
305
+ end
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ # The VIC-20's 6502, on the Vic20::Bus.
6
+ class CPU
7
+ include Badline::CPU::Core
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ # What VIA 2's ports read from outside the chip: the key matrix, with
6
+ # port B driving its columns and port A reading its rows. The KERNAL's
7
+ # SCNKEY ($EB1E) writes a column pattern to $9120 and reads the rows
8
+ # back from $9121, and its key table ($EC5E) puts each key at column
9
+ # times 8 plus row.
10
+ #
11
+ # PB7 is also the joystick's right switch. A held switch pulls that
12
+ # column line low before the matrix settles, so it reads as a key down
13
+ # in every row whose key in column 7 is held too. PB3 doubles as the
14
+ # cassette write line, which only the datasette listens to.
15
+ class KeyboardVIAPorts
16
+ # One row of keys per port A line, in port B column order. The keys
17
+ # are the C64's, under the same names, wired to other lines.
18
+ MATRIX = [
19
+ %i[1 left control run_stop space cbm q 2],
20
+ %i[3 w a lshift z s e 4],
21
+ %i[5 r d x c f t 6],
22
+ %i[7 y g v b h u 8],
23
+ %i[9 i j n m k o 0],
24
+ %i[+ p l , . : @ -],
25
+ %i[£ * ; / rshift = up clr_home],
26
+ %i[delete return cursor_h cursor_v f1 f3 f5 f7]
27
+ ].freeze
28
+
29
+ attr_reader :keyboard, :joystick
30
+
31
+ def initialize(keyboard:, joystick:)
32
+ @keyboard = keyboard
33
+ @joystick = joystick
34
+ @via = nil
35
+ end
36
+
37
+ # The VIA these ports hang off, whose other port's output the matrix
38
+ # settles against.
39
+ def connect(via)
40
+ @via = via
41
+ end
42
+
43
+ def read_a(lines) = @keyboard.scan(lines, columns(@via.port_b_output)).first
44
+
45
+ def read_b(lines) = @keyboard.scan(@via.port_a_output, columns(lines)).last
46
+
47
+ private
48
+
49
+ # The joystick's right switch is bit 3 of its port bits, and pulls PB7.
50
+ def columns(lines) = lines & (0x7f | ((@joystick.port_bits & 0x08) << 4))
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ # The datasette and the serial bus, wired to the VIAs.
6
+ #
7
+ # The serial bus's lines go out through inverters, so an output that
8
+ # drives high pulls its line low: ATN from VIA 1's PA7, the clock from
9
+ # VIA 2's CA2 and the data line from VIA 2's CB2. An output left as an
10
+ # input floats high, and pulls its line too, as after a reset. CLK and
11
+ # DATA come back on VIA 1's PA0 and PA1 (UserVIAPorts). The machine
12
+ # pushes the lines it pulls into the bus, as the C64's CIA 2 port A
13
+ # bits, after each write to a VIA that moves them.
14
+ #
15
+ # The datasette's read line pulses VIA 2's CA1, its sense switch reads
16
+ # on VIA 1's PA6, its motor runs while VIA 1's CA2 is low, and VIA 2's
17
+ # PB3 drives its write line, the same pin as a keyboard column.
18
+ module PortWiring
19
+ # The lines the machine pulls, as IECBus host lines.
20
+ def serial_lines
21
+ lines = 0
22
+ lines |= IECBus::HOST_ATN_OUT if @via1.port_a_output.anybits?(0x80)
23
+ lines |= IECBus::HOST_CLK_OUT if @via2.ca2_output
24
+ lines |= IECBus::HOST_DATA_OUT if @via2.cb2_output
25
+ lines
26
+ end
27
+
28
+ private
29
+
30
+ def wire_ports
31
+ @datasette.on_flag { tape_pulse }
32
+ @bus.on_via_write { via_written }
33
+ via_written
34
+ end
35
+
36
+ def via_written
37
+ lines = serial_lines
38
+ @iec_bus.host_lines = lines unless lines == @iec_bus.host_lines
39
+ @datasette.motor = !@via1.ca2_output
40
+ @datasette.write_line = @via2.port_b_output.anybits?(0x08)
41
+ end
42
+
43
+ # A falling edge on the read line, and the line back up.
44
+ def tape_pulse
45
+ @via2.ca1 = false
46
+ @via2.ca1 = true
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ MARKER = "VIC20"
6
+
7
+ # The RAM expansion of the machine a State from #snapshot was taken
8
+ # of, a key of Bus::RAM_CONFIGURATIONS.
9
+ def self.setup(state)
10
+ input = Snapshot::StateReader.new(state)
11
+ input.marker(MARKER)
12
+ input.check_stamp
13
+ SavedState.read_ram(input)
14
+ end
15
+
16
+ # A new machine, built as the one a State from #snapshot was taken of,
17
+ # at that state.
18
+ def self.restored(state) = new(ram: setup(state)).apply_state(state)
19
+
20
+ # The whole machine's state for a snapshot, as Computer's: the
21
+ # machine's own latches, the bus with its RAM and cartridge ROM, the
22
+ # VIC with its sound, both VIAs, the CPU, the datasette and its tape,
23
+ # the disk device 8 serves through the traps, and a true 1541. The
24
+ # serial lines go back into the bus from the restored VIAs. What the
25
+ # host holds stays out: the keyboard, the joystick, RESTORE, the traps
26
+ # and callbacks, whether the display renders, whether the sound
27
+ # records, and whether the datasette records.
28
+ module SavedState
29
+ RAM_CONFIGURATIONS = Bus::RAM_CONFIGURATIONS.keys.freeze
30
+
31
+ def self.read_ram(input)
32
+ RAM_CONFIGURATIONS.fetch(input.int)
33
+ rescue IndexError
34
+ raise Snapshot::FormatError, "the state names a VIC-20 RAM expansion badline doesn't know"
35
+ end
36
+
37
+ # How many more on_init handlers the machine a State was taken of had
38
+ # yet to run than this one has. Nil until the machine is restored.
39
+ attr_reader :init_handlers_lost
40
+
41
+ def snapshot
42
+ out = Snapshot::StateWriter.new
43
+ save_state(out)
44
+ out.state
45
+ end
46
+
47
+ # Takes the machine back to a State from #snapshot, trying it on a new
48
+ # machine first, so one that fails leaves this machine as it was.
49
+ # Raises Snapshot::FormatError for a state of another machine or RAM
50
+ # expansion.
51
+ def restore(state)
52
+ check_ram(Vic20.setup(state))
53
+ Vic20.restored(state)
54
+ apply_state(state)
55
+ end
56
+
57
+ # Puts a State into the machine without trying it on a new one first.
58
+ def apply_state(state)
59
+ input = Snapshot::StateReader.new(state)
60
+ load_state(input)
61
+ raise Snapshot::FormatError, "the state goes on past the machine" unless input.finished?
62
+
63
+ self
64
+ end
65
+
66
+ def save_state(out)
67
+ out.marker(MARKER).stamp
68
+ out.int(RAM_CONFIGURATIONS.index(@ram_configuration))
69
+ out.int(@cycles).boolean(@nmi_asserted).boolean(!@pending_keys.nil?)
70
+ out.ints(@pending_keys) if @pending_keys
71
+ out.int(@cycles <= @init_threshold ? @init_handlers.length : 0)
72
+ @bus.save_state(out)
73
+ @vic.save_state(out)
74
+ @sound.save_state(out)
75
+ @via1.save_state(out)
76
+ @via2.save_state(out)
77
+ @cpu.save_state(out)
78
+ @datasette.save_state(out)
79
+ save_trap_drive(out)
80
+ save_drive1541(out)
81
+ end
82
+
83
+ def load_state(input)
84
+ input.marker(MARKER)
85
+ input.check_stamp
86
+ check_ram(SavedState.read_ram(input))
87
+ @cycles = input.int
88
+ @nmi_asserted = input.boolean?
89
+ @pending_keys = nil
90
+ @pending_keys = input.ints if input.boolean?
91
+ @init_handlers_lost = [input.int - @init_handlers.length, 0].max
92
+ @bus.load_state(input)
93
+ @vic.load_state(input)
94
+ @sound.load_state(input)
95
+ @via1.load_state(input)
96
+ @via2.load_state(input)
97
+ @cpu.load_state(input)
98
+ via_written
99
+ @datasette.load_state(input)
100
+ load_trap_drive(input)
101
+ load_drive1541(input)
102
+ end
103
+
104
+ private
105
+
106
+ # The disk device 8 serves through the traps, with its channels.
107
+ def save_trap_drive(out)
108
+ out.boolean(!@drive.nil?)
109
+ return unless @drive
110
+
111
+ @drive.save_state(out)
112
+ @serial_trap.save_state(out)
113
+ @save_trap.save_state(out)
114
+ end
115
+
116
+ def load_trap_drive(input)
117
+ return unmount unless input.boolean?
118
+
119
+ storage = Storage.reopen(input)
120
+ unmount
121
+ mount(storage)
122
+ @drive.load_state(input)
123
+ @serial_trap.load_state(input)
124
+ @save_trap.load_state(input)
125
+ end
126
+
127
+ # A true 1541 on the serial bus, with its device number.
128
+ def save_drive1541(out)
129
+ out.boolean(!@drive1541.nil?)
130
+ return unless @drive1541
131
+
132
+ out.int(@drive1541.device)
133
+ @drive1541.save_state(out)
134
+ end
135
+
136
+ def load_drive1541(input)
137
+ return detach_drive1541 unless input.boolean?
138
+
139
+ device = input.int
140
+ detach_drive1541 if @drive1541 && @drive1541.device != device
141
+ attach_drive1541(Drive1541.new(device:)) unless @drive1541
142
+ @drive1541.load_state(input)
143
+ end
144
+
145
+ def check_ram(ram)
146
+ return if ram == @ram_configuration
147
+
148
+ raise Snapshot::FormatError, "the state is of a VIC-20 with #{ram} RAM, not #{@ram_configuration}"
149
+ end
150
+ end
151
+ end
152
+ end