badline 0.3.0 → 0.4.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/.claude/hooks/session-start.sh +57 -0
- data/.claude/settings.json +14 -0
- data/CHANGELOG.md +65 -0
- data/CLAUDE.md +45 -16
- data/CONTRIBUTING.md +95 -3
- data/README.md +111 -61
- data/SECURITY.md +9 -0
- data/doc/pinned-behaviour.md +275 -33
- data/exe/badline-ruby +59 -0
- data/lib/badline/address_bus.rb +22 -10
- data/lib/badline/audio/aiff.rb +8 -3
- data/lib/badline/audio/cli.rb +17 -12
- data/lib/badline/audio/console.rb +12 -58
- data/lib/badline/audio/playback.rb +3 -0
- data/lib/badline/audio/sdl_sink.rb +20 -52
- data/lib/badline/audio/terminal.rb +90 -0
- data/lib/badline/audio.rb +2 -2
- data/lib/badline/cartridge/geo_ram.rb +63 -0
- data/lib/badline/cartridge.rb +1 -0
- data/lib/badline/cia/interrupt_register.rb +34 -10
- data/lib/badline/cia.rb +15 -8
- data/lib/badline/computer.rb +28 -13
- data/lib/badline/gui/application.rb +33 -21
- data/lib/badline/gui/gamepads.rb +22 -22
- data/lib/badline/gui/joy_map.rb +1 -1
- data/lib/badline/gui/key_map.rb +6 -1
- data/lib/badline/gui/pane.rb +7 -6
- data/lib/badline/gui/screen_pane.rb +9 -13
- data/lib/badline/gui/window.rb +35 -20
- data/lib/badline/gui.rb +1 -1
- data/lib/badline/input/mouse1351.rb +21 -6
- data/lib/badline/input/paddles.rb +11 -6
- data/lib/badline/kernal_trap/channel.rb +3 -0
- data/lib/badline/kernal_trap/drive/block_commands.rb +33 -4
- data/lib/badline/kernal_trap/drive/status.rb +3 -0
- data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
- data/lib/badline/kernal_trap/drive/writes.rb +92 -0
- data/lib/badline/kernal_trap/drive.rb +51 -51
- data/lib/badline/kernal_trap/file.rb +6 -9
- data/lib/badline/kernal_trap/save.rb +9 -4
- data/lib/badline/keyboard.rb +19 -3
- data/lib/badline/media.rb +42 -11
- data/lib/badline/options.rb +209 -0
- data/lib/badline/ram_expansion/banking.rb +30 -0
- data/lib/badline/ram_expansion/plus256k.rb +38 -0
- data/lib/badline/ram_expansion/plus60k.rb +31 -0
- data/lib/badline/ram_expansion/unexpanded.rb +22 -0
- data/lib/badline/ram_expansion.rb +21 -0
- data/lib/badline/region.rb +51 -0
- data/lib/badline/sdl/events.rb +47 -0
- data/lib/badline/sdl/functions.rb +73 -0
- data/lib/badline/sdl.rb +67 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
- data/lib/badline/sid/waveform.rb +1 -1
- data/lib/badline/sid.rb +3 -1
- data/lib/badline/storage/d64_image.rb +10 -0
- data/lib/badline/storage/d71_image.rb +12 -0
- data/lib/badline/storage/d81_image.rb +9 -0
- data/lib/badline/storage/disk_image/bam.rb +111 -0
- data/lib/badline/storage/disk_image/directory.rb +79 -0
- data/lib/badline/storage/disk_image/writing.rb +110 -0
- data/lib/badline/storage/disk_image.rb +27 -4
- data/lib/badline/storage/host_directory.rb +2 -1
- data/lib/badline/storage.rb +15 -0
- data/lib/badline/time_of_day.rb +4 -2
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +104 -0
- data/lib/badline/via/interrupt_register.rb +58 -0
- data/lib/badline/via/shift_register.rb +135 -0
- data/lib/badline/via/timer1.rb +82 -0
- data/lib/badline/via/timer2.rb +84 -0
- data/lib/badline/via.rb +262 -0
- data/lib/badline/vic/bank.rb +4 -3
- data/lib/badline/vic/color_patches.rb +6 -3
- data/lib/badline/vic/display_state.rb +4 -0
- data/lib/badline/vic/graphics_shifter.rb +42 -7
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic/sequencer.rb +18 -18
- data/lib/badline/vic/sequencer_output.rb +4 -4
- data/lib/badline/vic/sprite/internal_bus.rb +4 -0
- data/lib/badline/vic/sprite/shifter.rb +25 -3
- data/lib/badline/vic/sprite.rb +5 -4
- data/lib/badline/vic/sprites.rb +8 -3
- data/lib/badline/vic.rb +93 -44
- data/lib/badline.rb +4 -0
- data/packaging/homebrew/badline.rb +34 -0
- metadata +30 -20
- data/exe/badline +0 -78
- data/exe/badline-sid +0 -34
- data/lib/badline/audio/options.rb +0 -137
|
@@ -13,6 +13,16 @@ module Badline
|
|
|
13
13
|
|
|
14
14
|
def directory_track = 18
|
|
15
15
|
def directory_sector = 1
|
|
16
|
+
def directory_interleave = 3
|
|
17
|
+
def interleave = 10
|
|
18
|
+
def reserved_tracks = [18]
|
|
19
|
+
|
|
20
|
+
# The BAM in the header block covers the 35 tracks of a standard
|
|
21
|
+
# disk. The layouts that extend it to 40 tracks differ between DOS
|
|
22
|
+
# versions, so the tracks past 35 stay unused.
|
|
23
|
+
def bam_tracks = 1..35
|
|
24
|
+
def bam_count(track) = [18, 0, 4 * track]
|
|
25
|
+
def bam_bitmap(track) = [18, 0, (4 * track) + 1]
|
|
16
26
|
|
|
17
27
|
def sectors_in(track)
|
|
18
28
|
case track
|
|
@@ -9,6 +9,18 @@ module Badline
|
|
|
9
9
|
|
|
10
10
|
def error_tables = ERROR_TABLES
|
|
11
11
|
|
|
12
|
+
def interleave = 6
|
|
13
|
+
def reserved_tracks = [18, 53]
|
|
14
|
+
|
|
15
|
+
# The header block flags a double-sided disk. The second side's free
|
|
16
|
+
# counts follow the first side's BAM there, and its bitmaps take the
|
|
17
|
+
# first block of track 53.
|
|
18
|
+
def bam_tracks = double_sided? ? 1..70 : super
|
|
19
|
+
def bam_count(track) = track > 35 ? [18, 0, 0xdd + track - 36] : super
|
|
20
|
+
def bam_bitmap(track) = track > 35 ? [53, 0, 3 * (track - 36)] : super
|
|
21
|
+
|
|
22
|
+
def double_sided? = @bytes[sector_offset(18, 0) + 3].anybits?(0x80)
|
|
23
|
+
|
|
12
24
|
def sectors_in(track)
|
|
13
25
|
track > 35 ? super(track - 35) : super
|
|
14
26
|
end
|
|
@@ -11,6 +11,15 @@ module Badline
|
|
|
11
11
|
|
|
12
12
|
def directory_track = 40
|
|
13
13
|
def directory_sector = 3
|
|
14
|
+
def directory_interleave = 1
|
|
15
|
+
def interleave = 1
|
|
16
|
+
def reserved_tracks = [40]
|
|
17
|
+
|
|
18
|
+
# Blocks 1 and 2 of track 40 hold the BAM for tracks 1 to 40 and 41
|
|
19
|
+
# to 80, six bytes a track from offset 16.
|
|
20
|
+
def bam_tracks = 1..80
|
|
21
|
+
def bam_count(track) = [40, track > 40 ? 2 : 1, 0x10 + (((track - 1) % 40) * 6)]
|
|
22
|
+
def bam_bitmap(track) = [40, track > 40 ? 2 : 1, 0x11 + (((track - 1) % 40) * 6)]
|
|
14
23
|
|
|
15
24
|
def sectors_in(_track) = 40
|
|
16
25
|
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
class DiskImage
|
|
6
|
+
# The block availability map: a free count and a bitmap for each
|
|
7
|
+
# track, with a set bit for each free sector. Each format says where
|
|
8
|
+
# a track's count and bitmap sit, which tracks the map covers and
|
|
9
|
+
# which of them hold the directory. Files are laid out as the DOS
|
|
10
|
+
# does, on the tracks nearest the directory first, below it before
|
|
11
|
+
# above, with the format's sector interleave within a track.
|
|
12
|
+
module Bam
|
|
13
|
+
# Whether the BAM has an entry for the block.
|
|
14
|
+
def bam_block?(track, sector)
|
|
15
|
+
bam_tracks.cover?(track) && sector.between?(0, sectors_in(track) - 1) &&
|
|
16
|
+
block?(*bam_bitmap(track).first(2)) && block?(*bam_count(track).first(2))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Whether the BAM marks the block free. A block it has no entry for
|
|
20
|
+
# never is.
|
|
21
|
+
def block_free?(track, sector)
|
|
22
|
+
bam_block?(track, sector) && @bytes[bitmap_offset(track) + (sector / 8)][sector % 8] == 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The first free block past the one given, on its track and then
|
|
26
|
+
# the tracks above it, for B-A to report when the block is taken.
|
|
27
|
+
# Empty when there is none.
|
|
28
|
+
def next_free_block(track, sector)
|
|
29
|
+
bam_tracks.each do |candidate|
|
|
30
|
+
next if candidate < track || reserved_tracks.include?(candidate)
|
|
31
|
+
|
|
32
|
+
first = candidate == track ? sector + 1 : 0
|
|
33
|
+
free = (first...sectors_in(candidate)).find { |s| block_free?(candidate, s) }
|
|
34
|
+
return [candidate, free] if free
|
|
35
|
+
end
|
|
36
|
+
[]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def bitmap_offset(track)
|
|
42
|
+
block_track, block_sector, offset = bam_bitmap(track)
|
|
43
|
+
sector_offset(block_track, block_sector) + offset
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def count_offset(track)
|
|
47
|
+
block_track, block_sector, offset = bam_count(track)
|
|
48
|
+
sector_offset(block_track, block_sector) + offset
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Marks the block free or in use, and moves the track's free count
|
|
52
|
+
# with it. A block the BAM doesn't cover is left alone.
|
|
53
|
+
def mark(track, sector, free)
|
|
54
|
+
return if !bam_block?(track, sector) || block_free?(track, sector) == free
|
|
55
|
+
|
|
56
|
+
byte = bitmap_offset(track) + (sector / 8)
|
|
57
|
+
bit = 1 << (sector % 8)
|
|
58
|
+
@bytes[byte] = free ? @bytes[byte] | bit : @bytes[byte] & ~bit
|
|
59
|
+
@bytes[count_offset(track)] = (@bytes[count_offset(track)] + (free ? 1 : -1)) & 0xff
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Takes blocks for a chain, each placed after the one before it.
|
|
63
|
+
# Raises DISK FULL when the disk runs out.
|
|
64
|
+
def allocate_chain(count)
|
|
65
|
+
block = nil
|
|
66
|
+
Array.new(count) do
|
|
67
|
+
block = next_block(*block) || raise(WriteError, WriteError::DISK_FULL)
|
|
68
|
+
mark(*block, false)
|
|
69
|
+
block
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A chain's first block goes on the free track nearest the
|
|
74
|
+
# directory. Each block after it goes the interleave further round
|
|
75
|
+
# the same track, or on the next track out once that one is full.
|
|
76
|
+
def next_block(track = nil, sector = nil)
|
|
77
|
+
candidates = track ? [track, *tracks_after(track)] : allocation_order
|
|
78
|
+
candidates.each do |candidate|
|
|
79
|
+
free = free_sector(candidate, candidate == track ? sector + interleave : 0)
|
|
80
|
+
return [candidate, free] if free
|
|
81
|
+
end
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def free_sector(track, first)
|
|
86
|
+
count = sectors_in(track)
|
|
87
|
+
(0...count).map { |i| (first + i) % count }.find { |s| block_free?(track, s) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The tracks a file can use, nearest the directory first, the one
|
|
91
|
+
# below before the one above.
|
|
92
|
+
def allocation_order
|
|
93
|
+
(bam_tracks.to_a - reserved_tracks).sort_by do |track|
|
|
94
|
+
[(track - directory_track).abs, track > directory_track ? 1 : 0]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The tracks further from the directory on the same side, then the
|
|
99
|
+
# rest.
|
|
100
|
+
def tracks_after(track)
|
|
101
|
+
distance = (track - directory_track).abs
|
|
102
|
+
below = track < directory_track
|
|
103
|
+
further, rest = (allocation_order - [track]).partition do |candidate|
|
|
104
|
+
(candidate < directory_track) == below && (candidate - directory_track).abs > distance
|
|
105
|
+
end
|
|
106
|
+
further + rest
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
class DiskImage
|
|
6
|
+
# Directory entries as writes make them: a closed file's type, its
|
|
7
|
+
# first block and its length in blocks, under a name padded with
|
|
8
|
+
# shifted spaces. The directory grows a block at a time on its track.
|
|
9
|
+
module Directory
|
|
10
|
+
CLOSED = 0x80
|
|
11
|
+
NAME_LENGTH = 16
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def entry_named(name)
|
|
16
|
+
wanted = decode_name(encode_name(name))
|
|
17
|
+
entries.find { |entry| entry[:name] == wanted }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def encode_name(name)
|
|
21
|
+
bytes = name.upcase.bytes.first(NAME_LENGTH)
|
|
22
|
+
bytes + Array.new(NAME_LENGTH - bytes.length, NAME_PADDING)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The new chain is written before the old one is freed, so a
|
|
26
|
+
# replace that runs out of room leaves the old file.
|
|
27
|
+
def rewrite(entry, bytes, type)
|
|
28
|
+
blocks = write_chain(bytes)
|
|
29
|
+
free_chain(entry)
|
|
30
|
+
write_entry(entry[:offset], type, blocks)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def add_file(name, bytes, type)
|
|
34
|
+
blocks = write_chain(bytes)
|
|
35
|
+
offset = free_entry_offset
|
|
36
|
+
@bytes[offset + 5, NAME_LENGTH] = encode_name(name)
|
|
37
|
+
write_entry(offset, type, blocks)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The slot's first two bytes link the directory block, so they
|
|
41
|
+
# stay. The name stays too, and what follows it up to the block
|
|
42
|
+
# count is cleared.
|
|
43
|
+
def write_entry(offset, type, blocks)
|
|
44
|
+
@bytes[offset + 2] = CLOSED | FILETYPES.fetch(type)
|
|
45
|
+
@bytes[offset + 3, 2] = blocks.first
|
|
46
|
+
@bytes[offset + 21, 9] = Array.new(9, 0)
|
|
47
|
+
@bytes[offset + 30, 2] = [blocks.length & 0xff, blocks.length >> 8]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The first free slot in the directory, which grows by a block on
|
|
51
|
+
# the directory track when every slot is taken.
|
|
52
|
+
def free_entry_offset
|
|
53
|
+
last = nil
|
|
54
|
+
each_sector(directory_track, directory_sector) do |data, track, sector|
|
|
55
|
+
slot = (0...ENTRIES_PER_SECTOR).find { |i| data[(i * ENTRY_SIZE) + 2].zero? }
|
|
56
|
+
return sector_offset(track, sector) + (slot * ENTRY_SIZE) if slot
|
|
57
|
+
|
|
58
|
+
last = [track, sector]
|
|
59
|
+
end
|
|
60
|
+
sector_offset(*extend_directory(*last))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A new directory block goes the directory interleave on from the
|
|
64
|
+
# last one. A full directory track fails as DISK FULL.
|
|
65
|
+
def extend_directory(track, sector)
|
|
66
|
+
count = sectors_in(directory_track)
|
|
67
|
+
free = (0...count).map { |i| (sector + directory_interleave + i) % count }
|
|
68
|
+
.find { |s| block_free?(directory_track, s) }
|
|
69
|
+
raise WriteError, WriteError::DISK_FULL unless free
|
|
70
|
+
|
|
71
|
+
mark(directory_track, free, false)
|
|
72
|
+
@bytes[sector_offset(track, sector), 2] = [directory_track, free]
|
|
73
|
+
put_sector(directory_track, free, [0, 0xff])
|
|
74
|
+
[directory_track, free]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
class DiskImage
|
|
6
|
+
# Writes to the image: files with their chains, directory entries and
|
|
7
|
+
# BAM, raw blocks, and BAM changes. Each write goes back to the host
|
|
8
|
+
# file at once. A write the disk refuses raises WriteError and leaves
|
|
9
|
+
# the image as it was, and so does one to a read-only image or one
|
|
10
|
+
# the host can't store, which fail as a write-protected disk does.
|
|
11
|
+
module Writing
|
|
12
|
+
# Whether the disk takes writes: the image wasn't opened read-only
|
|
13
|
+
# and the host file takes them.
|
|
14
|
+
def writable? = !read_only? && ::File.writable?(@path)
|
|
15
|
+
|
|
16
|
+
# Writes a new file, or with `replace` writes over the one of the
|
|
17
|
+
# same name in its directory entry. A name already on the disk
|
|
18
|
+
# fails as FILE EXISTS otherwise.
|
|
19
|
+
def write_file(name, bytes, type: :prg, replace: false)
|
|
20
|
+
existing = entry_named(name)
|
|
21
|
+
raise WriteError, WriteError::FILE_EXISTS if existing && !replace
|
|
22
|
+
|
|
23
|
+
update_image do
|
|
24
|
+
existing ? rewrite(existing, bytes, type) : add_file(name, bytes, type)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Adds the bytes to the end of a file, which keeps its type and its
|
|
29
|
+
# directory entry.
|
|
30
|
+
def append_file(name, bytes, type: nil)
|
|
31
|
+
entry = find_entry(name, type)
|
|
32
|
+
raise WriteError, WriteError::FILE_NOT_FOUND unless entry
|
|
33
|
+
|
|
34
|
+
update_image { rewrite(entry, read_chain(entry[:track], entry[:sector]) + bytes, entry[:type]) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Scratches the files matching the pattern, of any type, except
|
|
38
|
+
# locked ones, and frees their blocks. Returns how many it
|
|
39
|
+
# scratched.
|
|
40
|
+
def scratch(name)
|
|
41
|
+
pattern = Storage.matcher(name)
|
|
42
|
+
files = entries.select { |entry| !entry[:locked] && pattern.match?(entry[:name]) }
|
|
43
|
+
return 0 if files.empty?
|
|
44
|
+
|
|
45
|
+
update_image do
|
|
46
|
+
files.each do |entry|
|
|
47
|
+
free_chain(entry)
|
|
48
|
+
@bytes[entry[:offset] + 2] = 0
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
files.length
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Raw block access for the DOS `U2` command. A block that the error
|
|
55
|
+
# table marks bad reads cleanly once written.
|
|
56
|
+
def write_block(track, sector, data)
|
|
57
|
+
update_image { put_sector(track, sector, data) } if block?(track, sector)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Marks a block in use, as B-A does.
|
|
61
|
+
def allocate_block(track, sector) = update_image { mark(track, sector, false) }
|
|
62
|
+
|
|
63
|
+
# Marks a block free, as B-F does.
|
|
64
|
+
def free_block(track, sector) = update_image { mark(track, sector, true) }
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def update_image
|
|
69
|
+
raise WriteError, WriteError::WRITE_PROTECT_ON unless writable?
|
|
70
|
+
|
|
71
|
+
saved = [@bytes.dup, @errors&.dup]
|
|
72
|
+
begin
|
|
73
|
+
yield
|
|
74
|
+
::File.binwrite(@path, (@bytes + @errors.to_a).pack("C*"))
|
|
75
|
+
rescue WriteError, SystemCallError => e
|
|
76
|
+
@bytes, @errors = saved
|
|
77
|
+
raise e.is_a?(WriteError) ? e : WriteError.new(WriteError::WRITE_PROTECT_ON)
|
|
78
|
+
ensure
|
|
79
|
+
@entries = nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Every block links the next, and the last one holds the index of
|
|
84
|
+
# its last byte in place of a link. An empty file still takes one.
|
|
85
|
+
def write_chain(bytes)
|
|
86
|
+
chunks = bytes.each_slice(SECTOR_SIZE - 2).to_a
|
|
87
|
+
chunks = [[]] if chunks.empty?
|
|
88
|
+
blocks = allocate_chain(chunks.length)
|
|
89
|
+
blocks.each_with_index do |block, i|
|
|
90
|
+
link = blocks[i + 1] || [0, chunks[i].length + 1]
|
|
91
|
+
put_sector(*block, link + chunks[i])
|
|
92
|
+
end
|
|
93
|
+
blocks
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def free_chain(entry)
|
|
97
|
+
each_sector(entry[:track], entry[:sector]) do |_data, track, sector|
|
|
98
|
+
mark(track, sector, true)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def put_sector(track, sector, data)
|
|
103
|
+
block = data.first(SECTOR_SIZE)
|
|
104
|
+
@bytes[sector_offset(track, sector), SECTOR_SIZE] = block + Array.new(SECTOR_SIZE - block.length, 0)
|
|
105
|
+
@errors[track_offset(track) + sector] = 1 if @errors
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/storage/disk_image/bam"
|
|
4
|
+
require "badline/storage/disk_image/directory"
|
|
5
|
+
require "badline/storage/disk_image/writing"
|
|
6
|
+
|
|
3
7
|
module Badline
|
|
4
8
|
module Storage
|
|
9
|
+
# A disk image served as a drive. It reads files and blocks, and
|
|
10
|
+
# writes them back to the host file through Writing. One opened with
|
|
11
|
+
# `read_only` is a write-protected disk, and the host file is never
|
|
12
|
+
# written.
|
|
5
13
|
class DiskImage
|
|
14
|
+
include Bam
|
|
15
|
+
include Directory
|
|
16
|
+
include Writing
|
|
17
|
+
|
|
6
18
|
SECTOR_SIZE = 256
|
|
7
19
|
ENTRY_SIZE = 32
|
|
8
20
|
ENTRIES_PER_SECTOR = 8
|
|
@@ -13,11 +25,16 @@ module Badline
|
|
|
13
25
|
# the read, write and ID errors, 74 is DRIVE NOT READY.
|
|
14
26
|
DOS_ERRORS = (2..11).to_h { |code| [code, code + 18] }.merge(15 => 74).freeze
|
|
15
27
|
|
|
16
|
-
def initialize(path)
|
|
28
|
+
def initialize(path, read_only: false)
|
|
29
|
+
@path = path
|
|
30
|
+
@read_only = read_only
|
|
17
31
|
@bytes = File.binread(path).bytes
|
|
18
32
|
@errors = split_error_table
|
|
19
33
|
end
|
|
20
34
|
|
|
35
|
+
# Whether the image was opened write-protected.
|
|
36
|
+
def read_only? = @read_only
|
|
37
|
+
|
|
21
38
|
# A LOAD reads only PRG files. An OPEN names the type it wants, or
|
|
22
39
|
# takes the first file of any type with a nil `type`.
|
|
23
40
|
def read_file(name, type: :prg)
|
|
@@ -123,12 +140,16 @@ module Badline
|
|
|
123
140
|
def entries
|
|
124
141
|
@entries ||= begin
|
|
125
142
|
list = []
|
|
126
|
-
each_sector(directory_track, directory_sector)
|
|
143
|
+
each_sector(directory_track, directory_sector) do |data, track, sector|
|
|
144
|
+
list.concat(parse_entries(data, sector_offset(track, sector)))
|
|
145
|
+
end
|
|
127
146
|
list
|
|
128
147
|
end
|
|
129
148
|
end
|
|
130
149
|
|
|
131
|
-
|
|
150
|
+
# Each entry keeps the offset of its slot in the image, where a write
|
|
151
|
+
# updates it.
|
|
152
|
+
def parse_entries(data, offset)
|
|
132
153
|
(0...ENTRIES_PER_SECTOR).filter_map do |i|
|
|
133
154
|
entry = data[i * ENTRY_SIZE, ENTRY_SIZE]
|
|
134
155
|
type = FILETYPES.key(entry[2] & 0x07)
|
|
@@ -137,7 +158,9 @@ module Badline
|
|
|
137
158
|
{ name: decode_name(entry[5, 16]),
|
|
138
159
|
type:,
|
|
139
160
|
track: entry[3],
|
|
140
|
-
sector: entry[4]
|
|
161
|
+
sector: entry[4],
|
|
162
|
+
locked: entry[2].anybits?(0x40),
|
|
163
|
+
offset: offset + (i * ENTRY_SIZE) }
|
|
141
164
|
end
|
|
142
165
|
end
|
|
143
166
|
|
|
@@ -30,7 +30,8 @@ module Badline
|
|
|
30
30
|
{ error: NO_SYNC, track: 0, sector: 0, offset: 0 }
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
# Writes a .prg of any file type, over the one of the same name.
|
|
34
|
+
def write_file(name, bytes, **)
|
|
34
35
|
host_name = "#{name.downcase.tr('/', '_')}.prg"
|
|
35
36
|
File.binwrite(File.join(@path, host_name), bytes.pack("C*"))
|
|
36
37
|
true
|
data/lib/badline/storage.rb
CHANGED
|
@@ -16,6 +16,21 @@ module Badline
|
|
|
16
16
|
module Storage
|
|
17
17
|
FILE_TYPES = { "S" => :seq, "P" => :prg, "U" => :usr }.freeze
|
|
18
18
|
|
|
19
|
+
# A write the disk refuses, with the DOS error it fails with.
|
|
20
|
+
class WriteError < StandardError
|
|
21
|
+
WRITE_PROTECT_ON = 26
|
|
22
|
+
FILE_NOT_FOUND = 62
|
|
23
|
+
FILE_EXISTS = 63
|
|
24
|
+
DISK_FULL = 72
|
|
25
|
+
|
|
26
|
+
attr_reader :code
|
|
27
|
+
|
|
28
|
+
def initialize(code)
|
|
29
|
+
@code = code
|
|
30
|
+
super("DOS error #{code}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
19
34
|
class << self
|
|
20
35
|
# Folds shifted PETSCII letters to their ASCII equivalents.
|
|
21
36
|
def ascii(bytes)
|
data/lib/badline/time_of_day.rb
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
class TimeOfDay
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
# The TOD pin is fed from the AC supply. A CIA passes its region's
|
|
6
|
+
# clock and mains frequency, and these are PAL's.
|
|
7
|
+
CLOCK_HZ = Region::PAL.clock_hz
|
|
8
|
+
MAINS_HZ = Region::PAL.mains_hz
|
|
7
9
|
|
|
8
10
|
def initialize(clock_hz: CLOCK_HZ, mains_hz: MAINS_HZ)
|
|
9
11
|
# The accumulator advances mains_hz per cycle, so a TOD pin pulse has
|
data/lib/badline/version.rb
CHANGED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIA
|
|
5
|
+
# One port's pair of control lines, CA1 and CA2 or CB1 and CB2, set up
|
|
6
|
+
# by a nibble of the peripheral control register, and their two
|
|
7
|
+
# interrupt flags. Bit 0 picks the active edge of C1, rising when set.
|
|
8
|
+
# Bits 3-1 pick the C2 mode:
|
|
9
|
+
#
|
|
10
|
+
# 000 input, falling edge 100 handshake output
|
|
11
|
+
# 001 independent, falling 101 pulse output
|
|
12
|
+
# 010 input, rising edge 110 low output
|
|
13
|
+
# 011 independent, rising 111 high output
|
|
14
|
+
#
|
|
15
|
+
# A port access clears both flags, but an independent input keeps C2's.
|
|
16
|
+
# In handshake mode C2 goes low on a port access and back high on the
|
|
17
|
+
# next active C1 edge. In pulse mode it goes low for the cycle after the
|
|
18
|
+
# access. Port A handshakes on reads and writes, port B on writes only.
|
|
19
|
+
class ControlLines
|
|
20
|
+
attr_reader :control, :c1_high, :c2_high
|
|
21
|
+
|
|
22
|
+
# The level driven on C2, high while it is an input.
|
|
23
|
+
attr_reader :c2_output
|
|
24
|
+
|
|
25
|
+
def initialize(interrupts, c1_flag:, c2_flag:, handshake_on_read:)
|
|
26
|
+
@interrupts = interrupts
|
|
27
|
+
@c1_flag = c1_flag
|
|
28
|
+
@c2_flag = c2_flag
|
|
29
|
+
@handshake_on_read = handshake_on_read
|
|
30
|
+
@c1_high = @c2_high = true
|
|
31
|
+
reset!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Back to input mode with both handshake outputs released. The input
|
|
35
|
+
# levels carry over.
|
|
36
|
+
def reset!
|
|
37
|
+
@control = 0
|
|
38
|
+
@handshake_low = false
|
|
39
|
+
@pulse = 0
|
|
40
|
+
drive
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def control=(nibble)
|
|
44
|
+
@control = nibble & 0x0f
|
|
45
|
+
drive
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Only the pulse output needs the clock.
|
|
49
|
+
def pulsing? = @pulse.positive?
|
|
50
|
+
|
|
51
|
+
def cycle!
|
|
52
|
+
@pulse -= 1
|
|
53
|
+
drive if @pulse.zero?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def active_c1_edge?(high) = high == @control.anybits?(0x01)
|
|
57
|
+
|
|
58
|
+
# A new C1 level. An active edge sets the flag and ends a handshake.
|
|
59
|
+
def c1=(high)
|
|
60
|
+
return if high == @c1_high
|
|
61
|
+
|
|
62
|
+
@c1_high = high
|
|
63
|
+
return unless active_c1_edge?(high)
|
|
64
|
+
|
|
65
|
+
@handshake_low = false
|
|
66
|
+
drive
|
|
67
|
+
@interrupts.set(@c1_flag)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A new C2 level. An active edge sets the flag while C2 is an input.
|
|
71
|
+
def c2=(high)
|
|
72
|
+
return if high == @c2_high
|
|
73
|
+
|
|
74
|
+
@c2_high = high
|
|
75
|
+
@interrupts.set(@c2_flag) if @control.nobits?(0x08) && high == @control.anybits?(0x04)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A read or write of the handshaking port register.
|
|
79
|
+
def access!(write)
|
|
80
|
+
@interrupts.clear(independent? ? @c1_flag : @c1_flag | @c2_flag)
|
|
81
|
+
return unless write || @handshake_on_read
|
|
82
|
+
|
|
83
|
+
case @control >> 1
|
|
84
|
+
when 4 then @handshake_low = true
|
|
85
|
+
when 5 then @pulse = 2
|
|
86
|
+
end
|
|
87
|
+
drive
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def independent? = @control & 0x0a == 0x02
|
|
93
|
+
|
|
94
|
+
def drive
|
|
95
|
+
@c2_output = case @control >> 1
|
|
96
|
+
when 4 then !@handshake_low
|
|
97
|
+
when 5 then @pulse.zero?
|
|
98
|
+
when 6 then false
|
|
99
|
+
else true
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIA
|
|
5
|
+
# The interrupt flag and enable registers. Each source has a flag bit,
|
|
6
|
+
# and IRQ is pulled low while any flag is set with its enable bit.
|
|
7
|
+
class InterruptRegister
|
|
8
|
+
CA2 = 0x01
|
|
9
|
+
CA1 = 0x02
|
|
10
|
+
SR = 0x04
|
|
11
|
+
CB2 = 0x08
|
|
12
|
+
CB1 = 0x10
|
|
13
|
+
TIMER2 = 0x20
|
|
14
|
+
TIMER1 = 0x40
|
|
15
|
+
|
|
16
|
+
attr_reader :flags, :enable
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
reset!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset!
|
|
23
|
+
@flags = 0x00
|
|
24
|
+
@enable = 0x00
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def set(bits)
|
|
28
|
+
@flags |= bits
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def clear(bits)
|
|
32
|
+
@flags &= ~bits
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def irq? = @flags.anybits?(@enable)
|
|
36
|
+
|
|
37
|
+
# Bit 7 reads the IRQ output.
|
|
38
|
+
def read = irq? ? @flags | 0x80 : @flags
|
|
39
|
+
|
|
40
|
+
# Writing 1s clears the flags; bit 7 is ignored.
|
|
41
|
+
def write(value)
|
|
42
|
+
clear(value & 0x7f)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Bit 7 reads as 1.
|
|
46
|
+
def read_enable = @enable | 0x80
|
|
47
|
+
|
|
48
|
+
# Bit 7 picks between setting and clearing the enable bits given.
|
|
49
|
+
def write_enable(value)
|
|
50
|
+
if value.anybits?(0x80)
|
|
51
|
+
@enable |= value & 0x7f
|
|
52
|
+
else
|
|
53
|
+
@enable &= ~value & 0x7f
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|