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/snapshot.rb
CHANGED
|
@@ -27,8 +27,12 @@ module Badline
|
|
|
27
27
|
path
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# A VIC-20's snapshot holds the BADLINE module alone, in a container
|
|
31
|
+
# named as xvic names its machine.
|
|
30
32
|
def dump(computer)
|
|
31
33
|
state = computer.snapshot
|
|
34
|
+
return Container.new([MachineState.section(state)], machine: Container::VIC20).to_s if computer.family == :vic20
|
|
35
|
+
|
|
32
36
|
Container.new(Vice.export(state) + [MachineState.section(state)]).to_s
|
|
33
37
|
end
|
|
34
38
|
|
|
@@ -49,4 +53,10 @@ module Badline
|
|
|
49
53
|
|
|
50
54
|
def restore_snapshot(path, &) = Snapshot.restore(self, path, &)
|
|
51
55
|
end
|
|
56
|
+
|
|
57
|
+
class Vic20
|
|
58
|
+
def save_snapshot(path) = Snapshot.save(self, path)
|
|
59
|
+
|
|
60
|
+
def restore_snapshot(path, &) = Snapshot.restore(self, path, &)
|
|
61
|
+
end
|
|
52
62
|
end
|
|
@@ -6,13 +6,14 @@ module Badline
|
|
|
6
6
|
class FormatError < StandardError; end
|
|
7
7
|
|
|
8
8
|
SIGNATURE = "C64 CARTRIDGE ".b
|
|
9
|
+
VIC20_SIGNATURE = "VIC20 CARTRIDGE ".b
|
|
9
10
|
CHIP_SIGNATURE = "CHIP".b
|
|
10
11
|
CHIP_HEADER_SIZE = 0x10
|
|
11
12
|
RAM_CHIP = 1
|
|
12
13
|
|
|
13
14
|
Chip = Data.define(:chip_type, :bank, :address, :data)
|
|
14
15
|
|
|
15
|
-
attr_reader :hardware_type, :subtype, :exrom, :game, :name, :chips
|
|
16
|
+
attr_reader :machine, :hardware_type, :subtype, :exrom, :game, :name, :chips
|
|
16
17
|
|
|
17
18
|
# What .write needs of an image: the fields a parsed CRTFile reads.
|
|
18
19
|
Image = Data.define(:hardware_type, :subtype, :exrom, :game, :name, :chips)
|
|
@@ -33,16 +34,19 @@ module Badline
|
|
|
33
34
|
header + packets.join
|
|
34
35
|
end
|
|
35
36
|
|
|
36
|
-
# `bytes` stands in for the file's contents.
|
|
37
|
-
|
|
37
|
+
# `bytes` stands in for the file's contents. `machine` is the one the
|
|
38
|
+
# image has to be for, :c64 or :vic20, or nil for either.
|
|
39
|
+
def initialize(path = nil, bytes: nil, machine: :c64)
|
|
38
40
|
parse(bytes || File.binread(path))
|
|
41
|
+
return if machine.nil? || @machine == machine
|
|
42
|
+
|
|
43
|
+
raise FormatError, "Cartridge image is for the #{@machine}, not the #{machine}"
|
|
39
44
|
end
|
|
40
45
|
|
|
41
46
|
private
|
|
42
47
|
|
|
43
48
|
def parse(bytes)
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
@machine = signed_machine(bytes)
|
|
46
50
|
@hardware_type = bytes[0x16, 2].unpack1("n")
|
|
47
51
|
@exrom = bytes.getbyte(0x18)
|
|
48
52
|
@game = bytes.getbyte(0x19)
|
|
@@ -51,6 +55,13 @@ module Badline
|
|
|
51
55
|
@chips = parse_chips(bytes, bytes[0x10, 4].unpack1("N"))
|
|
52
56
|
end
|
|
53
57
|
|
|
58
|
+
def signed_machine(bytes)
|
|
59
|
+
return :c64 if bytes.start_with?(SIGNATURE)
|
|
60
|
+
return :vic20 if bytes.start_with?(VIC20_SIGNATURE)
|
|
61
|
+
|
|
62
|
+
raise FormatError, "Missing CRT signature"
|
|
63
|
+
end
|
|
64
|
+
|
|
54
65
|
# Trailing bytes too short to hold a CHIP header are ignored.
|
|
55
66
|
def parse_chips(bytes, offset)
|
|
56
67
|
chips = []
|
data/lib/badline/storage/t64.rb
CHANGED
|
@@ -12,15 +12,21 @@ module Badline
|
|
|
12
12
|
TAPE_NAME = 0x28
|
|
13
13
|
ID = "T64".bytes.freeze
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
# `bytes` stands in for the host file's contents, as a snapshot
|
|
16
|
+
# restores the archive it holds.
|
|
17
|
+
def initialize(path, bytes: nil)
|
|
16
18
|
@path = path
|
|
17
|
-
@bytes = File.binread(path).bytes
|
|
19
|
+
@bytes = bytes || File.binread(path).bytes
|
|
18
20
|
raise FormatError, "Missing T64 signature" unless @bytes[0, 3] == SIGNATURE
|
|
19
21
|
end
|
|
20
22
|
|
|
21
|
-
#
|
|
23
|
+
# The host file's path, as it was opened.
|
|
24
|
+
attr_reader :path
|
|
25
|
+
|
|
26
|
+
# What Storage.reopen needs to open the archive again: its path and
|
|
27
|
+
# its bytes.
|
|
22
28
|
def save_setup(out)
|
|
23
|
-
out.int(T64_ARCHIVE).string(::File.expand_path(@path))
|
|
29
|
+
out.int(T64_ARCHIVE).string(::File.expand_path(@path)).blob(@bytes)
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
def read_file(name, **)
|
data/lib/badline/storage/tap.rb
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Storage
|
|
5
5
|
# Raw datasette pulse stream. The header holds the signature, the
|
|
6
|
-
# version at $0c
|
|
7
|
-
#
|
|
8
|
-
# of 8 cycles.
|
|
6
|
+
# version at $0c, the platform at $0d (0 C64, 1 VIC-20, 2 C16) and the
|
|
7
|
+
# data size at $10; the data runs from $14, every byte the time to the
|
|
8
|
+
# next falling edge on the tape read line, in units of 8 cycles.
|
|
9
9
|
class TAP
|
|
10
10
|
class FormatError < StandardError; end
|
|
11
11
|
|
|
@@ -13,18 +13,28 @@ module Badline
|
|
|
13
13
|
HEADER_SIZE = 0x14
|
|
14
14
|
OVERFLOW = 0x100 * 8
|
|
15
15
|
|
|
16
|
-
# The file it was read from, expanded, and the
|
|
17
|
-
|
|
18
|
-
attr_reader :version, :path
|
|
16
|
+
# The file it was read from, expanded, and the file's bytes.
|
|
17
|
+
attr_reader :version, :platform, :path, :bytes
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
# A blank version 1 tape for `platform` (0 C64, 1 VIC-20) at `path`,
|
|
20
|
+
# written to the host file.
|
|
21
|
+
def self.create(path, platform:)
|
|
22
|
+
File.binwrite(path, (SIGNATURE + [1, platform, 0, 0, 0, 0, 0, 0]).pack("C*"))
|
|
23
|
+
new(path)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# `bytes` stands in for the host file's contents, as a snapshot
|
|
27
|
+
# restores the tape it holds.
|
|
28
|
+
def initialize(path, bytes: nil)
|
|
21
29
|
@path = File.expand_path(path)
|
|
22
|
-
@bytes = File.binread(path).bytes
|
|
30
|
+
@bytes = bytes || File.binread(path).bytes
|
|
23
31
|
raise FormatError, "Missing TAP signature" unless @bytes[0, SIGNATURE.length] == SIGNATURE
|
|
24
32
|
|
|
25
33
|
@version = @bytes[0x0c].to_i
|
|
26
34
|
raise FormatError, "Unsupported TAP version #{@version}" if @version > 1
|
|
27
35
|
|
|
36
|
+
@platform = @bytes[0x0d].to_i
|
|
37
|
+
|
|
28
38
|
@end = tape_end
|
|
29
39
|
rewind
|
|
30
40
|
end
|
|
@@ -33,6 +43,7 @@ module Badline
|
|
|
33
43
|
@pos = HEADER_SIZE
|
|
34
44
|
end
|
|
35
45
|
|
|
46
|
+
# The offset of the next pulse byte.
|
|
36
47
|
def position = @pos
|
|
37
48
|
|
|
38
49
|
def position=(offset)
|
|
@@ -41,6 +52,31 @@ module Badline
|
|
|
41
52
|
|
|
42
53
|
def end? = @pos >= @end
|
|
43
54
|
|
|
55
|
+
# Records a pulse `cycles` long at the position, in place of the rest
|
|
56
|
+
# of the tape: a byte of units of 8 cycles, or a zero and the 24-bit
|
|
57
|
+
# cycle count for a long one (a lone zero, an overflow, on a version 0
|
|
58
|
+
# tape).
|
|
59
|
+
def record_pulse(cycles)
|
|
60
|
+
units = (cycles + 4) / 8
|
|
61
|
+
encoded = if units.between?(1, 0xff) then [units]
|
|
62
|
+
elsif version.zero? then [0]
|
|
63
|
+
else
|
|
64
|
+
count = [cycles, 0xffffff].min
|
|
65
|
+
[0, count & 0xff, (count >> 8) & 0xff, count >> 16]
|
|
66
|
+
end
|
|
67
|
+
@bytes.slice!(@pos..)
|
|
68
|
+
@bytes.concat(encoded)
|
|
69
|
+
@pos = @end = @bytes.length
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Writes the tape back to its host file, with the data size the
|
|
73
|
+
# header states.
|
|
74
|
+
def save
|
|
75
|
+
size = @bytes.length - HEADER_SIZE
|
|
76
|
+
@bytes[0x10, 4] = [size & 0xff, (size >> 8) & 0xff, (size >> 16) & 0xff, size >> 24]
|
|
77
|
+
File.binwrite(@path, @bytes.pack("C*"))
|
|
78
|
+
end
|
|
79
|
+
|
|
44
80
|
# Cycles until the next falling edge, or nil once the tape has run out.
|
|
45
81
|
def next_pulse
|
|
46
82
|
return if end?
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Storage
|
|
5
|
-
# Stands in for a directory
|
|
5
|
+
# Stands in for a directory a detached restore leaves out
|
|
6
6
|
# (Snapshot::StateReader): it finds no files and refuses every write
|
|
7
7
|
# as a write-protected disk does.
|
|
8
8
|
class Unavailable
|
|
9
|
+
def path = ""
|
|
10
|
+
|
|
9
11
|
def read_file(_name, **) = nil
|
|
10
12
|
|
|
11
13
|
def write_file(_name, _bytes, **)
|
data/lib/badline/storage.rb
CHANGED
|
@@ -45,16 +45,16 @@ module Badline
|
|
|
45
45
|
T64_ARCHIVE = 4
|
|
46
46
|
|
|
47
47
|
class << self
|
|
48
|
-
# Opens the storage a save_setup wrote: the same directory
|
|
49
|
-
#
|
|
50
|
-
# all. A detached reader gets the image
|
|
51
|
-
# Unavailable for a directory
|
|
48
|
+
# Opens the storage a save_setup wrote: the same directory by its
|
|
49
|
+
# path, an archive with the bytes it held, or a disk image with the
|
|
50
|
+
# bytes it held, writes and all. A detached reader gets the image
|
|
51
|
+
# write-protected, and Unavailable for a directory.
|
|
52
52
|
def reopen(input)
|
|
53
53
|
kind = input.int
|
|
54
54
|
path = input.string
|
|
55
55
|
case kind
|
|
56
56
|
when HOST_DIRECTORY then input.detached? ? Unavailable.new : HostDirectory.new(path)
|
|
57
|
-
when T64_ARCHIVE then
|
|
57
|
+
when T64_ARCHIVE then T64.new(path, bytes: input.blob)
|
|
58
58
|
else reopen_image(kind, path, input)
|
|
59
59
|
end
|
|
60
60
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
# = Timing
|
|
5
|
+
#
|
|
6
|
+
# What a front end needs to run a machine's frames and show them: the CPU
|
|
7
|
+
# clock, the raster's cycles per line and lines per frame, and the
|
|
8
|
+
# [left, top, width, height] of the raster it shows, and how many of the
|
|
9
|
+
# window's square pixels wide each of its pixels shows. Each machine
|
|
10
|
+
# derives it from its region.
|
|
11
|
+
Timing = Data.define(:clock_hz, :cycles_per_line, :lines_per_frame, :crop, :pixel_width)
|
|
12
|
+
end
|
data/lib/badline/version.rb
CHANGED
|
@@ -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
|
|
@@ -75,11 +112,11 @@ module Badline
|
|
|
75
112
|
# In Ultimax mode the cartridge ROMH replaces the character ROM
|
|
76
113
|
# shadow, visible at $3000-$3FFF of the window.
|
|
77
114
|
def ultimax_peek(offset)
|
|
78
|
-
romh = @
|
|
115
|
+
romh = @romh
|
|
79
116
|
if romh && offset.allbits?(0x3000)
|
|
80
117
|
romh.peek(0xe000 + (offset & 0x1fff))
|
|
81
118
|
else
|
|
82
|
-
@
|
|
119
|
+
@ram.peek(@starts[bank_switch_register] + offset)
|
|
83
120
|
end
|
|
84
121
|
end
|
|
85
122
|
|
|
@@ -88,7 +125,7 @@ module Badline
|
|
|
88
125
|
def bank_switch_register
|
|
89
126
|
return 0 if @swapped
|
|
90
127
|
|
|
91
|
-
@
|
|
128
|
+
@cia2.port_a_lines & 0b11
|
|
92
129
|
end
|
|
93
130
|
end
|
|
94
131
|
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)
|
|
@@ -11,10 +11,9 @@ module Badline
|
|
|
11
11
|
# fetches or the compare run a cycle later than on the 6569, what
|
|
12
12
|
# hangs off them moves 8 pixels with them.
|
|
13
13
|
class Timing
|
|
14
|
-
# The X counter's value where it
|
|
15
|
-
# pixels,
|
|
14
|
+
# The X counter's value where it steps back on a raster of more than
|
|
15
|
+
# 512 pixels, to run over the eight values below it again.
|
|
16
16
|
HOLD_AT = 0x188
|
|
17
|
-
HOLD_BASE = 0x184
|
|
18
17
|
|
|
19
18
|
attr_reader :columns, :sprite_cycle, :display_off_x, :x_pixels
|
|
20
19
|
|
|
@@ -36,14 +35,11 @@ module Badline
|
|
|
36
35
|
def reload_x(index) = RELOAD_X + (8 * @shift) + (RELOAD_STEP * index)
|
|
37
36
|
|
|
38
37
|
# The X counter at a raster pixel: X_OFFSET behind it, wrapping at
|
|
39
|
-
# the end of the line, less the pixels it held for
|
|
40
|
-
# where it ran over $
|
|
38
|
+
# the end of the line, less the pixels it held for from HOLD_AT on,
|
|
39
|
+
# where it ran over $180-$187 a second time.
|
|
41
40
|
def self.xpos(pixel, width, hold)
|
|
42
41
|
count = (pixel - X_OFFSET) % width
|
|
43
|
-
|
|
44
|
-
return count - hold if count >= HOLD_AT + hold
|
|
45
|
-
|
|
46
|
-
HOLD_BASE + ((count - HOLD_AT) % 4)
|
|
42
|
+
count < HOLD_AT ? count : count - hold
|
|
47
43
|
end
|
|
48
44
|
|
|
49
45
|
# The raster pixels at which the X counter reads each coordinate, 512
|
data/lib/badline/vic/sprite.rb
CHANGED
|
@@ -37,8 +37,9 @@ module Badline
|
|
|
37
37
|
# The s-accesses reload the shift register partway through the line,
|
|
38
38
|
# at raster pixel 459 for sprite 0 and 16 later for each sprite after
|
|
39
39
|
# it. A sprite still shifting there loses the rest of its row, and the
|
|
40
|
-
# comparator is ignored for the twelve pixels from it
|
|
41
|
-
#
|
|
40
|
+
# comparator is ignored for the twelve pixels from it, running on into
|
|
41
|
+
# the next line from a reload near the end of this one (the spritescan
|
|
42
|
+
# dumps). Its output holds the last pixel for seven, through K+6
|
|
42
43
|
# (spritefetchbug).
|
|
43
44
|
RELOAD_X = 459
|
|
44
45
|
RELOAD_STEP = 16
|
|
@@ -275,7 +276,7 @@ module Badline
|
|
|
275
276
|
end
|
|
276
277
|
|
|
277
278
|
def sequence_hit(log, start, reload_bits)
|
|
278
|
-
return if (@stop_x && start >= @stop_x) || start < @show_from
|
|
279
|
+
return if (@stop_x && start >= @stop_x) || start < @show_from || start < @reload_x + RELOAD_DEAD - @width
|
|
279
280
|
|
|
280
281
|
if start < @reload_x
|
|
281
282
|
sequence_early(log, start)
|