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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -0
- data/CLAUDE.md +22 -5
- data/README.md +101 -11
- data/doc/media.md +19 -0
- data/doc/pinned-behaviour.md +145 -0
- data/doc/snapshot-format.md +244 -46
- data/doc/snapshots.md +18 -1
- data/exe/badline-ruby +1 -1
- data/lib/badline/audio/bare_player.rb +4 -8
- data/lib/badline/audio/machine_player.rb +4 -26
- data/lib/badline/c128/banks.rb +1 -3
- data/lib/badline/c128/bus.rb +19 -1
- data/lib/badline/c128/mmu_pages.rb +7 -0
- data/lib/badline/c64/bus/extra_sids.rb +80 -0
- data/lib/badline/c64/bus.rb +1 -0
- data/lib/badline/c64/cartridge/action_replay.rb +6 -2
- data/lib/badline/c64/cartridge/comal80.rb +1 -0
- data/lib/badline/c64/cartridge/dinamic.rb +3 -1
- data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
- data/lib/badline/c64/cartridge/fun_play.rb +2 -1
- data/lib/badline/c64/cartridge/game_system.rb +2 -1
- data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
- data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
- data/lib/badline/c64/cartridge/ocean.rb +1 -0
- data/lib/badline/c64/cartridge/pagefox.rb +1 -0
- data/lib/badline/c64/cartridge/rgcd.rb +2 -1
- data/lib/badline/c64/cartridge/super_games.rb +3 -2
- data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
- data/lib/badline/c64/cartridge.rb +3 -3
- data/lib/badline/c64/keyed_modules.rb +6 -2
- data/lib/badline/c64/true_drives.rb +13 -1
- data/lib/badline/c64.rb +6 -0
- data/lib/badline/chips/ted/beam.rb +13 -14
- data/lib/badline/chips/ted/fetches.rb +199 -0
- data/lib/badline/chips/ted/keyed_state.rb +156 -0
- data/lib/badline/chips/ted/modes.rb +134 -0
- data/lib/badline/chips/ted/palette.rb +43 -0
- data/lib/badline/chips/ted/picture.rb +182 -0
- data/lib/badline/chips/ted/sound/output.rb +86 -0
- data/lib/badline/chips/ted/sound.rb +284 -0
- data/lib/badline/chips/ted.rb +53 -6
- data/lib/badline/chips/tia.rb +28 -0
- data/lib/badline/chips/vic/register_log.rb +4 -1
- data/lib/badline/drives/datasette/keyed_state.rb +14 -4
- data/lib/badline/drives/datasette.rb +22 -5
- data/lib/badline/drives/engine/core.rb +6 -14
- data/lib/badline/drives/engine/idle.rb +42 -4
- data/lib/badline/drives/engine/idle_cpu.rb +18 -0
- data/lib/badline/drives/engine/serial_connection.rb +25 -0
- data/lib/badline/drives/engine/sleep.rb +1 -0
- data/lib/badline/drives/engine.rb +1 -0
- data/lib/badline/drives/model1541/byte_ready.rb +19 -3
- data/lib/badline/drives/model1541/mechanism.rb +10 -1
- data/lib/badline/drives/model1541.rb +1 -0
- data/lib/badline/drives/model1551/bus.rb +138 -0
- data/lib/badline/drives/model1551/cable.rb +104 -0
- data/lib/badline/drives/model1551/cpu.rb +14 -0
- data/lib/badline/drives/model1551/keyed_state.rb +65 -0
- data/lib/badline/drives/model1551/port.rb +74 -0
- data/lib/badline/drives/model1551/tia.rb +92 -0
- data/lib/badline/drives/model1551.rb +170 -0
- data/lib/badline/drives/model1571.rb +1 -0
- data/lib/badline/drives/model1581.rb +1 -0
- data/lib/badline/frontend/boot.rb +23 -2
- data/lib/badline/frontend/controls.rb +7 -3
- data/lib/badline/frontend/keys.rb +14 -2
- data/lib/badline/frontend/media_slots.rb +11 -1
- data/lib/badline/frontend/menu/menu_dialogs.rb +14 -5
- data/lib/badline/frontend/menu/menu_media.rb +102 -14
- data/lib/badline/frontend/menu/menu_pages.rb +16 -11
- data/lib/badline/frontend/menu/menu_ports.rb +13 -1
- data/lib/badline/frontend/player/player_controller.rb +11 -4
- data/lib/badline/frontend/screen.rb +33 -12
- data/lib/badline/frontend/screens.rb +2 -1
- data/lib/badline/frontend/sound.rb +4 -4
- data/lib/badline/frontend/timeline.rb +19 -6
- data/lib/badline/machine/settling.rb +1 -1
- data/lib/badline/machine.rb +30 -6
- data/lib/badline/media/plus4_media.rb +68 -0
- data/lib/badline/media/program.rb +20 -6
- data/lib/badline/media/true_drive.rb +7 -5
- data/lib/badline/media.rb +33 -12
- data/lib/badline/options/family.rb +1 -0
- data/lib/badline/options/help.rb +32 -6
- data/lib/badline/options/option.rb +2 -1
- data/lib/badline/options/table.rb +29 -4
- data/lib/badline/options/validation.rb +66 -4
- data/lib/badline/options.rb +71 -11
- data/lib/badline/plus4/acia/frames.rb +55 -0
- data/lib/badline/plus4/acia.rb +276 -16
- data/lib/badline/plus4/bus.rb +245 -46
- data/lib/badline/plus4/cassette_sense.rb +21 -0
- data/lib/badline/plus4/keyboard_port.rb +35 -10
- data/lib/badline/plus4/keyed_modules.rb +116 -0
- data/lib/badline/plus4/keyed_parts.rb +172 -0
- data/lib/badline/plus4/keyed_state.rb +44 -0
- data/lib/badline/plus4/ram_expansion.rb +145 -0
- data/lib/badline/plus4/sid_card.rb +95 -0
- data/lib/badline/plus4/sound_mix.rb +51 -0
- data/lib/badline/plus4/tcbm_drive.rb +120 -0
- data/lib/badline/plus4/user_port.rb +11 -2
- data/lib/badline/plus4.rb +184 -26
- data/lib/badline/roms/README +9 -0
- data/lib/badline/roms/dos1551.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
- data/lib/badline/snapshot/field_doc.rb +48 -7
- data/lib/badline/snapshot/image.rb +12 -3
- data/lib/badline/snapshot/plus4_setup.rb +81 -0
- data/lib/badline/snapshot.rb +9 -4
- data/lib/badline/storage/tap.rb +21 -12
- data/lib/badline/support/checkpoint.rb +3 -2
- data/lib/badline/support/plus4_checkpoint.rb +42 -0
- data/lib/badline/version.rb +1 -1
- 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)
|
|
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
|