badline 0.14.0 → 0.15.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -0
  3. data/CLAUDE.md +22 -5
  4. data/README.md +101 -11
  5. data/doc/media.md +19 -0
  6. data/doc/pinned-behaviour.md +145 -0
  7. data/doc/snapshot-format.md +244 -46
  8. data/doc/snapshots.md +18 -1
  9. data/exe/badline-ruby +1 -1
  10. data/lib/badline/audio/bare_player.rb +4 -8
  11. data/lib/badline/audio/machine_player.rb +4 -26
  12. data/lib/badline/c128/banks.rb +1 -3
  13. data/lib/badline/c128/bus.rb +19 -1
  14. data/lib/badline/c128/mmu_pages.rb +7 -0
  15. data/lib/badline/c64/bus/extra_sids.rb +80 -0
  16. data/lib/badline/c64/bus.rb +1 -0
  17. data/lib/badline/c64/cartridge/action_replay.rb +6 -2
  18. data/lib/badline/c64/cartridge/comal80.rb +1 -0
  19. data/lib/badline/c64/cartridge/dinamic.rb +3 -1
  20. data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
  21. data/lib/badline/c64/cartridge/fun_play.rb +2 -1
  22. data/lib/badline/c64/cartridge/game_system.rb +2 -1
  23. data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
  24. data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
  25. data/lib/badline/c64/cartridge/ocean.rb +1 -0
  26. data/lib/badline/c64/cartridge/pagefox.rb +1 -0
  27. data/lib/badline/c64/cartridge/rgcd.rb +2 -1
  28. data/lib/badline/c64/cartridge/super_games.rb +3 -2
  29. data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
  30. data/lib/badline/c64/cartridge.rb +3 -3
  31. data/lib/badline/c64/keyed_modules.rb +6 -2
  32. data/lib/badline/c64/true_drives.rb +13 -1
  33. data/lib/badline/c64.rb +6 -0
  34. data/lib/badline/chips/ted/beam.rb +13 -14
  35. data/lib/badline/chips/ted/fetches.rb +199 -0
  36. data/lib/badline/chips/ted/keyed_state.rb +156 -0
  37. data/lib/badline/chips/ted/modes.rb +134 -0
  38. data/lib/badline/chips/ted/palette.rb +43 -0
  39. data/lib/badline/chips/ted/picture.rb +182 -0
  40. data/lib/badline/chips/ted/sound/output.rb +86 -0
  41. data/lib/badline/chips/ted/sound.rb +284 -0
  42. data/lib/badline/chips/ted.rb +53 -6
  43. data/lib/badline/chips/tia.rb +28 -0
  44. data/lib/badline/chips/vic/register_log.rb +4 -1
  45. data/lib/badline/drives/datasette/keyed_state.rb +14 -4
  46. data/lib/badline/drives/datasette.rb +22 -5
  47. data/lib/badline/drives/engine/core.rb +6 -14
  48. data/lib/badline/drives/engine/idle.rb +42 -4
  49. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  50. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  51. data/lib/badline/drives/engine/sleep.rb +1 -0
  52. data/lib/badline/drives/engine.rb +1 -0
  53. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  54. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  55. data/lib/badline/drives/model1541.rb +1 -0
  56. data/lib/badline/drives/model1551/bus.rb +138 -0
  57. data/lib/badline/drives/model1551/cable.rb +104 -0
  58. data/lib/badline/drives/model1551/cpu.rb +14 -0
  59. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  60. data/lib/badline/drives/model1551/port.rb +74 -0
  61. data/lib/badline/drives/model1551/tia.rb +92 -0
  62. data/lib/badline/drives/model1551.rb +170 -0
  63. data/lib/badline/drives/model1571.rb +1 -0
  64. data/lib/badline/drives/model1581.rb +1 -0
  65. data/lib/badline/frontend/boot.rb +23 -2
  66. data/lib/badline/frontend/controls.rb +7 -3
  67. data/lib/badline/frontend/keys.rb +14 -2
  68. data/lib/badline/frontend/media_slots.rb +11 -1
  69. data/lib/badline/frontend/menu/menu_dialogs.rb +14 -5
  70. data/lib/badline/frontend/menu/menu_media.rb +102 -14
  71. data/lib/badline/frontend/menu/menu_pages.rb +16 -11
  72. data/lib/badline/frontend/menu/menu_ports.rb +13 -1
  73. data/lib/badline/frontend/player/player_controller.rb +11 -4
  74. data/lib/badline/frontend/screen.rb +33 -12
  75. data/lib/badline/frontend/screens.rb +2 -1
  76. data/lib/badline/frontend/sound.rb +4 -4
  77. data/lib/badline/frontend/timeline.rb +19 -6
  78. data/lib/badline/machine/settling.rb +1 -1
  79. data/lib/badline/machine.rb +30 -6
  80. data/lib/badline/media/plus4_media.rb +68 -0
  81. data/lib/badline/media/program.rb +20 -6
  82. data/lib/badline/media/true_drive.rb +7 -5
  83. data/lib/badline/media.rb +33 -12
  84. data/lib/badline/options/family.rb +1 -0
  85. data/lib/badline/options/help.rb +32 -6
  86. data/lib/badline/options/option.rb +2 -1
  87. data/lib/badline/options/table.rb +29 -4
  88. data/lib/badline/options/validation.rb +66 -4
  89. data/lib/badline/options.rb +71 -11
  90. data/lib/badline/plus4/acia/frames.rb +55 -0
  91. data/lib/badline/plus4/acia.rb +276 -16
  92. data/lib/badline/plus4/bus.rb +245 -46
  93. data/lib/badline/plus4/cassette_sense.rb +21 -0
  94. data/lib/badline/plus4/keyboard_port.rb +35 -10
  95. data/lib/badline/plus4/keyed_modules.rb +116 -0
  96. data/lib/badline/plus4/keyed_parts.rb +172 -0
  97. data/lib/badline/plus4/keyed_state.rb +44 -0
  98. data/lib/badline/plus4/ram_expansion.rb +145 -0
  99. data/lib/badline/plus4/sid_card.rb +95 -0
  100. data/lib/badline/plus4/sound_mix.rb +51 -0
  101. data/lib/badline/plus4/tcbm_drive.rb +120 -0
  102. data/lib/badline/plus4/user_port.rb +11 -2
  103. data/lib/badline/plus4.rb +184 -26
  104. data/lib/badline/roms/README +9 -0
  105. data/lib/badline/roms/dos1551.rom +0 -0
  106. data/lib/badline/snapshot/container.rb +2 -0
  107. data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
  108. data/lib/badline/snapshot/field_doc.rb +48 -7
  109. data/lib/badline/snapshot/image.rb +12 -3
  110. data/lib/badline/snapshot/plus4_setup.rb +81 -0
  111. data/lib/badline/snapshot.rb +9 -4
  112. data/lib/badline/storage/tap.rb +21 -12
  113. data/lib/badline/support/checkpoint.rb +3 -2
  114. data/lib/badline/support/plus4_checkpoint.rb +42 -0
  115. data/lib/badline/version.rb +1 -1
  116. metadata +29 -1
@@ -0,0 +1,172 @@
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 registers, the status, the shift register and when the
57
+ # transmitter's character ends, in a keyed snapshot module (BL-ACIA).
58
+ # What the device on the user port has still to send stays with the
59
+ # host, and the receiver starts on it again once loaded (#restored!).
60
+ # A snapshot from before the transmitter's timing was saved starts
61
+ # its character clock on load.
62
+ module KeyedState
63
+ def save_fields(out)
64
+ out.int("command", @command).int("control", @control).int("errors", @errors)
65
+ out.bool("receiver_full", @receiver_full).bool("transmitter_empty", @transmitter_empty).bool("irq", @irq)
66
+ out.int("receive_data", @receive_data).int("transmit_data", @transmit_data).int("shifting", @shifting)
67
+ out.int("transmit_at", @transmit_at).int("transmit_remainder", @transmit_remainder)
68
+ end
69
+
70
+ def load_fields(input)
71
+ @command = input.byte("command", @command)
72
+ @control = input.byte("control", @control)
73
+ @errors = input.masked("errors", 0, PARITY_ERROR | FRAMING_ERROR | OVERRUN)
74
+ @receiver_full = input.bool("receiver_full", false)
75
+ @transmitter_empty = input.bool("transmitter_empty", true)
76
+ @irq = input.bool("irq", false)
77
+ @receive_data = input.byte("receive_data", 0)
78
+ @transmit_data = input.byte("transmit_data", 0)
79
+ @shifting = input.int("shifting", IDLE)
80
+ @transmit_remainder = input.int("transmit_remainder", 0)
81
+ transmit_at = input.optional_int("transmit_at")
82
+ @transmit_at = transmit_at || NEVER
83
+ @clock_lost = transmit_at.nil?
84
+ end
85
+
86
+ def restored!
87
+ @receive_at = NEVER
88
+ @lines = modem_lines
89
+ start_transmitting if @clock_lost && transmitter_on?
90
+ @clock_lost = false
91
+ start_receiving
92
+ changed
93
+ end
94
+ end
95
+
96
+ include KeyedState
97
+ end
98
+
99
+ class Bus
100
+ # The ROM bank latch, whether ROM is selected and the last byte on the
101
+ # bus, under `prefix`, and the cartridges' ROMs by slot (#save_slot_fields),
102
+ # in keyed snapshot modules (Snapshot::Keyed). The TED learns which
103
+ # memory is selected, and the pages are mapped again, once they are
104
+ # loaded (#restored!). The BASIC, KERNAL and function ROMs come with
105
+ # the model.
106
+ module KeyedState
107
+ # The cartridge slots, as xplus4 names them: cartridge 1 or 2, the
108
+ # low ($8000) or the high ($C000) half.
109
+ SLOTS = %w[c1lo c2lo c1hi c2hi].freeze
110
+
111
+ def save_fields(out, prefix)
112
+ out.int("#{prefix}low_bank", @low_bank).int("#{prefix}high_bank", @high_bank)
113
+ out.bool("#{prefix}rom_selected", @rom_selected).int("#{prefix}data", @data)
114
+ end
115
+
116
+ def load_fields(input, prefix)
117
+ @low_bank = input.masked("#{prefix}low_bank", @low_bank, 0x03)
118
+ @high_bank = input.masked("#{prefix}high_bank", @high_bank, 0x03)
119
+ @rom_selected = input.bool("#{prefix}rom_selected", @rom_selected)
120
+ @data = input.byte("#{prefix}data", @data)
121
+ end
122
+
123
+ # Whether a cartridge's ROM is in any slot.
124
+ def cartridge?
125
+ index = 0
126
+ while index < SLOTS.length
127
+ return true unless slot_rom(index).nil?
128
+
129
+ index += 1
130
+ end
131
+ false
132
+ end
133
+
134
+ # Each slot's ROM, 16K, under its name. An empty slot writes
135
+ # nothing.
136
+ def save_slot_fields(out)
137
+ SLOTS.each_with_index do |name, index|
138
+ rom = slot_rom(index)
139
+ rom&.save_fields(out, name)
140
+ end
141
+ out
142
+ end
143
+
144
+ # A slot the module holds nothing for is left empty.
145
+ def load_slot_fields(input)
146
+ SLOTS.each_with_index do |name, index|
147
+ bytes = input.bytes(name, "".b).bytes
148
+ if index < 2
149
+ @low_roms[2 + index] = slot(bytes, 0x8000)
150
+ else
151
+ @high_roms[index] = slot(bytes, 0xc000)
152
+ end
153
+ end
154
+ end
155
+
156
+ def restored!
157
+ @ted.rom_selected = @rom_selected
158
+ map_ram
159
+ end
160
+
161
+ private
162
+
163
+ def slot_rom(index) = index < 2 ? @low_roms[2 + index] : @high_roms[index]
164
+
165
+ # A slot's ROM from its bytes at `start`, or none.
166
+ def slot(bytes, start) = bytes.empty? ? nil : ROM.new(bytes, length: bytes.length, start:)
167
+ end
168
+
169
+ include KeyedState
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,44 @@
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 and the SID card's, run up to this cycle.
20
+ def settle_chips_for_snapshot
21
+ @sound.catch_up!
22
+ @sid_card&.catch_up!
23
+ end
24
+
25
+ def save_machine_fields(out)
26
+ out.text("family", "plus4").text("written_by", VERSION)
27
+ Snapshot::Plus4Setup.of(self).write_fields(out)
28
+ out.int("cycles", @cycles)
29
+ pending = @pending_keys
30
+ out.ints("host.pending_keys", pending) if pending
31
+ out.int("host.init_pending", booting? ? @init_handlers.length : 0)
32
+ end
33
+
34
+ def load_machine_fields(input)
35
+ @cycles = input.int("cycles", @cycles)
36
+ pending = input.ints("host.pending_keys", [])
37
+ @pending_keys = pending.empty? ? nil : pending.dup
38
+ @init_handlers_lost = [input.int("host.init_pending", 0) - @init_handlers.length, 0].max
39
+ end
40
+ end
41
+
42
+ include KeyedState
43
+ end
44
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # Hannes's RAM expansion, 256K or 1M in 64K banks, and Csory's 256K on
6
+ # the same board, from Solder's register notes and the board's GAL
7
+ # equations (plus4world, "256K RAM (Hannes)").
8
+ #
9
+ # The control register is a 6529 at $FD16, which reads back what was
10
+ # written. The expansion decodes $FD10-$FD1F further, so the user port
11
+ # answers only at $FD10, and the rest of the range is open.
12
+ #
13
+ # - Bits 0-1, and bits 2-3 on the 1M: the bank the CPU sees above the
14
+ # switch bound. All ones is the standard bank, the RAM below the
15
+ # bound, which bits 0-3 pick inverted: bank 3 of the 256K and bank
16
+ # 15 of the 1M.
17
+ # - Bit 6: the TED fetches from the CPU's bank when set, and from the
18
+ # standard bank when clear.
19
+ # - Bit 7: the switch bound, $4000 when set, $1000 when clear.
20
+ # - Bits 4-5: on the Csory board, two different bits switch Csory's
21
+ # mapping on (the notes' gate version; its table names 10 and its
22
+ # GAL 01).
23
+ #
24
+ # The register is all ones at power-on and after a reset, so the
25
+ # machine starts in the standard bank, with the TED following it.
26
+ #
27
+ # Csory's mapping puts one 16K block of the 256K, picked by bits 0-3
28
+ # of a write-only latch at $FD15, in the 16K window of the CPU's 64K
29
+ # that bits 6-7 pick. The standard bank holds blocks 0-3. The TED sees
30
+ # the block too while bit 6 of $FD16 is set. On the Csory board the
31
+ # TED reads the KERNAL wherever it fetches from the high ROM, whichever
32
+ # bank the ROM latch selects.
33
+ class RAMExpansion
34
+ # Each expansion by name: its 64K banks, and whether it has Csory's
35
+ # mapping.
36
+ KINDS = { hannes256: [4, false], hannes1m: [16, false], csory256: [4, true] }.freeze
37
+
38
+ # Whose design each is, as the pause menu names it.
39
+ NAMES = { hannes256: "HANNES", hannes1m: "HANNES", csory256: "CSORY" }.freeze
40
+
41
+ CONTROL = 0x06
42
+ CSORY_LATCH = 0x05
43
+
44
+ # The register at $FD16, and Csory's latch at $FD15.
45
+ attr_reader :kind, :control, :csory_latch
46
+
47
+ # +ram+ is the machine's own 64K, the standard bank, and +view+ the
48
+ # Bus::MirroredRAM the bus reads it through.
49
+ def initialize(kind, ram, view)
50
+ @kind = kind
51
+ count, csory = KINDS.fetch(kind)
52
+ @csory = csory
53
+ @mask = count - 1
54
+ size = (count - 1) * 0x10000
55
+ @extra = Memory.new(Array.new(size, Bus::RAM_POWER_ON), length: size, start: 0)
56
+ @banks = [view] + Array.new(count - 1) { |index| Bus::MirroredRAM.new(@extra, 0xffff, index * 0x10000) }
57
+ @blocks = csory ? Array.new(16) { |block| block_view(ram, block) } : []
58
+ @control = 0xff
59
+ @csory_latch = 0
60
+ end
61
+
62
+ # The size in K.
63
+ def size_kb = (@mask + 1) * 64
64
+
65
+ # Whether the TED reads the KERNAL for the whole high ROM.
66
+ def kernal_for_ted? = @csory
67
+
68
+ # A write to $FD11-$FD1F: the control register at $FD16, and on the
69
+ # Csory board, Csory's latch at $FD15.
70
+ def poke(addr, value)
71
+ case addr & 0x0f
72
+ when CONTROL then @control = value
73
+ when CSORY_LATCH then @csory_latch = value if @csory
74
+ end
75
+ end
76
+
77
+ # The RESET line sets the register, as power-on does. Csory's latch
78
+ # keeps its value.
79
+ def reset!
80
+ @control = 0xff
81
+ end
82
+
83
+ def power_on!
84
+ @extra.clear!(Array.new(@extra.length, Bus::RAM_POWER_ON))
85
+ @csory_latch = 0
86
+ reset!
87
+ end
88
+
89
+ # Lays the RAM the CPU sees out in +cpu+ and the RAM the TED sees in
90
+ # +ted+, a view for each of the 256 pages.
91
+ def map(cpu, ted)
92
+ control = @control
93
+ bound = control.anybits?(0x80) ? 0x40 : 0x10
94
+ standard = @banks[0]
95
+ cpu.fill(standard, 0x00, bound)
96
+ cpu.fill(@banks[~control & @mask], bound, 0x100 - bound)
97
+ follows = control.anybits?(0x40)
98
+ if follows
99
+ page = 0
100
+ while page < 0x100
101
+ ted[page] = cpu[page]
102
+ page += 1
103
+ end
104
+ else
105
+ ted.fill(standard, 0x00, 0x100)
106
+ end
107
+ return unless csory_on?
108
+
109
+ latch = @csory_latch
110
+ block = @blocks[latch & 0x0f]
111
+ window = (latch >> 6) * 0x40
112
+ cpu.fill(block, window, 0x40)
113
+ ted.fill(block, window, 0x40) if follows
114
+ end
115
+
116
+ # The banks beyond the standard one, and Csory's latch, in a keyed
117
+ # snapshot module (BL-RAMEXP).
118
+ def save_fields(out)
119
+ out.int("control", @control)
120
+ out.int("csory_latch", @csory_latch) if @csory
121
+ @extra.save_fields(out, "ram")
122
+ end
123
+
124
+ def load_fields(input)
125
+ @control = input.byte("control", @control)
126
+ @csory_latch = input.byte("csory_latch", @csory_latch)
127
+ @extra.load_fields(input, "ram")
128
+ end
129
+
130
+ private
131
+
132
+ def csory_on?
133
+ control = @control
134
+ @csory && control.anybits?(0x10) != control.anybits?(0x20)
135
+ end
136
+
137
+ # Block +block+ of the 256K, from the standard bank's four on.
138
+ def block_view(ram, block)
139
+ return Bus::MirroredRAM.new(ram, 0x3fff, block * 0x4000) if block < 4
140
+
141
+ Bus::MirroredRAM.new(@extra, 0x3fff, (block - 4) * 0x4000)
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # A SID card's SID at $FD40-$FD5F, or at $FE80-$FE9F as Csory's card
6
+ # puts it, clocked once a TED cycle, as the cards clock it from the
7
+ # TED's single clock. The SID counts the
8
+ # cycles that have passed whenever something touches it, rather than
9
+ # every cycle: a CPU access runs it through the cycle the access is in,
10
+ # and the host, between cycles, up to the cycle about to start.
11
+ # `clock` answers the machine's cycles.
12
+ class SIDCard
13
+ # Where a card can put its SID.
14
+ ADDRESSES = [0xfd40, 0xfe80].freeze
15
+
16
+ attr_reader :sid, :address
17
+
18
+ def initialize(clock, model, address = ADDRESSES.first)
19
+ raise ArgumentError, format("no SID card at $%04X", address) unless ADDRESSES.include?(address)
20
+
21
+ @clock = clock
22
+ @address = address
23
+ @sid = Chips::SID.new(model:, at: address)
24
+ @clocked = 0
25
+ end
26
+
27
+ def model = @sid.model
28
+
29
+ def model=(model)
30
+ catch_up!
31
+ @sid.model = model
32
+ end
33
+
34
+ def peek(addr)
35
+ run_to(@clock.cycles + 1)
36
+ @sid.peek(addr)
37
+ end
38
+
39
+ def poke(addr, value)
40
+ run_to(@clock.cycles + 1)
41
+ @sid.poke(addr, value)
42
+ end
43
+
44
+ # The RES line, from the host between cycles.
45
+ def reset!
46
+ run_to(@clock.cycles)
47
+ @sid.reset!
48
+ end
49
+
50
+ # Runs the SID, its voices and its filter, up to the current cycle.
51
+ def catch_up!
52
+ run_to(@clock.cycles)
53
+ @sid.catch_up!
54
+ end
55
+
56
+ def record(rate:, clock_hz:)
57
+ run_to(@clock.cycles)
58
+ @sid.record(rate:, clock_hz:)
59
+ end
60
+
61
+ def drain_samples
62
+ run_to(@clock.cycles)
63
+ @sid.drain_samples
64
+ end
65
+
66
+ # The SID's fields in a keyed snapshot module (BL-SID), saved once the
67
+ # machine has settled it (#catch_up!).
68
+ def save_fields(out) = @sid.save_fields(out)
69
+
70
+ def load_fields(input)
71
+ @sid.load_fields(input)
72
+ end
73
+
74
+ # The SID counts on from the machine's cycle the snapshot was taken at.
75
+ def restored!
76
+ @sid.restored!
77
+ @clocked = @clock.cycles
78
+ end
79
+
80
+ private
81
+
82
+ def run_to(target)
83
+ clocked = @clocked
84
+ return if clocked >= target
85
+
86
+ sid = @sid
87
+ while clocked < target
88
+ sid.cycle!
89
+ clocked += 1
90
+ end
91
+ @clocked = clocked
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # What a front end plays from a 264 with a SID card fitted: the TED's
6
+ # sound and the card's SID, each recorded at the audio rate and added
7
+ # together, clipped to a signed 16-bit sample. A sample one source has
8
+ # closed waits for the other's. The SID's model is the mix's, as it
9
+ # is a C64's sound source's, for the pause menu's Sound page.
10
+ class SoundMix
11
+ def initialize(ted_sound, sid)
12
+ @ted_sound = ted_sound
13
+ @sid = sid
14
+ @ted_waiting = []
15
+ @sid_waiting = []
16
+ end
17
+
18
+ def model = @sid.model
19
+
20
+ def model=(model)
21
+ @sid.model = model
22
+ end
23
+
24
+ # Starts both sources recording at `rate` samples a second, their
25
+ # cycles taken to run at `clock_hz`.
26
+ def record(rate:, clock_hz:)
27
+ @ted_sound.record(rate:, clock_hz:)
28
+ @sid.record(rate:, clock_hz:)
29
+ @ted_waiting = []
30
+ @sid_waiting = []
31
+ end
32
+
33
+ # Runs both sources up to the current cycle.
34
+ def catch_up!
35
+ @ted_sound.catch_up!
36
+ @sid.catch_up!
37
+ end
38
+
39
+ # The mixed samples both sources have closed since the last drain.
40
+ def drain_samples
41
+ ted = @ted_waiting.concat(@ted_sound.drain_samples)
42
+ sid = @sid_waiting.concat(@sid.drain_samples)
43
+ count = [ted.length, sid.length].min
44
+ mixed = Array.new(count) { |index| (ted[index] + sid[index]).clamp(-0x8000, 0x7fff) }
45
+ @ted_waiting = ted[count, ted.length - count] || []
46
+ @sid_waiting = sid[count, sid.length - count] || []
47
+ mixed
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # The 264 series' true drives: a Drives::Model1541 and a
6
+ # Drives::Model1581 on the serial bus (C64::TrueDrives), and a
7
+ # Drives::Model1551 on TCBM, its interface's TIA in the expansion port
8
+ # at $FEE0 for device 8 or $FEC0 for device 9. Each sits in a field of
9
+ # its own, so none makes another's per-cycle call a union.
10
+ #
11
+ # A device number has one drive at a time: a 1551 takes the place of a
12
+ # serial drive with its number, and the other way round.
13
+ module TCBMDrive
14
+ # The true drive models plug_true_drive builds.
15
+ TRUE_DRIVE_MODELS = %w[1541 1551].freeze
16
+
17
+ # The 1551 on TCBM, or nil.
18
+ attr_reader :drive1551
19
+
20
+ # The drive plug_true_drive plugs in, "1541" or "1551".
21
+ attr_reader :true_drive_model
22
+
23
+ # Plugs in a Drives::Model1551, its interface's TIA where its device
24
+ # number puts it, in place of the TCBM interface's own there, and in
25
+ # place of a serial drive with its number. The traps stop answering
26
+ # device 8 when the drive is device 8.
27
+ def attach_drive1551(drive)
28
+ detach_drive1551
29
+ device = drive.device
30
+ detach_drive1541 if @drive1541&.device == device
31
+ detach_drive1581 if @drive1581&.device == device
32
+ drive.host_clock_hz = timing.clock_hz
33
+ @drive1551 = drive
34
+ @bus.plug_tia(drive.device, drive.host_tia)
35
+ @serial_trap&.device = serial_trap_device
36
+ @init_threshold = [@init_threshold, Badline::Drives::Model1551::BOOT_CYCLES].max if @cycles < @init_threshold
37
+ end
38
+
39
+ # Unplugs the Drives::Model1551, leaving the TCBM interface's own TIA,
40
+ # if there is one, in its place.
41
+ def detach_drive1551
42
+ drive = @drive1551
43
+ return unless drive
44
+
45
+ @bus.plug_tia(drive.device, tcbm_tia(drive.device))
46
+ @drive1551 = nil
47
+ @serial_trap&.device = serial_trap_device
48
+ end
49
+
50
+ # The true drive: a 1581 ahead of a 1551 ahead of a 1541.
51
+ def true_drive = @drive1581 || @drive1551 || @drive1541
52
+
53
+ # Plugs in the model true_drive_model names as device 8, in place of
54
+ # the drive there, and returns it.
55
+ def plug_true_drive
56
+ return plug_drive1551 if @true_drive_model == "1551"
57
+
58
+ unplug_device8
59
+ detach_drive1581 if drive1581_on_device8?
60
+ Badline::Drives::Model1541.new.tap { |drive| attach_drive1541(drive) }
61
+ end
62
+
63
+ # Plugs in a 1551 as device 8, in place of the drive there, and
64
+ # returns it.
65
+ def plug_drive1551
66
+ Badline::Drives::Model1551.new.tap { |drive| attach_drive1551(drive) }
67
+ end
68
+
69
+ private
70
+
71
+ def init_drives(true_drive_model)
72
+ unless TRUE_DRIVE_MODELS.include?(true_drive_model)
73
+ raise ArgumentError, "no true drive model #{true_drive_model} for the Plus/4"
74
+ end
75
+
76
+ @true_drive_model = true_drive_model
77
+ @drive1541 = nil
78
+ @drive1551 = nil
79
+ @drive1581 = nil
80
+ end
81
+
82
+ def save_drive1551_modules(sections)
83
+ drive = @drive1551
84
+ drive&.save_modules(sections, "BL-D#{drive.device}")
85
+ sections
86
+ end
87
+
88
+ def load_drive1551_modules(modules, input, device)
89
+ return input.adjust("a 1551 as device #{device}, left out") unless [8, 9].include?(device)
90
+
91
+ detach_drive1551 if @drive1551 && @drive1551.device != device
92
+ attach_drive1551(Badline::Drives::Model1551.new(device:)) unless @drive1551
93
+ @drive1551.load_modules(modules, input, "BL-D#{device}")
94
+ end
95
+
96
+ # The TCBM interface's own TIA for +device+, or nil without one.
97
+ def tcbm_tia(device)
98
+ tias = @tias
99
+ return nil if tias.empty?
100
+
101
+ device == 9 ? tias[1] : tias[0]
102
+ end
103
+
104
+ # Device 8, unless a true drive is device 8.
105
+ def serial_trap_device
106
+ device = KernalTrap::Routine::DEVICE
107
+ on_device8 = @drive1551&.device == device || @drive1541&.device == device || drive1581_on_device8?
108
+ on_device8 ? nil : device
109
+ end
110
+
111
+ # Unplugs the 1541 and the 1551 if either is device 8, for a 1581 to
112
+ # take its place.
113
+ def unplug_device8
114
+ device = KernalTrap::Routine::DEVICE
115
+ detach_drive1541 if @drive1541&.device == device
116
+ detach_drive1551 if @drive1551&.device == device
117
+ end
118
+ end
119
+ end
120
+ end
@@ -6,22 +6,31 @@ module Badline
6
6
  # latch: a line reads low where the latch or the outside pulls it. With
7
7
  # nothing plugged into the user port, the outside pulls only bit 2, the
8
8
  # cassette sense switch, low while a key is down on the deck. The
9
- # KERNAL's IRQ reads it there ($CFBF).
9
+ # KERNAL's IRQ reads it there ($CFBF), and takes P1 as RS-232's clear
10
+ # to send, sending through the ACIA only while it reads high.
11
+ #
12
+ # A device on the user port drives P0-P7 through #inputs, low where its
13
+ # bit is clear.
10
14
  class UserPort
11
15
  TAPE_SENSE = 0x04
12
16
 
13
17
  attr_reader :output
18
+ attr_accessor :inputs
14
19
 
15
20
  def initialize(datasette)
16
21
  @datasette = datasette
17
22
  @output = 0xff
23
+ @inputs = 0xff
18
24
  end
19
25
 
20
26
  def reset!
21
27
  @output = 0xff
22
28
  end
23
29
 
24
- def peek(_addr) = @datasette.sense_low? ? @output & ~TAPE_SENSE : @output
30
+ def peek(_addr)
31
+ lines = @output & @inputs
32
+ @datasette.sense_low? ? lines & ~TAPE_SENSE : lines
33
+ end
25
34
 
26
35
  def poke(_addr, value)
27
36
  @output = value