badline 0.14.0 → 0.15.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/CLAUDE.md +15 -5
  4. data/README.md +68 -8
  5. data/doc/pinned-behaviour.md +145 -0
  6. data/doc/snapshot-format.md +188 -38
  7. data/doc/snapshots.md +18 -1
  8. data/lib/badline/audio/bare_player.rb +4 -8
  9. data/lib/badline/audio/machine_player.rb +2 -12
  10. data/lib/badline/c128/banks.rb +1 -3
  11. data/lib/badline/c128/bus.rb +19 -1
  12. data/lib/badline/c128/mmu_pages.rb +7 -0
  13. data/lib/badline/c64/bus/extra_sids.rb +80 -0
  14. data/lib/badline/c64/bus.rb +1 -0
  15. data/lib/badline/c64/cartridge/action_replay.rb +6 -2
  16. data/lib/badline/c64/cartridge/comal80.rb +1 -0
  17. data/lib/badline/c64/cartridge/dinamic.rb +3 -1
  18. data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
  19. data/lib/badline/c64/cartridge/fun_play.rb +2 -1
  20. data/lib/badline/c64/cartridge/game_system.rb +2 -1
  21. data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
  22. data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
  23. data/lib/badline/c64/cartridge/ocean.rb +1 -0
  24. data/lib/badline/c64/cartridge/pagefox.rb +1 -0
  25. data/lib/badline/c64/cartridge/rgcd.rb +2 -1
  26. data/lib/badline/c64/cartridge/super_games.rb +3 -2
  27. data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
  28. data/lib/badline/c64/cartridge.rb +3 -3
  29. data/lib/badline/c64/keyed_modules.rb +6 -2
  30. data/lib/badline/chips/ted/beam.rb +13 -14
  31. data/lib/badline/chips/ted/fetches.rb +199 -0
  32. data/lib/badline/chips/ted/keyed_state.rb +156 -0
  33. data/lib/badline/chips/ted/modes.rb +134 -0
  34. data/lib/badline/chips/ted/palette.rb +43 -0
  35. data/lib/badline/chips/ted/picture.rb +182 -0
  36. data/lib/badline/chips/ted/sound/output.rb +86 -0
  37. data/lib/badline/chips/ted/sound.rb +284 -0
  38. data/lib/badline/chips/ted.rb +53 -6
  39. data/lib/badline/chips/tia.rb +12 -0
  40. data/lib/badline/drives/datasette/keyed_state.rb +7 -2
  41. data/lib/badline/drives/datasette.rb +22 -5
  42. data/lib/badline/frontend/boot.rb +3 -1
  43. data/lib/badline/frontend/controls.rb +7 -3
  44. data/lib/badline/frontend/keys.rb +14 -2
  45. data/lib/badline/frontend/media_slots.rb +11 -1
  46. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  47. data/lib/badline/frontend/menu/menu_media.rb +13 -3
  48. data/lib/badline/frontend/menu/menu_pages.rb +8 -8
  49. data/lib/badline/frontend/menu/menu_ports.rb +13 -1
  50. data/lib/badline/frontend/screen.rb +33 -12
  51. data/lib/badline/frontend/screens.rb +2 -1
  52. data/lib/badline/frontend/sound.rb +4 -4
  53. data/lib/badline/frontend/timeline.rb +19 -6
  54. data/lib/badline/machine/settling.rb +1 -1
  55. data/lib/badline/machine.rb +26 -6
  56. data/lib/badline/media/plus4_media.rb +68 -0
  57. data/lib/badline/media/program.rb +20 -6
  58. data/lib/badline/media.rb +33 -12
  59. data/lib/badline/options/family.rb +1 -0
  60. data/lib/badline/options/help.rb +30 -6
  61. data/lib/badline/options/table.rb +15 -2
  62. data/lib/badline/options/validation.rb +26 -4
  63. data/lib/badline/options.rb +37 -9
  64. data/lib/badline/plus4/bus.rb +77 -14
  65. data/lib/badline/plus4/cassette_sense.rb +21 -0
  66. data/lib/badline/plus4/keyboard_port.rb +35 -10
  67. data/lib/badline/plus4/keyed_modules.rb +99 -0
  68. data/lib/badline/plus4/keyed_parts.rb +143 -0
  69. data/lib/badline/plus4/keyed_state.rb +41 -0
  70. data/lib/badline/plus4.rb +92 -16
  71. data/lib/badline/snapshot/container.rb +2 -0
  72. data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
  73. data/lib/badline/snapshot/field_doc.rb +46 -7
  74. data/lib/badline/snapshot/image.rb +12 -3
  75. data/lib/badline/snapshot/plus4_setup.rb +39 -0
  76. data/lib/badline/snapshot.rb +9 -4
  77. data/lib/badline/storage/g64_image.rb +1 -1
  78. data/lib/badline/storage/tap.rb +21 -12
  79. data/lib/badline/support/checkpoint.rb +3 -2
  80. data/lib/badline/support/plus4_checkpoint.rb +42 -0
  81. data/lib/badline/version.rb +1 -1
  82. metadata +15 -1
@@ -2,12 +2,19 @@
2
2
 
3
3
  module Badline
4
4
  class Plus4
5
- # The keyboard: the 6529 at $FD30-$FD3F, whose eight outputs drive the
6
- # key matrix's lines, and the TED's K0-K7 inputs, which $FF08 latches
7
- # (Chips::TED#keyboard_port=). The KERNAL's scan ($DB11) writes each
8
- # line's mask to $FD30 and $FF08 and reads the keys back from $FF08,
9
- # and its key table at $E026 puts each key at line times 8 plus input
10
- # bit.
5
+ # The keyboard and the joysticks: the 6529 at $FD30-$FD3F, whose eight
6
+ # outputs drive the key matrix's lines, and the TED's K0-K7 inputs,
7
+ # which $FF08 latches (Chips::TED#keyboard_port=). The KERNAL's scan
8
+ # ($DB11) writes each line's mask to $FD30 and $FF08 and reads the keys
9
+ # back from $FF08, and its key table at $E026 puts each key at line
10
+ # times 8 plus input bit.
11
+ #
12
+ # The joysticks share K0-K7. The byte written to $FF08 selects them,
13
+ # bit 2 low joystick 1 and bit 1 low joystick 2, as BASIC's JOY does
14
+ # ($FA and $FD). A selected joystick pulls up, down, left and right
15
+ # onto K0-K3 and its fire button onto K6 (joystick 1) or K7
16
+ # (joystick 2), and those lines settle against the matrix with the
17
+ # keys'.
11
18
  #
12
19
  # The 6529 is a quasi-bidirectional latch: a read returns what was
13
20
  # written to it.
@@ -28,8 +35,10 @@ module Badline
28
35
 
29
36
  attr_reader :keyboard, :output
30
37
 
31
- def initialize(keyboard)
38
+ def initialize(keyboard, joystick1, joystick2)
32
39
  @keyboard = keyboard
40
+ @joystick1 = joystick1
41
+ @joystick2 = joystick2
33
42
  @output = 0xff
34
43
  end
35
44
 
@@ -43,9 +52,25 @@ module Badline
43
52
  @output = value
44
53
  end
45
54
 
46
- # The K0-K7 inputs the TED latches on a write of +_value+ to $FF08:
47
- # high but where a held key joins a line the 6529 pulls low.
48
- def latch(_value) = @keyboard.scan(@output, 0xff).last
55
+ # The K0-K7 inputs the TED latches on a write of +value+ to $FF08:
56
+ # high but where a held key joins a line the 6529 pulls low, or a
57
+ # joystick +value+ selects holds a switch closed.
58
+ def latch(value) = @keyboard.scan(@output, joystick_lines(value)).last
59
+
60
+ private
61
+
62
+ def joystick_lines(value)
63
+ lines = 0xff
64
+ lines &= joystick_inputs(@joystick1.port_bits, 0x40) if value.nobits?(0x04)
65
+ lines &= joystick_inputs(@joystick2.port_bits, 0x80) if value.nobits?(0x02)
66
+ lines
67
+ end
68
+
69
+ # A joystick's switches, Input::Joystick's bits 0-4, on K0-K7: the
70
+ # directions on K0-K3 and the fire button on +fire+.
71
+ def joystick_inputs(bits, fire)
72
+ bits.anybits?(0x10) ? bits | 0xf0 : (bits | 0xf0) & ~fire & 0xff
73
+ end
49
74
  end
50
75
  end
51
76
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # The whole machine as keyed snapshot modules (Snapshot::Keyed): BL-MACHINE
6
+ # with the setup, the cartridges' ROMs by slot, the CPU with its port,
7
+ # the TED, its sound, the keyboard's 6529, the user port's 6529 and the
8
+ # ACIA where the model has them, the TCBM interface's TIAs where one is
9
+ # plugged in, the RAM with the ROM bank latch and the ROM/RAM switch
10
+ # under bus.*, the datasette, device 8 through the traps and the true
11
+ # drives.
12
+ #
13
+ # A load maps the banks before the chips load, and works out what each
14
+ # unit derives from its fields once every unit is loaded.
15
+ module KeyedModules
16
+ def save_modules
17
+ bus = @bus
18
+ sections = [Snapshot::Keyed.section("BL-MACHINE") { |out| save_machine_fields(out) }]
19
+ sections << Snapshot::Keyed.section("BL-CART") { |out| bus.save_slot_fields(out) } if bus.cartridge?
20
+ save_chip_modules(sections)
21
+ sections << Snapshot::Keyed.section("BL-RAM") do |out|
22
+ bus.ram.save_fields(out, "ram")
23
+ bus.save_fields(out, "bus.")
24
+ end
25
+ sections << Snapshot::Keyed.section("BL-TAPE") { |out| @datasette.save_fields(out) }
26
+ sections << Snapshot::Keyed.section("BL-TRAPDOS") { |out| save_trap_dos_fields(out) } if mounted?
27
+ save_drive_modules(sections)
28
+ end
29
+
30
+ # Raises Snapshot::FormatError for the modules of a machine built
31
+ # another way.
32
+ def load_modules(modules)
33
+ machine = modules.machine
34
+ check_snapshot_setup(Snapshot::Plus4Setup.read_fields(machine))
35
+ load_machine_fields(machine)
36
+ bus = @bus
37
+ memory = modules.required("BL-RAM")
38
+ bus.ram.load_fields(memory, "ram")
39
+ bus.load_fields(memory, "bus.")
40
+ bus.load_slot_fields(modules.optional("BL-CART") || Snapshot::Keyed::Reader.new("BL-CART"))
41
+ bus.restored!
42
+ load_chip_modules(modules)
43
+ @datasette.load_fields(modules.required("BL-TAPE"))
44
+ trap_dos = modules.optional("BL-TRAPDOS")
45
+ trap_dos ? load_trap_dos_fields(trap_dos, detached: modules.detached?) : unmount
46
+ load_drive_modules(modules)
47
+ modules_restored!
48
+ self
49
+ end
50
+
51
+ private
52
+
53
+ def save_chip_modules(sections)
54
+ sections << Snapshot::Keyed.section("BL-CPU") do |out|
55
+ @cpu.save_fields(out)
56
+ @port.save_fields(out, "port.")
57
+ end
58
+ sections << Snapshot::Keyed.section("BL-TED") { |out| @ted.save_fields(out) }
59
+ sections << Snapshot::Keyed.section("BL-TEDSOUND") { |out| @sound.save_fields(out) }
60
+ sections << Snapshot::Keyed.section("BL-KEYPORT") { |out| @keyboard_port.save_fields(out) }
61
+ user_port = @user_port
62
+ sections << Snapshot::Keyed.section("BL-USERPORT") { |out| user_port.save_fields(out) } if user_port
63
+ acia = @acia
64
+ sections << Snapshot::Keyed.section("BL-ACIA") { |out| acia.save_fields(out) } if acia
65
+ @tias.each_with_index do |tia, index|
66
+ sections << Snapshot::Keyed.section("BL-TIA#{index + 1}") { |out| tia.save_fields(out) }
67
+ end
68
+ end
69
+
70
+ def load_chip_modules(modules)
71
+ cpu = modules.required("BL-CPU")
72
+ @port.load_fields(cpu, "port.")
73
+ @ted.load_fields(modules.required("BL-TED"))
74
+ @sound.load_fields(modules.required("BL-TEDSOUND"))
75
+ @keyboard_port.load_fields(modules.required("BL-KEYPORT"))
76
+ @user_port&.load_fields(modules.required("BL-USERPORT"))
77
+ @acia&.load_fields(modules.required("BL-ACIA"))
78
+ @tias.each_with_index { |tia, index| tia.load_fields(modules.required("BL-TIA#{index + 1}")) }
79
+ @cpu.load_fields(cpu)
80
+ end
81
+
82
+ def modules_restored!
83
+ @cpu.restored!
84
+ @ted.restored!
85
+ @sound.restored!(@ted)
86
+ @port.restored!
87
+ end
88
+
89
+ def check_snapshot_setup(setup)
90
+ ours = Snapshot::Plus4Setup.of(self)
91
+ return if setup == ours
92
+
93
+ raise Snapshot::FormatError, "the snapshot is of a #{setup}, not a #{ours}"
94
+ end
95
+ end
96
+
97
+ include KeyedModules
98
+ end
99
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ class CPUPort
6
+ # The port's registers in a keyed snapshot module (Snapshot::Keyed),
7
+ # under `prefix`. The serial bus's lines and the datasette's write
8
+ # line are driven again from them once everything is loaded
9
+ # (#restored!), without recording an edge on the tape.
10
+ module KeyedState
11
+ def save_fields(out, prefix)
12
+ out.int("#{prefix}ddr", @ddr).int("#{prefix}data", @data)
13
+ end
14
+
15
+ def load_fields(input, prefix)
16
+ @ddr = input.byte("#{prefix}ddr", @ddr)
17
+ @data = input.byte("#{prefix}data", @data)
18
+ end
19
+
20
+ def restored!
21
+ @serial_bus.host_lines = serial_lines
22
+ @datasette.write_level = levels.anybits?(CLK_OUT)
23
+ end
24
+ end
25
+
26
+ include KeyedState
27
+ end
28
+
29
+ class KeyboardPort
30
+ # The 6529's latch in a keyed snapshot module (BL-KEYPORT).
31
+ module KeyedState
32
+ def save_fields(out) = out.int("output", @output)
33
+
34
+ def load_fields(input)
35
+ @output = input.byte("output", @output)
36
+ end
37
+ end
38
+
39
+ include KeyedState
40
+ end
41
+
42
+ class UserPort
43
+ # The 6529's latch in a keyed snapshot module (BL-USERPORT).
44
+ module KeyedState
45
+ def save_fields(out) = out.int("output", @output)
46
+
47
+ def load_fields(input)
48
+ @output = input.byte("output", @output)
49
+ end
50
+ end
51
+
52
+ include KeyedState
53
+ end
54
+
55
+ class ACIA
56
+ # The command and control registers in a keyed snapshot module
57
+ # (BL-ACIA).
58
+ module KeyedState
59
+ def save_fields(out) = out.int("command", @command).int("control", @control)
60
+
61
+ def load_fields(input)
62
+ @command = input.byte("command", @command)
63
+ @control = input.byte("control", @control)
64
+ end
65
+ end
66
+
67
+ include KeyedState
68
+ end
69
+
70
+ class Bus
71
+ # The ROM bank latch, whether ROM is selected and the last byte on the
72
+ # bus, under `prefix`, and the cartridges' ROMs by slot (#save_slot_fields),
73
+ # in keyed snapshot modules (Snapshot::Keyed). The TED learns which
74
+ # memory is selected, and the pages are mapped again, once they are
75
+ # loaded (#restored!). The BASIC, KERNAL and function ROMs come with
76
+ # the model.
77
+ module KeyedState
78
+ # The cartridge slots, as xplus4 names them: cartridge 1 or 2, the
79
+ # low ($8000) or the high ($C000) half.
80
+ SLOTS = %w[c1lo c2lo c1hi c2hi].freeze
81
+
82
+ def save_fields(out, prefix)
83
+ out.int("#{prefix}low_bank", @low_bank).int("#{prefix}high_bank", @high_bank)
84
+ out.bool("#{prefix}rom_selected", @rom_selected).int("#{prefix}data", @data)
85
+ end
86
+
87
+ def load_fields(input, prefix)
88
+ @low_bank = input.masked("#{prefix}low_bank", @low_bank, 0x03)
89
+ @high_bank = input.masked("#{prefix}high_bank", @high_bank, 0x03)
90
+ @rom_selected = input.bool("#{prefix}rom_selected", @rom_selected)
91
+ @data = input.byte("#{prefix}data", @data)
92
+ end
93
+
94
+ # Whether a cartridge's ROM is in any slot.
95
+ def cartridge?
96
+ index = 0
97
+ while index < SLOTS.length
98
+ return true unless slot_rom(index).nil?
99
+
100
+ index += 1
101
+ end
102
+ false
103
+ end
104
+
105
+ # Each slot's ROM, 16K, under its name. An empty slot writes
106
+ # nothing.
107
+ def save_slot_fields(out)
108
+ SLOTS.each_with_index do |name, index|
109
+ rom = slot_rom(index)
110
+ rom&.save_fields(out, name)
111
+ end
112
+ out
113
+ end
114
+
115
+ # A slot the module holds nothing for is left empty.
116
+ def load_slot_fields(input)
117
+ SLOTS.each_with_index do |name, index|
118
+ bytes = input.bytes(name, "".b).bytes
119
+ if index < 2
120
+ @low_roms[2 + index] = slot(bytes, 0x8000)
121
+ else
122
+ @high_roms[index] = slot(bytes, 0xc000)
123
+ end
124
+ end
125
+ end
126
+
127
+ def restored!
128
+ @ted.rom_selected = @rom_selected
129
+ map_reads
130
+ end
131
+
132
+ private
133
+
134
+ def slot_rom(index) = index < 2 ? @low_roms[2 + index] : @high_roms[index]
135
+
136
+ # A slot's ROM from its bytes at `start`, or none.
137
+ def slot(bytes, start) = bytes.empty? ? nil : ROM.new(bytes, length: bytes.length, start:)
138
+ end
139
+
140
+ include KeyedState
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # The machine's own fields in a keyed snapshot module (Snapshot::Keyed,
6
+ # BL-MACHINE): the family, its setup by name (Snapshot::Plus4Setup), the
7
+ # cycle count, and the host's typing still to feed and on_init handlers
8
+ # still to run under host.*. The badline version that wrote it is
9
+ # written for reports and never read.
10
+ module KeyedState
11
+ # How many more on_init handlers the machine a snapshot was taken of had
12
+ # yet to run than this one has. Nil until the machine is restored.
13
+ attr_reader :init_handlers_lost
14
+
15
+ # Whether a snapshot saves now (Machine::Settling): the CPU between
16
+ # instructions or jammed.
17
+ def settled_for_snapshot? = @cpu.boundary? || @cpu.jammed?
18
+
19
+ # The sound's voices, run up to this cycle.
20
+ def settle_chips_for_snapshot = @sound.catch_up!
21
+
22
+ def save_machine_fields(out)
23
+ out.text("family", "plus4").text("written_by", VERSION)
24
+ Snapshot::Plus4Setup.of(self).write_fields(out)
25
+ out.int("cycles", @cycles)
26
+ pending = @pending_keys
27
+ out.ints("host.pending_keys", pending) if pending
28
+ out.int("host.init_pending", booting? ? @init_handlers.length : 0)
29
+ end
30
+
31
+ def load_machine_fields(input)
32
+ @cycles = input.int("cycles", @cycles)
33
+ pending = input.ints("host.pending_keys", [])
34
+ @pending_keys = pending.empty? ? nil : pending.dup
35
+ @init_handlers_lost = [input.int("host.init_pending", 0) - @init_handlers.length, 0].max
36
+ end
37
+ end
38
+
39
+ include KeyedState
40
+ end
41
+ end
data/lib/badline/plus4.rb CHANGED
@@ -7,8 +7,13 @@ require "badline/plus4/cpu"
7
7
  require "badline/plus4/cpu_port"
8
8
  require "badline/plus4/keyboard_port"
9
9
  require "badline/plus4/user_port"
10
+ require "badline/plus4/cassette_sense"
10
11
  require "badline/plus4/acia"
11
12
  require "badline/plus4/bus"
13
+ require "badline/snapshot/plus4_setup"
14
+ require "badline/plus4/keyed_parts"
15
+ require "badline/plus4/keyed_state"
16
+ require "badline/plus4/keyed_modules"
12
17
 
13
18
  module Badline
14
19
  # The Commodore 264 series: the Plus/4, the C16 and the C116. A 7501 on
@@ -17,13 +22,17 @@ module Badline
17
22
  # 6529. Only the TED pulls IRQ, and nothing pulls NMI, which the 7501
18
23
  # doesn't have.
19
24
  #
20
- # It runs headless only: the TED paints no picture yet. Device 8
21
- # serves disks through the KERNAL traps (C64::KernalTraps), and
22
- # cartridges go in cartridge 1's slots.
25
+ # The TED paints the picture a front end shows (#video, #timing). Device 8
26
+ # serves disks through the KERNAL traps (C64::KernalTraps), or a true
27
+ # 1541 or 1581 on the serial bus does (C64::TrueDrives), cartridges go
28
+ # in the two cartridges' slots, and tapes play on the datasette, whose read
29
+ # line's level the KERNAL times with the TED's timers.
23
30
  class Plus4
24
31
  include IntegerHelper
25
32
  include Machine::KeyboardBuffer
26
33
  include Machine::Clocking
34
+ include Machine::Settling
35
+ include C64::TrueDrives
27
36
  include C64::KernalTraps
28
37
 
29
38
  # The cycle by which the KERNAL has booted and BASIC printed READY,
@@ -31,15 +40,42 @@ module Badline
31
40
  # Plus/4, 126,000 on an NTSC one and sooner on a C16.
32
41
  INIT_THRESHOLD = 200_000
33
42
 
34
- attr_reader :model, :bus, :cpu, :ted, :port, :keyboard, :keyboard_port, :acia, :user_port, :datasette, :tias,
35
- :iec_bus, :cycles, :ram_kb
43
+ # The cycle by which a 1541 switched on with the machine has run its
44
+ # DOS's reset, the RAM test and the ROM checksum, and waits on the
45
+ # bus, with a margin, in TED cycles.
46
+ DRIVE_BOOT_CYCLES = 1_200_000
47
+
48
+ attr_reader :model, :bus, :cpu, :ted, :sound, :port, :keyboard, :keyboard_port, :joystick1, :joystick2, :acia,
49
+ :user_port, :datasette, :tias, :iec_bus, :cycles, :ram_kb, :drive1541, :init_threshold
36
50
 
37
51
  def family = :plus4
38
52
 
39
- def init_threshold = INIT_THRESHOLD
53
+ # The joysticks reach the TED's keyboard latch, so there are no C64
54
+ # control ports to plug a pot device into.
55
+ def control_ports = nil
40
56
 
41
57
  def ram = @bus.ram
42
58
 
59
+ # What a front end plays: the TED's sound, which answers #record and
60
+ # #drain_samples.
61
+ def sound_source = @sound
62
+
63
+ # The chip whose #display a front end shows.
64
+ def video = @ted
65
+
66
+ # The TED's clock and raster, and the part of its picture xplus4 shows:
67
+ # all 384 pixels of a line, and 288 lines from line 276 on PAL or 242
68
+ # from line 248 on NTSC, running on past the frame's last line to its
69
+ # first, as the top border is the end of the frame. The TED draws 8
70
+ # pixels a cycle, so each shows one window pixel wide, as on the C64.
71
+ def timing
72
+ profile = @ted.profile
73
+ ntsc = profile == Chips::TED::NTSC
74
+ Timing.new(clock_hz: profile.clock_hz, cycles_per_line: Chips::TED::Beam::CYCLES_PER_LINE,
75
+ lines_per_frame: ntsc ? 262 : 312, crop: ntsc ? [0, 248, 384, 242] : [0, 276, 384, 288],
76
+ pixel_width: 1)
77
+ end
78
+
43
79
  # Reads and writes memory as the CPU sees it, through the bus.
44
80
  def peek(addr) = @bus.peek(addr)
45
81
 
@@ -56,7 +92,9 @@ module Badline
56
92
 
57
93
  @ted = Chips::TED.new(@model.ted)
58
94
  @keyboard = Input::Keyboard.new(matrix: KeyboardPort::MATRIX)
59
- @keyboard_port = KeyboardPort.new(@keyboard)
95
+ @joystick1 = Input::Joystick.new
96
+ @joystick2 = Input::Joystick.new
97
+ @keyboard_port = KeyboardPort.new(@keyboard, @joystick1, @joystick2)
60
98
  @ted.keyboard_port = @keyboard_port
61
99
  @iec_bus = Drives::SerialBus.new
62
100
  @datasette = Drives::Datasette.new(level: true)
@@ -64,24 +102,31 @@ module Badline
64
102
  plus4_parts = @model.plus4_parts
65
103
  @acia = plus4_parts ? ACIA.new : nil
66
104
  @user_port = plus4_parts ? UserPort.new(@datasette) : nil
67
- io = Bus::IOPage.new(keyboard_port: @keyboard_port, acia: @acia, user_port: @user_port)
105
+ io = Bus::IOPage.new(keyboard_port: @keyboard_port, acia: @acia, user_port: @user_port,
106
+ cassette_sense: plus4_parts ? nil : CassetteSense.new(@datasette))
68
107
  @bus = Bus.new(ted: @ted, port: @port, io:, model: @model, ram_kb: @ram_kb)
108
+ @ted.connect(@bus)
69
109
  @tias = tcbm ? [Chips::TIA.new, Chips::TIA.new] : []
70
110
  @bus.plug_tcbm(drive8: @tias[0], drive9: @tias[1]) if tcbm
71
111
  @cpu = CPU.new(@bus, debug:)
72
112
  @cycles = 0
113
+ @sound = Chips::TED::Sound.new(self, @model.ted.clock_hz)
114
+ @ted.sound = @sound
73
115
  @init_handlers = []
74
116
  @pending_keys = nil
75
117
  @dos = nil
76
118
  @serial_trap = nil
77
119
  @save_trap = nil
78
120
  @capture_output = nil
121
+ @drive1541 = nil
122
+ @drive1581 = nil
123
+ @init_threshold = INIT_THRESHOLD
79
124
  end
80
125
 
81
126
  # One TED cycle: the TED moves on and decides the CPU's clock, then the
82
127
  # CPU runs its cycles in it, none while the TED holds the bus.
83
128
  def cycle!
84
- handle_init if @cycles == INIT_THRESHOLD
129
+ handle_init if @cycles == @init_threshold
85
130
  feed_keyboard if @pending_keys
86
131
 
87
132
  ted = @ted
@@ -95,35 +140,41 @@ module Badline
95
140
  clock_cpu(ted)
96
141
  clock_cpu(ted) if slots == 2
97
142
  end
143
+ @drive1541&.host_cycle!
144
+ @drive1581&.host_cycle!
98
145
 
99
146
  @cycles += 1
100
147
  end
101
148
 
102
149
  # The RESET line: the CPU, the port, the 6529s, the ACIA, the bank
103
- # latch and ROM selection. The TED has no reset pin.
150
+ # latch and ROM selection, and through the serial bus's RESET line, the
151
+ # drives. The TED has no reset pin.
104
152
  def reset!
105
153
  @port.reset!
106
154
  @keyboard_port.reset!
107
155
  @user_port&.reset!
108
156
  @dos&.reset!
157
+ @drive1541&.reset!
158
+ @drive1581&.reset!
109
159
  @acia&.reset!
110
160
  @tias.each(&:reset!)
111
161
  @bus.reset!
112
162
  @cpu.reset!
113
163
  end
114
164
 
115
- # Switches the machine off and on: RAM and the TED start from their
116
- # power-on state, and the RES line resets everything else.
165
+ # Switches the machine off and on: RAM, the TED and its sound start
166
+ # from their power-on state, and the RES line resets everything else.
117
167
  def power_cycle!
118
168
  @bus.power_on!
119
169
  @ted.power_on!
170
+ @sound.power_on!
120
171
  reset!
121
172
  end
122
173
 
123
174
  # Calls the block with the exit code a VICE testprog writes to $FDCF.
124
175
  def install_debug_register(&) = @bus.install_debug_register(&)
125
176
 
126
- # Puts a cartridge's ROM chips in cartridge 1's slots (Bus#attach_cartridge)
177
+ # Puts a cartridge's ROM chips in their slots (Bus#attach_cartridge)
127
178
  # and switches the machine off and on, as a cartridge goes in with the
128
179
  # power off. The KERNAL starts it at reset when it signs itself with CBM
129
180
  # at $8007.
@@ -132,6 +183,24 @@ module Badline
132
183
  power_cycle!
133
184
  end
134
185
 
186
+ # Takes both cartridges out (Bus#detach_cartridges) and switches the
187
+ # machine off and on.
188
+ def detach_cartridges
189
+ @bus.detach_cartridges
190
+ power_cycle!
191
+ end
192
+
193
+ # The cartridge slots with a ROM in them (Bus#cartridge_slots).
194
+ def cartridge_slots = @bus.cartridge_slots
195
+
196
+ # The model and its RAM as the pause menu names them, such as
197
+ # "PLUS/4 PAL, 64K RAM".
198
+ def model_label
199
+ name = @model.plus4_parts ? "PLUS/4" : "C16"
200
+ standard = @model.ted == Chips::TED::NTSC ? "NTSC" : "PAL"
201
+ "#{name} #{standard}, #{@ram_kb}K RAM"
202
+ end
203
+
135
204
  # Stores a PRG's bytes from its load address on, into RAM, and returns
136
205
  # that address.
137
206
  def load_prg(data)
@@ -152,9 +221,16 @@ module Badline
152
221
 
153
222
  def trap_bus = @bus
154
223
 
155
- # Device 8, which the serial traps answer for, with no true drive on
156
- # the bus to take it.
157
- def serial_trap_device = KernalTrap::Routine::DEVICE
224
+ # A drive that powers on with the machine takes longer to boot than
225
+ # the KERNAL, and doesn't answer the bus until it has, so the
226
+ # machine's on_init handlers wait for it too.
227
+ def drive1541_attached
228
+ @init_threshold = [@init_threshold, DRIVE_BOOT_CYCLES].max if @cycles < @init_threshold
229
+ end
230
+
231
+ def drive1581_attached
232
+ @init_threshold = [@init_threshold, Drives::Model1581::BOOT_CYCLES].max if @cycles < @init_threshold
233
+ end
158
234
 
159
235
  # BA stops the CPU at a read, and a write goes ahead.
160
236
  def clock_cpu(ted)
@@ -34,6 +34,8 @@ module Badline
34
34
  VIC20 = "VIC20"
35
35
  # x128 names its machine C128.
36
36
  C128 = "C128"
37
+ # xplus4 names its machine PLUS4, for the C16 too.
38
+ PLUS4 = "PLUS4"
37
39
  GZIP_MAGIC = "\x1f\x8b".b
38
40
 
39
41
  # The VICE version block, when VICE wrote the file: four version
@@ -7,7 +7,7 @@ module Badline
7
7
  # its default, with keys that differ only in a unit's number, such as
8
8
  # voice1 to voice3, on one row.
9
9
  class Markdown
10
- NAMES = { "c64" => "C64", "vic20" => "VIC-20", "c128" => "C128" }.freeze
10
+ NAMES = { "c64" => "C64", "vic20" => "VIC-20", "c128" => "C128", "plus4" => "Plus/4" }.freeze
11
11
  TAG_TYPES = { Keyed::INT => "int", Keyed::BOOL => "bool", Keyed::BYTES => "bytes", Keyed::INTS => "ints",
12
12
  Keyed::TEXT => "text" }.freeze
13
13
 
@@ -31,11 +31,12 @@ module Badline
31
31
  `int & 0x..` are ints cut to the latch's width. A text that lists
32
32
  its values takes one of them. An optional int is absent when the
33
33
  latch holds nothing. A true drive's modules are named from its
34
- device number, `BL-D8` to `BL-D11`. Keys with `<N>` stand for each
35
- numbered unit. A default of "written only" is a key the load
36
- doesn't read, or reads only when the snapshot has it. Where the
37
- machines differ, the default is given for each, and a key only
38
- some of them have names them.
34
+ device number, `BL-D8` to `BL-D11`, and the SIDs past the first
35
+ are `BL-SID2` onwards, in the order they were fitted. Keys with
36
+ `<N>` stand for each numbered unit. A default of "written only" is
37
+ a key the load doesn't read, or reads only when the snapshot has
38
+ it. Where the machines differ, the default is given for each, and a
39
+ key only some of them have names them.
39
40
  MARKDOWN
40
41
 
41
42
  def initialize(loads)