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
|
@@ -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
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Vic20
|
|
5
|
+
MARKER = "VIC20"
|
|
6
|
+
|
|
7
|
+
# The RAM expansion of the machine a State from #snapshot was taken
|
|
8
|
+
# of, a key of Bus::RAM_CONFIGURATIONS.
|
|
9
|
+
def self.setup(state)
|
|
10
|
+
input = Snapshot::StateReader.new(state)
|
|
11
|
+
input.marker(MARKER)
|
|
12
|
+
input.check_stamp
|
|
13
|
+
SavedState.read_ram(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) = new(ram: setup(state)).apply_state(state)
|
|
19
|
+
|
|
20
|
+
# The whole machine's state for a snapshot, as Computer's: the
|
|
21
|
+
# machine's own latches, the bus with its RAM and cartridge ROM, the
|
|
22
|
+
# VIC with its sound, both VIAs, the CPU, the datasette and its tape,
|
|
23
|
+
# the disk device 8 serves through the traps, and a true 1541. The
|
|
24
|
+
# serial lines go back into the bus from the restored VIAs. What the
|
|
25
|
+
# host holds stays out: the keyboard, the joystick, RESTORE, the traps
|
|
26
|
+
# and callbacks, whether the display renders, whether the sound
|
|
27
|
+
# records, and whether the datasette records.
|
|
28
|
+
module SavedState
|
|
29
|
+
RAM_CONFIGURATIONS = Bus::RAM_CONFIGURATIONS.keys.freeze
|
|
30
|
+
|
|
31
|
+
def self.read_ram(input)
|
|
32
|
+
RAM_CONFIGURATIONS.fetch(input.int)
|
|
33
|
+
rescue IndexError
|
|
34
|
+
raise Snapshot::FormatError, "the state names a VIC-20 RAM expansion badline doesn't know"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# How many more on_init handlers the machine a State was taken of had
|
|
38
|
+
# yet to run than this one has. Nil until the machine is restored.
|
|
39
|
+
attr_reader :init_handlers_lost
|
|
40
|
+
|
|
41
|
+
def snapshot
|
|
42
|
+
out = Snapshot::StateWriter.new
|
|
43
|
+
save_state(out)
|
|
44
|
+
out.state
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Takes the machine back to a State from #snapshot, trying it on a new
|
|
48
|
+
# machine first, so one that fails leaves this machine as it was.
|
|
49
|
+
# Raises Snapshot::FormatError for a state of another machine or RAM
|
|
50
|
+
# expansion.
|
|
51
|
+
def restore(state)
|
|
52
|
+
check_ram(Vic20.setup(state))
|
|
53
|
+
Vic20.restored(state)
|
|
54
|
+
apply_state(state)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Puts a State into the machine without trying it on a new one first.
|
|
58
|
+
def apply_state(state)
|
|
59
|
+
input = Snapshot::StateReader.new(state)
|
|
60
|
+
load_state(input)
|
|
61
|
+
raise Snapshot::FormatError, "the state goes on past the machine" unless input.finished?
|
|
62
|
+
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def save_state(out)
|
|
67
|
+
out.marker(MARKER).stamp
|
|
68
|
+
out.int(RAM_CONFIGURATIONS.index(@ram_configuration))
|
|
69
|
+
out.int(@cycles).boolean(@nmi_asserted).boolean(!@pending_keys.nil?)
|
|
70
|
+
out.ints(@pending_keys) if @pending_keys
|
|
71
|
+
out.int(@cycles <= @init_threshold ? @init_handlers.length : 0)
|
|
72
|
+
@bus.save_state(out)
|
|
73
|
+
@vic.save_state(out)
|
|
74
|
+
@sound.save_state(out)
|
|
75
|
+
@via1.save_state(out)
|
|
76
|
+
@via2.save_state(out)
|
|
77
|
+
@cpu.save_state(out)
|
|
78
|
+
@datasette.save_state(out)
|
|
79
|
+
save_trap_drive(out)
|
|
80
|
+
save_drive1541(out)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def load_state(input)
|
|
84
|
+
input.marker(MARKER)
|
|
85
|
+
input.check_stamp
|
|
86
|
+
check_ram(SavedState.read_ram(input))
|
|
87
|
+
@cycles = input.int
|
|
88
|
+
@nmi_asserted = input.boolean?
|
|
89
|
+
@pending_keys = nil
|
|
90
|
+
@pending_keys = input.ints if input.boolean?
|
|
91
|
+
@init_handlers_lost = [input.int - @init_handlers.length, 0].max
|
|
92
|
+
@bus.load_state(input)
|
|
93
|
+
@vic.load_state(input)
|
|
94
|
+
@sound.load_state(input)
|
|
95
|
+
@via1.load_state(input)
|
|
96
|
+
@via2.load_state(input)
|
|
97
|
+
@cpu.load_state(input)
|
|
98
|
+
via_written
|
|
99
|
+
@datasette.load_state(input)
|
|
100
|
+
load_trap_drive(input)
|
|
101
|
+
load_drive1541(input)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# The disk device 8 serves through the traps, with its channels.
|
|
107
|
+
def save_trap_drive(out)
|
|
108
|
+
out.boolean(!@drive.nil?)
|
|
109
|
+
return unless @drive
|
|
110
|
+
|
|
111
|
+
@drive.save_state(out)
|
|
112
|
+
@serial_trap.save_state(out)
|
|
113
|
+
@save_trap.save_state(out)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def load_trap_drive(input)
|
|
117
|
+
return unmount unless input.boolean?
|
|
118
|
+
|
|
119
|
+
storage = Storage.reopen(input)
|
|
120
|
+
unmount
|
|
121
|
+
mount(storage)
|
|
122
|
+
@drive.load_state(input)
|
|
123
|
+
@serial_trap.load_state(input)
|
|
124
|
+
@save_trap.load_state(input)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# A true 1541 on the serial bus, with its device number.
|
|
128
|
+
def save_drive1541(out)
|
|
129
|
+
out.boolean(!@drive1541.nil?)
|
|
130
|
+
return unless @drive1541
|
|
131
|
+
|
|
132
|
+
out.int(@drive1541.device)
|
|
133
|
+
@drive1541.save_state(out)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def load_drive1541(input)
|
|
137
|
+
return detach_drive1541 unless input.boolean?
|
|
138
|
+
|
|
139
|
+
device = input.int
|
|
140
|
+
detach_drive1541 if @drive1541 && @drive1541.device != device
|
|
141
|
+
attach_drive1541(Drive1541.new(device:)) unless @drive1541
|
|
142
|
+
@drive1541.load_state(input)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def check_ram(ram)
|
|
146
|
+
return if ram == @ram_configuration
|
|
147
|
+
|
|
148
|
+
raise Snapshot::FormatError, "the state is of a VIC-20 with #{ram} RAM, not #{@ram_configuration}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Vic20
|
|
5
|
+
class Sound
|
|
6
|
+
# The sound's level averaged over each sample of the audio rate, then
|
|
7
|
+
# through the machine's output stage: a one-pole low-pass at 1,420 Hz
|
|
8
|
+
# and a one-pole high-pass at 158 Hz, both run at the audio rate.
|
|
9
|
+
#
|
|
10
|
+
# The two corners are fitted to xvic's step response: a voice turned
|
|
11
|
+
# on at volume 15 rises and decays back to the line as those two RC
|
|
12
|
+
# stages do, within 6 parts in 11,000.
|
|
13
|
+
class Output
|
|
14
|
+
LOW_PASS_HZ = 1_420
|
|
15
|
+
HIGH_PASS_HZ = 158
|
|
16
|
+
|
|
17
|
+
# Scales the loudest level, four voices high at volume 15, to the
|
|
18
|
+
# top of a signed 16-bit sample.
|
|
19
|
+
SCALE = 32_767.0 / 570
|
|
20
|
+
|
|
21
|
+
attr_reader :samples
|
|
22
|
+
|
|
23
|
+
# `level` is the sound's level as the recording starts, which the
|
|
24
|
+
# stage takes as settled.
|
|
25
|
+
def initialize(clock_hz:, rate:, level:)
|
|
26
|
+
@clock_hz = clock_hz
|
|
27
|
+
@rate = rate
|
|
28
|
+
@remainder = 0
|
|
29
|
+
@window = next_window
|
|
30
|
+
@left = @window
|
|
31
|
+
@sum = 0
|
|
32
|
+
@low = @drift = level.to_f
|
|
33
|
+
@low_pass = 1.0 - Math.exp(-2.0 * Math::PI * LOW_PASS_HZ / rate)
|
|
34
|
+
@high_pass = 1.0 - Math.exp(-2.0 * Math::PI * HIGH_PASS_HZ / rate)
|
|
35
|
+
@samples = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Adds `cycles` cycles at `level`, closing every sample they finish.
|
|
39
|
+
def hold(level, cycles)
|
|
40
|
+
left = @left
|
|
41
|
+
while cycles >= left
|
|
42
|
+
@sum += level * left
|
|
43
|
+
cycles -= left
|
|
44
|
+
close
|
|
45
|
+
left = @left
|
|
46
|
+
end
|
|
47
|
+
@sum += level * cycles
|
|
48
|
+
@left = left - cycles
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The samples closed since the last drain.
|
|
52
|
+
def drain
|
|
53
|
+
samples = @samples
|
|
54
|
+
@samples = []
|
|
55
|
+
samples
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def close
|
|
61
|
+
@low += @low_pass * (@sum.fdiv(@window) - @low)
|
|
62
|
+
@drift += @high_pass * (@low - @drift)
|
|
63
|
+
sample = ((@low - @drift) * SCALE).round
|
|
64
|
+
sample = 32_767 if sample > 32_767
|
|
65
|
+
sample = -32_768 if sample < -32_768
|
|
66
|
+
@samples << sample
|
|
67
|
+
@sum = 0
|
|
68
|
+
@window = next_window
|
|
69
|
+
@left = @window
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Each window is a whole number of cycles, and they add up to the
|
|
73
|
+
# clock's cycles over a second.
|
|
74
|
+
def next_window
|
|
75
|
+
@remainder += @clock_hz
|
|
76
|
+
cycles = @remainder / @rate
|
|
77
|
+
@remainder -= cycles * @rate
|
|
78
|
+
cycles
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|