badline 0.4.2 → 0.5.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 +37 -0
- data/CLAUDE.md +32 -27
- data/CONTRIBUTING.md +5 -0
- data/README.md +55 -24
- data/doc/pinned-behaviour.md +223 -0
- data/exe/badline-ruby +7 -4
- data/lib/badline/address_bus/ultimax_pages.rb +38 -0
- data/lib/badline/address_bus.rb +19 -29
- data/lib/badline/audio/jukebox.rb +6 -4
- data/lib/badline/audio/machine_player.rb +10 -6
- data/lib/badline/cartridge/kcs_power.rb +3 -3
- data/lib/badline/cia.rb +21 -3
- data/lib/badline/computer.rb +86 -7
- data/lib/badline/cpu.rb +39 -0
- data/lib/badline/drive1541/bus.rb +197 -0
- data/lib/badline/drive1541/disk.rb +256 -0
- data/lib/badline/drive1541/disk_via.rb +33 -0
- data/lib/badline/drive1541/gcr.rb +57 -0
- data/lib/badline/drive1541/idle.rb +136 -0
- data/lib/badline/drive1541/mechanism.rb +277 -0
- data/lib/badline/drive1541/orbit.rb +81 -0
- data/lib/badline/drive1541/rotation.rb +111 -0
- data/lib/badline/drive1541/sector_reader.rb +76 -0
- data/lib/badline/drive1541/serial_port.rb +67 -0
- data/lib/badline/drive1541/sleep.rb +114 -0
- data/lib/badline/drive1541/track.rb +98 -0
- data/lib/badline/drive1541.rb +234 -0
- data/lib/badline/gui/application.rb +15 -3
- data/lib/badline/gui/drive_led_pane.rb +29 -0
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/iec_bus.rb +109 -0
- data/lib/badline/kernal_trap/channel.rb +9 -1
- data/lib/badline/kernal_trap/serial.rb +9 -3
- data/lib/badline/media/true_drive.rb +54 -0
- data/lib/badline/media.rb +22 -1
- data/lib/badline/memory.rb +3 -0
- data/lib/badline/options.rb +24 -4
- data/lib/badline/region.rb +35 -10
- data/lib/badline/reu/dma.rb +228 -0
- data/lib/badline/reu/ram.rb +65 -0
- data/lib/badline/reu/trigger.rb +32 -0
- data/lib/badline/reu.rb +276 -0
- data/lib/badline/roms/README +10 -0
- data/lib/badline/roms/dos1541.rom +0 -0
- data/lib/badline/sdl/functions.rb +1 -0
- data/lib/badline/storage/d64_image.rb +18 -9
- data/lib/badline/storage/d81_image.rb +1 -0
- data/lib/badline/storage/disk_image/catalog.rb +52 -0
- data/lib/badline/storage/disk_image/writing.rb +16 -0
- data/lib/badline/storage/disk_image.rb +2 -0
- data/lib/badline/storage/g64_image.rb +222 -0
- data/lib/badline/storage/host_directory.rb +21 -1
- data/lib/badline/storage/listing.rb +118 -0
- data/lib/badline/storage/t64.rb +14 -0
- data/lib/badline/storage.rb +2 -0
- data/lib/badline/traps.rb +4 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +3 -0
- data/lib/badline/via/fast_forward.rb +53 -0
- data/lib/badline/via/shift_register.rb +8 -3
- data/lib/badline/via/timer1.rb +36 -0
- data/lib/badline/via/timer2.rb +31 -0
- data/lib/badline/via.rb +2 -0
- data/lib/badline/vic/display_state.rb +7 -7
- data/lib/badline/vic/sprite/internal_bus.rb +11 -7
- data/lib/badline/vic/sprite/shifter.rb +10 -15
- data/lib/badline/vic/sprite/timing.rb +60 -0
- data/lib/badline/vic/sprite.rb +14 -12
- data/lib/badline/vic/sprites.rb +2 -2
- data/lib/badline/vic.rb +86 -39
- data/lib/badline.rb +3 -0
- metadata +28 -1
|
@@ -8,6 +8,7 @@ module Badline
|
|
|
8
8
|
# can't be read is left out, and a failed write reports false.
|
|
9
9
|
class HostDirectory
|
|
10
10
|
NO_SYNC = 21
|
|
11
|
+
ID = "DIR".bytes.freeze
|
|
11
12
|
|
|
12
13
|
def initialize(path)
|
|
13
14
|
@path = path
|
|
@@ -39,8 +40,27 @@ module Badline
|
|
|
39
40
|
false
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
# The directory as LOAD"$" lists it, under its host name: every file
|
|
44
|
+
# as a PRG, of the blocks it would take on a disk. A host directory
|
|
45
|
+
# counts no free blocks.
|
|
46
|
+
def directory
|
|
47
|
+
Listing.new(name: Listing.encode(File.basename(@path)), id: ID,
|
|
48
|
+
entries: entries.map { |entry| listing_entry(entry) }, blocks_free: 0)
|
|
49
|
+
end
|
|
50
|
+
|
|
42
51
|
private
|
|
43
52
|
|
|
53
|
+
def listing_entry(entry)
|
|
54
|
+
entry[:listed] || Listing.program(entry[:name], file_length(entry[:file]))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def file_length(file)
|
|
58
|
+
size = File.size(File.join(@path, file))
|
|
59
|
+
File.extname(file).casecmp?(".p00") ? size - P00::HEADER_SIZE : size
|
|
60
|
+
rescue SystemCallError
|
|
61
|
+
0
|
|
62
|
+
end
|
|
63
|
+
|
|
44
64
|
def find(name)
|
|
45
65
|
pattern = Storage.matcher(name)
|
|
46
66
|
entries.find { |e| pattern.match?(e[:name]) }
|
|
@@ -72,7 +92,7 @@ module Badline
|
|
|
72
92
|
|
|
73
93
|
def t64_entries(file)
|
|
74
94
|
archive = T64.new(File.join(@path, file))
|
|
75
|
-
archive.names.map { |name| { name:, archive: } }
|
|
95
|
+
archive.names.zip(archive.listing_entries).map { |name, listed| { name:, archive:, listed: } }
|
|
76
96
|
rescue T64::FormatError, SystemCallError
|
|
77
97
|
nil
|
|
78
98
|
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
# A directory as a 1541 sends it for LOAD"$",8: a BASIC program loaded
|
|
6
|
+
# at $0401. The header line is line 0, the disk's name and ID in
|
|
7
|
+
# reverse video, the name quoted. Each file is a line numbered by its
|
|
8
|
+
# length in blocks, with the name quoted and padded to 16 characters,
|
|
9
|
+
# then the type, marked `*` when the file was never closed and `<` when
|
|
10
|
+
# it is locked. The last line numbers the free blocks. Lines are
|
|
11
|
+
# padded with spaces to the drive's fixed widths, 27 characters for a
|
|
12
|
+
# file and 25 for the others, and shifted spaces print as plain ones.
|
|
13
|
+
#
|
|
14
|
+
# Names come as the bytes on the disk, padded with shifted spaces to 16.
|
|
15
|
+
# A file's name ends at its first shifted space, where the closing
|
|
16
|
+
# quote goes, and whatever follows it in the entry shows after the
|
|
17
|
+
# quote, as it does on a 1541.
|
|
18
|
+
class Listing
|
|
19
|
+
LOAD_ADDRESS = 0x0401
|
|
20
|
+
NAME_LENGTH = 16
|
|
21
|
+
NAME_PADDING = 0xa0
|
|
22
|
+
SPACE = 0x20
|
|
23
|
+
QUOTE = 0x22
|
|
24
|
+
REVERSE_ON = 0x12
|
|
25
|
+
FILE_WIDTH = 27
|
|
26
|
+
WIDTH = 25
|
|
27
|
+
TYPES = %w[DEL SEQ PRG USR REL CBM].freeze
|
|
28
|
+
PRG = 2
|
|
29
|
+
|
|
30
|
+
# One directory entry: its name's bytes, its type code (0 DEL to
|
|
31
|
+
# 4 REL, and the 1581's 5 CBM), its length in blocks, and whether it
|
|
32
|
+
# was closed and is locked.
|
|
33
|
+
Entry = Data.define(:name, :type, :blocks, :closed, :locked)
|
|
34
|
+
|
|
35
|
+
attr_reader :name, :id, :entries, :blocks_free
|
|
36
|
+
|
|
37
|
+
def initialize(name:, id:, entries:, blocks_free:)
|
|
38
|
+
@name = name
|
|
39
|
+
@id = id
|
|
40
|
+
@entries = entries
|
|
41
|
+
@blocks_free = blocks_free
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Name bytes padded with shifted spaces, from ASCII text in upper
|
|
45
|
+
# case, as a host file's name is listed.
|
|
46
|
+
def self.encode(text)
|
|
47
|
+
bytes = text.upcase.bytes.first(NAME_LENGTH)
|
|
48
|
+
bytes + Array.new(NAME_LENGTH - bytes.length, NAME_PADDING)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A host file listed as a closed PRG, named in ASCII, of the blocks
|
|
52
|
+
# its bytes would take on a disk, 254 to a block.
|
|
53
|
+
def self.program(name, length)
|
|
54
|
+
Entry.new(name: encode(name), type: PRG, blocks: (length + 253) / 254, closed: true, locked: false)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The file's bytes, load address first. A `$:NAME` or `$0:A*,B*`
|
|
58
|
+
# pattern lists only the files that match one of its names.
|
|
59
|
+
def bytes(request = "$")
|
|
60
|
+
lines = [[0, padded(header, WIDTH)]]
|
|
61
|
+
lines.concat(listed(request).map { |entry| [entry.blocks, file_line(entry)] })
|
|
62
|
+
lines << [@blocks_free, padded("BLOCKS FREE.".bytes, WIDTH)]
|
|
63
|
+
program(lines)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def listed(request)
|
|
69
|
+
patterns = request.sub(/\A\$\d*/, "")
|
|
70
|
+
return @entries unless patterns.start_with?(":")
|
|
71
|
+
|
|
72
|
+
matchers = patterns[1..].split(",").map { |pattern| Storage.matcher(pattern) }
|
|
73
|
+
@entries.select do |entry|
|
|
74
|
+
name = Storage.ascii(entry.name.take_while { |byte| byte != NAME_PADDING }).downcase
|
|
75
|
+
matchers.any? { |matcher| matcher.match?(name) }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def header
|
|
80
|
+
[REVERSE_ON, QUOTE, *spaced(@name.first(NAME_LENGTH)), QUOTE, SPACE, *spaced(@id.first(5))]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# A block count under 10 or 100 takes more spaces before the name, so
|
|
84
|
+
# the names line up.
|
|
85
|
+
def file_line(entry)
|
|
86
|
+
indent = 1 + (entry.blocks < 100 ? 1 : 0) + (entry.blocks < 10 ? 1 : 0)
|
|
87
|
+
line = Array.new(indent, SPACE) + quoted(entry.name) + [entry.closed ? SPACE : "*".ord]
|
|
88
|
+
line.concat(TYPES.fetch(entry.type, "???").bytes)
|
|
89
|
+
line << (entry.locked ? "<".ord : SPACE)
|
|
90
|
+
padded(line, FILE_WIDTH)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The closing quote takes the first shifted space's place, or follows
|
|
94
|
+
# a name that fills all 16.
|
|
95
|
+
def quoted(name)
|
|
96
|
+
name = name.first(NAME_LENGTH)
|
|
97
|
+
name += Array.new(NAME_LENGTH - name.length, NAME_PADDING)
|
|
98
|
+
cut = name.index(NAME_PADDING)
|
|
99
|
+
name = cut ? [*name[0, cut], QUOTE, *name[(cut + 1)..], SPACE] : [*name, QUOTE]
|
|
100
|
+
[QUOTE, *spaced(name)]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def padded(line, width) = line + Array.new([width - line.length, 0].max, SPACE)
|
|
104
|
+
|
|
105
|
+
def spaced(bytes) = bytes.map { |byte| byte == NAME_PADDING ? SPACE : byte }
|
|
106
|
+
|
|
107
|
+
def program(lines)
|
|
108
|
+
address = LOAD_ADDRESS
|
|
109
|
+
bytes = [address & 0xff, address >> 8]
|
|
110
|
+
lines.each do |number, text|
|
|
111
|
+
address += text.length + 5
|
|
112
|
+
bytes.push(address & 0xff, address >> 8, number & 0xff, (number >> 8) & 0xff, *text, 0)
|
|
113
|
+
end
|
|
114
|
+
bytes.push(0, 0)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/badline/storage/t64.rb
CHANGED
|
@@ -9,6 +9,8 @@ module Badline
|
|
|
9
9
|
HEADER_SIZE = 0x40
|
|
10
10
|
ENTRY_SIZE = 32
|
|
11
11
|
NORMAL_FILE = 1
|
|
12
|
+
TAPE_NAME = 0x28
|
|
13
|
+
ID = "T64".bytes.freeze
|
|
12
14
|
|
|
13
15
|
def initialize(path)
|
|
14
16
|
@bytes = File.binread(path).bytes
|
|
@@ -29,6 +31,18 @@ module Badline
|
|
|
29
31
|
entries.map { |e| e[:name] }
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
# The archive as LOAD"$" lists it: the tape's name, and each file as
|
|
35
|
+
# a PRG of the blocks it would take on a disk, load address and all.
|
|
36
|
+
# An archive has no free blocks.
|
|
37
|
+
def directory
|
|
38
|
+
Listing.new(name: Listing.encode(decode_name(@bytes[TAPE_NAME, Listing::NAME_LENGTH].to_a)),
|
|
39
|
+
id: ID, entries: listing_entries, blocks_free: 0)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def listing_entries
|
|
43
|
+
entries.map { |e| Listing.program(e[:name], e[:length] + 2) }
|
|
44
|
+
end
|
|
45
|
+
|
|
32
46
|
private
|
|
33
47
|
|
|
34
48
|
def entries
|
data/lib/badline/storage.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/storage/listing"
|
|
3
4
|
require "badline/storage/p00"
|
|
4
5
|
require "badline/storage/host_directory"
|
|
5
6
|
require "badline/storage/disk_image"
|
|
6
7
|
require "badline/storage/d64_image"
|
|
7
8
|
require "badline/storage/d71_image"
|
|
8
9
|
require "badline/storage/d81_image"
|
|
10
|
+
require "badline/storage/g64_image"
|
|
9
11
|
require "badline/storage/t64"
|
|
10
12
|
require "badline/storage/tap"
|
|
11
13
|
require "badline/storage/crt_file"
|
data/lib/badline/traps.rb
CHANGED
data/lib/badline/version.rb
CHANGED
|
@@ -55,6 +55,9 @@ module Badline
|
|
|
55
55
|
|
|
56
56
|
def active_c1_edge?(high) = high == @control.anybits?(0x01)
|
|
57
57
|
|
|
58
|
+
# Everything the lines hold, for comparing them at two points.
|
|
59
|
+
def state = [@control, @c1_high, @c2_high, @handshake_low, @pulse, @c2_output]
|
|
60
|
+
|
|
58
61
|
# A new C1 level. An active edge sets the flag and ends a handshake.
|
|
59
62
|
def c1=(high)
|
|
60
63
|
return if high == @c1_high
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIA
|
|
5
|
+
# Running a VIA a stretch of cycles at once while nothing but its
|
|
6
|
+
# counters would change, for Drive1541::Idle.
|
|
7
|
+
module FastForward
|
|
8
|
+
# quiet_cycles for a VIA that will never set a flag of its own.
|
|
9
|
+
QUIET = 1 << 40
|
|
10
|
+
|
|
11
|
+
# How many cycles the VIA can run with nothing but its counters
|
|
12
|
+
# changing: no flag set, no pulse on a control line and no bit
|
|
13
|
+
# shifted. See Drive1541::Idle.
|
|
14
|
+
def quiet_cycles
|
|
15
|
+
return 0 if @sr_uses_t2 || @shift_register.clocking? || @ca.pulsing? || @cb.pulsing?
|
|
16
|
+
|
|
17
|
+
[@t1.quiet_cycles, @t2.quiet_cycles].min
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Runs +cycles+ cycles at once, as many as quiet_cycles allows, which
|
|
21
|
+
# moves only the counters.
|
|
22
|
+
def fast_forward(cycles)
|
|
23
|
+
return if cycles.zero?
|
|
24
|
+
|
|
25
|
+
@t1.fast_forward(cycles)
|
|
26
|
+
@t2.fast_forward(cycles)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The counters an orbit (see Drive1541::Idle) has to bring back: those
|
|
30
|
+
# of the armed timers, and of the timers +touched+ names (bit 0 for
|
|
31
|
+
# timer 1, bit 1 for timer 2). The others only count.
|
|
32
|
+
def counter_state(touched)
|
|
33
|
+
[(@t1.count_state if @t1.armed? || touched.anybits?(1)),
|
|
34
|
+
(@t2.count_state if @t2.armed? || touched.anybits?(2))]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Runs +cycles+ cycles of whole orbits at once: the counters
|
|
38
|
+
# counter_state leaves out move on, and the rest come back round.
|
|
39
|
+
def skip_orbits(cycles, touched)
|
|
40
|
+
@t1.fast_forward(cycles) unless @t1.armed? || touched.anybits?(1)
|
|
41
|
+
@t2.fast_forward(cycles) unless @t2.armed? || touched.anybits?(2)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Everything the VIA holds but its counters, for comparing it at two
|
|
45
|
+
# points. Timer 1's PB7 level counts only while ACR bit 7 puts it on
|
|
46
|
+
# the pin: nothing reads it otherwise.
|
|
47
|
+
def idle_state
|
|
48
|
+
[@ora, @orb, @ddra, @ddrb, @latch_a, @latch_b, @acr, @pcr, @pb6_high, @ifr.flags, @ifr.enable,
|
|
49
|
+
@acr.anybits?(0x80) && @t1.pb7, *@t1.state, *@t2.state, *@ca.state, *@cb.state, *@shift_register.state]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -35,8 +35,6 @@ module Badline
|
|
|
35
35
|
OUT_PHI2 = 6
|
|
36
36
|
OUT_EXTERNAL = 7
|
|
37
37
|
|
|
38
|
-
BITS = 8
|
|
39
|
-
|
|
40
38
|
attr_reader :mode, :data
|
|
41
39
|
attr_accessor :cb2_input
|
|
42
40
|
|
|
@@ -62,9 +60,16 @@ module Badline
|
|
|
62
60
|
# Every read or write of the data register (re)starts the count of
|
|
63
61
|
# eight bits.
|
|
64
62
|
def access!
|
|
65
|
-
@bits =
|
|
63
|
+
@bits = 8
|
|
66
64
|
end
|
|
67
65
|
|
|
66
|
+
# Whether the register is counting bits off its own clock, which
|
|
67
|
+
# changes it from cycle to cycle.
|
|
68
|
+
def clocking? = @bits.nonzero? && internal_clock?
|
|
69
|
+
|
|
70
|
+
# Everything the register holds, for comparing it at two points.
|
|
71
|
+
def state = [@mode, @data, @bits, @clock, @cb2, @cb2_input]
|
|
72
|
+
|
|
68
73
|
# The CB1 level the register drives, or nil when CB1 is an input.
|
|
69
74
|
def cb1_output
|
|
70
75
|
internal_clock? ? @clock : nil
|
data/lib/badline/via/timer1.rb
CHANGED
|
@@ -66,6 +66,42 @@ module Badline
|
|
|
66
66
|
@counter == 0xffff && timeout(free_run)
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# How many cycles the timer can run without setting its flag: until
|
|
70
|
+
# an armed timer's timeout, and forever for an unarmed one, whose
|
|
71
|
+
# timeouts only reload the counter. None while an armed timer loads
|
|
72
|
+
# or reloads.
|
|
73
|
+
def quiet_cycles
|
|
74
|
+
return FastForward::QUIET unless @armed
|
|
75
|
+
return 0 if @hold || @reload
|
|
76
|
+
|
|
77
|
+
@counter
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Runs +cycles+ cycles at once, as many as quiet_cycles allows. An
|
|
81
|
+
# unarmed timer goes round its period of latch + 2 cycles: the
|
|
82
|
+
# timeout at $FFFF, the reload, and latch down to 0.
|
|
83
|
+
def fast_forward(cycles)
|
|
84
|
+
if @hold || @reload
|
|
85
|
+
cycle!(false)
|
|
86
|
+
cycles -= 1
|
|
87
|
+
end
|
|
88
|
+
return @counter -= cycles if cycles <= @counter
|
|
89
|
+
|
|
90
|
+
phase = (cycles - @counter - 1) % (@latch + 2)
|
|
91
|
+
@reload = phase.zero?
|
|
92
|
+
@counter = @reload ? 0xffff : @latch - phase + 1
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# What the counter and PB7 don't hold, for comparing the timer at two
|
|
96
|
+
# points.
|
|
97
|
+
def state = [@latch, @armed]
|
|
98
|
+
|
|
99
|
+
# Whether a timeout sets the flag.
|
|
100
|
+
def armed? = @armed
|
|
101
|
+
|
|
102
|
+
# The count, for comparing the timer at two points where it matters.
|
|
103
|
+
def count_state = [@counter, @hold, @reload]
|
|
104
|
+
|
|
69
105
|
private
|
|
70
106
|
|
|
71
107
|
# The counter reloads either way. Armed, PB7 inverts and the flag
|
data/lib/badline/via/timer2.rb
CHANGED
|
@@ -63,6 +63,37 @@ module Badline
|
|
|
63
63
|
# A falling edge on PB6. Returns true when it sets the flag.
|
|
64
64
|
def pulse! = @counting_pulses && decrement
|
|
65
65
|
|
|
66
|
+
# How many cycles the timer can run without setting its flag, taken
|
|
67
|
+
# without the shift register clocking off it: until an armed timer
|
|
68
|
+
# underflows, and forever for one counting pulses or unarmed, which
|
|
69
|
+
# only rolls on. None while a load or a switch of mode is under way.
|
|
70
|
+
def quiet_cycles
|
|
71
|
+
return 0 if @hold || @counting_pulses != @count_pulses
|
|
72
|
+
return FastForward::QUIET if @counting_pulses || !@armed
|
|
73
|
+
|
|
74
|
+
@counter
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Runs +cycles+ cycles at once, as many as quiet_cycles allows. The
|
|
78
|
+
# low byte's underflow is set when the last cycle took it from $00.
|
|
79
|
+
def fast_forward(cycles)
|
|
80
|
+
@low_underflowed = false
|
|
81
|
+
return if @counting_pulses
|
|
82
|
+
|
|
83
|
+
@counter = (@counter - cycles) & 0xffff
|
|
84
|
+
@low_underflowed = @counter.allbits?(0xff)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# What the counter and the low byte's underflow don't hold, for
|
|
88
|
+
# comparing the timer at two points.
|
|
89
|
+
def state = [@latch_low, @armed, @hold, @low_reload, @count_pulses, @counting_pulses]
|
|
90
|
+
|
|
91
|
+
# Whether the next underflow sets the flag.
|
|
92
|
+
def armed? = @armed
|
|
93
|
+
|
|
94
|
+
# The count, for comparing the timer at two points where it matters.
|
|
95
|
+
def count_state = [@counter, @low_underflowed]
|
|
96
|
+
|
|
66
97
|
private
|
|
67
98
|
|
|
68
99
|
# A cycle while counting pulses, or held after a load or a low byte
|
data/lib/badline/via.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "badline/via/control_lines"
|
|
4
|
+
require "badline/via/fast_forward"
|
|
4
5
|
require "badline/via/interrupt_register"
|
|
5
6
|
require "badline/via/shift_register"
|
|
6
7
|
require "badline/via/timer1"
|
|
@@ -16,6 +17,7 @@ module Badline
|
|
|
16
17
|
# feed the counter.
|
|
17
18
|
class VIA
|
|
18
19
|
include Addressable
|
|
20
|
+
include FastForward
|
|
19
21
|
|
|
20
22
|
attr_reader :start, :peripheral, :shift_register, :acr, :pcr
|
|
21
23
|
|
|
@@ -12,10 +12,6 @@ module Badline
|
|
|
12
12
|
LAST_LINE = 0xf7 # 247
|
|
13
13
|
COLUMNS_PER_ROW = 40
|
|
14
14
|
|
|
15
|
-
# The last column of a rasterline already compares against the line
|
|
16
|
-
# about to start, the same wrap the raster IRQ latch uses.
|
|
17
|
-
WRAP_COLUMN = 62
|
|
18
|
-
|
|
19
15
|
# AEC follows BA three cycles later; the c-accesses before it read
|
|
20
16
|
# the bus the CPU still drives.
|
|
21
17
|
BA_DELAY = 3
|
|
@@ -32,12 +28,16 @@ module Badline
|
|
|
32
28
|
LOAD_COLUMN = 12
|
|
33
29
|
RC_COLUMN = 56
|
|
34
30
|
|
|
35
|
-
GRAPHICS_COLUMNS = Array.new(
|
|
31
|
+
GRAPHICS_COLUMNS = Array.new(65) { |column| column.between?(GRAPHICS_FIRST, GRAPHICS_LAST) }.freeze
|
|
36
32
|
|
|
37
33
|
attr_reader :vc_base, :vc, :vmli, :rc
|
|
38
34
|
|
|
39
|
-
|
|
35
|
+
# The wrap column is the last column of a rasterline, 62 on the 6569,
|
|
36
|
+
# which already compares against the line about to start, the same
|
|
37
|
+
# wrap the raster IRQ latch uses.
|
|
38
|
+
def initialize(registers, wrap_column = 62)
|
|
40
39
|
@registers = registers
|
|
40
|
+
@wrap_column = wrap_column
|
|
41
41
|
@vc_base = 0
|
|
42
42
|
@vc = 0
|
|
43
43
|
@vmli = 0
|
|
@@ -77,7 +77,7 @@ module Badline
|
|
|
77
77
|
|
|
78
78
|
# Returns true when this compare opened display state.
|
|
79
79
|
def cycle(rasterline, column)
|
|
80
|
-
match = column ==
|
|
80
|
+
match = column == @wrap_column ? wrap_match(rasterline + 1) : line_match
|
|
81
81
|
@matched = match
|
|
82
82
|
opened = false
|
|
83
83
|
if match
|
|
@@ -10,12 +10,15 @@ module Badline
|
|
|
10
10
|
# the CPU reads or writes a VIC register in that cycle, and the middle
|
|
11
11
|
# one is the idle fetch at $3fff (sbsprf24's readme).
|
|
12
12
|
class InternalBus
|
|
13
|
-
# The columns of a raster line
|
|
14
|
-
|
|
13
|
+
# The columns of a raster line, and where the region puts the
|
|
14
|
+
# sprites in it.
|
|
15
|
+
attr_reader :columns, :timing
|
|
15
16
|
|
|
16
|
-
def initialize(bank,
|
|
17
|
+
def initialize(bank, timing)
|
|
17
18
|
@bank = bank
|
|
18
|
-
@
|
|
19
|
+
@timing = timing
|
|
20
|
+
@columns = timing.columns
|
|
21
|
+
@sprite_cycle = timing.sprite_cycle
|
|
19
22
|
@lines = 0
|
|
20
23
|
@ghost = 0xff
|
|
21
24
|
@value = Array.new(columns, 0xff)
|
|
@@ -37,10 +40,11 @@ module Badline
|
|
|
37
40
|
@line[column] = column.zero? ? @lines + 1 : @lines
|
|
38
41
|
end
|
|
39
42
|
|
|
40
|
-
# The row sprite `index` (3-7) fetched this line, in
|
|
41
|
-
# 2n-5 and 2n-4
|
|
43
|
+
# The row sprite `index` (3-7) fetched this line, in the phi2 halves
|
|
44
|
+
# of the two cycles from its p-access: Bauer cycles 2n-5 and 2n-4 on
|
|
45
|
+
# the 6569.
|
|
42
46
|
def row(index)
|
|
43
|
-
column = (2 * index) -
|
|
47
|
+
column = (@sprite_cycle + (2 * index) - 1) % @columns
|
|
44
48
|
(phi2(column) << 16) | (@ghost << 8) | phi2(column + 1)
|
|
45
49
|
end
|
|
46
50
|
|
|
@@ -6,18 +6,13 @@ module Badline
|
|
|
6
6
|
# The X comparator and the 24-bit shift register behind it: where a
|
|
7
7
|
# line fires the sprite, and the pixels each firing shifts out.
|
|
8
8
|
module Shifter
|
|
9
|
-
EMPTY = [].freeze
|
|
10
|
-
|
|
11
9
|
private
|
|
12
10
|
|
|
13
11
|
# The first pixel of every comparator match along the line. Writes to
|
|
14
12
|
# $d000/$d010 move the compare value mid-line, so a sprite can miss
|
|
15
13
|
# its match entirely and pick up a later one, a second one, or none.
|
|
16
14
|
def comparator_hits(log)
|
|
17
|
-
unless log
|
|
18
|
-
match = compare_x(@registers)
|
|
19
|
-
return match ? [(match + 1) % @width] : EMPTY
|
|
20
|
-
end
|
|
15
|
+
return compare_x(@registers) unless log
|
|
21
16
|
|
|
22
17
|
hits = []
|
|
23
18
|
log.rewind
|
|
@@ -25,8 +20,10 @@ module Badline
|
|
|
25
20
|
from = 0
|
|
26
21
|
while from < @width
|
|
27
22
|
upto = [log.next_x, @width].min
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
compare_x(log).each do |start|
|
|
24
|
+
match = (start - 1) % @width
|
|
25
|
+
hits << start if match >= from && match < upto
|
|
26
|
+
end
|
|
30
27
|
break if upto >= @width
|
|
31
28
|
|
|
32
29
|
from = upto
|
|
@@ -35,15 +32,13 @@ module Badline
|
|
|
35
32
|
hits
|
|
36
33
|
end
|
|
37
34
|
|
|
38
|
-
# The
|
|
39
|
-
#
|
|
40
|
-
#
|
|
35
|
+
# The pixels the sprite starts at, one past each comparator match.
|
|
36
|
+
# The 6569's X counter only runs to 503, so the eight coordinates
|
|
37
|
+
# above it never match and the sprite stays dark — the gap the
|
|
38
|
+
# spritegap tests find at $1f8.
|
|
41
39
|
def compare_x(view)
|
|
42
40
|
msb = view[0x10].anybits?(@bit) ? 0x100 : 0
|
|
43
|
-
|
|
44
|
-
return if xpos >= @width
|
|
45
|
-
|
|
46
|
-
(xpos + COMPARE_OFFSET) % @width
|
|
41
|
+
@timing.x_pixels[msb | view[index * 2]]
|
|
47
42
|
end
|
|
48
43
|
|
|
49
44
|
# Pixels from the X match to the next reload.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIC < Cycleable
|
|
5
|
+
class Sprite
|
|
6
|
+
# = Timing
|
|
7
|
+
#
|
|
8
|
+
# Where a region puts the sprites in the line: the cycles of their
|
|
9
|
+
# fetches and of the display compare, the pixels those land on, and
|
|
10
|
+
# the raster pixels the X counter reads each coordinate at. Where the
|
|
11
|
+
# fetches or the compare run a cycle later than on the 6569, what
|
|
12
|
+
# hangs off them moves 8 pixels with them.
|
|
13
|
+
class Timing
|
|
14
|
+
# The X counter's value where it holds on a raster of more than 512
|
|
15
|
+
# pixels, and the four values it repeats from there.
|
|
16
|
+
HOLD_AT = 0x188
|
|
17
|
+
HOLD_BASE = 0x184
|
|
18
|
+
|
|
19
|
+
attr_reader :columns, :sprite_cycle, :display_off_x, :x_pixels
|
|
20
|
+
|
|
21
|
+
def initialize(region)
|
|
22
|
+
@columns = region.cycles_per_line
|
|
23
|
+
@sprite_cycle = region.sprite_cycle
|
|
24
|
+
@shift = region.sprite_cycle - Region::PAL.sprite_cycle
|
|
25
|
+
@display_off_x = DISPLAY_OFF_X + (8 * (region.sprite_display_cycle - Region::PAL.sprite_display_cycle))
|
|
26
|
+
@x_pixels = Timing.x_pixels(@columns * 8, region.x_hold)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The column BA falls in for a sprite's own fetch: 55 for sprite 0 on
|
|
30
|
+
# the 6569, and two later for each sprite after it.
|
|
31
|
+
def ba_column(index) = 55 + @shift + (2 * index)
|
|
32
|
+
|
|
33
|
+
# The raster pixel of a sprite's reload, counted from the start of
|
|
34
|
+
# the line it is fetched on, so past the end of it for sprites 3-7
|
|
35
|
+
# on the 6569.
|
|
36
|
+
def reload_x(index) = RELOAD_X + (8 * @shift) + (RELOAD_STEP * index)
|
|
37
|
+
|
|
38
|
+
# 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 past HOLD_AT,
|
|
40
|
+
# where it ran over $184-$187 again instead.
|
|
41
|
+
def self.xpos(pixel, width, hold)
|
|
42
|
+
count = (pixel - X_OFFSET) % width
|
|
43
|
+
return count if count < HOLD_AT
|
|
44
|
+
return count - hold if count >= HOLD_AT + hold
|
|
45
|
+
|
|
46
|
+
HOLD_BASE + ((count - HOLD_AT) % 4)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The raster pixels at which the X counter reads each coordinate, 512
|
|
50
|
+
# lists. A sprite starts at one where its X matches. On the 6569 the
|
|
51
|
+
# coordinates past 503 have none.
|
|
52
|
+
def self.x_pixels(width, hold)
|
|
53
|
+
pixels = Array.new(512) { [] }
|
|
54
|
+
width.times { |pixel| pixels[xpos(pixel, width, hold)] << pixel }
|
|
55
|
+
pixels.each(&:freeze).freeze
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|