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