badline 0.10.1 → 0.11.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 +23 -0
- data/CLAUDE.md +25 -6
- data/CONTRIBUTING.md +8 -3
- data/README.md +63 -4
- data/doc/pinned-behaviour.md +75 -0
- data/doc/snapshots.md +15 -0
- data/lib/badline/address_bus/pla.rb +3 -16
- data/lib/badline/address_bus.rb +14 -0
- data/lib/badline/c128/banks.rb +35 -0
- data/lib/badline/c128/bus.rb +347 -0
- data/lib/badline/c128/bus_state.rb +53 -0
- data/lib/badline/c128/color_lines.rb +39 -0
- data/lib/badline/c128/cpu.rb +10 -0
- data/lib/badline/c128/drives.rb +95 -0
- data/lib/badline/c128/fast_serial.rb +62 -0
- data/lib/badline/c128/keys.rb +35 -0
- data/lib/badline/c128/mmu.rb +197 -0
- data/lib/badline/c128/mmu_pages.rb +105 -0
- data/lib/badline/c128/model.rb +39 -0
- data/lib/badline/c128/modes.rb +48 -0
- data/lib/badline/c128/saved_state.rb +121 -0
- data/lib/badline/c128/vdc/memory.rb +54 -0
- data/lib/badline/c128/vdc/painter.rb +143 -0
- data/lib/badline/c128/vdc/raster.rb +97 -0
- data/lib/badline/c128/vdc/window.rb +215 -0
- data/lib/badline/c128/vdc.rb +268 -0
- data/lib/badline/c128.rb +322 -0
- data/lib/badline/chrout_trap.rb +3 -0
- data/lib/badline/cia/fast_forward.rb +42 -0
- data/lib/badline/cia/serial.rb +12 -0
- data/lib/badline/cia/timer.rb +39 -0
- data/lib/badline/cia.rb +2 -0
- data/lib/badline/computer/attachments.rb +10 -13
- data/lib/badline/computer/kernal_traps.rb +29 -0
- data/lib/badline/computer.rb +5 -0
- data/lib/badline/drive/core.rb +172 -0
- data/lib/badline/{drive1541 → drive}/idle.rb +18 -19
- data/lib/badline/drive/idle_cpu.rb +28 -0
- data/lib/badline/{drive1541 → drive}/orbit.rb +2 -1
- data/lib/badline/drive/serial_lines.rb +73 -0
- data/lib/badline/{drive1541 → drive}/sleep.rb +4 -3
- data/lib/badline/drive/watch.rb +150 -0
- data/lib/badline/drive.rb +9 -0
- data/lib/badline/drive1541/bus.rb +5 -129
- data/lib/badline/drive1541/byte_ready.rb +48 -0
- data/lib/badline/drive1541/cpu.rb +1 -19
- data/lib/badline/drive1541/disk.rb +77 -20
- data/lib/badline/drive1541/disk_state.rb +7 -2
- data/lib/badline/drive1541/disk_via.rb +3 -0
- data/lib/badline/drive1541/mechanism.rb +48 -14
- data/lib/badline/drive1541/mechanism_state.rb +5 -4
- data/lib/badline/drive1541/rotation.rb +5 -5
- data/lib/badline/drive1541/serial_port.rb +4 -60
- data/lib/badline/drive1541.rb +13 -172
- data/lib/badline/drive1571/bus.rb +111 -0
- data/lib/badline/drive1571/cpu.rb +11 -0
- data/lib/badline/drive1571/serial_port.rb +37 -0
- data/lib/badline/drive1571/serial_via.rb +25 -0
- data/lib/badline/drive1571/wd1770.rb +34 -0
- data/lib/badline/drive1571.rb +225 -0
- data/lib/badline/frontend/app.rb +35 -22
- data/lib/badline/frontend/boot.rb +31 -1
- data/lib/badline/frontend/controls.rb +11 -0
- data/lib/badline/frontend/drive_led.rb +1 -1
- data/lib/badline/frontend/keys.rb +28 -1
- data/lib/badline/frontend/machine_window.rb +16 -13
- data/lib/badline/frontend/menu_media.rb +3 -3
- data/lib/badline/frontend/menu_pages.rb +8 -2
- data/lib/badline/frontend/menu_ports.rb +1 -1
- data/lib/badline/frontend/pause_menu.rb +1 -0
- data/lib/badline/frontend/screen.rb +33 -8
- data/lib/badline/frontend/screens.rb +90 -0
- data/lib/badline/frontend/timeline.rb +15 -4
- data/lib/badline/frontend.rb +1 -0
- data/lib/badline/iec_bus.rb +54 -4
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/layout.rb +58 -8
- data/lib/badline/kernal_trap/load.rb +1 -1
- data/lib/badline/kernal_trap/save.rb +1 -1
- data/lib/badline/kernal_trap/serial.rb +5 -0
- data/lib/badline/machine.rb +20 -9
- data/lib/badline/media/true_drive.rb +26 -23
- data/lib/badline/media.rb +8 -6
- data/lib/badline/options/event.rb +5 -1
- data/lib/badline/options/family.rb +52 -0
- data/lib/badline/options/help.rb +7 -2
- data/lib/badline/options/table.rb +8 -2
- data/lib/badline/options/validation.rb +14 -1
- data/lib/badline/options.rb +20 -19
- data/lib/badline/roms/README +36 -0
- data/lib/badline/roms/c128/basic.rom +0 -0
- data/lib/badline/roms/c128/character.rom +0 -0
- data/lib/badline/roms/c128/kernal.rom +0 -0
- data/lib/badline/roms/dos1571.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/image.rb +14 -0
- data/lib/badline/snapshot.rb +9 -2
- data/lib/badline/storage/d64_image.rb +3 -0
- data/lib/badline/storage/d71_image.rb +9 -4
- data/lib/badline/storage/g64_image.rb +7 -2
- data/lib/badline/time_of_day.rb +9 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/fast_forward.rb +3 -3
- data/lib/badline/vic/display_state.rb +12 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic.rb +108 -7
- data/lib/badline/vic20/attachments.rb +6 -0
- data/lib/badline/z80/arithmetic.rb +117 -0
- data/lib/badline/z80/bit_page.rb +55 -0
- data/lib/badline/z80/block_io.rb +58 -0
- data/lib/badline/z80/block_transfers.rb +59 -0
- data/lib/badline/z80/bus_cycles.rb +111 -0
- data/lib/badline/z80/core.rb +111 -0
- data/lib/badline/z80/exchanges.rb +50 -0
- data/lib/badline/z80/extended_page.rb +89 -0
- data/lib/badline/z80/flags.rb +37 -0
- data/lib/badline/z80/interrupt_acceptance.rb +46 -0
- data/lib/badline/z80/jumps.rb +82 -0
- data/lib/badline/z80/loads.rb +98 -0
- data/lib/badline/z80/main_page.rb +134 -0
- data/lib/badline/z80/registers.rb +120 -0
- data/lib/badline/z80/shifts.rb +64 -0
- data/lib/badline/z80.rb +11 -0
- data/lib/badline.rb +1 -0
- metadata +59 -4
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
# The 8722 MMU's registers, $D500-$D50B in C128 mode:
|
|
6
|
+
#
|
|
7
|
+
# $D500 CR: I/O, the ROMs and the RAM bank the CPU sees
|
|
8
|
+
# $D501-$D504 PCRA-PCRD: preset configurations
|
|
9
|
+
# $D505 MCR: the CPU (bit 0, 1 for the 8502), FSDIR, GAME and EXROM
|
|
10
|
+
# in, C64 mode (bit 6) and the 40/80 key
|
|
11
|
+
# $D506 RCR: common RAM and the VIC's RAM bank
|
|
12
|
+
# $D507-$D50A P0L, P0H, P1L, P1H: page 0 and page 1 relocation
|
|
13
|
+
# $D50B VR: the version, 2 banks of version 0
|
|
14
|
+
#
|
|
15
|
+
# The rest of the page reads $FF. $FF00 is CR again, and a write to
|
|
16
|
+
# $FF01-$FF04, the LCRs, copies PCRA-PCRD into CR. Those five answer in
|
|
17
|
+
# every configuration, whatever CR maps there.
|
|
18
|
+
#
|
|
19
|
+
# A write to P0H or P1H waits until the matching P0L or P1L write
|
|
20
|
+
# takes it.
|
|
21
|
+
#
|
|
22
|
+
# On the C128 the Z80 runs first and hands the bus to the 8502. Here the
|
|
23
|
+
# 8502 runs from reset, with MCR as the Z80 leaves it. A machine built
|
|
24
|
+
# for C64 mode resets into C64 mode, the state the C128 KERNAL leaves
|
|
25
|
+
# when C= is held at power-on. In C64 mode the MMU answers nowhere, but
|
|
26
|
+
# the CPU's RAM bank, common RAM and the VIC's bank it holds stay in
|
|
27
|
+
# force.
|
|
28
|
+
class MMU
|
|
29
|
+
CR = 0
|
|
30
|
+
MCR = 5
|
|
31
|
+
RCR = 6
|
|
32
|
+
P0L = 7
|
|
33
|
+
P0H = 8
|
|
34
|
+
P1L = 9
|
|
35
|
+
P1H = 10
|
|
36
|
+
VR = 11
|
|
37
|
+
|
|
38
|
+
MCR_8502 = 0x01
|
|
39
|
+
MCR_FSDIR = 0x08
|
|
40
|
+
MCR_C64_MODE = 0x40
|
|
41
|
+
VERSION = 0x20
|
|
42
|
+
|
|
43
|
+
# MCR's bits that read back what was written: the CPU, FSDIR and C64
|
|
44
|
+
# mode. Bits 1 and 2 have no pins and read 1.
|
|
45
|
+
MCR_WRITABLE = 0x49
|
|
46
|
+
MCR_UNUSED = 0x06
|
|
47
|
+
MCR_GAME = 0x10
|
|
48
|
+
MCR_EXROM = 0x20
|
|
49
|
+
MCR_DISPLAY_KEY = 0x80
|
|
50
|
+
|
|
51
|
+
# The common RAM sizes RCR bits 0-1 pick, in pages.
|
|
52
|
+
COMMON_PAGES = [4, 16, 32, 64].freeze
|
|
53
|
+
|
|
54
|
+
# The registers, CR first.
|
|
55
|
+
attr_reader :registers
|
|
56
|
+
|
|
57
|
+
# Whether the 40/80 DISPLAY key is locked down, which MCR bit 7 reads
|
|
58
|
+
# as 0 in C128 mode.
|
|
59
|
+
attr_accessor :display_key
|
|
60
|
+
|
|
61
|
+
# The cartridge's GAME and EXROM lines, which MCR bits 4 and 5 read.
|
|
62
|
+
attr_writer :game, :exrom
|
|
63
|
+
|
|
64
|
+
# +mode+ is the mode a reset leaves, :c128 or :c64.
|
|
65
|
+
def initialize(mode = :c64)
|
|
66
|
+
@reset_mode = mode
|
|
67
|
+
@registers = Array.new(12, 0)
|
|
68
|
+
@display_key = false
|
|
69
|
+
@game = 1
|
|
70
|
+
@exrom = 1
|
|
71
|
+
@on_change = nil
|
|
72
|
+
@on_fast_serial_change = nil
|
|
73
|
+
reset!
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Calls the block whenever the mapping the registers ask for changes.
|
|
77
|
+
def on_change(&block)
|
|
78
|
+
@on_change = block
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Calls the block whenever FSDIR turns the fast serial buffers round.
|
|
82
|
+
def on_fast_serial_change(&block)
|
|
83
|
+
@on_fast_serial_change = block
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def reset!
|
|
87
|
+
fast_serial_out = fast_serial_out?
|
|
88
|
+
@registers.fill(0)
|
|
89
|
+
@registers[MCR] = @reset_mode == :c64 ? MCR_C64_MODE | MCR_8502 : MCR_8502
|
|
90
|
+
@registers[P1L] = 0x01
|
|
91
|
+
@registers[VR] = VERSION
|
|
92
|
+
@p0h_latch = 0
|
|
93
|
+
@p1h_latch = 0
|
|
94
|
+
@on_change&.call
|
|
95
|
+
@on_fast_serial_change&.call if fast_serial_out
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def c64_mode? = @registers[MCR].anybits?(MCR_C64_MODE)
|
|
99
|
+
|
|
100
|
+
# Whether MCR bit 3, FSDIR, turns the fast serial buffers outwards, so
|
|
101
|
+
# CIA 1's CNT and SP drive SRQ and DATA.
|
|
102
|
+
def fast_serial_out? = @registers[MCR].anybits?(MCR_FSDIR)
|
|
103
|
+
|
|
104
|
+
# :c64 or :c128, the mode MCR bit 6 selects.
|
|
105
|
+
def mode = c64_mode? ? :c64 : :c128
|
|
106
|
+
|
|
107
|
+
# The 64K bank the VIC sees, RCR bit 6. Bit 7 picks banks a 256K
|
|
108
|
+
# machine has.
|
|
109
|
+
def vic_bank = (@registers[RCR] >> 6) & 0x01
|
|
110
|
+
|
|
111
|
+
# The RAM bank the CPU sees, CR bit 6.
|
|
112
|
+
def cpu_bank = (@registers[CR] >> 6) & 0x01
|
|
113
|
+
|
|
114
|
+
def cr = @registers[CR]
|
|
115
|
+
|
|
116
|
+
# The pages from $0000 up that common RAM keeps in bank 0, or 0.
|
|
117
|
+
def common_low_pages = @registers[RCR].anybits?(0x04) ? common_pages : 0
|
|
118
|
+
|
|
119
|
+
# The first page of the common RAM below $FFFF, or 256 for none.
|
|
120
|
+
def common_high_start = @registers[RCR].anybits?(0x08) ? 256 - common_pages : 256
|
|
121
|
+
|
|
122
|
+
# The page and the bank page 0 moves to.
|
|
123
|
+
def p0_page = @registers[P0L]
|
|
124
|
+
def p0_bank = @registers[P0H] & 0x01
|
|
125
|
+
|
|
126
|
+
# The page and the bank page 1 moves to.
|
|
127
|
+
def p1_page = @registers[P1L]
|
|
128
|
+
def p1_bank = @registers[P1H] & 0x01
|
|
129
|
+
|
|
130
|
+
# The registers at $D500-$D5FF.
|
|
131
|
+
def peek(addr)
|
|
132
|
+
offset = addr & 0xff
|
|
133
|
+
case offset
|
|
134
|
+
when MCR then mcr
|
|
135
|
+
when P0H, P1H then 0xf0 | @registers[offset]
|
|
136
|
+
when 0..VR then @registers[offset]
|
|
137
|
+
else 0xff
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def poke(addr, value)
|
|
142
|
+
offset = addr & 0xff
|
|
143
|
+
case offset
|
|
144
|
+
when P0H then @p0h_latch = value & 0x0f
|
|
145
|
+
when P1H then @p1h_latch = value & 0x0f
|
|
146
|
+
when P0L then relocate(P0L, P0H, @p0h_latch, value)
|
|
147
|
+
when P1L then relocate(P1L, P1H, @p1h_latch, value)
|
|
148
|
+
when CR..RCR then write(offset, value)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The registers and the P0H and P1H writes waiting for P0L and P1L.
|
|
153
|
+
# The bus maps the restored registers once its own state is back.
|
|
154
|
+
def save_state(out)
|
|
155
|
+
out.ints(@registers).int(@p0h_latch).int(@p1h_latch)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def load_state(input)
|
|
159
|
+
input.ints_into(@registers)
|
|
160
|
+
@p0h_latch = input.int
|
|
161
|
+
@p1h_latch = input.int
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# CR and the LCRs at $FF00-$FF04: an LCR reads its PCR.
|
|
165
|
+
def peek_configuration(addr) = @registers[addr & 0x07]
|
|
166
|
+
|
|
167
|
+
def poke_configuration(addr, value)
|
|
168
|
+
offset = addr & 0x07
|
|
169
|
+
write(CR, offset.zero? ? value : @registers[offset])
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def common_pages = COMMON_PAGES[@registers[RCR] & 0x03]
|
|
175
|
+
|
|
176
|
+
def mcr
|
|
177
|
+
value = (@registers[MCR] & MCR_WRITABLE) | MCR_UNUSED
|
|
178
|
+
value |= MCR_GAME if @game == 1
|
|
179
|
+
value |= MCR_EXROM if @exrom == 1
|
|
180
|
+
value |= MCR_DISPLAY_KEY unless @display_key
|
|
181
|
+
value
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def relocate(low, high, latch, value)
|
|
185
|
+
@registers[high] = latch
|
|
186
|
+
write(low, value)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def write(offset, value)
|
|
190
|
+
fast_serial_out = fast_serial_out?
|
|
191
|
+
@registers[offset] = value
|
|
192
|
+
@on_change&.call
|
|
193
|
+
@on_fast_serial_change&.call if fast_serial_out? != fast_serial_out
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
class Bus
|
|
6
|
+
# What the MMU maps. For the CPU: the RAM bank, common RAM and the
|
|
7
|
+
# pages page 0 and page 1 move to in both modes, and CR's ROMs and I/O
|
|
8
|
+
# in C128 mode. C64 mode lays the PLA's ROMs and I/O over the same RAM
|
|
9
|
+
# (AddressBus::PLA).
|
|
10
|
+
module MMUPages
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# CR's ROMs and I/O over the RAM, with the configuration registers on
|
|
14
|
+
# top at $FF00.
|
|
15
|
+
def map_c128_pages
|
|
16
|
+
cr = @mmu.cr
|
|
17
|
+
map_ram_pages
|
|
18
|
+
@read_pages.fill(@basic_low_rom, 0x40, 0x40) if cr.nobits?(0x02)
|
|
19
|
+
map_rom_block((cr >> 2) & 0x03, 0x80)
|
|
20
|
+
map_rom_block((cr >> 4) & 0x03, 0xc0)
|
|
21
|
+
map_io_pages if cr.nobits?(0x01)
|
|
22
|
+
@configuration_reads.below = @read_pages[0xff]
|
|
23
|
+
@configuration_writes.below = @write_pages[0xff]
|
|
24
|
+
@read_pages[0xff] = @configuration_reads
|
|
25
|
+
@write_pages[0xff] = @configuration_writes
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A 16K block's two CR bits pick the system ROM, the internal or the
|
|
29
|
+
# external function ROM, neither of which is fitted, or RAM.
|
|
30
|
+
def map_rom_block(select, first_page)
|
|
31
|
+
case select
|
|
32
|
+
when 0 then map_system_rom(first_page)
|
|
33
|
+
when 1, 2 then @read_pages.fill(@open_bus, first_page, 0x40)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# BASIC's high half at $8000, or the screen editor, the character ROM
|
|
38
|
+
# and the KERNAL at $C000.
|
|
39
|
+
def map_system_rom(first_page)
|
|
40
|
+
if first_page == 0x80
|
|
41
|
+
@read_pages.fill(@basic_high_rom, 0x80, 0x40)
|
|
42
|
+
else
|
|
43
|
+
@read_pages.fill(@editor_rom, 0xc0, 0x10)
|
|
44
|
+
@read_pages.fill(@c128_character_rom, 0xd0, 0x10)
|
|
45
|
+
@read_pages.fill(@c128_kernal_rom, 0xe0, 0x20)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The bank CR picks, with common RAM in bank 0, then the pages page 0
|
|
50
|
+
# and page 1 move to.
|
|
51
|
+
def map_ram_pages
|
|
52
|
+
@io_mapped = false
|
|
53
|
+
bank_ram = @mmu.cpu_bank.zero? ? @ram : @bank1
|
|
54
|
+
@read_pages.fill(bank_ram)
|
|
55
|
+
@write_pages.fill(bank_ram)
|
|
56
|
+
map_common_ram
|
|
57
|
+
relocate_pages
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def map_common_ram
|
|
61
|
+
low = @mmu.common_low_pages
|
|
62
|
+
high = @mmu.common_high_start
|
|
63
|
+
@read_pages.fill(@ram, 0, low)
|
|
64
|
+
@write_pages.fill(@ram, 0, low)
|
|
65
|
+
@read_pages.fill(@ram, high, 256 - high)
|
|
66
|
+
@write_pages.fill(@ram, high, 256 - high)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# In C128 mode page 0 and page 1 go to the pages P0 and P1 name, in
|
|
70
|
+
# bank 0 if common RAM holds them. In both modes, the page each moved
|
|
71
|
+
# to reaches back to page 0 or page 1, in the bank it named, if the
|
|
72
|
+
# CPU sees that bank there. Page 1's takes the page they both name.
|
|
73
|
+
# C64 mode moves neither page 0 nor page 1 forward, so either can be
|
|
74
|
+
# the page that reaches back.
|
|
75
|
+
def relocate_pages
|
|
76
|
+
mmu = @mmu
|
|
77
|
+
forward = !mmu.c64_mode?
|
|
78
|
+
if forward
|
|
79
|
+
map_relocated(0, @relocated[0], bank_of(0, mmu.p0_bank), mmu.p0_page)
|
|
80
|
+
map_relocated(1, @relocated[1], bank_of(1, mmu.p1_bank), mmu.p1_page)
|
|
81
|
+
end
|
|
82
|
+
reach_back(@relocated[2], mmu.p0_page, mmu.p0_bank, 0, forward)
|
|
83
|
+
reach_back(@relocated[3], mmu.p1_page, mmu.p1_bank, 1, forward)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def reach_back(window, page, bank, home, forward)
|
|
87
|
+
return if forward && page < 2
|
|
88
|
+
return unless bank_of(page, @mmu.cpu_bank) == bank
|
|
89
|
+
|
|
90
|
+
map_relocated(page, window, bank, home)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def map_relocated(page, window, bank, target)
|
|
94
|
+
window.base = (bank << 16) | (target << 8)
|
|
95
|
+
@read_pages[page] = @write_pages[page] = window
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The bank +page+ reaches when the CPU or a pointer asks for +bank+.
|
|
99
|
+
def bank_of(page, bank)
|
|
100
|
+
page < @mmu.common_low_pages || page >= @mmu.common_high_start ? 0 : bank
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
# The C128s badline builds by name: the chips each board has, and its
|
|
6
|
+
# video standard. The plastic C128D is a "c128" board with a 1571 built
|
|
7
|
+
# in. A VDC on either board can have 16K or 64K of RAM.
|
|
8
|
+
module Model
|
|
9
|
+
Profile = Data.define(:name, :vic_model, :cia_model, :sid_model, :region, :vdc_model, :vdc_ram_kb)
|
|
10
|
+
|
|
11
|
+
# The C128 and the plastic C128D, PAL: the 8566 VIC-IIe, 6526 CIAs,
|
|
12
|
+
# the 6581 SID and the 8563 VDC with 16K.
|
|
13
|
+
C128 = Profile.new(name: "c128", vic_model: :mos8566, cia_model: :mos6526, sid_model: :mos6581,
|
|
14
|
+
region: Region::PAL, vdc_model: :mos8563, vdc_ram_kb: 16)
|
|
15
|
+
|
|
16
|
+
# The NTSC C128, with the 8564.
|
|
17
|
+
C128_NTSC = Profile.new(name: "c128ntsc", vic_model: :mos8564, cia_model: :mos6526, sid_model: :mos6581,
|
|
18
|
+
region: Region::NTSC, vdc_model: :mos8563, vdc_ram_kb: 16)
|
|
19
|
+
|
|
20
|
+
# The metal C128DCR, PAL: 6526A CIAs, the 8580 SID and the 8568 VDC
|
|
21
|
+
# with 64K.
|
|
22
|
+
DCR = Profile.new(name: "c128dcr", vic_model: :mos8566, cia_model: :mos6526a, sid_model: :mos8580,
|
|
23
|
+
region: Region::PAL, vdc_model: :mos8568, vdc_ram_kb: 64)
|
|
24
|
+
|
|
25
|
+
# The NTSC C128DCR, with the 8564.
|
|
26
|
+
DCR_NTSC = Profile.new(name: "c128dcrntsc", vic_model: :mos8564, cia_model: :mos6526a, sid_model: :mos8580,
|
|
27
|
+
region: Region::NTSC, vdc_model: :mos8568, vdc_ram_kb: 64)
|
|
28
|
+
|
|
29
|
+
ALL = [C128, C128_NTSC, DCR, DCR_NTSC].freeze
|
|
30
|
+
|
|
31
|
+
def self.named(name)
|
|
32
|
+
model = ALL.find { |candidate| candidate.name == name }
|
|
33
|
+
raise ArgumentError, "no C128 model named #{name}" if model.nil?
|
|
34
|
+
|
|
35
|
+
model
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
# Following the MMU between C128 and C64 mode: the KERNAL traps go to
|
|
6
|
+
# the KERNAL the machine runs, C= held from power-on starts it in C64
|
|
7
|
+
# mode through the C128 KERNAL, and #active_screen tells a front end
|
|
8
|
+
# which screen the mode's editor prints to.
|
|
9
|
+
module Modes
|
|
10
|
+
# Holds C= down from power-on until the C128 KERNAL has taken the
|
|
11
|
+
# machine to C64 mode, as a user does to start a C128 in C64 mode.
|
|
12
|
+
# #on_init's handlers then wait for the C64 KERNAL's boot.
|
|
13
|
+
def hold_commodore_key
|
|
14
|
+
keyboard.press(:cbm)
|
|
15
|
+
@holding_commodore = true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The screen the machine prints to: in C128 mode the screen editor's,
|
|
19
|
+
# :vdc while bit 7 of its 40/80 flag at $D7 is set, as GRAPHIC 5,
|
|
20
|
+
# ESC X and a reset with 40/80 DISPLAY down leave it, or :vic. C64
|
|
21
|
+
# mode has the VIC-IIe's alone.
|
|
22
|
+
def active_screen = mode == :c128 && ram.peek(0xd7).anybits?(0x80) ? :vdc : :vic
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# The KERNAL the traps stand in for: the C128's in C128 mode, the C64's
|
|
27
|
+
# in C64 mode.
|
|
28
|
+
def trap_layout = mode == :c128 ? KernalTrap::C128_LAYOUT : KernalTrap::C64_LAYOUT
|
|
29
|
+
|
|
30
|
+
# The machine went to C64 mode or came back: the traps move to the
|
|
31
|
+
# KERNAL it now runs. C= held from power-on lets go, and #on_init's
|
|
32
|
+
# handlers wait for the C64 KERNAL to boot.
|
|
33
|
+
def mode_changed
|
|
34
|
+
layout = trap_layout
|
|
35
|
+
@capture_output&.layout = layout
|
|
36
|
+
if @drive
|
|
37
|
+
remove_kernal_traps
|
|
38
|
+
install_kernal_traps(layout)
|
|
39
|
+
end
|
|
40
|
+
return unless @holding_commodore && mode == :c64
|
|
41
|
+
|
|
42
|
+
keyboard.release(:cbm)
|
|
43
|
+
@holding_commodore = false
|
|
44
|
+
@init_threshold = @cycles + Computer::INIT_THRESHOLD if @cycles < @init_threshold
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
MARKER = "C128"
|
|
6
|
+
|
|
7
|
+
# The model, the SID and the mode it resets into of the machine a State
|
|
8
|
+
# from #snapshot was taken of, the arguments C128.new takes.
|
|
9
|
+
def self.setup(state)
|
|
10
|
+
input = Snapshot::StateReader.new(state)
|
|
11
|
+
input.marker(MARKER)
|
|
12
|
+
input.check_stamp
|
|
13
|
+
SavedState.read_setup(input)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# A new machine, built as the one a State from #snapshot was taken of,
|
|
17
|
+
# at that state.
|
|
18
|
+
def self.restored(state)
|
|
19
|
+
setup = setup(state)
|
|
20
|
+
new(model: setup[0], sid_model: setup[1], mode: setup[2]).apply_state(state)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The whole machine's state for a snapshot, as Computer's: the
|
|
24
|
+
# machine's own latches, FAST and TEST as they last took hold, the bus with its RAM, the MMU, the chips and
|
|
25
|
+
# the VDC, the CPU, the cartridge, the disk device 8 serves through the
|
|
26
|
+
# traps and a true 1541. The serial lines go back into the bus from the
|
|
27
|
+
# restored CIA 2. What the host holds stays out: the keyboard, the
|
|
28
|
+
# joysticks, CAPS LOCK and 40/80 DISPLAY, the traps and callbacks, and
|
|
29
|
+
# which chip renders.
|
|
30
|
+
module SavedState
|
|
31
|
+
SID_MODELS = %i[mos6581 mos8580].freeze
|
|
32
|
+
MODES = %i[c64 c128].freeze
|
|
33
|
+
|
|
34
|
+
# The model's name, the SID's model and the mode.
|
|
35
|
+
def self.read_setup(input)
|
|
36
|
+
name = input.string
|
|
37
|
+
known = Model::ALL.any? { |model| model.name == name }
|
|
38
|
+
raise Snapshot::FormatError, "the state names a C128 model badline doesn't know" unless known
|
|
39
|
+
|
|
40
|
+
sid_model = SID_MODELS.fetch(input.int)
|
|
41
|
+
[name, sid_model, MODES.fetch(input.int)]
|
|
42
|
+
rescue IndexError
|
|
43
|
+
raise Snapshot::FormatError, "the state names a SID or a mode badline doesn't know"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# How many more on_init handlers the machine a State was taken of had
|
|
47
|
+
# yet to run than this one has. Nil until the machine is restored.
|
|
48
|
+
attr_reader :init_handlers_lost
|
|
49
|
+
|
|
50
|
+
def snapshot
|
|
51
|
+
out = Snapshot::StateWriter.new
|
|
52
|
+
save_state(out)
|
|
53
|
+
out.state
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Takes the machine back to a State from #snapshot, trying it on a new
|
|
57
|
+
# machine first, so one that fails leaves this machine as it was.
|
|
58
|
+
# Raises Snapshot::FormatError for a state of another model or SID.
|
|
59
|
+
def restore(state)
|
|
60
|
+
check_setup(C128.setup(state))
|
|
61
|
+
C128.restored(state)
|
|
62
|
+
apply_state(state)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Puts a State into the machine without trying it on a new one first.
|
|
66
|
+
def apply_state(state)
|
|
67
|
+
input = Snapshot::StateReader.new(state)
|
|
68
|
+
load_state(input)
|
|
69
|
+
raise Snapshot::FormatError, "the state goes on past the machine" unless input.finished?
|
|
70
|
+
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def save_state(out)
|
|
75
|
+
out.marker(MARKER).stamp
|
|
76
|
+
out.string(@model.name).int(SID_MODELS.index(@sid.model)).int(@c64_built ? 0 : 1)
|
|
77
|
+
out.int(@cycles).int(@clock_bits).boolean(@nmi_asserted).boolean(@cartridge_nmi).boolean(@restore_pulse)
|
|
78
|
+
out.boolean(!@pending_keys.nil?)
|
|
79
|
+
out.ints(@pending_keys) if @pending_keys
|
|
80
|
+
out.int(@cycles <= init_threshold ? @init_handlers.length : 0)
|
|
81
|
+
save_cartridge(out)
|
|
82
|
+
@bus.save_state(out)
|
|
83
|
+
@cpu.save_state(out)
|
|
84
|
+
save_trap_drive(out)
|
|
85
|
+
save_serial_bus(out)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def load_state(input)
|
|
89
|
+
input.marker(MARKER)
|
|
90
|
+
input.check_stamp
|
|
91
|
+
check_setup(SavedState.read_setup(input))
|
|
92
|
+
@cycles = input.int
|
|
93
|
+
@clock_bits = input.int
|
|
94
|
+
@nmi_asserted = input.boolean?
|
|
95
|
+
@cartridge_nmi = input.boolean?
|
|
96
|
+
@restore_pulse = input.boolean?
|
|
97
|
+
@pending_keys = nil
|
|
98
|
+
@pending_keys = input.ints if input.boolean?
|
|
99
|
+
@init_handlers_lost = [input.int - @init_handlers.length, 0].max
|
|
100
|
+
load_cartridge(input)
|
|
101
|
+
@bus.load_state(input)
|
|
102
|
+
push_serial_lines
|
|
103
|
+
@cpu.load_state(input)
|
|
104
|
+
load_trap_drive(input)
|
|
105
|
+
load_serial_bus(input)
|
|
106
|
+
push_fast_serial
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def check_setup(setup)
|
|
112
|
+
ours = [@model.name, @sid.model, @c64_built ? :c64 : :c128]
|
|
113
|
+
return if setup == ours
|
|
114
|
+
|
|
115
|
+
raise Snapshot::FormatError, "the state is of a #{setup.join(' with a ')}, not a #{ours.join(' with a ')}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
include SavedState
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
class VDC
|
|
6
|
+
# The VDC's own DRAM: 16K of two 4416s (16K by 4) or 64K of 4164s or
|
|
7
|
+
# 4464s, behind the 16-bit addresses the VDC computes.
|
|
8
|
+
#
|
|
9
|
+
# The VDC drives an address onto the DRAM's pins as a row and a
|
|
10
|
+
# column, and R28 bit 4 picks the order (Programmer's Reference Guide,
|
|
11
|
+
# R28): set, for 64K parts, the row is A0-A7 and the column A8-A15;
|
|
12
|
+
# clear, for 4416s, the column pins carry A8, A8, A9-A13 and A15, and
|
|
13
|
+
# A14 goes nowhere. A 4416 has only six column lines and ignores the
|
|
14
|
+
# first and the last column pin. That gives the four layouts
|
|
15
|
+
# VDC/vdcdump's patterns.txt records from real machines: a 16K VDC in
|
|
16
|
+
# 64K mode sees A9-A14, so pages p, p ^ 1 and p ^ $80 are one page,
|
|
17
|
+
# and a 64K VDC in 16K mode sees page p at page
|
|
18
|
+
# (p & $80) | (p & $3E) << 1 | (p & 1) * 3.
|
|
19
|
+
class Memory
|
|
20
|
+
attr_reader :ram
|
|
21
|
+
|
|
22
|
+
def initialize(ram_kb)
|
|
23
|
+
@ram = Array.new(ram_kb * 1024, 0)
|
|
24
|
+
@layouts = [false, true].map { |mode64| Array.new(256) { |page| page_base(page, mode64) }.freeze }
|
|
25
|
+
@pages = @layouts[0]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Selects 64K mode, R28 bit 4, or 16K mode.
|
|
29
|
+
def mode64=(on)
|
|
30
|
+
@pages = @layouts[on ? 1 : 0]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fetch(addr) = @ram[@pages[(addr >> 8) & 0xff] | (addr & 0xff)]
|
|
34
|
+
|
|
35
|
+
def store(addr, value)
|
|
36
|
+
@ram[@pages[(addr >> 8) & 0xff] | (addr & 0xff)] = value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def clear!
|
|
40
|
+
@ram.fill(0)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def page_base(page, mode64)
|
|
46
|
+
pins = mode64 ? page : (page & 0x80) | ((page & 0x3e) << 1) | ((page & 1) * 3)
|
|
47
|
+
return pins << 8 if @ram.length == 0x10000
|
|
48
|
+
|
|
49
|
+
((pins >> 1) & 0x3f) << 8
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|