badline 0.9.0 → 0.10.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +39 -4
  5. data/doc/library.md +15 -1
  6. data/doc/pinned-behaviour.md +146 -0
  7. data/doc/snapshots.md +17 -3
  8. data/lib/badline/address_bus/fittings.rb +38 -0
  9. data/lib/badline/address_bus/pla.rb +160 -0
  10. data/lib/badline/address_bus/roms.rb +12 -5
  11. data/lib/badline/address_bus.rb +15 -90
  12. data/lib/badline/chrout_trap.rb +4 -3
  13. data/lib/badline/computer/attachments.rb +47 -7
  14. data/lib/badline/computer/saved_state.rb +1 -0
  15. data/lib/badline/computer.rb +52 -37
  16. data/lib/badline/control_ports.rb +10 -3
  17. data/lib/badline/core.rb +3 -0
  18. data/lib/badline/datasette.rb +39 -1
  19. data/lib/badline/drive1541/serial_port.rb +5 -8
  20. data/lib/badline/drive1541/sleep.rb +5 -6
  21. data/lib/badline/drive1541.rb +3 -3
  22. data/lib/badline/frontend/app.rb +62 -64
  23. data/lib/badline/frontend/boot.rb +33 -8
  24. data/lib/badline/frontend/controls.rb +12 -6
  25. data/lib/badline/frontend/drive_led.rb +9 -10
  26. data/lib/badline/frontend/frame_rate.rb +4 -4
  27. data/lib/badline/frontend/machine_window.rb +87 -0
  28. data/lib/badline/frontend/menu_media.rb +49 -24
  29. data/lib/badline/frontend/menu_pages.rb +23 -40
  30. data/lib/badline/frontend/menu_ports.rb +69 -0
  31. data/lib/badline/frontend/pacer.rb +6 -5
  32. data/lib/badline/frontend/pause_menu.rb +30 -33
  33. data/lib/badline/frontend/player_screen.rb +1 -1
  34. data/lib/badline/frontend/player_window.rb +1 -1
  35. data/lib/badline/frontend/screen.rb +39 -37
  36. data/lib/badline/frontend/sid_view.rb +1 -1
  37. data/lib/badline/frontend/sound.rb +11 -9
  38. data/lib/badline/frontend/timeline.rb +36 -5
  39. data/lib/badline/frontend.rb +2 -0
  40. data/lib/badline/iec_bus.rb +16 -28
  41. data/lib/badline/kernal_trap/drive.rb +3 -0
  42. data/lib/badline/kernal_trap/file.rb +1 -1
  43. data/lib/badline/kernal_trap/layout.rb +112 -0
  44. data/lib/badline/kernal_trap/load.rb +16 -27
  45. data/lib/badline/kernal_trap/routine.rb +11 -27
  46. data/lib/badline/kernal_trap/save.rb +13 -23
  47. data/lib/badline/kernal_trap/serial.rb +21 -19
  48. data/lib/badline/kernal_trap.rb +1 -0
  49. data/lib/badline/keyboard.rb +28 -14
  50. data/lib/badline/machine.rb +60 -0
  51. data/lib/badline/media/vic20_basic.rb +54 -0
  52. data/lib/badline/media/vic20_cartridge.rb +74 -0
  53. data/lib/badline/media/vic20_media.rb +132 -0
  54. data/lib/badline/media.rb +32 -18
  55. data/lib/badline/model.rb +29 -12
  56. data/lib/badline/options/event.rb +5 -3
  57. data/lib/badline/options/help.rb +11 -3
  58. data/lib/badline/options/table.rb +12 -2
  59. data/lib/badline/options/validation.rb +55 -4
  60. data/lib/badline/options.rb +33 -33
  61. data/lib/badline/roms/README +51 -0
  62. data/lib/badline/roms/c64gs.rom +0 -0
  63. data/lib/badline/roms/kernal-pet64.rom +0 -0
  64. data/lib/badline/roms/vic20/basic.rom +0 -0
  65. data/lib/badline/roms/vic20/character.rom +0 -0
  66. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  67. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  68. data/lib/badline/snapshot/container.rb +2 -0
  69. data/lib/badline/snapshot/image.rb +15 -0
  70. data/lib/badline/snapshot/setup.rb +15 -10
  71. data/lib/badline/snapshot/state.rb +5 -3
  72. data/lib/badline/snapshot/vice.rb +2 -1
  73. data/lib/badline/snapshot.rb +10 -0
  74. data/lib/badline/storage/crt_file.rb +16 -5
  75. data/lib/badline/storage/disk_image.rb +3 -0
  76. data/lib/badline/storage/host_directory.rb +3 -0
  77. data/lib/badline/storage/t64.rb +3 -0
  78. data/lib/badline/storage/tap.rb +38 -4
  79. data/lib/badline/storage/unavailable.rb +2 -0
  80. data/lib/badline/timing.rb +12 -0
  81. data/lib/badline/version.rb +1 -1
  82. data/lib/badline/via/shift_register.rb +43 -16
  83. data/lib/badline/vic/bank.rb +55 -17
  84. data/lib/badline/vic/registers.rb +20 -4
  85. data/lib/badline/vic.rb +48 -15
  86. data/lib/badline/vic20/attachments.rb +103 -0
  87. data/lib/badline/vic20/bus.rb +308 -0
  88. data/lib/badline/vic20/cpu.rb +10 -0
  89. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  90. data/lib/badline/vic20/port_wiring.rb +50 -0
  91. data/lib/badline/vic20/saved_state.rb +152 -0
  92. data/lib/badline/vic20/sound/output.rb +83 -0
  93. data/lib/badline/vic20/sound.rb +233 -0
  94. data/lib/badline/vic20/user_via_ports.rb +47 -0
  95. data/lib/badline/vic20/vic/painter.rb +239 -0
  96. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  97. data/lib/badline/vic20/vic/text_window.rb +71 -0
  98. data/lib/badline/vic20/vic.rb +267 -0
  99. data/lib/badline/vic20.rb +220 -0
  100. metadata +31 -2
  101. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
@@ -4,18 +4,25 @@ module Badline
4
4
  class AddressBus
5
5
  # The BASIC, character and KERNAL ROMs, and which KERNAL is fitted.
6
6
  module ROMs
7
- # The KERNAL ROMs a machine can be fitted with, by name: the C64's
8
- # and the SX-64's.
9
- KERNALS = { c64: "kernal.rom", sx64: "kernal-sx64.rom" }.freeze
7
+ # The KERNAL ROMs a machine can be fitted with, by name: the C64's,
8
+ # the SX-64's, the PET 64's and the C64GS's. The C64GS's is a 16K
9
+ # image of its BASIC and its KERNAL, and fits both.
10
+ KERNALS = {
11
+ c64: "kernal.rom", sx64: "kernal-sx64.rom", pet64: "kernal-pet64.rom", gs: "c64gs.rom"
12
+ }.freeze
10
13
 
11
14
  attr_reader :basic_rom, :character_rom, :kernal_rom, :kernal
12
15
 
13
- # Fits the KERNAL ROM KERNALS names `name`, in place of the one before.
16
+ # Fits the KERNAL ROM KERNALS names `name`, in place of the one before,
17
+ # with the BASIC of a 16K image or else the C64's.
14
18
  def kernal=(name)
15
19
  raise ArgumentError, "no KERNAL named #{name}" unless KERNALS.key?(name)
16
20
 
17
21
  @kernal = name
18
- @kernal_rom = ROM.load(KERNALS[name], 0xe000)
22
+ data = ROM.read(KERNALS[name])
23
+ basic = data.length == 0x4000 ? data[0, 0x2000] : ROM.read("basic.rom")
24
+ @basic_rom = ROM.new(basic, length: 0x2000, start: 0xa000)
25
+ @kernal_rom = ROM.new(data[-0x2000, 0x2000], length: 0x2000, start: 0xe000)
19
26
  update_overlays!
20
27
  end
21
28
 
@@ -3,8 +3,9 @@
3
3
  require "badline/address_bus/saved_state"
4
4
  require "badline/address_bus/sid_slots"
5
5
  require "badline/address_bus/extra_sids"
6
- require "badline/address_bus/ultimax_pages"
6
+ require "badline/address_bus/pla"
7
7
  require "badline/address_bus/roms"
8
+ require "badline/address_bus/fittings"
8
9
 
9
10
  module Badline
10
11
  # Memory layout:
@@ -34,9 +35,10 @@ module Badline
34
35
  # 0xE000-0xFFFF - KERNAL ROM / Cartridge ROM (high) - 8kb
35
36
  class AddressBus
36
37
  include Addressable
37
- include UltimaxPages
38
+ include PLA
38
39
  include ExtraSIDs
39
40
  include ROMs
41
+ include Fittings
40
42
 
41
43
  # I/O 1 and 2, and the Ultimax holes, with nothing on the bus. A read
42
44
  # picks up the byte the VIC fetched in the preceding phi1 half-cycle,
@@ -68,6 +70,7 @@ module Badline
68
70
  def initialize(sid_model: :mos6581, cia_model: :mos6526, vic_model: :mos6569, region: Region::PAL,
69
71
  ram_expansion: nil)
70
72
  @region = region
73
+ @board = :c64
71
74
  @ram = Memory.new(RAM_POWER_ON, length: 2**16, start: 0)
72
75
  @ram_expansion = RAMExpansion.build(ram_expansion, @ram) { update_overlays! }
73
76
  @cartridge = @reu = nil
@@ -79,7 +82,7 @@ module Badline
79
82
  @joystick1 = Joystick.new
80
83
  @joystick2 = Joystick.new
81
84
  @control_ports = ControlPorts.new(keyboard: @keyboard, joystick1: @joystick1, joystick2: @joystick2)
82
- @vic = VIC.new(self, model: vic_model, region:)
85
+ @vic = VIC.new(model: vic_model, region:)
83
86
  @cia1 = CIA.new(start: 0xdc00, peripheral: @control_ports, model: cia_model, region:)
84
87
  @cia2 = CIA.new(start: 0xdd00, model: cia_model, region:)
85
88
  @control_ports.port_a_source = @cia1
@@ -92,6 +95,8 @@ module Badline
92
95
  @datasette.on_sense_change { @io_port.value = port_value }
93
96
 
94
97
  @color_ram = ColorMemory.new(@vic)
98
+ @vic.vic_bank.connect(cia2: @cia2, color_ram: @color_ram)
99
+ @vic.vic_bank.map_character_rom(character_rom)
95
100
  @open_bus = OpenBus.new(@vic)
96
101
  @cpu_off_bus = false
97
102
 
@@ -199,98 +204,18 @@ module Badline
199
204
  # so reads and writes dispatch through per-page handler tables instead
200
205
  # of range checks.
201
206
  def update_overlays!
202
- @ultimax = @cartridge ? @cartridge.ultimax? : false
203
- @phi1_ultimax = @cartridge ? @cartridge.phi1_ultimax? : false
204
- @ram_expansion.map(@read_pages, @write_pages)
207
+ max = @board == :max
208
+ @ultimax = max || (@cartridge ? @cartridge.ultimax? : false)
209
+ @phi1_ultimax = max || (@cartridge ? @cartridge.phi1_ultimax? : false)
205
210
  @video_ram = @ram_expansion.video_ram
211
+ @vic.vic_bank.map(@video_ram, phi1_ultimax: @phi1_ultimax, ultimax: @ultimax, romh: @cartridge&.romh)
206
212
 
207
- @ultimax ? map_ultimax_pages : map_banked_pages
213
+ map_pla_pages
208
214
  @write_pages[0xff] = @reu.trigger.wrap(@write_pages[0xff]) if @reu
209
215
  end
210
216
 
211
- def map_banked_pages
212
- map_rom_overlays
213
- map_cartridge_ram
214
- @write_pages.fill(@cartridge.romh_writes, 0xe0, 0x20) if @cartridge&.romh_writes
215
-
216
- if io?
217
- map_io_pages
218
- elsif character?
219
- @read_pages.fill(character_rom, 0xd0, 0x10)
220
- end
221
- end
222
-
223
- # EXROM and GAME select the cartridge whether or not it has a chip
224
- # there, and an empty socket leaves the bus floating.
225
- def map_rom_overlays
226
- @read_pages.fill(@cartridge.roml || @open_bus, 0x80, 0x20) if roml?
227
- if romh?
228
- @read_pages.fill(@cartridge.romh || @open_bus, 0xa0, 0x20)
229
- elsif basic?
230
- @read_pages.fill(basic_rom, 0xa0, 0x20)
231
- end
232
- @read_pages.fill(kernal_rom, 0xe0, 0x20) if kernal?
233
- end
234
-
235
- # Cartridge RAM in the ROML or ROMH window decodes writes itself,
236
- # whatever the $01 lines say.
237
- def map_cartridge_ram
238
- return unless @cartridge&.exrom&.zero?
239
-
240
- map_cartridge_ram_bank(@cartridge.roml, 0x80)
241
- map_cartridge_ram_bank(@cartridge.romh, 0xa0) if @cartridge.game.zero?
242
- end
243
-
244
- def map_cartridge_ram_bank(bank, first_page)
245
- @write_pages.fill(bank, first_page, 0x20) if bank.is_a?(Cartridge::RAMBank)
246
- end
247
-
248
- def map_io_pages
249
- {
250
- vic => 0xd0..0xd3, sid => 0xd4..0xd7, color_ram => 0xd8..0xdb,
251
- cia1 => 0xdc..0xdc, cia2 => 0xdd..0xdd, @open_bus => 0xde..0xdf
252
- }.each do |chip, pages|
253
- pages.each { |p| @read_pages[p] = @write_pages[p] = chip }
254
- end
255
- @ram_expansion.map_io(@read_pages, @write_pages)
256
- @read_pages[0xd7] = @write_pages[0xd7] = @debug_register if @debug_register
257
- @read_pages[0xdf] = @write_pages[0xdf] = @reu if @reu
258
- map_cartridge_io if @cartridge
259
- map_extra_sids unless @sid_slots.empty?
260
- end
261
-
262
- def map_cartridge_io
263
- @write_pages.fill(@cartridge, 0xde, 2)
264
- @cartridge.readable_io_pages.each { |p| @read_pages[p] = @cartridge }
265
- end
266
-
267
- def basic?
268
- io_port.kernal? && io_port.basic? && game_high?
269
- end
270
-
271
- def game_high?
272
- @cartridge.nil? || @cartridge.game == 1
273
- end
274
-
275
- def roml?
276
- @cartridge&.exrom&.zero? && io_port.kernal? && io_port.basic?
277
- end
278
-
279
- def romh?
280
- @cartridge&.exrom&.zero? && @cartridge.game.zero? && io_port.kernal?
281
- end
282
-
283
- # In 16K mode LORAM alone leaves $d000 as RAM, where it still maps I/O.
284
- def character?
285
- (io_port.kernal? || (io_port.basic? && game_high?)) && !io_port.io?
286
- end
287
-
288
- def io?
289
- (io_port.basic? || io_port.kernal?) && io_port.io?
290
- end
291
-
292
- def kernal?
293
- io_port.kernal?
217
+ def map_ram_pages
218
+ @ram_expansion.map(@read_pages, @write_pages)
294
219
  end
295
220
  end
296
221
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Badline
4
4
  # Observe-only PC trap on the KERNAL CHROUT routine ($FFD2). Records each
5
- # character written while the KERNAL is banked in, then lets execution fall
5
+ # character written while the layout finds the KERNAL mapped in, then lets execution fall
6
6
  # through to the ROM, so the screen output is unaffected. Used for headless
7
7
  # capture of program output.
8
8
  class ChroutTrap
@@ -10,14 +10,15 @@ module Badline
10
10
 
11
11
  attr_reader :output
12
12
 
13
- def initialize(cpu:, bus:)
13
+ def initialize(cpu:, bus:, layout:)
14
14
  @cpu = cpu
15
15
  @bus = bus
16
+ @layout = layout
16
17
  @output = +""
17
18
  end
18
19
 
19
20
  def call
20
- @output << ascii(@cpu.a) if @bus.io_port.kernal?
21
+ @output << ascii(@cpu.a) if @layout.kernal_mapped?(@bus)
21
22
  end
22
23
 
23
24
  def inspect
@@ -22,11 +22,13 @@ module Badline
22
22
  return @drive.insert(storage) if @drive
23
23
 
24
24
  @drive = KernalTrap::Drive.new(storage)
25
- load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
26
- cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
27
- @serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive, device: serial_trap_device).install
28
- save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
29
- cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
25
+ layout = KernalTrap::C64_LAYOUT
26
+ load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, layout:, drive: @drive)
27
+ cpu.install_trap(layout.load) { load_trap.call }
28
+ @serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, layout:, drive: @drive,
29
+ device: serial_trap_device).install
30
+ save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, layout:, drive: @drive)
31
+ cpu.install_trap(layout.save) { save_trap.call }
30
32
  end
31
33
 
32
34
  # Takes device 8's mounted storage out, and with it the LOAD, SAVE and
@@ -35,8 +37,8 @@ module Badline
35
37
  def unmount
36
38
  return unless @drive
37
39
 
38
- cpu.remove_trap(KernalTrap::Load::ADDRESS)
39
- cpu.remove_trap(KernalTrap::Save::ADDRESS)
40
+ cpu.remove_trap(KernalTrap::C64_LAYOUT.load)
41
+ cpu.remove_trap(KernalTrap::C64_LAYOUT.save)
40
42
  @serial_trap.device = nil
41
43
  @serial_trap = nil
42
44
  @save_trap = nil
@@ -46,6 +48,24 @@ module Badline
46
48
  # Whether device 8 serves a disk or directory through the traps.
47
49
  def mounted? = !@drive.nil?
48
50
 
51
+ # The serial bus, which CIA 2's port A drives through the C64's own
52
+ # inverters and reads back on PA6 and PA7, with or without a drive on
53
+ # it.
54
+ attr_reader :iec_bus
55
+
56
+ # Plugs in a Drive1541, which then runs alongside the C64 on its own
57
+ # clock and talks to it over the serial bus. The serial traps stop
58
+ # answering the drive's device number, so the KERNAL's TALK, LISTEN and
59
+ # byte transfers reach the drive. The LOAD and SAVE traps stay, and
60
+ # still serve a mounted image.
61
+ def attach_drive1541(drive)
62
+ @iec_bus.detach(@drive1541) if @drive1541
63
+ drive.host_clock_hz = region.clock_hz
64
+ @drive1541 = drive
65
+ drive.connect(iec_bus)
66
+ @serial_trap&.device = serial_trap_device
67
+ end
68
+
49
69
  # Unplugs the Drive1541, leaving the serial bus with nothing on it.
50
70
  def detach_drive1541
51
71
  return unless @drive1541
@@ -57,6 +77,26 @@ module Badline
57
77
 
58
78
  private
59
79
 
80
+ # The device number the serial traps answer: device 8, unless a true
81
+ # drive is on the bus as device 8.
82
+ def serial_trap_device
83
+ @drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
84
+ end
85
+
86
+ # CIA 2's port A drives the serial bus, which reads CLK and DATA back
87
+ # into it.
88
+ def plug_serial_bus
89
+ @iec_bus = IECBus.new
90
+ @cia2.peripheral = @iec_bus
91
+ @cia2.on_port_a_write { push_serial_lines }
92
+ push_serial_lines
93
+ end
94
+
95
+ # Pushes the lines CIA 2's port A pulls into the serial bus.
96
+ def push_serial_lines
97
+ @iec_bus.host_lines = @cia2.port_a_lines
98
+ end
99
+
60
100
  def save_cartridge(out)
61
101
  cartridge = address_bus.cartridge
62
102
  out.boolean(!cartridge.nil?)
@@ -97,6 +97,7 @@ module Badline
97
97
  @init_handlers_lost = [input.int - @init_handlers.length, 0].max
98
98
  load_cartridge(input)
99
99
  address_bus.load_state(input)
100
+ push_serial_lines
100
101
  @cpu.load_state(input)
101
102
  load_trap_drive(input)
102
103
  load_serial_bus(input)
@@ -12,6 +12,12 @@ module Badline
12
12
 
13
13
  attr_reader :address_bus, :cpu, :cycles, :drive1541
14
14
 
15
+ # The path of the disk or directory device 8 serves through the traps
16
+ # (Attachments), or an empty one.
17
+ def mounted_path = @drive.nil? ? "" : @drive.path
18
+
19
+ def family = :c64
20
+
15
21
  def region = address_bus.region
16
22
 
17
23
  def vic = address_bus.vic
@@ -42,13 +48,12 @@ module Badline
42
48
  # TOD clocks count. The KERNAL tells PAL from NTSC by the raster, so
43
49
  # every region boots the same ROMs. reu plugs an REU of that many K
44
50
  # into the expansion port, where it drives the IRQ line and takes the
45
- # bus for its transfers. kernal: names the KERNAL ROM
46
- # (AddressBus::ROMs::KERNALS), and datasette: false leaves the cassette port
47
- # empty, as on the SX-64.
48
- def initialize(debug: false, reu: nil, kernal: :c64, datasette: true, **machine)
49
- @address_bus = AddressBus.new(**machine)
50
- @address_bus.kernal = kernal unless kernal == :c64
51
- @address_bus.datasette.disconnect! unless datasette
51
+ # bus for its transfers. kernal:, datasette: and board: fit the bus
52
+ # afterwards (AddressBus::Fittings): the KERNAL ROM, the cassette port,
53
+ # which is empty on the SX-64, and the board and case.
54
+ def initialize(debug: false, reu: nil, **machine)
55
+ @address_bus = AddressBus.new(**machine.except(*AddressBus::Fittings::NAMES))
56
+ @address_bus.fit(**machine.slice(*AddressBus::Fittings::NAMES))
52
57
  @cpu = CPU.new(@address_bus, debug:)
53
58
  @vic = @address_bus.vic
54
59
  @vic.open_bus = -> { @address_bus.ram.peek(@cpu.program_counter) }
@@ -70,7 +75,7 @@ module Badline
70
75
  @serial_trap = nil
71
76
  @save_trap = nil
72
77
  @drive1541 = nil
73
- @iec_bus = IECBus.new(host: @cia2).tap(&:plug_in!)
78
+ plug_serial_bus
74
79
  @reu = reu ? plug_reu(reu) : nil
75
80
  end
76
81
 
@@ -98,6 +103,40 @@ module Badline
98
103
  @cycles += 1
99
104
  end
100
105
 
106
+ # Runs `count` cycles, one #cycle! after another.
107
+ def run_cycles(count)
108
+ i = 0
109
+ while i < count
110
+ cycle!
111
+ i += 1
112
+ end
113
+ end
114
+
115
+ # Runs cycles until the block returns true or the cycle count passes
116
+ # `limit`, checking before each cycle.
117
+ def run_until(limit)
118
+ cycle! until yield || @cycles > limit
119
+ end
120
+
121
+ # The cycle at which #on_init's handlers run, once the KERNAL has booted.
122
+ def init_threshold
123
+ INIT_THRESHOLD
124
+ end
125
+
126
+ # The clock, the raster and the crop of the machine's region, a pixel
127
+ # to a window pixel.
128
+ def timing
129
+ region = address_bus.region
130
+ Timing.new(clock_hz: region.clock_hz, cycles_per_line: region.cycles_per_line,
131
+ lines_per_frame: region.lines_per_frame, crop: region.crop, pixel_width: 1)
132
+ end
133
+
134
+ # The chip whose #display a front end shows.
135
+ def video = @vic
136
+
137
+ # The chip a front end records the machine's sound from.
138
+ def sound_source = @sid
139
+
101
140
  def load_prg(data)
102
141
  uint16(data[0], data[1]).tap do |load_addr|
103
142
  ram.write(load_addr, data[2..])
@@ -155,30 +194,16 @@ module Badline
155
194
 
156
195
  # RESTORE isn't in the key matrix. It fires a one-shot that pulses the
157
196
  # NMI line however long the key is held, modelled here as one cycle.
197
+ # A machine without a keyboard has no RESTORE key either.
158
198
  def press_restore
159
- @restore_pulse = true
199
+ @restore_pulse = true if keyboard.connected?
160
200
  end
161
201
 
162
- # Plugs in a Drive1541, which then runs alongside the C64 on its own
163
- # clock and talks to it over the serial bus. The serial traps stop
164
- # answering the drive's device number, so the KERNAL's TALK, LISTEN and
165
- # byte transfers reach the drive. The LOAD and SAVE traps stay, and
166
- # still serve a mounted image.
167
- def attach_drive1541(drive)
168
- @iec_bus.detach(@drive1541) if @drive1541
169
- drive.host_clock_hz = region.clock_hz
170
- @drive1541 = drive
171
- drive.connect(iec_bus)
172
- @serial_trap&.device = serial_trap_device
173
- end
174
-
175
- # The serial bus, which CIA 2's port A drives through the C64's own
176
- # inverters and reads back on PA6 and PA7, with or without a drive on
177
- # it.
178
- attr_reader :iec_bus
202
+ # The one-shot has pulsed already, so letting go does nothing.
203
+ def release_restore; end
179
204
 
180
205
  def capture_output
181
- @capture_output ||= ChroutTrap.new(cpu:, bus: address_bus).tap do |trap|
206
+ @capture_output ||= ChroutTrap.new(cpu:, bus: address_bus, layout: KernalTrap::C64_LAYOUT).tap do |trap|
182
207
  cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
183
208
  end
184
209
  end
@@ -197,12 +222,6 @@ module Badline
197
222
 
198
223
  private
199
224
 
200
- # The device number the serial traps answer: device 8, unless a true
201
- # drive is on the bus as device 8.
202
- def serial_trap_device
203
- @drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
204
- end
205
-
206
225
  # The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
207
226
  # key, and the CPU takes an interrupt on its falling edge.
208
227
  def drive_nmi
@@ -258,9 +277,5 @@ module Badline
258
277
  def handle_init
259
278
  @init_handlers.each(&:call)
260
279
  end
261
-
262
- def init_threshold
263
- INIT_THRESHOLD
264
- end
265
280
  end
266
281
  end
@@ -16,13 +16,19 @@ module Badline
16
16
  # resistances; close neither and the lines float high. Reading a pot asks the
17
17
  # CIA for the current select lines, since a program sets them and then goes
18
18
  # straight to SID without touching the ports again.
19
+ #
20
+ # A matrix may have rows beyond port A's eight. Their select lines come from
21
+ # outside the CIA and are pushed into +extra_rows+, bit 0 for the ninth row
22
+ # upwards, active low like port A; idle, every line is high and none of
23
+ # those rows is selected. They settle with port A's lines as rows 8 and up,
24
+ # so their keys reach port B, but port A reads only its own eight.
19
25
  class ControlPorts
20
26
  PORT1_POTS = 0x40
21
27
  PORT2_POTS = 0x80
22
28
  FIRE = 0x10
23
29
 
24
30
  attr_reader :keyboard, :joystick1, :joystick2
25
- attr_accessor :device1, :device2, :port_a_source
31
+ attr_accessor :device1, :device2, :port_a_source, :extra_rows
26
32
 
27
33
  def initialize(keyboard:, joystick1:, joystick2:)
28
34
  @keyboard = keyboard
@@ -31,9 +37,10 @@ module Badline
31
37
  @device1 = nil
32
38
  @device2 = nil
33
39
  @port_a_source = nil
40
+ @extra_rows = 0xff
34
41
  end
35
42
 
36
- def read_a(port_a, port_b) = scan(port_a, port_b).first
43
+ def read_a(port_a, port_b) = scan(port_a, port_b).first & 0xff
37
44
 
38
45
  def read_b(port_a, port_b) = scan(port_a, port_b).last
39
46
 
@@ -58,7 +65,7 @@ module Badline
58
65
  end
59
66
 
60
67
  def scan(port_a, port_b)
61
- keyboard.scan(port_a & port_bits(@joystick2, @device2),
68
+ keyboard.scan((port_a & port_bits(@joystick2, @device2)) | (@extra_rows << 8),
62
69
  port_b & port_bits(@joystick1, @device1))
63
70
  end
64
71
 
data/lib/badline/core.rb CHANGED
@@ -8,6 +8,7 @@
8
8
  require "badline/version"
9
9
  require "badline/integer_helper"
10
10
  require "badline/region"
11
+ require "badline/timing"
11
12
  require "badline/model"
12
13
  require "badline/addressable"
13
14
  require "badline/memory"
@@ -40,9 +41,11 @@ require "badline/drive1541"
40
41
  require "badline/snapshot/state"
41
42
  require "badline/snapshot/setup"
42
43
  require "badline/computer"
44
+ require "badline/vic20"
43
45
  require "badline/checkpoint"
44
46
  require "badline/storage"
45
47
  require "badline/cartridge"
46
48
  require "badline/kernal_trap"
47
49
  require "badline/chrout_trap"
48
50
  require "badline/media"
51
+ require "badline/machine"
@@ -3,7 +3,8 @@
3
3
  module Badline
4
4
  # The 1530 datasette. Plays a pulse stream into CIA 1's FLAG pin while the
5
5
  # motor runs with a key pressed, and reports the keys through the cassette
6
- # sense line on the $01 port.
6
+ # sense line on the $01 port. With RECORD down too, it records the
7
+ # machine's write line onto the tape instead.
7
8
  class Datasette
8
9
  # What inserting a tape raises on a machine without a cassette port.
9
10
  class Missing < ArgumentError; end
@@ -15,6 +16,9 @@ module Badline
15
16
  @tape = tape
16
17
  @connected = true
17
18
  @playing = false
19
+ @recording = false
20
+ @write_high = true
21
+ @since = 0
18
22
  @motor = false
19
23
  @countdown = 0
20
24
  @flag_handler = nil
@@ -60,6 +64,30 @@ module Badline
60
64
  def play! = press(true)
61
65
  def stop! = press(false)
62
66
 
67
+ # Presses RECORD and PLAY together.
68
+ def record!
69
+ return if !@connected || @tape.nil?
70
+
71
+ press(true)
72
+ @recording = true
73
+ @since = 0
74
+ end
75
+
76
+ def recording? = @recording
77
+
78
+ # The machine's tape write line. While the deck records with its motor
79
+ # running, each rising edge ends a pulse on the tape, as long as the
80
+ # time since the edge before, and plays back as a falling edge on the
81
+ # read line.
82
+ def write_line=(high)
83
+ rose = high && !@write_high
84
+ @write_high = high
85
+ return unless rose && @recording && running?
86
+
87
+ @tape.record_pulse(@since)
88
+ @since = 0
89
+ end
90
+
63
91
  def playing? = @playing
64
92
 
65
93
  # Sense reads low while a key is down on the deck.
@@ -98,6 +126,7 @@ module Badline
98
126
 
99
127
  def cycle!
100
128
  return unless running?
129
+ return @since += 1 if @recording
101
130
 
102
131
  @countdown = @tape.next_pulse.to_i if @countdown.zero?
103
132
  return if @countdown.zero?
@@ -116,8 +145,17 @@ module Badline
116
145
  def press(down)
117
146
  return if down == @playing || !@connected
118
147
 
148
+ finish_recording unless down
119
149
  @playing = down
120
150
  @sense_handler&.call
121
151
  end
152
+
153
+ # Releasing the keys ends a recording, and writes the tape out.
154
+ def finish_recording
155
+ return unless @recording
156
+
157
+ @recording = false
158
+ @tape.save
159
+ end
122
160
  end
123
161
  end
@@ -27,7 +27,7 @@ module Badline
27
27
 
28
28
  attr_reader :bus
29
29
 
30
- def initialize(device: 8, bus: nil)
30
+ def initialize(device: 8, bus: IECBus.new)
31
31
  @port_b = (0xff & ~(SERIAL_INPUTS | JUMPERS)) | (((device - 8) & 0x03) << 5)
32
32
  @host = 0
33
33
  self.bus = bus
@@ -38,10 +38,10 @@ module Badline
38
38
  latch_host
39
39
  end
40
40
 
41
- # Takes the C64's lines as they stand, for the drive cycles of the
42
- # next host cycle.
41
+ # Takes the C64's lines as the machine last pushed them into the bus,
42
+ # for the drive cycles of the next host cycle.
43
43
  def latch_host
44
- @host = @bus ? @bus.host_lines : 0
44
+ @host = @bus.host_lines
45
45
  end
46
46
 
47
47
  # The C64's lines as last latched. The jumpers and the bus are the
@@ -55,10 +55,7 @@ module Badline
55
55
  end
56
56
 
57
57
  # Whether the C64 has moved ATN since the last latch_host.
58
- def atn_moved?
59
- lines = @bus ? @bus.host_lines : 0
60
- (lines ^ @host).anybits?(IECBus::HOST_ATN_OUT)
61
- end
58
+ def atn_moved? = (@bus.host_lines ^ @host).anybits?(IECBus::HOST_ATN_OUT)
62
59
 
63
60
  def atn_low? = @bus.atn_low?(@host)
64
61
 
@@ -8,9 +8,8 @@ module Badline
8
8
  # passes go in bulk, and the rest cycle by cycle, which may find the
9
9
  # idle loop and put the drive back to sleep.
10
10
  #
11
- # The drive looks at ATN only when the C64 may have moved it, on a
12
- # bus that says when (IECBus#notifies?), and every host cycle on one
13
- # that doesn't.
11
+ # The drive looks at ATN only when the machine may have moved it, as
12
+ # it pushes its lines into the bus (IECBus#host_lines=).
14
13
  module Sleep
15
14
  # Whether the drive is skipping its idle loop right now.
16
15
  def asleep? = @asleep
@@ -40,10 +39,10 @@ module Badline
40
39
  @wake_at = 0
41
40
  end
42
41
 
43
- # A host cycle asleep that may wake the drive. The bus says whether
44
- # the next ones need a look at ATN.
42
+ # A host cycle asleep that may wake the drive. The next ones need no
43
+ # look at ATN until the machine pushes its lines again.
45
44
  def doze
46
- @host_still = @serial_bus.notifies?
45
+ @host_still = true
47
46
  if @serial_port.atn_moved?
48
47
  settle!
49
48
  elsif @slept == @wake_at
@@ -149,8 +149,8 @@ module Badline
149
149
  # The host has run this cycle already, and the drive sees what it did
150
150
  # from the next one on (see SerialPort). Asleep, the drive owes the
151
151
  # cycles instead, until ATN moves or its counters are due (see Idle).
152
- # It looks at ATN only after the C64 wrote CIA 2's port A, on a bus
153
- # that says so (see host_written!).
152
+ # It looks at ATN only after the machine pushed its lines into the bus
153
+ # (see host_written!).
154
154
  def host_cycle!
155
155
  if @asleep
156
156
  return if (@slept += 1) != @wake_at && @host_still
@@ -169,7 +169,7 @@ module Badline
169
169
  plan_wake if @asleep
170
170
  end
171
171
 
172
- # The C64 wrote CIA 2's port A, so ATN may have moved.
172
+ # The machine pushed its lines into the bus, so ATN may have moved.
173
173
  def host_written!
174
174
  @host_still = false
175
175
  end