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
data/lib/badline/plus4.rb
CHANGED
|
@@ -2,28 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
require "badline/chips/ted"
|
|
4
4
|
require "badline/chips/tia"
|
|
5
|
+
require "badline/drives/model1551"
|
|
5
6
|
require "badline/plus4/model"
|
|
6
7
|
require "badline/plus4/cpu"
|
|
7
8
|
require "badline/plus4/cpu_port"
|
|
8
9
|
require "badline/plus4/keyboard_port"
|
|
9
10
|
require "badline/plus4/user_port"
|
|
11
|
+
require "badline/plus4/cassette_sense"
|
|
10
12
|
require "badline/plus4/acia"
|
|
13
|
+
require "badline/plus4/sid_card"
|
|
14
|
+
require "badline/plus4/sound_mix"
|
|
15
|
+
require "badline/plus4/ram_expansion"
|
|
11
16
|
require "badline/plus4/bus"
|
|
17
|
+
require "badline/plus4/tcbm_drive"
|
|
18
|
+
require "badline/snapshot/plus4_setup"
|
|
19
|
+
require "badline/plus4/keyed_parts"
|
|
20
|
+
require "badline/plus4/keyed_state"
|
|
21
|
+
require "badline/plus4/keyed_modules"
|
|
12
22
|
|
|
13
23
|
module Badline
|
|
14
24
|
# The Commodore 264 series: the Plus/4, the C16 and the C116. A 7501 on
|
|
15
25
|
# the TED's clock, which runs it once or twice a TED cycle (Chips::TED's
|
|
16
26
|
# Beam), the TED and, on the Plus/4, a 6551 ACIA and the user port's
|
|
17
|
-
# 6529.
|
|
18
|
-
# doesn't have.
|
|
27
|
+
# 6529. The TED and the ACIA pull IRQ, and nothing pulls NMI, which the
|
|
28
|
+
# 7501 doesn't have.
|
|
19
29
|
#
|
|
20
|
-
#
|
|
21
|
-
# serves disks through the KERNAL traps (C64::KernalTraps),
|
|
22
|
-
#
|
|
30
|
+
# The TED paints the picture a front end shows (#video, #timing). Device 8
|
|
31
|
+
# serves disks through the KERNAL traps (C64::KernalTraps), or a true
|
|
32
|
+
# 1541 or 1581 on the serial bus or a 1551 on TCBM does (TCBMDrive),
|
|
33
|
+
# cartridges go
|
|
34
|
+
# in the two cartridges' slots, and tapes play on the datasette, whose read
|
|
35
|
+
# line's level the KERNAL times with the TED's timers.
|
|
36
|
+
#
|
|
37
|
+
# A SID card, as third parties built for the 264s, puts a SID at
|
|
38
|
+
# $FD40-$FD5F, or at $FE80-$FE9F as Csory's does. The cards clock it from the TED's single clock, one SID
|
|
39
|
+
# cycle a TED cycle, 886,724 Hz on PAL, below the C64's 985,248 Hz, so a
|
|
40
|
+
# C64 tune plays about a semitone flat (plus4world forum threads 38287
|
|
41
|
+
# and 20130).
|
|
42
|
+
#
|
|
43
|
+
# A RAM expansion, Hannes's 256K or 1M or Csory's 256K, banks more RAM
|
|
44
|
+
# in through a register at $FD16 (RAMExpansion).
|
|
23
45
|
class Plus4
|
|
24
46
|
include IntegerHelper
|
|
25
47
|
include Machine::KeyboardBuffer
|
|
26
48
|
include Machine::Clocking
|
|
49
|
+
include Machine::Settling
|
|
50
|
+
include C64::TrueDrives
|
|
51
|
+
include TCBMDrive
|
|
27
52
|
include C64::KernalTraps
|
|
28
53
|
|
|
29
54
|
# The cycle by which the KERNAL has booted and BASIC printed READY,
|
|
@@ -31,15 +56,42 @@ module Badline
|
|
|
31
56
|
# Plus/4, 126,000 on an NTSC one and sooner on a C16.
|
|
32
57
|
INIT_THRESHOLD = 200_000
|
|
33
58
|
|
|
34
|
-
|
|
35
|
-
|
|
59
|
+
# The cycle by which a 1541 switched on with the machine has run its
|
|
60
|
+
# DOS's reset, the RAM test and the ROM checksum, and waits on the
|
|
61
|
+
# bus, with a margin, in TED cycles.
|
|
62
|
+
DRIVE_BOOT_CYCLES = 1_200_000
|
|
63
|
+
|
|
64
|
+
attr_reader :model, :bus, :cpu, :ted, :sound, :port, :keyboard, :keyboard_port, :joystick1, :joystick2, :acia,
|
|
65
|
+
:user_port, :datasette, :tias, :iec_bus, :cycles, :ram_kb, :drive1541, :init_threshold, :sid_card
|
|
36
66
|
|
|
37
67
|
def family = :plus4
|
|
38
68
|
|
|
39
|
-
|
|
69
|
+
# The joysticks reach the TED's keyboard latch, so there are no C64
|
|
70
|
+
# control ports to plug a pot device into.
|
|
71
|
+
def control_ports = nil
|
|
40
72
|
|
|
41
73
|
def ram = @bus.ram
|
|
42
74
|
|
|
75
|
+
# What a front end plays: the TED's sound, which answers #record and
|
|
76
|
+
# #drain_samples, mixed with the SID card's where one is fitted.
|
|
77
|
+
def sound_source = @sound_mix || @sound
|
|
78
|
+
|
|
79
|
+
# The chip whose #display a front end shows.
|
|
80
|
+
def video = @ted
|
|
81
|
+
|
|
82
|
+
# The TED's clock and raster, and the part of its picture xplus4 shows:
|
|
83
|
+
# all 384 pixels of a line, and 288 lines from line 276 on PAL or 242
|
|
84
|
+
# from line 248 on NTSC, running on past the frame's last line to its
|
|
85
|
+
# first, as the top border is the end of the frame. The TED draws 8
|
|
86
|
+
# pixels a cycle, so each shows one window pixel wide, as on the C64.
|
|
87
|
+
def timing
|
|
88
|
+
profile = @ted.profile
|
|
89
|
+
ntsc = profile == Chips::TED::NTSC
|
|
90
|
+
Timing.new(clock_hz: profile.clock_hz, cycles_per_line: Chips::TED::Beam::CYCLES_PER_LINE,
|
|
91
|
+
lines_per_frame: ntsc ? 262 : 312, crop: ntsc ? [0, 248, 384, 242] : [0, 276, 384, 288],
|
|
92
|
+
pixel_width: 1)
|
|
93
|
+
end
|
|
94
|
+
|
|
43
95
|
# Reads and writes memory as the CPU sees it, through the bus.
|
|
44
96
|
def peek(addr) = @bus.peek(addr)
|
|
45
97
|
|
|
@@ -48,46 +100,66 @@ module Badline
|
|
|
48
100
|
# +model+ names one of Model::ALL, or c116 for the C16, and +ram+ the
|
|
49
101
|
# RAM in K, one of Model::RAM_SIZES, when not the model's own. +tcbm+
|
|
50
102
|
# plugs in a 1551's TCBM interface, the TIAs at $FEC0-$FEFF, with no
|
|
51
|
-
# drive answering on the other side.
|
|
52
|
-
|
|
103
|
+
# drive answering on the other side. +true_drive_model+ names the drive
|
|
104
|
+
# plug_true_drive plugs in, "1541" or "1551". +sid_card+ fits a SID
|
|
105
|
+
# card with a SID of that model, :mos6581 or :mos8580, at
|
|
106
|
+
# +sid_card_address+, $FD40 or $FE80. +ram_expansion+
|
|
107
|
+
# fits a RAMExpansion of that kind, :hannes256, :hannes1m or
|
|
108
|
+
# :csory256, over 64K of the machine's own.
|
|
109
|
+
def initialize(model: "plus4", ram: nil, tcbm: false, true_drive_model: "1551", sid_card: nil,
|
|
110
|
+
sid_card_address: SIDCard::ADDRESSES.first, ram_expansion: nil, debug: false)
|
|
53
111
|
@model = Model.named(model)
|
|
54
|
-
@ram_kb = ram
|
|
55
|
-
raise ArgumentError, "no Plus/4 RAM size of #{@ram_kb}K" unless Model::RAM_SIZES.include?(@ram_kb)
|
|
112
|
+
@ram_kb = fitted_ram(ram, ram_expansion)
|
|
56
113
|
|
|
57
114
|
@ted = Chips::TED.new(@model.ted)
|
|
58
115
|
@keyboard = Input::Keyboard.new(matrix: KeyboardPort::MATRIX)
|
|
59
|
-
@
|
|
116
|
+
@joystick1 = Input::Joystick.new
|
|
117
|
+
@joystick2 = Input::Joystick.new
|
|
118
|
+
@keyboard_port = KeyboardPort.new(@keyboard, @joystick1, @joystick2)
|
|
60
119
|
@ted.keyboard_port = @keyboard_port
|
|
61
120
|
@iec_bus = Drives::SerialBus.new
|
|
62
121
|
@datasette = Drives::Datasette.new(level: true)
|
|
63
122
|
@port = CPUPort.new(serial_bus: @iec_bus, datasette: @datasette)
|
|
64
123
|
plus4_parts = @model.plus4_parts
|
|
65
|
-
@
|
|
124
|
+
@cycles = 0
|
|
125
|
+
@init_threshold = INIT_THRESHOLD
|
|
126
|
+
@acia_irq = false
|
|
127
|
+
@acia_due = ACIA::NEVER
|
|
128
|
+
@due = INIT_THRESHOLD
|
|
129
|
+
@sid_card = sid_card ? SIDCard.new(self, sid_card, sid_card_address) : nil
|
|
130
|
+
@acia = plus4_parts ? ACIA.new(self, @model.ted.clock_hz) : nil
|
|
66
131
|
@user_port = plus4_parts ? UserPort.new(@datasette) : nil
|
|
67
|
-
io = Bus::IOPage.new(keyboard_port: @keyboard_port, acia: @acia, user_port: @user_port
|
|
68
|
-
|
|
132
|
+
io = Bus::IOPage.new(keyboard_port: @keyboard_port, acia: @acia, user_port: @user_port,
|
|
133
|
+
cassette_sense: plus4_parts ? nil : CassetteSense.new(@datasette),
|
|
134
|
+
sid_card: sid_card_at(0xfd40))
|
|
135
|
+
@bus = Bus.new(ted: @ted, port: @port, io:, model: @model, ram_kb: @ram_kb, ram_expansion:)
|
|
136
|
+
@ted.connect(@bus)
|
|
137
|
+
@bus.plug_sid_card(sid_card_at(0xfe80))
|
|
69
138
|
@tias = tcbm ? [Chips::TIA.new, Chips::TIA.new] : []
|
|
70
139
|
@bus.plug_tcbm(drive8: @tias[0], drive9: @tias[1]) if tcbm
|
|
71
140
|
@cpu = CPU.new(@bus, debug:)
|
|
72
|
-
@
|
|
141
|
+
@sound = Chips::TED::Sound.new(self, @model.ted.clock_hz)
|
|
142
|
+
@ted.sound = @sound
|
|
143
|
+
@sound_mix = @sid_card ? SoundMix.new(@sound, @sid_card) : nil
|
|
73
144
|
@init_handlers = []
|
|
74
145
|
@pending_keys = nil
|
|
75
146
|
@dos = nil
|
|
76
147
|
@serial_trap = nil
|
|
77
148
|
@save_trap = nil
|
|
78
149
|
@capture_output = nil
|
|
150
|
+
init_drives(true_drive_model)
|
|
79
151
|
end
|
|
80
152
|
|
|
81
153
|
# One TED cycle: the TED moves on and decides the CPU's clock, then the
|
|
82
154
|
# CPU runs its cycles in it, none while the TED holds the bus.
|
|
83
155
|
def cycle!
|
|
84
|
-
|
|
156
|
+
handle_due if @cycles >= @due
|
|
85
157
|
feed_keyboard if @pending_keys
|
|
86
158
|
|
|
87
159
|
ted = @ted
|
|
88
160
|
ted.cycle!
|
|
89
161
|
@datasette.cycle!
|
|
90
|
-
@cpu.irq = ted.irq?
|
|
162
|
+
@cpu.irq = ted.irq? || @acia_irq
|
|
91
163
|
slots = ted.cpu_slots
|
|
92
164
|
if slots.zero?
|
|
93
165
|
@cpu.stall!
|
|
@@ -95,35 +167,52 @@ module Badline
|
|
|
95
167
|
clock_cpu(ted)
|
|
96
168
|
clock_cpu(ted) if slots == 2
|
|
97
169
|
end
|
|
170
|
+
@drive1541&.host_cycle!
|
|
171
|
+
@drive1581&.host_cycle!
|
|
172
|
+
@drive1551&.host_cycle!
|
|
98
173
|
|
|
99
174
|
@cycles += 1
|
|
100
175
|
end
|
|
101
176
|
|
|
102
177
|
# The RESET line: the CPU, the port, the 6529s, the ACIA, the bank
|
|
103
|
-
# latch and ROM selection
|
|
178
|
+
# latch and ROM selection, a RAM expansion's register, the SID card,
|
|
179
|
+
# and through the serial bus's RESET line, the drives. The TED has no
|
|
180
|
+
# reset pin.
|
|
104
181
|
def reset!
|
|
105
182
|
@port.reset!
|
|
106
183
|
@keyboard_port.reset!
|
|
107
184
|
@user_port&.reset!
|
|
108
185
|
@dos&.reset!
|
|
186
|
+
@drive1541&.reset!
|
|
187
|
+
@drive1581&.reset!
|
|
188
|
+
@drive1551&.reset!
|
|
109
189
|
@acia&.reset!
|
|
190
|
+
@sid_card&.reset!
|
|
110
191
|
@tias.each(&:reset!)
|
|
111
192
|
@bus.reset!
|
|
112
193
|
@cpu.reset!
|
|
113
194
|
end
|
|
114
195
|
|
|
115
|
-
# Switches the machine off and on: RAM
|
|
116
|
-
# power-on state, and the RES line resets everything else.
|
|
196
|
+
# Switches the machine off and on: RAM, the TED and its sound start
|
|
197
|
+
# from their power-on state, and the RES line resets everything else.
|
|
117
198
|
def power_cycle!
|
|
118
199
|
@bus.power_on!
|
|
119
200
|
@ted.power_on!
|
|
201
|
+
@sound.power_on!
|
|
120
202
|
reset!
|
|
121
203
|
end
|
|
122
204
|
|
|
205
|
+
# The ACIA's IRQ line, and the cycle it next has something to do at.
|
|
206
|
+
def acia_changed(irq, due)
|
|
207
|
+
@acia_irq = irq
|
|
208
|
+
@acia_due = due
|
|
209
|
+
schedule
|
|
210
|
+
end
|
|
211
|
+
|
|
123
212
|
# Calls the block with the exit code a VICE testprog writes to $FDCF.
|
|
124
213
|
def install_debug_register(&) = @bus.install_debug_register(&)
|
|
125
214
|
|
|
126
|
-
# Puts a cartridge's ROM chips in
|
|
215
|
+
# Puts a cartridge's ROM chips in their slots (Bus#attach_cartridge)
|
|
127
216
|
# and switches the machine off and on, as a cartridge goes in with the
|
|
128
217
|
# power off. The KERNAL starts it at reset when it signs itself with CBM
|
|
129
218
|
# at $8007.
|
|
@@ -132,6 +221,36 @@ module Badline
|
|
|
132
221
|
power_cycle!
|
|
133
222
|
end
|
|
134
223
|
|
|
224
|
+
# Takes both cartridges out (Bus#detach_cartridges) and switches the
|
|
225
|
+
# machine off and on.
|
|
226
|
+
def detach_cartridges
|
|
227
|
+
@bus.detach_cartridges
|
|
228
|
+
power_cycle!
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# The cartridge slots with a ROM in them (Bus#cartridge_slots).
|
|
232
|
+
def cartridge_slots = @bus.cartridge_slots
|
|
233
|
+
|
|
234
|
+
# The model and its RAM as the pause menu names them, such as
|
|
235
|
+
# "PLUS/4 PAL, 64K RAM".
|
|
236
|
+
def model_label
|
|
237
|
+
name = @model.plus4_parts ? "PLUS/4" : "C16"
|
|
238
|
+
standard = @model.ted == Chips::TED::NTSC ? "NTSC" : "PAL"
|
|
239
|
+
"#{name} #{standard}, #{@ram_kb}K RAM"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# The RAMExpansion fitted, or nil.
|
|
243
|
+
def ram_expansion = @bus.ram_expansion
|
|
244
|
+
|
|
245
|
+
# The RAM expansion as the pause menu names it, such as "HANNES, 256
|
|
246
|
+
# KB", or nil without one.
|
|
247
|
+
def ram_expansion_label
|
|
248
|
+
expansion = @bus.ram_expansion
|
|
249
|
+
return if expansion.nil?
|
|
250
|
+
|
|
251
|
+
"#{RAMExpansion::NAMES.fetch(expansion.kind)}, #{expansion.size_kb} KB"
|
|
252
|
+
end
|
|
253
|
+
|
|
135
254
|
# Stores a PRG's bytes from its load address on, into RAM, and returns
|
|
136
255
|
# that address.
|
|
137
256
|
def load_prg(data)
|
|
@@ -144,6 +263,22 @@ module Badline
|
|
|
144
263
|
|
|
145
264
|
private
|
|
146
265
|
|
|
266
|
+
# The SID card if it is at +address+, or nil.
|
|
267
|
+
def sid_card_at(address)
|
|
268
|
+
card = @sid_card
|
|
269
|
+
card if card && card.address == address
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# The RAM in K: +ram+, or the model's own, or 64K under a RAM
|
|
273
|
+
# expansion, which needs that much.
|
|
274
|
+
def fitted_ram(ram, ram_expansion)
|
|
275
|
+
ram_kb = ram || (ram_expansion ? 64 : @model.ram_kb)
|
|
276
|
+
raise ArgumentError, "no Plus/4 RAM size of #{ram_kb}K" unless Model::RAM_SIZES.include?(ram_kb)
|
|
277
|
+
raise ArgumentError, "a RAM expansion needs 64K, not #{ram_kb}K" if ram_expansion && ram_kb != 64
|
|
278
|
+
|
|
279
|
+
ram_kb
|
|
280
|
+
end
|
|
281
|
+
|
|
147
282
|
# The 264 KERNAL's keyboard buffer.
|
|
148
283
|
def keyboard_buffer = Machine::KeyboardBuffer::PLUS4_LAYOUT
|
|
149
284
|
|
|
@@ -152,9 +287,32 @@ module Badline
|
|
|
152
287
|
|
|
153
288
|
def trap_bus = @bus
|
|
154
289
|
|
|
155
|
-
#
|
|
156
|
-
# the bus
|
|
157
|
-
|
|
290
|
+
# A drive that powers on with the machine takes longer to boot than
|
|
291
|
+
# the KERNAL, and doesn't answer the bus until it has, so the
|
|
292
|
+
# machine's on_init handlers wait for it too.
|
|
293
|
+
def drive1541_attached
|
|
294
|
+
@init_threshold = [@init_threshold, DRIVE_BOOT_CYCLES].max if @cycles < @init_threshold
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def drive1581_attached
|
|
298
|
+
@init_threshold = [@init_threshold, Drives::Model1581::BOOT_CYCLES].max if @cycles < @init_threshold
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# What falls due at the start of a cycle: the on_init handlers at the
|
|
302
|
+
# init threshold, and the ACIA's next character.
|
|
303
|
+
def handle_due
|
|
304
|
+
handle_init if @cycles == @init_threshold
|
|
305
|
+
acia = @acia
|
|
306
|
+
acia.run!(@cycles) if acia && @cycles >= @acia_due
|
|
307
|
+
schedule
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# The next cycle anything falls due at. A drive that raises the init
|
|
311
|
+
# threshold leaves it early, and handle_due looks again then.
|
|
312
|
+
def schedule
|
|
313
|
+
init = @cycles < @init_threshold ? @init_threshold : ACIA::NEVER
|
|
314
|
+
@due = [@acia_due, init].min
|
|
315
|
+
end
|
|
158
316
|
|
|
159
317
|
# BA stops the CPU at a read, and a write goes ahead.
|
|
160
318
|
def clock_cpu(ted)
|
data/lib/badline/roms/README
CHANGED
|
@@ -79,6 +79,15 @@ Downloaded from
|
|
|
79
79
|
https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1581/1581-rom.318045-02.bin
|
|
80
80
|
SHA-1 01228eae6f066bd9b7b2b6a7fa3f667e41dad393.
|
|
81
81
|
|
|
82
|
+
dos1551.rom
|
|
83
|
+
-----------
|
|
84
|
+
The 1551 disk drive's DOS 2.6, 318008-01, 16 KB for $C000-$FFFF.
|
|
85
|
+
Drives::Model1551 loads it.
|
|
86
|
+
|
|
87
|
+
Downloaded from
|
|
88
|
+
https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1551/1551.318008-01.bin
|
|
89
|
+
SHA-1 fae3c788ad9a6cc2dbdfbcf6c0264b2ca921d55e.
|
|
90
|
+
|
|
82
91
|
Commodore VIC-20 ROMs
|
|
83
92
|
=====================
|
|
84
93
|
|
|
Binary file
|
|
@@ -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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "badline"
|
|
4
4
|
require "badline/c128"
|
|
5
|
+
require "badline/plus4"
|
|
5
6
|
|
|
6
7
|
module Badline
|
|
7
8
|
module Snapshot
|
|
@@ -27,29 +28,47 @@ module Badline
|
|
|
27
28
|
# each module the keys its records hold, with their tags, and the fields the load read.
|
|
28
29
|
Load = Data.define(:family, :written, :fields)
|
|
29
30
|
|
|
30
|
-
# The
|
|
31
|
-
#
|
|
31
|
+
# The cartridges by CRT hardware type: EasyFlash, then a mapper of
|
|
32
|
+
# each kind of register: Ocean's bank, Comal-80's bank for both
|
|
33
|
+
# windows, Zaxxon's ROMH bank, and the Action Replay's and the
|
|
34
|
+
# Pagefox's control registers.
|
|
35
|
+
CARTRIDGES = [32, 5, 21, 18, 1, 53].freeze
|
|
36
|
+
|
|
37
|
+
# The machines the doc is made from: each family at power-on, the C64
|
|
38
|
+
# with each kind of expansion, cartridge and media, and a second SID,
|
|
39
|
+
# and the 264 series' models, its TCBM interface, cartridge, tape and
|
|
40
|
+
# true drives, the 1541 and the 1551.
|
|
32
41
|
def self.machines
|
|
33
42
|
{
|
|
34
|
-
"c64" => [C64.new, C64.new(ram_expansion: :plus256k), C64.new(reu: 512),
|
|
35
|
-
|
|
43
|
+
"c64" => [C64.new, C64.new(ram_expansion: :plus256k), C64.new(reu: 512),
|
|
44
|
+
*CARTRIDGES.map { |type| with_cartridge(type) }, with_trap_dos, with_tape, with_drive(:d64),
|
|
45
|
+
with_drive(:d81), with_extra_sids],
|
|
36
46
|
"vic20" => [Vic20.new, with_rom],
|
|
37
|
-
"c128" => [C128.new(mode: :c128), with_drive(:d71)]
|
|
47
|
+
"c128" => [C128.new(mode: :c128), with_drive(:d71)],
|
|
48
|
+
"plus4" => [Plus4.new, Plus4.new(model: "c16"), Plus4.new(tcbm: true), Plus4.new(sid_card: :mos6581),
|
|
49
|
+
Plus4.new(sid_card: :mos8580, sid_card_address: 0xfe80),
|
|
50
|
+
Plus4.new(ram_expansion: :csory256),
|
|
51
|
+
plus4_with_cartridge, plus4_with_tape, plus4_with_drive, plus4_with_drive("1551")]
|
|
38
52
|
}
|
|
39
53
|
end
|
|
40
54
|
|
|
41
55
|
def self.blank(length) = Array.new(length, 0)
|
|
42
56
|
|
|
43
|
-
def self.with_cartridge
|
|
57
|
+
def self.with_cartridge(type)
|
|
44
58
|
chips = [0x8000, 0xa000].map do |address|
|
|
45
59
|
Storage::CRTFile::Chip.new(chip_type: 2, bank: 0, address:, data: blank(0x2000))
|
|
46
60
|
end
|
|
47
|
-
image = Storage::CRTFile::Image.new(hardware_type:
|
|
61
|
+
image = Storage::CRTFile::Image.new(hardware_type: type, subtype: 0, exrom: 1, game: 0, name: "DOC", chips:)
|
|
48
62
|
C64.new.tap do |machine|
|
|
49
63
|
machine.attach_cartridge(C64::Cartridge.from_crt(Storage::CRTFile.new(bytes: Storage::CRTFile.encode(image))))
|
|
50
64
|
end
|
|
51
65
|
end
|
|
52
66
|
|
|
67
|
+
# The SID player's machine for a tune on two SIDs.
|
|
68
|
+
def self.with_extra_sids
|
|
69
|
+
C64.new.tap { |machine| machine.address_bus.add_sid(Chips::SID.new(at: 0xd420)) }
|
|
70
|
+
end
|
|
71
|
+
|
|
53
72
|
def self.with_trap_dos
|
|
54
73
|
C64.new.tap { |machine| machine.mount(Storage::D64Image.new("", read_only: true, bytes: blank(174_848))) }
|
|
55
74
|
end
|
|
@@ -78,6 +97,28 @@ module Badline
|
|
|
78
97
|
end
|
|
79
98
|
end
|
|
80
99
|
|
|
100
|
+
# A Plus/4 with a cartridge in each of cartridge 1's slots.
|
|
101
|
+
def self.plus4_with_cartridge
|
|
102
|
+
chips = [0x8000, 0xc000].map do |address|
|
|
103
|
+
Storage::CRTFile::Chip.new(chip_type: 0, bank: 0, address:, data: blank(0x4000))
|
|
104
|
+
end
|
|
105
|
+
Plus4.new.tap { |machine| machine.attach_cartridge(chips) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# A C16 with a version 2 tape in its datasette, which times the read
|
|
109
|
+
# line's level.
|
|
110
|
+
def self.plus4_with_tape
|
|
111
|
+
bytes = "C16-TAPE-RAW".bytes + [2, 2, 0, 0, 4, 0, 0, 0, 0x30, 0x30, 0x30, 0x30]
|
|
112
|
+
Plus4.new(model: "c16").tap { |machine| machine.datasette.insert(Storage::TAP.new("", bytes:)) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.plus4_with_drive(model = "1541")
|
|
116
|
+
Plus4.new(true_drive_model: model).tap do |machine|
|
|
117
|
+
image = Storage::D64Image.new("", bytes: blank(174_848))
|
|
118
|
+
machine.plug_true_drive.insert(Drives::Model1541::Disk.decode(image))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
81
122
|
def self.with_rom
|
|
82
123
|
Vic20.new.tap { |machine| machine.bus.map_rom(0xa000, blank(0x2000)) }
|
|
83
124
|
end
|
|
@@ -34,7 +34,7 @@ module Badline
|
|
|
34
34
|
# of a machine in memory (.of).
|
|
35
35
|
class Image
|
|
36
36
|
MACHINE = "BL-MACHINE"
|
|
37
|
-
FAMILIES = { "c64" => "C64", "vic20" => "VIC-20", "c128" => "C128" }.freeze
|
|
37
|
+
FAMILIES = { "c64" => "C64", "vic20" => "VIC-20", "c128" => "C128", "plus4" => "Plus/4" }.freeze
|
|
38
38
|
|
|
39
39
|
attr_reader :container
|
|
40
40
|
|
|
@@ -54,8 +54,11 @@ module Badline
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
# The snapshot with VICE's modules ahead of the BL-* ones for a C64,
|
|
57
|
-
# so x64sc opens it too.
|
|
57
|
+
# so x64sc opens it too. Raises FormatError for a Plus/4, whose
|
|
58
|
+
# snapshots badline doesn't write for xplus4.
|
|
58
59
|
def self.exported(computer)
|
|
60
|
+
raise FormatError, "badline doesn't export a Plus/4's snapshot for VICE" if computer.family == :plus4
|
|
61
|
+
|
|
59
62
|
computer.settle_for_snapshot!
|
|
60
63
|
sections = computer.save_modules
|
|
61
64
|
sections = Vice.export(computer) + sections if computer.family == :c64
|
|
@@ -67,6 +70,7 @@ module Badline
|
|
|
67
70
|
case computer.family
|
|
68
71
|
when :vic20 then Container::VIC20
|
|
69
72
|
when :c128 then Container::C128
|
|
73
|
+
when :plus4 then Container::PLUS4
|
|
70
74
|
else Container::MACHINE
|
|
71
75
|
end
|
|
72
76
|
end
|
|
@@ -77,6 +81,7 @@ module Badline
|
|
|
77
81
|
case machine.choice("family", FAMILIES.keys, "c64")
|
|
78
82
|
when "vic20" then Vic20Setup.read_fields(machine)
|
|
79
83
|
when "c128" then C128Setup.read_fields(machine)
|
|
84
|
+
when "plus4" then Plus4Setup.read_fields(machine)
|
|
80
85
|
else C64Setup.read_fields(machine)
|
|
81
86
|
end
|
|
82
87
|
end
|
|
@@ -92,7 +97,7 @@ module Badline
|
|
|
92
97
|
|
|
93
98
|
# A new machine, built as the snapshot's was and restored from it. A
|
|
94
99
|
# badline snapshot restores from its BL-* modules alone, and a VICE
|
|
95
|
-
# one through the modules badline reads. xvic's and
|
|
100
|
+
# one through the modules badline reads. xvic's, x128's and xplus4's own
|
|
96
101
|
# modules aren't read. Yields a line for each thing left out, or
|
|
97
102
|
# warns it without a block. `detached` restores it without the host's
|
|
98
103
|
# files (Keyed::Modules).
|
|
@@ -100,6 +105,7 @@ module Badline
|
|
|
100
105
|
refuse_old_layout
|
|
101
106
|
raise FormatError, "badline reads only its own VIC-20 snapshots, not xvic's" if vic20? && !badline?
|
|
102
107
|
raise FormatError, "badline reads only its own C128 snapshots, not x128's" if c128? && !badline?
|
|
108
|
+
raise FormatError, "badline reads only its own Plus/4 snapshots, not xplus4's" if plus4? && !badline?
|
|
103
109
|
return load_vice(&) unless badline?
|
|
104
110
|
|
|
105
111
|
modules = Keyed::Modules.new(sections, detached:)
|
|
@@ -114,6 +120,9 @@ module Badline
|
|
|
114
120
|
# Whether the machine is a C128, as the container names it.
|
|
115
121
|
def c128? = @container.machine == Container::C128
|
|
116
122
|
|
|
123
|
+
# Whether the machine is of the 264 series, as the container names it.
|
|
124
|
+
def plus4? = @container.machine == Container::PLUS4
|
|
125
|
+
|
|
117
126
|
# Restores `computer` from the snapshot, and returns the Report. The
|
|
118
127
|
# snapshot goes into a new machine first, so one that fails, or is of
|
|
119
128
|
# a machine built another way, leaves `computer` as it was.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
# How a 264 series machine was built: its model's name, one of
|
|
6
|
+
# Plus4::Model::ALL, which also says whether the Plus/4's function ROMs
|
|
7
|
+
# are fitted, its RAM in K, whether a 1551's TCBM interface is plugged
|
|
8
|
+
# in, the SID card's SID model, or nil without one, and its address,
|
|
9
|
+
# and the RAM expansion's kind, or nil without one, the arguments
|
|
10
|
+
# Plus4.new takes.
|
|
11
|
+
Plus4Setup = Data.define(:model, :ram_kb, :tcbm, :sid_card, :sid_card_address, :ram_expansion)
|
|
12
|
+
|
|
13
|
+
class Plus4Setup
|
|
14
|
+
MODELS = Plus4::Model::ALL.map(&:name).freeze
|
|
15
|
+
|
|
16
|
+
SID_CARDS = %w[none mos6581 mos8580].freeze
|
|
17
|
+
|
|
18
|
+
RAM_EXPANSIONS = ["none", *Plus4::RAMExpansion::KINDS.keys.map(&:to_s)].freeze
|
|
19
|
+
|
|
20
|
+
# The setup of `machine`.
|
|
21
|
+
def self.of(machine)
|
|
22
|
+
card = machine.sid_card
|
|
23
|
+
new(model: machine.model.name, ram_kb: machine.ram_kb, tcbm: !machine.tias.empty?, sid_card: card&.model,
|
|
24
|
+
sid_card_address: card ? card.address : Plus4::SIDCard::ADDRESSES.first,
|
|
25
|
+
ram_expansion: machine.ram_expansion&.kind)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The model by name, the RAM, the TCBM interface, the SID card's
|
|
29
|
+
# model and address and the RAM expansion's kind, "none" without
|
|
30
|
+
# them, in a keyed snapshot module.
|
|
31
|
+
def write_fields(out)
|
|
32
|
+
out.text("model", model).int("ram_kb", ram_kb).bool("tcbm", tcbm).text("sid_card", (sid_card || :none).to_s)
|
|
33
|
+
out.int("sid_card_address", sid_card_address).text("ram_expansion", (ram_expansion || :none).to_s)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.read_fields(input)
|
|
37
|
+
model = input.choice("model", MODELS, "plus4")
|
|
38
|
+
ram_kb = input.int("ram_kb", Plus4::Model.named(model).ram_kb)
|
|
39
|
+
unless Plus4::Model::RAM_SIZES.include?(ram_kb)
|
|
40
|
+
raise FormatError, "the snapshot is of a Plus/4 with #{ram_kb}K of RAM, which badline doesn't build"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sid_card = named(input.choice("sid_card", SID_CARDS, "none"))
|
|
44
|
+
ram_expansion = named(input.choice("ram_expansion", RAM_EXPANSIONS, "none"))
|
|
45
|
+
new(model:, ram_kb:, tcbm: input.bool("tcbm", false), sid_card:, sid_card_address: sid_card_address(input),
|
|
46
|
+
ram_expansion:)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A choice's name as a Symbol, or nil for "none".
|
|
50
|
+
def self.named(name) = name == "none" ? nil : name.to_sym
|
|
51
|
+
|
|
52
|
+
def self.sid_card_address(input)
|
|
53
|
+
address = input.int("sid_card_address", Plus4::SIDCard::ADDRESSES.first)
|
|
54
|
+
return address if Plus4::SIDCard::ADDRESSES.include?(address)
|
|
55
|
+
|
|
56
|
+
raise FormatError, format("the snapshot is of a Plus/4 with a SID card at $%<address>04X, " \
|
|
57
|
+
"which badline doesn't build", address:)
|
|
58
|
+
end
|
|
59
|
+
private_class_method :named, :sid_card_address
|
|
60
|
+
|
|
61
|
+
# A machine built this way, at power-on.
|
|
62
|
+
def build = Plus4.new(model:, ram: ram_kb, tcbm:, sid_card:, sid_card_address:, ram_expansion:)
|
|
63
|
+
|
|
64
|
+
def to_s
|
|
65
|
+
parts = ["#{ram_kb}K"]
|
|
66
|
+
parts << "a TCBM interface" if tcbm
|
|
67
|
+
parts << sid_card_part if sid_card
|
|
68
|
+
parts << "a #{ram_expansion} RAM expansion" if ram_expansion
|
|
69
|
+
"#{model} with #{parts.join(' and ')}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def sid_card_part
|
|
75
|
+
format("%<article>s %<model>s SID card at $%<address>04X", article: sid_card == :mos8580 ? "an" : "a",
|
|
76
|
+
model: sid_card.to_s.delete_prefix("mos"),
|
|
77
|
+
address: sid_card_address)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/badline/snapshot.rb
CHANGED
|
@@ -35,8 +35,9 @@ module Badline
|
|
|
35
35
|
# The machine's snapshot file. Saving first runs the machine on to the
|
|
36
36
|
# end of the instruction its CPU is in, and of an REU transfer under
|
|
37
37
|
# way (Machine::Settling), so a save advances it by a few cycles, and a
|
|
38
|
-
# second save straight after runs none. A VIC-20's
|
|
39
|
-
# is in a container named as xvic
|
|
38
|
+
# second save straight after runs none. A VIC-20's, a C128's or a
|
|
39
|
+
# Plus/4's snapshot is in a container named as xvic, x128 or xplus4
|
|
40
|
+
# names its machine.
|
|
40
41
|
def dump(computer) = Image.of(computer).to_s
|
|
41
42
|
|
|
42
43
|
def read(path) = Image.new(Container.parse(File.binread(path)), name: File.basename(path))
|
|
@@ -50,8 +51,8 @@ module Badline
|
|
|
50
51
|
# snapshot that fails leaves the machine as it was.
|
|
51
52
|
def restore(computer, path, &) = read(path).restore(computer, &)
|
|
52
53
|
|
|
53
|
-
# A machine's snapshots, as .vsf files and in memory. C64, Vic20
|
|
54
|
-
#
|
|
54
|
+
# A machine's snapshots, as .vsf files and in memory. C64, Vic20, C128
|
|
55
|
+
# and Plus4 each include it.
|
|
55
56
|
module Files
|
|
56
57
|
def save_snapshot(path) = Snapshot.save(self, path)
|
|
57
58
|
|
|
@@ -90,4 +91,8 @@ module Badline
|
|
|
90
91
|
class C128
|
|
91
92
|
include Snapshot::Files
|
|
92
93
|
end
|
|
94
|
+
|
|
95
|
+
class Plus4
|
|
96
|
+
include Snapshot::Files
|
|
97
|
+
end
|
|
93
98
|
end
|