badline 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -0
- data/CLAUDE.md +6 -4
- data/README.md +60 -19
- data/doc/library.md +10 -3
- data/doc/media.md +11 -2
- data/doc/pinned-behaviour.md +199 -9
- data/doc/sid-player.md +8 -4
- data/doc/snapshots.md +79 -33
- data/lib/badline/address_bus/pla.rb +130 -0
- data/lib/badline/address_bus/roms.rb +32 -0
- data/lib/badline/address_bus.rb +13 -92
- data/lib/badline/audio/bare_player.rb +24 -0
- data/lib/badline/audio/checkpoints.rb +42 -0
- data/lib/badline/audio/jukebox.rb +11 -4
- data/lib/badline/audio/machine_player.rb +16 -0
- data/lib/badline/audio/renderer.rb +80 -9
- data/lib/badline/audio/stereo.rb +3 -5
- data/lib/badline/audio.rb +1 -0
- data/lib/badline/chrout_trap.rb +4 -3
- data/lib/badline/computer/attachments.rb +52 -12
- data/lib/badline/computer/saved_state.rb +1 -0
- data/lib/badline/computer.rb +50 -33
- data/lib/badline/control_ports.rb +10 -3
- data/lib/badline/core.rb +51 -0
- data/lib/badline/data_path.rb +37 -0
- data/lib/badline/datasette.rb +63 -10
- data/lib/badline/drive1541/disk.rb +3 -3
- data/lib/badline/drive1541/disk_state.rb +14 -5
- 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 +63 -64
- data/lib/badline/frontend/boot.rb +34 -4
- data/lib/badline/frontend/confirmation.rb +17 -11
- 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_dialogs.rb +52 -14
- data/lib/badline/frontend/menu_media.rb +70 -16
- data/lib/badline/frontend/menu_pages.rb +47 -42
- data/lib/badline/frontend/menu_ports.rb +69 -0
- data/lib/badline/frontend/name_field.rb +88 -0
- data/lib/badline/frontend/pacer.rb +6 -5
- data/lib/badline/frontend/pause_menu.rb +52 -37
- data/lib/badline/frontend/player_screen.rb +1 -1
- data/lib/badline/frontend/player_window.rb +4 -4
- data/lib/badline/frontend/screen.rb +39 -37
- data/lib/badline/frontend/sid_view.rb +1 -1
- data/lib/badline/frontend/snapshots.rb +145 -17
- data/lib/badline/frontend/sound.rb +17 -9
- data/lib/badline/frontend/timeline.rb +36 -5
- data/lib/badline/frontend.rb +3 -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 +24 -25
- data/lib/badline/kernal_trap/routine.rb +17 -5
- data/lib/badline/kernal_trap/save.rb +13 -23
- data/lib/badline/kernal_trap/serial.rb +38 -24
- data/lib/badline/kernal_trap.rb +1 -0
- data/lib/badline/keyboard.rb +17 -14
- data/lib/badline/machine.rb +60 -0
- data/lib/badline/media/disk_list.rb +75 -0
- data/lib/badline/media/disk_set.rb +13 -3
- 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 +39 -20
- data/lib/badline/model.rb +62 -0
- data/lib/badline/options/event.rb +10 -6
- data/lib/badline/options/help.rb +25 -13
- data/lib/badline/options/table.rb +15 -1
- data/lib/badline/options/validation.rb +63 -3
- data/lib/badline/options.rb +37 -34
- data/lib/badline/region.rb +16 -4
- data/lib/badline/reu/ram.rb +27 -0
- data/lib/badline/reu/saved_state.rb +27 -0
- data/lib/badline/roms/README +37 -0
- data/lib/badline/roms/kernal-sx64.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/sid/decimator.rb +13 -0
- data/lib/badline/sid/filter.rb +6 -3
- data/lib/badline/sid.rb +19 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/fields.rb +1 -0
- data/lib/badline/snapshot/image.rb +15 -0
- data/lib/badline/snapshot/setup.rb +20 -8
- data/lib/badline/snapshot/state.rb +15 -7
- data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
- data/lib/badline/snapshot/vice/peripherals.rb +12 -2
- data/lib/badline/snapshot/vice/restore.rb +3 -1
- data/lib/badline/snapshot/vice/reu1764.rb +55 -0
- data/lib/badline/snapshot/vice/vicii.rb +59 -13
- data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
- data/lib/badline/snapshot/vice.rb +25 -23
- 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 +10 -4
- data/lib/badline/storage/tap.rb +44 -8
- data/lib/badline/storage/unavailable.rb +3 -1
- data/lib/badline/storage.rb +5 -5
- 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 +53 -16
- data/lib/badline/vic/registers.rb +20 -4
- data/lib/badline/vic/sprite/timing.rb +5 -9
- data/lib/badline/vic/sprite.rb +4 -3
- data/lib/badline/vic.rb +34 -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
- data/lib/badline.rb +2 -38
- metadata +37 -2
- data/lib/badline/address_bus/ultimax_pages.rb +0 -38
data/lib/badline/vic.rb
CHANGED
|
@@ -11,10 +11,20 @@ module Badline
|
|
|
11
11
|
include Addressable
|
|
12
12
|
include IntegerHelper
|
|
13
13
|
|
|
14
|
-
# The 6569 of the breadbin C64,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
# The 6569 of the breadbin C64, the HMOS 8565 of the C64C, and the
|
|
15
|
+
# C128's VIC-IIe, the 8566 (PAL) and 8564 (NTSC). The VIC-IIe draws as
|
|
16
|
+
# the 8565 and adds the $D02F and $D030 registers.
|
|
17
|
+
MODELS = %i[mos6569 mos8565 mos8566 mos8564].freeze
|
|
18
|
+
|
|
19
|
+
# The sixteen colours #display's palette indices stand for, as RGB.
|
|
20
|
+
PALETTE = [
|
|
21
|
+
0x000000, 0xffffff, 0x924a40, 0x84c5cc,
|
|
22
|
+
0x9351b6, 0x72b14b, 0x483aaa, 0xd5df7c,
|
|
23
|
+
0x675200, 0xc33d00, 0xc18178, 0x606060,
|
|
24
|
+
0x8a8a8a, 0xb3ec91, 0x867ade, 0xb3b3b3
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
attr_reader :display, :width, :height, :vic_bank, :column,
|
|
18
28
|
:rasterline, :dirty_lines, :model, :region
|
|
19
29
|
|
|
20
30
|
# The parts a VICE snapshot reads and sets.
|
|
@@ -76,19 +86,20 @@ module Badline
|
|
|
76
86
|
SPRITE_BA_FIRST = 54
|
|
77
87
|
SPRITE_BA_LENGTH = 5
|
|
78
88
|
|
|
79
|
-
def initialize(
|
|
89
|
+
def initialize(debug: false, model: :mos6569, region: Region::PAL)
|
|
80
90
|
raise ArgumentError, "unknown VIC-II model #{model}" unless MODELS.include?(model)
|
|
81
91
|
|
|
82
92
|
addressable_at(0xd000, length: 2**10)
|
|
83
93
|
@model = model
|
|
94
|
+
@iie = %i[mos8566 mos8564].include?(model)
|
|
95
|
+
@core = model == :mos6569 ? :mos6569 : :mos8565
|
|
84
96
|
@region = region
|
|
85
|
-
@lightpen_extra =
|
|
86
|
-
@grey_dots =
|
|
87
|
-
@delayed_fetch =
|
|
88
|
-
@dma_delay_idle =
|
|
89
|
-
@bank_swaps =
|
|
90
|
-
@
|
|
91
|
-
@vic_bank = VIC::Bank.new(@address_bus)
|
|
97
|
+
@lightpen_extra = @core == :mos8565 ? 1 : 2
|
|
98
|
+
@grey_dots = @core == :mos8565
|
|
99
|
+
@delayed_fetch = @core == :mos8565
|
|
100
|
+
@dma_delay_idle = @core == :mos6569
|
|
101
|
+
@bank_swaps = @core == :mos8565
|
|
102
|
+
@vic_bank = VIC::Bank.new(self)
|
|
92
103
|
@debug = debug
|
|
93
104
|
|
|
94
105
|
@columns_per_line = region.cycles_per_line
|
|
@@ -114,12 +125,12 @@ module Badline
|
|
|
114
125
|
# registers, the raster position and the fetch and sprite state it
|
|
115
126
|
# starts with. The display keeps its buffers, cleared to black.
|
|
116
127
|
def power_on!
|
|
117
|
-
@registers = VIC::Registers.new
|
|
128
|
+
@registers = VIC::Registers.new(iie: @iie)
|
|
118
129
|
@register_bytes = @registers.bytes
|
|
119
130
|
@display_state = VIC::DisplayState.new(@registers, @last_column)
|
|
120
|
-
@sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank, model: @
|
|
131
|
+
@sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank, model: @core, region: @region)
|
|
121
132
|
@sequencer.render = @render
|
|
122
|
-
@sprites = VIC::Sprites.new(@registers, @vic_bank, @width, model: @
|
|
133
|
+
@sprites = VIC::Sprites.new(@registers, @vic_bank, @width, model: @core, region: @region)
|
|
123
134
|
@display.fill(0)
|
|
124
135
|
@lines.each { |line| line.fill(0) }
|
|
125
136
|
@dirty_lines.fill(true)
|
|
@@ -222,6 +233,12 @@ module Badline
|
|
|
222
233
|
@registers.irq_line?
|
|
223
234
|
end
|
|
224
235
|
|
|
236
|
+
# The VIC-IIe's $D02F lines K0-K2 and $D030 bits, for the machine to
|
|
237
|
+
# read. The 6569 and 8565 give 7, false and false.
|
|
238
|
+
def extra_keyboard_lines = @registers.extra_keyboard_lines
|
|
239
|
+
def fast? = @registers.fast?
|
|
240
|
+
def test? = @registers.test?
|
|
241
|
+
|
|
225
242
|
def peek(addr)
|
|
226
243
|
i = offset_of(addr) % (2**6)
|
|
227
244
|
case i
|
|
@@ -363,6 +380,8 @@ module Badline
|
|
|
363
380
|
@dirty_lines.fill(false)
|
|
364
381
|
end
|
|
365
382
|
|
|
383
|
+
def palette = PALETTE
|
|
384
|
+
|
|
366
385
|
private
|
|
367
386
|
|
|
368
387
|
def load_beam(input)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Vic20
|
|
5
|
+
# What plugs into the VIC-20 besides its chips: cartridge ROM in the
|
|
6
|
+
# expansion blocks, the disk device 8 serves through the KERNAL traps,
|
|
7
|
+
# and a true 1541 on the serial bus.
|
|
8
|
+
module Attachments
|
|
9
|
+
# The cycle by which a 1541 switched on with the machine has run its
|
|
10
|
+
# DOS's reset, the RAM test and the ROM checksum, and waits on the
|
|
11
|
+
# bus, with a margin: about 1.1 million of the VIC-20's cycles.
|
|
12
|
+
DRIVE_BOOT_CYCLES = 1_300_000
|
|
13
|
+
|
|
14
|
+
# Where BASIC's program starts, from TXTTAB, which the KERNAL sets at
|
|
15
|
+
# boot by the RAM it finds: $1001 unexpanded, $0401 with the 3K
|
|
16
|
+
# expansion and $1201 with 8K or more in BLK1.
|
|
17
|
+
def basic_start = ram.peek16(0x2b)
|
|
18
|
+
|
|
19
|
+
# Puts a cartridge's ROM chips, each a CRTFile::Chip with its address
|
|
20
|
+
# and bytes, into their blocks, and switches the machine off and on,
|
|
21
|
+
# as a cartridge goes in with the power off. The KERNAL then starts
|
|
22
|
+
# the cartridge in BLK5 that signs itself with A0CBM.
|
|
23
|
+
def attach_cartridge(chips)
|
|
24
|
+
chips.each { |chip| @bus.map_rom(chip.address, chip.data) }
|
|
25
|
+
power_cycle!
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Puts the storage in device 8. Mounting again swaps the disk at any
|
|
29
|
+
# point while the machine runs: the drive keeps its RAM and its
|
|
30
|
+
# status.
|
|
31
|
+
def mount(storage)
|
|
32
|
+
return @drive.insert(storage) if @drive
|
|
33
|
+
|
|
34
|
+
@drive = KernalTrap::Drive.new(storage)
|
|
35
|
+
layout = KernalTrap::VIC20_LAYOUT
|
|
36
|
+
load_trap = KernalTrap::Load.new(cpu:, bus:, layout:, drive: @drive)
|
|
37
|
+
cpu.install_trap(layout.load) { load_trap.call }
|
|
38
|
+
@serial_trap = KernalTrap::Serial.new(cpu:, bus:, layout:, drive: @drive, device: serial_trap_device).install
|
|
39
|
+
save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus:, layout:, drive: @drive)
|
|
40
|
+
cpu.install_trap(layout.save) { save_trap.call }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Takes device 8's mounted storage out, and with it the LOAD, SAVE and
|
|
44
|
+
# serial traps, so the KERNAL's routines go out over the serial bus.
|
|
45
|
+
# Mounting again starts a new drive, with its RAM cleared.
|
|
46
|
+
def unmount
|
|
47
|
+
return unless @drive
|
|
48
|
+
|
|
49
|
+
cpu.remove_trap(KernalTrap::VIC20_LAYOUT.load)
|
|
50
|
+
cpu.remove_trap(KernalTrap::VIC20_LAYOUT.save)
|
|
51
|
+
@serial_trap.device = nil
|
|
52
|
+
@serial_trap = nil
|
|
53
|
+
@save_trap = nil
|
|
54
|
+
@drive = nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Whether device 8 serves a disk or directory through the traps.
|
|
58
|
+
def mounted? = !@drive.nil?
|
|
59
|
+
|
|
60
|
+
# Plugs in a Drive1541, which then runs alongside the VIC-20 on its own
|
|
61
|
+
# clock and talks to it over the serial bus. The serial traps stop
|
|
62
|
+
# answering the drive's device number, so the KERNAL's TALK, LISTEN
|
|
63
|
+
# and byte transfers reach the drive. The LOAD and SAVE traps stay,
|
|
64
|
+
# and still serve a mounted image.
|
|
65
|
+
#
|
|
66
|
+
# A drive that powers on with the machine takes longer to boot than
|
|
67
|
+
# the KERNAL, and doesn't answer the bus until it has, so the
|
|
68
|
+
# machine's on_init handlers wait for it too.
|
|
69
|
+
def attach_drive1541(drive)
|
|
70
|
+
@init_threshold = [@init_threshold, DRIVE_BOOT_CYCLES].max if @cycles < @init_threshold
|
|
71
|
+
@iec_bus.detach(@drive1541) if @drive1541
|
|
72
|
+
drive.host_clock_hz = timing.clock_hz
|
|
73
|
+
@drive1541 = drive
|
|
74
|
+
drive.connect(@iec_bus)
|
|
75
|
+
@serial_trap&.device = serial_trap_device
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Unplugs the Drive1541, leaving the serial bus with nothing on it.
|
|
79
|
+
def detach_drive1541
|
|
80
|
+
return unless @drive1541
|
|
81
|
+
|
|
82
|
+
@iec_bus.detach(@drive1541)
|
|
83
|
+
@drive1541 = nil
|
|
84
|
+
@serial_trap&.device = serial_trap_device
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Records what the KERNAL prints through CHROUT (ChroutTrap).
|
|
88
|
+
def capture_output
|
|
89
|
+
@capture_output ||= ChroutTrap.new(cpu:, bus:, layout: KernalTrap::VIC20_LAYOUT).tap do |trap|
|
|
90
|
+
cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# The device number the serial traps answer: device 8, unless a true
|
|
97
|
+
# drive is on the bus as device 8.
|
|
98
|
+
def serial_trap_device
|
|
99
|
+
@drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Vic20
|
|
5
|
+
# The CPU's bus:
|
|
6
|
+
#
|
|
7
|
+
# $0000-$03FF - 1K of RAM
|
|
8
|
+
# $0400-$0FFF - RAM1-3, 3K of expansion RAM, or nothing
|
|
9
|
+
# $1000-$1FFF - 4K of RAM
|
|
10
|
+
# $2000-$7FFF - BLK1, BLK2 and BLK3, 8K of expansion RAM each, or nothing
|
|
11
|
+
# $8000-$8FFF - the character ROM
|
|
12
|
+
# $9000-$93FF - I/O 0: the VIC and both VIAs (see IO0)
|
|
13
|
+
# $9400-$97FF - colour RAM, 1K by four bits
|
|
14
|
+
# $9800-$9FFF - I/O 2 and I/O 3, for cartridges
|
|
15
|
+
# $A000-$BFFF - BLK5, 8K of expansion RAM, or nothing
|
|
16
|
+
# $C000-$DFFF - the BASIC ROM
|
|
17
|
+
# $E000-$FFFF - the KERNAL ROM
|
|
18
|
+
#
|
|
19
|
+
# The VIC reaches $0000-$1FFF and $8000-$9FFF, the pages where A13 and
|
|
20
|
+
# A14 are both low, over the V-bus, and the CPU reaches them through it
|
|
21
|
+
# too. Elsewhere the CPU is on its own side of the bus.
|
|
22
|
+
#
|
|
23
|
+
# Each side's data lines hold the last byte they carried, so a read
|
|
24
|
+
# from an empty spot finds that byte: the V-bus's in the 3K hole, and
|
|
25
|
+
# the CPU's own in the empty blocks, I/O 2 and I/O 3. Colour RAM drives
|
|
26
|
+
# the low four lines only, and the V-bus's high four come through.
|
|
27
|
+
# Writes to ROM and to empty spots go nowhere.
|
|
28
|
+
class Bus
|
|
29
|
+
include Addressable
|
|
30
|
+
|
|
31
|
+
# The pages each expansion block covers, as [first page, count].
|
|
32
|
+
BLOCKS = {
|
|
33
|
+
ram123: [0x04, 0x0c], blk1: [0x20, 0x20], blk2: [0x40, 0x20], blk3: [0x60, 0x20], blk5: [0xa0, 0x20]
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
# The blocks each of the usual RAM expansions fills, and every block,
|
|
37
|
+
# which is what xvic's -memory all fits.
|
|
38
|
+
RAM_CONFIGURATIONS = {
|
|
39
|
+
unexpanded: [],
|
|
40
|
+
"3k": %i[ram123],
|
|
41
|
+
"8k": %i[blk1],
|
|
42
|
+
"16k": %i[blk1 blk2],
|
|
43
|
+
"24k": %i[blk1 blk2 blk3],
|
|
44
|
+
"32k": %i[blk1 blk2 blk3 blk5],
|
|
45
|
+
all: %i[ram123 blk1 blk2 blk3 blk5]
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
# RAM, internal and expansion alike, powers on in alternating $FF
|
|
49
|
+
# and $00 bytes, $FF at the even addresses, the alternation most of
|
|
50
|
+
# the machines in VIC20/raminitpattern/readme.txt show. Colour RAM
|
|
51
|
+
# powers on at zero. See doc/pinned-behaviour.md.
|
|
52
|
+
RAM_POWER_ON = Array.new(0xc000) { |addr| addr.even? ? 0xff : 0x00 }.freeze
|
|
53
|
+
|
|
54
|
+
# The last byte on the V-bus's data lines, which an empty spot on
|
|
55
|
+
# the VIC's side reads back.
|
|
56
|
+
class VideoOpenBus
|
|
57
|
+
def initialize(bus)
|
|
58
|
+
@bus = bus
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def peek(_addr) = @bus.video_data
|
|
62
|
+
def poke(_addr, _value); end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The last byte the CPU moved, which an empty spot on the CPU's side
|
|
66
|
+
# reads back.
|
|
67
|
+
class OpenBus
|
|
68
|
+
def initialize(bus)
|
|
69
|
+
@bus = bus
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def peek(_addr) = @bus.data
|
|
73
|
+
def poke(_addr, _value); end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Colour RAM: four bits a cell, under the V-bus's high four lines.
|
|
77
|
+
class ColorRAM
|
|
78
|
+
# The 1,024 cells, which the VIC reads its colour nibbles from.
|
|
79
|
+
attr_reader :cells
|
|
80
|
+
|
|
81
|
+
def initialize(bus)
|
|
82
|
+
@bus = bus
|
|
83
|
+
@cells = Array.new(0x400, 0)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def peek(addr) = (@bus.video_data & 0xf0) | @cells[addr & 0x3ff]
|
|
87
|
+
|
|
88
|
+
def poke(addr, value)
|
|
89
|
+
@cells[addr & 0x3ff] = value & 0x0f
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The four bits the cell at +addr+ stores.
|
|
93
|
+
def nibble(addr) = @cells[addr & 0x3ff]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# I/O 0, $9000-$93FF. The VIC answers in $9000-$90FF, its sixteen
|
|
97
|
+
# registers repeated. The VIAs answer all through I/O 0, on the
|
|
98
|
+
# address lines alone: A4 selects VIA 1 and A5 selects VIA 2. Where
|
|
99
|
+
# more than one chip answers a read, each pulls the lines it drives
|
|
100
|
+
# low, so the CPU reads the AND of them, and a write reaches them
|
|
101
|
+
# all. Where none answers, the CPU reads the V-bus's last byte: the
|
|
102
|
+
# VIC's fetch in a cycle it fetches in, as the real 6561E's dump of
|
|
103
|
+
# VIC20/split-tests/timing shows at $9100 and $9200.
|
|
104
|
+
class IO0
|
|
105
|
+
def initialize(bus, vic:, via1:, via2:)
|
|
106
|
+
@bus = bus
|
|
107
|
+
@vic = vic
|
|
108
|
+
@via1 = via1
|
|
109
|
+
@via2 = via2
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def peek(addr)
|
|
113
|
+
vic = addr < 0x9100
|
|
114
|
+
return @bus.video_data unless vic || addr.anybits?(0x30)
|
|
115
|
+
|
|
116
|
+
value = vic ? @vic.peek(addr) : 0xff
|
|
117
|
+
value &= @via1.peek(addr) if addr.anybits?(0x10)
|
|
118
|
+
value &= @via2.peek(addr) if addr.anybits?(0x20)
|
|
119
|
+
value
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def poke(addr, value)
|
|
123
|
+
@vic.poke(addr, value) if addr < 0x9100
|
|
124
|
+
return unless addr.anybits?(0x30)
|
|
125
|
+
|
|
126
|
+
@via1.poke(addr, value) if addr.anybits?(0x10)
|
|
127
|
+
@via2.poke(addr, value) if addr.anybits?(0x20)
|
|
128
|
+
@bus.via_written
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# VICE's debug cartridge register at $910F, a spot in I/O 0 that
|
|
133
|
+
# nothing answers. A write there calls the handler with the byte, the
|
|
134
|
+
# exit code the VICE testprogs report. Every other access falls
|
|
135
|
+
# through to I/O 0.
|
|
136
|
+
class DebugRegister
|
|
137
|
+
ADDRESS = 0x910f
|
|
138
|
+
|
|
139
|
+
def initialize(io0, &handler)
|
|
140
|
+
@io0 = io0
|
|
141
|
+
@handler = handler
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def peek(addr) = @io0.peek(addr)
|
|
145
|
+
|
|
146
|
+
def poke(addr, value)
|
|
147
|
+
return @handler.call(value) if addr == ADDRESS
|
|
148
|
+
|
|
149
|
+
@io0.poke(addr, value)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
attr_reader :ram, :color_ram, :blocks
|
|
154
|
+
|
|
155
|
+
# The last byte the CPU read or wrote.
|
|
156
|
+
attr_reader :data
|
|
157
|
+
|
|
158
|
+
# The last byte on the V-bus.
|
|
159
|
+
attr_reader :video_data
|
|
160
|
+
|
|
161
|
+
# +vic+, +via1+ and +via2+ are the chips in I/O 0, each answering
|
|
162
|
+
# peek and poke with the CPU's address. +blocks+ names the expansion
|
|
163
|
+
# blocks that hold RAM (BLOCKS), and +kernal+ the KERNAL ROM's file.
|
|
164
|
+
def initialize(vic:, via1:, via2:, blocks: [], kernal: "vic20/kernal-pal.rom")
|
|
165
|
+
addressable_at(0, length: 2**16)
|
|
166
|
+
@ram = Memory.new(RAM_POWER_ON, length: 0xc000, start: 0)
|
|
167
|
+
@color_ram = ColorRAM.new(self)
|
|
168
|
+
@character_rom = ROM.load("vic20/character.rom", 0x8000)
|
|
169
|
+
@basic_rom = ROM.load("vic20/basic.rom", 0xc000)
|
|
170
|
+
@kernal_rom = ROM.load(kernal, 0xe000)
|
|
171
|
+
@io0 = IO0.new(self, vic:, via1:, via2:)
|
|
172
|
+
@open_bus = OpenBus.new(self)
|
|
173
|
+
@video_open_bus = VideoOpenBus.new(self)
|
|
174
|
+
@data = @video_data = 0
|
|
175
|
+
@read_pages = Array.new(256, @open_bus)
|
|
176
|
+
@write_pages = Array.new(256, @open_bus)
|
|
177
|
+
@roms = []
|
|
178
|
+
@debug_register = nil
|
|
179
|
+
@via_write_handler = nil
|
|
180
|
+
self.blocks = blocks
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Puts a cartridge's ROM chip of +bytes+, whole pages of them, at
|
|
184
|
+
# +address+. It answers reads on the pages it covers in place of
|
|
185
|
+
# whatever was there, and writes to them go nowhere.
|
|
186
|
+
def map_rom(address, bytes)
|
|
187
|
+
@roms << ROM.new(bytes, length: bytes.length, start: address)
|
|
188
|
+
map_pages
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Calls the block after each write to a VIA, which may have moved the
|
|
192
|
+
# lines its ports drive.
|
|
193
|
+
def on_via_write(&handler)
|
|
194
|
+
@via_write_handler = handler
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def via_written
|
|
198
|
+
@via_write_handler&.call
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Calls the block with each byte written to $910F (DebugRegister).
|
|
202
|
+
def install_debug_register(&)
|
|
203
|
+
@debug_register = DebugRegister.new(@io0, &)
|
|
204
|
+
map_pages
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Fills the expansion blocks named in +names+ with RAM and empties
|
|
208
|
+
# the rest.
|
|
209
|
+
def blocks=(names)
|
|
210
|
+
@blocks = names.dup.freeze
|
|
211
|
+
map_pages
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def power_on!
|
|
215
|
+
@ram.clear!(RAM_POWER_ON)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# The RAM, the colour RAM, the byte each side's data lines hold and the
|
|
219
|
+
# cartridge's ROM chips, for a snapshot. The expansion blocks are how
|
|
220
|
+
# the machine was built.
|
|
221
|
+
def save_state(out)
|
|
222
|
+
out.marker("VIC20 BUS")
|
|
223
|
+
@ram.save_state(out)
|
|
224
|
+
out.blob(@color_ram.cells).int(@data).int(@video_data).int(@roms.length)
|
|
225
|
+
@roms.each do |rom|
|
|
226
|
+
out.int(rom.start)
|
|
227
|
+
rom.save_state(out)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def load_state(input)
|
|
232
|
+
input.marker("VIC20 BUS")
|
|
233
|
+
@ram.load_state(input)
|
|
234
|
+
input.blob_into(@color_ram.cells)
|
|
235
|
+
@data = input.int
|
|
236
|
+
@video_data = input.int
|
|
237
|
+
chips = Array.new(input.int) { [input.int, input.blob] }
|
|
238
|
+
@roms = []
|
|
239
|
+
chips.each { |chip| @roms << ROM.new(chip[1], length: chip[1].length, start: chip[0]) }
|
|
240
|
+
map_pages
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Whether +block+ (BLOCKS) holds RAM.
|
|
244
|
+
def ram?(block) = @blocks.include?(block)
|
|
245
|
+
|
|
246
|
+
# The VIC's fetch from +address+, its own 14-bit address, which leaves
|
|
247
|
+
# the byte on the V-bus. The VIC's A13 is the CPU's A15 inverted, so
|
|
248
|
+
# $2000-$3FFF are the CPU's $0000-$1FFF and $0000-$1FFF its
|
|
249
|
+
# $8000-$9FFF. It reaches the internal RAM and the character ROM, and
|
|
250
|
+
# no expansion RAM. Elsewhere nothing drives the V-bus's data lines,
|
|
251
|
+
# and the VIC reads the byte they hold, as does a fetch from colour
|
|
252
|
+
# RAM, which drives only the colour bus while the VIC fetches.
|
|
253
|
+
def video_fetch(address)
|
|
254
|
+
value =
|
|
255
|
+
if address >= 0x2000
|
|
256
|
+
internal = address & 0x1fff
|
|
257
|
+
internal < 0x0400 || internal >= 0x1000 ? @ram.peek(internal) : @video_data
|
|
258
|
+
elsif address < 0x1000
|
|
259
|
+
@character_rom.peek(address | 0x8000)
|
|
260
|
+
else
|
|
261
|
+
@video_data
|
|
262
|
+
end
|
|
263
|
+
@video_data = value
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def peek(addr)
|
|
267
|
+
value = @read_pages[addr >> 8].peek(addr)
|
|
268
|
+
@video_data = value if addr.nobits?(0x6000)
|
|
269
|
+
@data = value
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def poke(addr, value)
|
|
273
|
+
@data = value
|
|
274
|
+
@video_data = value if addr.nobits?(0x6000)
|
|
275
|
+
@write_pages[addr >> 8].poke(addr, value)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
private
|
|
279
|
+
|
|
280
|
+
def map_pages
|
|
281
|
+
map(@open_bus, 0x00, 0x100)
|
|
282
|
+
@read_pages.fill(@video_open_bus, 0x04, 0x0c)
|
|
283
|
+
map(@ram, 0x00, 0x04)
|
|
284
|
+
map(@ram, 0x10, 0x10)
|
|
285
|
+
BLOCKS.each { |block, pages| map(@ram, pages[0], pages[1]) if ram?(block) }
|
|
286
|
+
@read_pages.fill(@character_rom, 0x80, 0x10)
|
|
287
|
+
map(@io0, 0x90, 0x04)
|
|
288
|
+
map(@color_ram, 0x94, 0x04)
|
|
289
|
+
@read_pages.fill(@basic_rom, 0xc0, 0x20)
|
|
290
|
+
@read_pages.fill(@kernal_rom, 0xe0, 0x20)
|
|
291
|
+
@roms.each { |rom| map_rom_pages(rom) }
|
|
292
|
+
@write_pages[0x91] = @debug_register if @debug_register
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def map_rom_pages(rom)
|
|
296
|
+
first = rom.start >> 8
|
|
297
|
+
count = rom.length >> 8
|
|
298
|
+
@read_pages.fill(rom, first, count)
|
|
299
|
+
@write_pages.fill(@open_bus, first, count)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def map(chip, first, count)
|
|
303
|
+
@read_pages.fill(chip, first, count)
|
|
304
|
+
@write_pages.fill(chip, first, count)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Vic20
|
|
5
|
+
# What VIA 2's ports read from outside the chip: the key matrix, with
|
|
6
|
+
# port B driving its columns and port A reading its rows. The KERNAL's
|
|
7
|
+
# SCNKEY ($EB1E) writes a column pattern to $9120 and reads the rows
|
|
8
|
+
# back from $9121, and its key table ($EC5E) puts each key at column
|
|
9
|
+
# times 8 plus row.
|
|
10
|
+
#
|
|
11
|
+
# PB7 is also the joystick's right switch. A held switch pulls that
|
|
12
|
+
# column line low before the matrix settles, so it reads as a key down
|
|
13
|
+
# in every row whose key in column 7 is held too. PB3 doubles as the
|
|
14
|
+
# cassette write line, which only the datasette listens to.
|
|
15
|
+
class KeyboardVIAPorts
|
|
16
|
+
# One row of keys per port A line, in port B column order. The keys
|
|
17
|
+
# are the C64's, under the same names, wired to other lines.
|
|
18
|
+
MATRIX = [
|
|
19
|
+
%i[1 left control run_stop space cbm q 2],
|
|
20
|
+
%i[3 w a lshift z s e 4],
|
|
21
|
+
%i[5 r d x c f t 6],
|
|
22
|
+
%i[7 y g v b h u 8],
|
|
23
|
+
%i[9 i j n m k o 0],
|
|
24
|
+
%i[+ p l , . : @ -],
|
|
25
|
+
%i[£ * ; / rshift = up clr_home],
|
|
26
|
+
%i[delete return cursor_h cursor_v f1 f3 f5 f7]
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
attr_reader :keyboard, :joystick
|
|
30
|
+
|
|
31
|
+
def initialize(keyboard:, joystick:)
|
|
32
|
+
@keyboard = keyboard
|
|
33
|
+
@joystick = joystick
|
|
34
|
+
@via = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The VIA these ports hang off, whose other port's output the matrix
|
|
38
|
+
# settles against.
|
|
39
|
+
def connect(via)
|
|
40
|
+
@via = via
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def read_a(lines) = @keyboard.scan(lines, columns(@via.port_b_output)).first
|
|
44
|
+
|
|
45
|
+
def read_b(lines) = @keyboard.scan(@via.port_a_output, columns(lines)).last
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# The joystick's right switch is bit 3 of its port bits, and pulls PB7.
|
|
50
|
+
def columns(lines) = lines & (0x7f | ((@joystick.port_bits & 0x08) << 4))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Vic20
|
|
5
|
+
# The datasette and the serial bus, wired to the VIAs.
|
|
6
|
+
#
|
|
7
|
+
# The serial bus's lines go out through inverters, so an output that
|
|
8
|
+
# drives high pulls its line low: ATN from VIA 1's PA7, the clock from
|
|
9
|
+
# VIA 2's CA2 and the data line from VIA 2's CB2. An output left as an
|
|
10
|
+
# input floats high, and pulls its line too, as after a reset. CLK and
|
|
11
|
+
# DATA come back on VIA 1's PA0 and PA1 (UserVIAPorts). The machine
|
|
12
|
+
# pushes the lines it pulls into the bus, as the C64's CIA 2 port A
|
|
13
|
+
# bits, after each write to a VIA that moves them.
|
|
14
|
+
#
|
|
15
|
+
# The datasette's read line pulses VIA 2's CA1, its sense switch reads
|
|
16
|
+
# on VIA 1's PA6, its motor runs while VIA 1's CA2 is low, and VIA 2's
|
|
17
|
+
# PB3 drives its write line, the same pin as a keyboard column.
|
|
18
|
+
module PortWiring
|
|
19
|
+
# The lines the machine pulls, as IECBus host lines.
|
|
20
|
+
def serial_lines
|
|
21
|
+
lines = 0
|
|
22
|
+
lines |= IECBus::HOST_ATN_OUT if @via1.port_a_output.anybits?(0x80)
|
|
23
|
+
lines |= IECBus::HOST_CLK_OUT if @via2.ca2_output
|
|
24
|
+
lines |= IECBus::HOST_DATA_OUT if @via2.cb2_output
|
|
25
|
+
lines
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def wire_ports
|
|
31
|
+
@datasette.on_flag { tape_pulse }
|
|
32
|
+
@bus.on_via_write { via_written }
|
|
33
|
+
via_written
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def via_written
|
|
37
|
+
lines = serial_lines
|
|
38
|
+
@iec_bus.host_lines = lines unless lines == @iec_bus.host_lines
|
|
39
|
+
@datasette.motor = !@via1.ca2_output
|
|
40
|
+
@datasette.write_line = @via2.port_b_output.anybits?(0x08)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A falling edge on the read line, and the line back up.
|
|
44
|
+
def tape_pulse
|
|
45
|
+
@via2.ca1 = false
|
|
46
|
+
@via2.ca1 = true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|