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
|
@@ -13,14 +13,20 @@ module Badline
|
|
|
13
13
|
# modes 3 and 7 off whatever drives CB1 from outside.
|
|
14
14
|
#
|
|
15
15
|
# Under its own clock the register drives CB1, idling high. Reading or
|
|
16
|
-
# writing the data register starts a byte
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
16
|
+
# writing the data register starts a byte, but only while the register
|
|
17
|
+
# is idle and enabled: an access in mode 0, or one before the eighth bit
|
|
18
|
+
# is in, leaves the count alone. CB1 falls on the first clock tick of
|
|
19
|
+
# the byte and rises on the second, and so on, so a byte is eight pulses
|
|
20
|
+
# over sixteen ticks. The interrupt flag is set on the tick that raises
|
|
21
|
+
# CB1 over the eighth bit, and CB1 then stays high until the next
|
|
22
|
+
# access.
|
|
23
|
+
#
|
|
24
|
+
# In modes 2 and 6 a tick is a φ2 cycle, from the second cycle after
|
|
25
|
+
# the access on, which makes a byte sixteen cycles. In modes 1, 4 and 5
|
|
26
|
+
# it comes two cycles after each timer 2 low byte underflow, on the
|
|
27
|
+
# cycle after the low byte reloads, which makes a byte sixteen
|
|
28
|
+
# underflows. Mode 4 never stops and never interrupts: its counter is
|
|
29
|
+
# disabled, and the byte goes round and round.
|
|
24
30
|
#
|
|
25
31
|
# Under an external clock the register shifts on every CB1 edge, access
|
|
26
32
|
# or not. The access only arms the counter: the flag is set on the eighth
|
|
@@ -43,6 +49,10 @@ module Badline
|
|
|
43
49
|
@data = 0x00
|
|
44
50
|
# Bits still to count before the flag, zero when the counter is idle.
|
|
45
51
|
@bits = 0
|
|
52
|
+
# The clock's last two cycles, newest in bit 0: timer 2 low byte
|
|
53
|
+
# underflows in modes 1, 4 and 5, and whether the byte has started
|
|
54
|
+
# in modes 2 and 6.
|
|
55
|
+
@delay = 0
|
|
46
56
|
@clock = true
|
|
47
57
|
@cb2 = true
|
|
48
58
|
# Nothing drives CB2 from outside, so it floats high.
|
|
@@ -57,10 +67,13 @@ module Badline
|
|
|
57
67
|
@data = value & 0xff
|
|
58
68
|
end
|
|
59
69
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
70
|
+
# A read or write of the data register, which starts a byte if the
|
|
71
|
+
# register is idle and enabled.
|
|
62
72
|
def access!
|
|
73
|
+
return if @mode == DISABLED || @bits.nonzero?
|
|
74
|
+
|
|
63
75
|
@bits = 8
|
|
76
|
+
@delay = 0 if @mode == IN_PHI2 || @mode == OUT_PHI2
|
|
64
77
|
end
|
|
65
78
|
|
|
66
79
|
# Whether the register is counting bits off its own clock, which
|
|
@@ -68,20 +81,21 @@ module Badline
|
|
|
68
81
|
def clocking? = @bits.nonzero? && internal_clock?
|
|
69
82
|
|
|
70
83
|
def save_state(out)
|
|
71
|
-
out.int(@mode).int(@data).int(@bits).boolean(@clock).boolean(@cb2).boolean(@cb2_input)
|
|
84
|
+
out.int(@mode).int(@data).int(@bits).int(@delay).boolean(@clock).boolean(@cb2).boolean(@cb2_input)
|
|
72
85
|
end
|
|
73
86
|
|
|
74
87
|
def load_state(input)
|
|
75
88
|
@mode = input.int
|
|
76
89
|
@data = input.int
|
|
77
90
|
@bits = input.int
|
|
91
|
+
@delay = input.schema > 4 ? input.int : 0
|
|
78
92
|
@clock = input.boolean?
|
|
79
93
|
@cb2 = input.boolean?
|
|
80
94
|
@cb2_input = input.boolean?
|
|
81
95
|
end
|
|
82
96
|
|
|
83
97
|
# Everything the register holds, for comparing it at two points.
|
|
84
|
-
def state = [@mode, @data, @bits, @clock, @cb2, @cb2_input]
|
|
98
|
+
def state = [@mode, @data, @bits, @delay, @clock, @cb2, @cb2_input]
|
|
85
99
|
|
|
86
100
|
# The CB1 level the register drives, or nil when CB1 is an input.
|
|
87
101
|
def cb1_output
|
|
@@ -96,11 +110,9 @@ module Badline
|
|
|
96
110
|
# Clocked once per φ2 cycle, with whether timer 2's low byte underflowed
|
|
97
111
|
# on it. Returns true on the cycle that sets the interrupt flag.
|
|
98
112
|
def cycle!(t2_low_underflow)
|
|
99
|
-
return false if @bits.zero?
|
|
100
|
-
|
|
101
113
|
case @mode
|
|
102
|
-
when IN_PHI2, OUT_PHI2 then
|
|
103
|
-
when IN_T2, OUT_T2, OUT_FREE_RUNNING then t2_low_underflow
|
|
114
|
+
when IN_PHI2, OUT_PHI2 then @bits.nonzero? && phi2_tick
|
|
115
|
+
when IN_T2, OUT_T2, OUT_FREE_RUNNING then t2_tick(t2_low_underflow)
|
|
104
116
|
else false
|
|
105
117
|
end
|
|
106
118
|
end
|
|
@@ -123,6 +135,21 @@ module Badline
|
|
|
123
135
|
@mode != DISABLED && @mode != IN_EXTERNAL && @mode != OUT_EXTERNAL
|
|
124
136
|
end
|
|
125
137
|
|
|
138
|
+
# A φ2 cycle of a byte: every one but the first ticks.
|
|
139
|
+
def phi2_tick
|
|
140
|
+
started = @delay.nonzero?
|
|
141
|
+
@delay = 1
|
|
142
|
+
started && tick
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Ticks on the underflow from two cycles back, while a byte is under
|
|
146
|
+
# way.
|
|
147
|
+
def t2_tick(underflow)
|
|
148
|
+
due = @delay.anybits?(2)
|
|
149
|
+
@delay = ((@delay << 1) & 2) | (underflow ? 1 : 0)
|
|
150
|
+
due && @bits.nonzero? && tick
|
|
151
|
+
end
|
|
152
|
+
|
|
126
153
|
# One half period of the register's own clock: a falling edge puts a
|
|
127
154
|
# bit out, and a rising one takes a bit in and counts it.
|
|
128
155
|
def tick
|
data/lib/badline/vic/bank.rb
CHANGED
|
@@ -5,44 +5,81 @@ module Badline
|
|
|
5
5
|
# The 16K window the VIC sees into RAM, selected by CIA2 $DD00, in the
|
|
6
6
|
# bank a +256K picks for it. Character
|
|
7
7
|
# ROM shadows $1000-$1FFF in banks 0 and 2.
|
|
8
|
+
#
|
|
9
|
+
# The machine's bus pushes in what the VIC sees through #connect,
|
|
10
|
+
# #map_character_rom and #map whenever it changes. Until then the bank
|
|
11
|
+
# has a CIA 2, RAM, colour RAM and character ROM of its own.
|
|
8
12
|
class Bank
|
|
9
13
|
include Addressable
|
|
10
14
|
|
|
11
15
|
# The CIA2 port A bank bits are inverted: %11 selects bank 0.
|
|
12
16
|
BANK_STARTS = [0xc000, 0x8000, 0x4000, 0x0000].freeze
|
|
13
17
|
|
|
14
|
-
attr_reader :
|
|
18
|
+
attr_reader :ram, :color_ram
|
|
15
19
|
|
|
16
20
|
# The bank lines as #sample_lines last saw them.
|
|
17
21
|
attr_accessor :lines
|
|
18
22
|
|
|
19
|
-
def initialize(
|
|
23
|
+
def initialize(vic)
|
|
20
24
|
addressable_at(0x0000, length: 2**14)
|
|
21
|
-
|
|
25
|
+
connect(cia2: CIA.new(start: 0xdd00), color_ram: ColorMemory.new(vic))
|
|
26
|
+
map_character_rom(ROM.load("character.rom", 0xd000))
|
|
27
|
+
@base = 0
|
|
28
|
+
@starts = BANK_STARTS
|
|
29
|
+
map(Memory.new([], length: 2**16, start: 0))
|
|
22
30
|
@lines = 0b11
|
|
23
31
|
@swapped = false
|
|
24
32
|
end
|
|
25
33
|
|
|
34
|
+
# CIA 2, whose port A picks the bank, and the colour RAM, read from
|
|
35
|
+
# the address +color_base+ gives offset 0.
|
|
36
|
+
def connect(cia2:, color_ram:, color_base: 0xd800)
|
|
37
|
+
@cia2 = cia2
|
|
38
|
+
@color_ram = color_ram
|
|
39
|
+
@color_base = color_base
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The character ROM, read from the address +base+ gives offset 0. It
|
|
43
|
+
# shows in the banks whose low line is high, unless +shadow+ is false.
|
|
44
|
+
def map_character_rom(rom, base: 0xc000, shadow: true)
|
|
45
|
+
@character_rom = rom
|
|
46
|
+
@character_base = base
|
|
47
|
+
@shadow_mask = shadow ? 0b01 : 0b00
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The RAM the VIC's 64K starts at +base+ in, and the cartridge's
|
|
51
|
+
# Ultimax lines in each half of the cycle with the ROMH bank they
|
|
52
|
+
# show.
|
|
53
|
+
def map(ram, base: 0, phi1_ultimax: false, ultimax: false, romh: nil)
|
|
54
|
+
@ram = ram
|
|
55
|
+
unless base == @base
|
|
56
|
+
@base = base
|
|
57
|
+
@starts = BANK_STARTS.map { |start| base + start }.freeze
|
|
58
|
+
end
|
|
59
|
+
@phi1_ultimax = phi1_ultimax
|
|
60
|
+
@ultimax = ultimax
|
|
61
|
+
@romh = romh
|
|
62
|
+
end
|
|
63
|
+
|
|
26
64
|
# On the 8565, a port A write that swaps the two bank lines, driving
|
|
27
65
|
# one high as the other goes low, shows the VIC both lines low for the
|
|
28
66
|
# cycle after it: every access in that cycle reads bank 3. A swap that
|
|
29
67
|
# the DDR makes, releasing a line to float high, shows nothing.
|
|
30
68
|
# Called once a cycle, ahead of the cycle's accesses.
|
|
31
69
|
def sample_lines
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@swapped = swap?(@lines, lines) && cia2.port_registers[2].allbits?(0b11)
|
|
70
|
+
lines = @cia2.port_a_lines & 0b11
|
|
71
|
+
@swapped = swap?(@lines, lines) && @cia2.port_registers[2].allbits?(0b11)
|
|
35
72
|
@lines = lines
|
|
36
73
|
end
|
|
37
74
|
|
|
38
75
|
def peek(offset)
|
|
39
|
-
return ultimax_peek(offset) if @
|
|
76
|
+
return ultimax_peek(offset) if @phi1_ultimax
|
|
40
77
|
|
|
41
78
|
bits = bank_switch_register
|
|
42
|
-
if bits
|
|
43
|
-
@
|
|
79
|
+
if (bits & @shadow_mask) == 0b01 && (offset & 0xf000) == 0x1000
|
|
80
|
+
@character_rom.peek(@character_base + offset)
|
|
44
81
|
else
|
|
45
|
-
@
|
|
82
|
+
@ram.peek(@starts[bits] + offset)
|
|
46
83
|
end
|
|
47
84
|
end
|
|
48
85
|
|
|
@@ -50,11 +87,11 @@ module Badline
|
|
|
50
87
|
# memory configuration the CPU does in that half. No cartridge drives
|
|
51
88
|
# Ultimax mode in the first half alone, so otherwise it reads as #peek.
|
|
52
89
|
def peek_phi2(offset)
|
|
53
|
-
@
|
|
90
|
+
@ultimax ? ultimax_peek(offset) : peek(offset)
|
|
54
91
|
end
|
|
55
92
|
|
|
56
93
|
def peek_color(offset)
|
|
57
|
-
|
|
94
|
+
@color_ram.nibble(@color_base + offset)
|
|
58
95
|
end
|
|
59
96
|
|
|
60
97
|
def poke(_addr, _value)
|
|
@@ -63,7 +100,7 @@ module Badline
|
|
|
63
100
|
|
|
64
101
|
# True if the VIC reads the character ROM at this offset.
|
|
65
102
|
def character_rom?(offset)
|
|
66
|
-
!@
|
|
103
|
+
!@phi1_ultimax && (bank_switch_register & @shadow_mask) == 0b01 && (offset & 0xf000) == 0x1000
|
|
67
104
|
end
|
|
68
105
|
|
|
69
106
|
def start
|
|
@@ -73,13 +110,14 @@ module Badline
|
|
|
73
110
|
private
|
|
74
111
|
|
|
75
112
|
# In Ultimax mode the cartridge ROMH replaces the character ROM
|
|
76
|
-
# shadow, visible at $3000-$3FFF of the window.
|
|
113
|
+
# shadow, visible at $3000-$3FFF of the window. The MAX is in
|
|
114
|
+
# Ultimax mode with no cartridge too.
|
|
77
115
|
def ultimax_peek(offset)
|
|
78
|
-
romh = @
|
|
116
|
+
romh = @romh
|
|
79
117
|
if romh && offset.allbits?(0x3000)
|
|
80
118
|
romh.peek(0xe000 + (offset & 0x1fff))
|
|
81
119
|
else
|
|
82
|
-
@
|
|
120
|
+
@ram.peek(@starts[bank_switch_register] + offset)
|
|
83
121
|
end
|
|
84
122
|
end
|
|
85
123
|
|
|
@@ -88,7 +126,7 @@ module Badline
|
|
|
88
126
|
def bank_switch_register
|
|
89
127
|
return 0 if @swapped
|
|
90
128
|
|
|
91
|
-
@
|
|
129
|
+
@cia2.port_a_lines & 0b11
|
|
92
130
|
end
|
|
93
131
|
end
|
|
94
132
|
end
|
|
@@ -69,7 +69,13 @@ module Badline
|
|
|
69
69
|
# $D02D: Sprite 6 color
|
|
70
70
|
# $D02E: Sprite 7 color
|
|
71
71
|
#
|
|
72
|
-
#
|
|
72
|
+
# == VIC-IIe (8566 and 8564)
|
|
73
|
+
#
|
|
74
|
+
# $D02F: Extra keyboard lines K0-K2 in bits 0-2, bits 3-7 read 1
|
|
75
|
+
# $D030: FAST (2 MHz) in bit 0 and TEST in bit 1, bits 2-7 read 1
|
|
76
|
+
#
|
|
77
|
+
# $D02F-$D03F: Not in use on the 6569 and 8565, always reads 0xff,
|
|
78
|
+
# and $D031-$D03F on the VIC-IIe
|
|
73
79
|
# $D040-$D3FF: Repeat $D0000 to $D03F every 64 bytes
|
|
74
80
|
class Registers
|
|
75
81
|
include IntegerHelper
|
|
@@ -77,10 +83,12 @@ module Badline
|
|
|
77
83
|
# The raw register bytes, for the per-column reads on the hot path.
|
|
78
84
|
attr_reader :bytes
|
|
79
85
|
|
|
80
|
-
# Every register powers on as zero, as in VICE.
|
|
81
|
-
|
|
86
|
+
# Every register powers on as zero, as in VICE. A VIC-IIe reads
|
|
87
|
+
# $D02F and $D030 back as registers.
|
|
88
|
+
def initialize(iie: false)
|
|
82
89
|
@bytes = Array.new(2**6, 0)
|
|
83
90
|
@irq_line = false
|
|
91
|
+
@iie = iie
|
|
84
92
|
end
|
|
85
93
|
|
|
86
94
|
# True while any enabled latch bit is set in $D019/$D01A. Cached and
|
|
@@ -95,7 +103,9 @@ module Badline
|
|
|
95
103
|
when 0x18 then @bytes[reg] | 0x01 # bit 0 unused, reads 1
|
|
96
104
|
when 0x1a, 0x20..0x2e then @bytes[reg] | 0xf0
|
|
97
105
|
when 0x1e, 0x1f then read_clear(reg) # collision registers clear on read
|
|
98
|
-
when 0x2f
|
|
106
|
+
when 0x2f then @iie ? @bytes[reg] | 0xf8 : 0xff
|
|
107
|
+
when 0x30 then @iie ? @bytes[reg] | 0xfc : 0xff
|
|
108
|
+
when 0x31..0x3f then 0xff
|
|
99
109
|
else @bytes[reg]
|
|
100
110
|
end
|
|
101
111
|
end
|
|
@@ -134,6 +144,12 @@ module Badline
|
|
|
134
144
|
def border = @bytes[0x20] & 0x0f
|
|
135
145
|
def background(index = 0) = @bytes[0x21 + index] & 0x0f
|
|
136
146
|
|
|
147
|
+
# The VIC-IIe's $D02F and $D030 bits. The 6569 and 8565 drive no
|
|
148
|
+
# extra keyboard lines and run at 1 MHz.
|
|
149
|
+
def extra_keyboard_lines = @iie ? @bytes[0x2f] & 0x07 : 0x07
|
|
150
|
+
def fast? = @iie && @bytes[0x30].anybits?(0x01)
|
|
151
|
+
def test? = @iie && @bytes[0x30].anybits?(0x02)
|
|
152
|
+
|
|
137
153
|
def raster_target = uint16(@bytes[0x12], (@bytes[0x11] & 0x80) >> 7)
|
|
138
154
|
|
|
139
155
|
def latch_irq!(bit)
|
data/lib/badline/vic.rb
CHANGED
|
@@ -11,12 +11,37 @@ 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
|
+
# The sixteen colours' luminances, out of 32, as Philip Timmermann
|
|
28
|
+
# (Pepto) measured them on the 6569.
|
|
29
|
+
LUMINANCES = [0, 32, 10, 20, 12, 16, 8, 24, 12, 8, 16, 10, 15, 24, 15, 20].freeze
|
|
30
|
+
|
|
31
|
+
# The sixteen colours on a green monochrome monitor, which shows only
|
|
32
|
+
# their luminance, as the PET 64's does.
|
|
33
|
+
GREEN_PALETTE = LUMINANCES.map do |luminance|
|
|
34
|
+
green = luminance * 255 / 32
|
|
35
|
+
((green / 4) << 16) | (green << 8) | (green / 4)
|
|
36
|
+
end.freeze
|
|
37
|
+
|
|
38
|
+
attr_reader :display, :width, :height, :vic_bank, :column,
|
|
18
39
|
:rasterline, :dirty_lines, :model, :region
|
|
19
40
|
|
|
41
|
+
# The RGB colours #display's palette indices stand for: PALETTE, or
|
|
42
|
+
# GREEN_PALETTE on a monochrome monitor.
|
|
43
|
+
attr_accessor :palette
|
|
44
|
+
|
|
20
45
|
# The parts a VICE snapshot reads and sets.
|
|
21
46
|
attr_reader :registers, :display_state, :sequencer, :sprites, :character_buffer, :color_buffer,
|
|
22
47
|
:fetch_d011
|
|
@@ -76,19 +101,21 @@ module Badline
|
|
|
76
101
|
SPRITE_BA_FIRST = 54
|
|
77
102
|
SPRITE_BA_LENGTH = 5
|
|
78
103
|
|
|
79
|
-
def initialize(
|
|
104
|
+
def initialize(debug: false, model: :mos6569, region: Region::PAL)
|
|
80
105
|
raise ArgumentError, "unknown VIC-II model #{model}" unless MODELS.include?(model)
|
|
81
106
|
|
|
82
107
|
addressable_at(0xd000, length: 2**10)
|
|
83
108
|
@model = model
|
|
109
|
+
@iie = %i[mos8566 mos8564].include?(model)
|
|
110
|
+
@core = model == :mos6569 ? :mos6569 : :mos8565
|
|
84
111
|
@region = region
|
|
85
|
-
@
|
|
86
|
-
@
|
|
87
|
-
@
|
|
88
|
-
@
|
|
89
|
-
@
|
|
90
|
-
@
|
|
91
|
-
@vic_bank = VIC::Bank.new(
|
|
112
|
+
@palette = PALETTE
|
|
113
|
+
@lightpen_extra = @core == :mos8565 ? 1 : 2
|
|
114
|
+
@grey_dots = @core == :mos8565
|
|
115
|
+
@delayed_fetch = @core == :mos8565
|
|
116
|
+
@dma_delay_idle = @core == :mos6569
|
|
117
|
+
@bank_swaps = @core == :mos8565
|
|
118
|
+
@vic_bank = VIC::Bank.new(self)
|
|
92
119
|
@debug = debug
|
|
93
120
|
|
|
94
121
|
@columns_per_line = region.cycles_per_line
|
|
@@ -114,12 +141,12 @@ module Badline
|
|
|
114
141
|
# registers, the raster position and the fetch and sprite state it
|
|
115
142
|
# starts with. The display keeps its buffers, cleared to black.
|
|
116
143
|
def power_on!
|
|
117
|
-
@registers = VIC::Registers.new
|
|
144
|
+
@registers = VIC::Registers.new(iie: @iie)
|
|
118
145
|
@register_bytes = @registers.bytes
|
|
119
146
|
@display_state = VIC::DisplayState.new(@registers, @last_column)
|
|
120
|
-
@sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank, model: @
|
|
147
|
+
@sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank, model: @core, region: @region)
|
|
121
148
|
@sequencer.render = @render
|
|
122
|
-
@sprites = VIC::Sprites.new(@registers, @vic_bank, @width, model: @
|
|
149
|
+
@sprites = VIC::Sprites.new(@registers, @vic_bank, @width, model: @core, region: @region)
|
|
123
150
|
@display.fill(0)
|
|
124
151
|
@lines.each { |line| line.fill(0) }
|
|
125
152
|
@dirty_lines.fill(true)
|
|
@@ -222,6 +249,12 @@ module Badline
|
|
|
222
249
|
@registers.irq_line?
|
|
223
250
|
end
|
|
224
251
|
|
|
252
|
+
# The VIC-IIe's $D02F lines K0-K2 and $D030 bits, for the machine to
|
|
253
|
+
# read. The 6569 and 8565 give 7, false and false.
|
|
254
|
+
def extra_keyboard_lines = @registers.extra_keyboard_lines
|
|
255
|
+
def fast? = @registers.fast?
|
|
256
|
+
def test? = @registers.test?
|
|
257
|
+
|
|
225
258
|
def peek(addr)
|
|
226
259
|
i = offset_of(addr) % (2**6)
|
|
227
260
|
case i
|
|
@@ -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
|