badline 0.16.1 → 0.18.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 +213 -0
- data/CLAUDE.md +11 -7
- data/CONTRIBUTING.md +7 -1
- data/README.md +265 -26
- data/doc/media.md +5 -2
- data/doc/pinned-behaviour.md +189 -0
- data/doc/snapshot-format.md +616 -74
- data/doc/snapshots.md +192 -24
- data/exe/badline-ruby +5 -0
- data/lib/badline/audio/terminal.rb +34 -2
- data/lib/badline/c128/keyed_state.rb +2 -0
- data/lib/badline/c128.rb +1 -0
- data/lib/badline/c64/keyed_state.rb +2 -0
- data/lib/badline/c64.rb +1 -0
- data/lib/badline/cbm2/acia.rb +10 -0
- data/lib/badline/cbm2/bus.rb +263 -0
- data/lib/badline/cbm2/cpu.rb +58 -0
- data/lib/badline/cbm2/crtc_video.rb +133 -0
- data/lib/badline/cbm2/ieee_drive.rb +18 -0
- data/lib/badline/cbm2/ieee_layout.rb +69 -0
- data/lib/badline/cbm2/ieee_ports.rb +104 -0
- data/lib/badline/cbm2/ieee_traps.rb +81 -0
- data/lib/badline/cbm2/keyboard_ports.rb +60 -0
- data/lib/badline/cbm2/keyed_modules.rb +67 -0
- data/lib/badline/cbm2/keyed_state.rb +95 -0
- data/lib/badline/cbm2/model.rb +44 -0
- data/lib/badline/cbm2/p500/control_ports.rb +80 -0
- data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
- data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
- data/lib/badline/cbm2/p500/model.rb +39 -0
- data/lib/badline/cbm2/p500/pages.rb +57 -0
- data/lib/badline/cbm2/p500.rb +264 -0
- data/lib/badline/cbm2/sid_page.rb +19 -0
- data/lib/badline/cbm2.rb +270 -0
- data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
- data/lib/badline/chips/acia/keyed_state.rb +47 -0
- data/lib/badline/chips/acia.rb +307 -0
- data/lib/badline/chips/cia/timer.rb +2 -2
- data/lib/badline/chips/cpu/instruction_set.rb +1 -1
- data/lib/badline/chips/crtc.rb +3 -2
- data/lib/badline/chips/m6809/arithmetic.rb +140 -0
- data/lib/badline/chips/m6809/bus_steps.rb +223 -0
- data/lib/badline/chips/m6809/core.rb +149 -0
- data/lib/badline/chips/m6809/indexed.rb +82 -0
- data/lib/badline/chips/m6809/interrupts.rb +159 -0
- data/lib/badline/chips/m6809/keyed_state.rb +147 -0
- data/lib/badline/chips/m6809/opcodes.rb +159 -0
- data/lib/badline/chips/m6809/operations.rb +258 -0
- data/lib/badline/chips/m6809/plans.rb +143 -0
- data/lib/badline/chips/m6809/registers.rb +82 -0
- data/lib/badline/chips/m6809/stack.rb +140 -0
- data/lib/badline/chips/m6809.rb +13 -0
- data/lib/badline/chips/riot/fast_forward.rb +72 -0
- data/lib/badline/chips/riot/keyed_parts.rb +69 -0
- data/lib/badline/chips/riot/timer.rb +91 -0
- data/lib/badline/chips/riot.rb +162 -0
- data/lib/badline/chips/rriot/keyed_state.rb +30 -0
- data/lib/badline/chips/rriot.rb +134 -0
- data/lib/badline/chips/sid/filter.rb +4 -4
- data/lib/badline/chips/sid/waveform.rb +6 -6
- data/lib/badline/chips/tpi/keyed_state.rb +38 -0
- data/lib/badline/chips/tpi.rb +233 -0
- data/lib/badline/chips/vic/bank.rb +60 -5
- data/lib/badline/chips/vic/sprite.rb +4 -3
- data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
- data/lib/badline/chips/vic_i.rb +84 -21
- data/lib/badline/data_path.rb +3 -3
- data/lib/badline/drives/datasette/keyed_state.rb +9 -1
- data/lib/badline/drives/datasette.rb +27 -3
- data/lib/badline/drives/ieee_models.rb +54 -0
- data/lib/badline/drives/model1001.rb +37 -0
- data/lib/badline/drives/model2031.rb +6 -0
- data/lib/badline/drives/model4040/disk.rb +62 -0
- data/lib/badline/drives/model4040/ip_bus.rb +46 -0
- data/lib/badline/drives/model4040/keyed_state.rb +57 -0
- data/lib/badline/drives/model4040/mechanism.rb +192 -0
- data/lib/badline/drives/model4040/parts.rb +20 -0
- data/lib/badline/drives/model4040.rb +179 -0
- data/lib/badline/drives/model8050/board.rb +77 -0
- data/lib/badline/drives/model8050/disk.rb +163 -0
- data/lib/badline/drives/model8050/dos_board.rb +91 -0
- data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
- data/lib/badline/drives/model8050/idle.rb +181 -0
- data/lib/badline/drives/model8050/ieee_port.rb +147 -0
- data/lib/badline/drives/model8050/ip_bus.rb +93 -0
- data/lib/badline/drives/model8050/keyed_state.rb +163 -0
- data/lib/badline/drives/model8050/mechanism.rb +230 -0
- data/lib/badline/drives/model8050/parts.rb +37 -0
- data/lib/badline/drives/model8050.rb +131 -0
- data/lib/badline/drives/model9060/idle.rb +149 -0
- data/lib/badline/drives/model9060/keyed_state.rb +123 -0
- data/lib/badline/drives/model9060/parts.rb +98 -0
- data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
- data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
- data/lib/badline/drives/model9060/sasi_target.rb +161 -0
- data/lib/badline/drives/model9060.rb +195 -0
- data/lib/badline/frontend/app.rb +18 -3
- data/lib/badline/frontend/boot.rb +4 -3
- data/lib/badline/frontend/controls.rb +107 -15
- data/lib/badline/frontend/gamepads.rb +3 -3
- data/lib/badline/frontend/keys.rb +97 -2
- data/lib/badline/frontend/kim1_panel.rb +208 -0
- data/lib/badline/frontend/media_slots.rb +8 -1
- data/lib/badline/frontend/menu/drive_types.rb +56 -0
- data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
- data/lib/badline/frontend/menu/menu_media.rb +47 -14
- data/lib/badline/frontend/menu/menu_pages.rb +113 -4
- data/lib/badline/frontend/menu/menu_ports.rb +77 -4
- data/lib/badline/frontend/menu/pause_menu.rb +6 -2
- data/lib/badline/frontend/pet_keyboard.rb +45 -7
- data/lib/badline/frontend/screen.rb +1 -1
- data/lib/badline/frontend/screens.rb +40 -2
- data/lib/badline/frontend/teletype_pane.rb +152 -0
- data/lib/badline/frontend/timeline.rb +49 -11
- data/lib/badline/frontend.rb +3 -0
- data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
- data/lib/badline/kernal_trap/dos/status.rb +3 -0
- data/lib/badline/kernal_trap/dos.rb +18 -5
- data/lib/badline/kernal_trap/ieee.rb +4 -2
- data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
- data/lib/badline/kim1/bus.rb +117 -0
- data/lib/badline/kim1/cpu.rb +25 -0
- data/lib/badline/kim1/host_terminal.rb +74 -0
- data/lib/badline/kim1/key_entry.rb +74 -0
- data/lib/badline/kim1/keyed_modules.rb +53 -0
- data/lib/badline/kim1/keyed_state.rb +133 -0
- data/lib/badline/kim1/panel.rb +176 -0
- data/lib/badline/kim1/rriots.rb +28 -0
- data/lib/badline/kim1/sound.rb +29 -0
- data/lib/badline/kim1/tape.rb +116 -0
- data/lib/badline/kim1/teletype.rb +193 -0
- data/lib/badline/kim1.rb +190 -0
- data/lib/badline/machine/ieee_drives.rb +171 -0
- data/lib/badline/machine/keyboard_buffer.rb +20 -3
- data/lib/badline/machine/line_sound.rb +122 -0
- data/lib/badline/machine/settling.rb +4 -0
- data/lib/badline/machine.rb +62 -14
- data/lib/badline/media/cbm2_media.rb +29 -0
- data/lib/badline/media/extensions.rb +8 -4
- data/lib/badline/media/kim1_media.rb +150 -0
- data/lib/badline/media/pet_media.rb +3 -1
- data/lib/badline/media/program.rb +6 -3
- data/lib/badline/media/true_drive.rb +51 -25
- data/lib/badline/media.rb +23 -8
- data/lib/badline/options/family.rb +108 -6
- data/lib/badline/options/help.rb +58 -12
- data/lib/badline/options/table.rb +60 -14
- data/lib/badline/options/validation.rb +71 -11
- data/lib/badline/options.rb +46 -13
- data/lib/badline/pet/bus.rb +16 -2
- data/lib/badline/pet/cassettes.rb +22 -1
- data/lib/badline/pet/ieee_drive.rb +5 -64
- data/lib/badline/pet/ieee_ports.rb +7 -0
- data/lib/badline/pet/keyboard_ports.rb +9 -2
- data/lib/badline/pet/keyed_state.rb +4 -0
- data/lib/badline/pet/model.rb +11 -2
- data/lib/badline/pet/rom_sets.rb +1 -0
- data/lib/badline/pet/sound.rb +5 -105
- data/lib/badline/pet/superpet/acia.rb +12 -0
- data/lib/badline/pet/superpet/board.rb +221 -0
- data/lib/badline/pet/superpet/dongle.rb +81 -0
- data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
- data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
- data/lib/badline/pet/superpet/m6809.rb +32 -0
- data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
- data/lib/badline/pet/superpet.rb +216 -0
- data/lib/badline/pet.rb +33 -6
- data/lib/badline/plus4/acia.rb +4 -298
- data/lib/badline/plus4/keyed_parts.rb +0 -44
- data/lib/badline/plus4/keyed_state.rb +2 -0
- data/lib/badline/plus4.rb +2 -0
- data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
- data/lib/badline/roms/4040/dos-1.2.rom +0 -0
- data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
- data/lib/badline/roms/4040/dos-2.1.rom +0 -0
- data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
- data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
- data/lib/badline/roms/8050/dos-2.7.rom +0 -0
- data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
- data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
- data/lib/badline/roms/9060/dos-3.0.rom +0 -0
- data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
- data/lib/badline/roms/README +277 -0
- data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
- data/lib/badline/roms/cbm2/basic128.rom +0 -0
- data/lib/badline/roms/cbm2/basic256.rom +0 -0
- data/lib/badline/roms/cbm2/characters-600.rom +0 -0
- data/lib/badline/roms/cbm2/characters-700.rom +0 -0
- data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
- data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
- data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
- data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
- data/lib/badline/roms/kim1/6530-002.rom +0 -0
- data/lib/badline/roms/kim1/6530-003.rom +0 -0
- data/lib/badline/roms/pet/characters-superpet.rom +0 -0
- data/lib/badline/roms/pet/superpet-a000.rom +0 -0
- data/lib/badline/roms/pet/superpet-c000.rom +0 -0
- data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
- data/lib/badline/roms/pet/superpet-e000.rom +0 -0
- data/lib/badline/snapshot/cbm2_setup.rb +53 -0
- data/lib/badline/snapshot/container.rb +9 -2
- data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
- data/lib/badline/snapshot/field_doc.rb +59 -3
- data/lib/badline/snapshot/image.rb +65 -9
- data/lib/badline/snapshot/kim1_setup.rb +28 -0
- data/lib/badline/snapshot/pet_setup.rb +27 -11
- data/lib/badline/snapshot/storage_setup.rb +4 -1
- data/lib/badline/snapshot/vic20_setup.rb +11 -7
- data/lib/badline/snapshot/vice/cbm2.rb +93 -0
- data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
- data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
- data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
- data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
- data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
- data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
- data/lib/badline/snapshot/vice/cias.rb +13 -9
- data/lib/badline/snapshot/vice/p500.rb +96 -0
- data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
- data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
- data/lib/badline/snapshot/vice/pet.rb +20 -10
- data/lib/badline/snapshot/vice/pet_cpu.rb +42 -5
- data/lib/badline/snapshot/vice/pet_crtc.rb +25 -17
- data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
- data/lib/badline/snapshot/vice/pet_drive8050.rb +336 -0
- data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
- data/lib/badline/snapshot/vice/pet_restore.rb +100 -7
- data/lib/badline/snapshot/vice/pet_via.rb +6 -5
- data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
- data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
- data/lib/badline/snapshot/vice.rb +3 -0
- data/lib/badline/snapshot.rb +15 -3
- data/lib/badline/status.rb +30 -30
- data/lib/badline/storage/crt_file.rb +7 -1
- data/lib/badline/storage/d64_image.rb +7 -0
- data/lib/badline/storage/d67_image.rb +55 -0
- data/lib/badline/storage/d80_image.rb +17 -1
- data/lib/badline/storage/d81_image.rb +14 -0
- data/lib/badline/storage/d90_image.rb +180 -0
- data/lib/badline/storage/disk_image/bam.rb +18 -3
- data/lib/badline/storage/disk_image/formatting.rb +69 -0
- data/lib/badline/storage/disk_image/writing.rb +37 -2
- data/lib/badline/storage/disk_image.rb +2 -0
- data/lib/badline/storage/kim_tape.rb +254 -0
- data/lib/badline/storage/papertape.rb +90 -0
- data/lib/badline/storage.rb +5 -0
- data/lib/badline/support/cbm2_checkpoint.rb +48 -0
- data/lib/badline/support/kim1_checkpoint.rb +41 -0
- data/lib/badline/support/p500_checkpoint.rb +35 -0
- data/lib/badline/support/superpet_checkpoint.rb +35 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic20/keyed_modules.rb +6 -3
- data/lib/badline/vic20/keyed_state.rb +6 -4
- data/lib/badline/vic20.rb +26 -12
- data/lib/badline.rb +2 -0
- metadata +148 -2
|
@@ -71,17 +71,32 @@ module Badline
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# A chain's first block goes on the free track nearest the
|
|
74
|
-
# directory. Each block after it
|
|
75
|
-
#
|
|
74
|
+
# directory, from its sector 0. Each block after it looks from the
|
|
75
|
+
# interleave further round the track (#next_sector), on the same
|
|
76
|
+
# track or, once that one is full, from the same sector on the next
|
|
77
|
+
# track out.
|
|
76
78
|
def next_block(track = nil, sector = nil)
|
|
77
79
|
candidates = track ? [track, *tracks_after(track)] : allocation_order
|
|
80
|
+
start = track ? next_sector(track, sector) : 0
|
|
78
81
|
candidates.each do |candidate|
|
|
79
|
-
free = free_sector(candidate,
|
|
82
|
+
free = free_sector(candidate, start)
|
|
80
83
|
return [candidate, free] if free
|
|
81
84
|
end
|
|
82
85
|
nil
|
|
83
86
|
end
|
|
84
87
|
|
|
88
|
+
# The interleave on from the sector. Past the track's last sector
|
|
89
|
+
# it comes round again one short, but never below sector 0, as
|
|
90
|
+
# the 2031, 8050, 8250 and 1581 DOS do.
|
|
91
|
+
def next_sector(track, sector)
|
|
92
|
+
start = sector + interleave
|
|
93
|
+
count = sectors_in(track)
|
|
94
|
+
return start if start < count
|
|
95
|
+
|
|
96
|
+
start -= count
|
|
97
|
+
start.positive? ? start - 1 : start
|
|
98
|
+
end
|
|
99
|
+
|
|
85
100
|
def free_sector(track, first)
|
|
86
101
|
count = sectors_in(track)
|
|
87
102
|
(0...count).map { |i| (first + i) % count }.find { |s| block_free?(track, s) }
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
class DiskImage
|
|
6
|
+
# The DOS's N command. With an ID it formats the disk: every block of
|
|
7
|
+
# the tracks the BAM covers is cleared, and the header, the BAM and an
|
|
8
|
+
# empty directory are written. Without one it only clears the disk:
|
|
9
|
+
# the header takes the new name and keeps its ID and whatever else it
|
|
10
|
+
# held, the BAM marks every block free, and the directory's first
|
|
11
|
+
# block is emptied. Either way the header, the BAM's blocks and the
|
|
12
|
+
# first directory block stay in use. Each format says where these go.
|
|
13
|
+
module Formatting
|
|
14
|
+
# The disk's ID, as the header holds it.
|
|
15
|
+
def disk_id = sector_at(*header_block)[header_name + Catalog::HEADER_ID, 2]
|
|
16
|
+
|
|
17
|
+
# Formats the disk under the name, or with no ID clears it.
|
|
18
|
+
def format(name, id = nil)
|
|
19
|
+
update_image do
|
|
20
|
+
full = !id.nil?
|
|
21
|
+
put_sector(*header_block, []) if full
|
|
22
|
+
id = (id || disk_id).first(2)
|
|
23
|
+
write_header(name, id)
|
|
24
|
+
clear_blocks if full
|
|
25
|
+
headers = bam_headers(id)
|
|
26
|
+
headers.each { |track, sector, bytes| write_at(track, sector, 0, bytes) }
|
|
27
|
+
put_sector(directory_track, directory_sector, [0, 0xff])
|
|
28
|
+
bam_tracks.each { |track| free_track(track) }
|
|
29
|
+
[header_block, *headers.map { |track, sector, _| [track, sector] }, [directory_track, directory_sector]]
|
|
30
|
+
.each { |block| mark(*block, false) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Every block the BAM covers but the header.
|
|
37
|
+
def clear_blocks
|
|
38
|
+
bam_tracks.each do |track|
|
|
39
|
+
(0...sectors_in(track)).each do |sector|
|
|
40
|
+
put_sector(track, sector, []) unless [track, sector] == header_block
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The link to the next block, the format's letter, the name, the ID
|
|
46
|
+
# and the DOS type, each field after the name set off with shifted
|
|
47
|
+
# spaces.
|
|
48
|
+
def write_header(name, id)
|
|
49
|
+
write_at(*header_block, 0, [*header_link, dos_type.getbyte(1), 0])
|
|
50
|
+
write_at(*header_block, header_name,
|
|
51
|
+
encode_name(name) + [NAME_PADDING, NAME_PADDING] + id + [NAME_PADDING] +
|
|
52
|
+
dos_type.bytes + Array.new(header_tail, NAME_PADDING))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A free track counts its sectors and sets a bit for each.
|
|
56
|
+
def free_track(track)
|
|
57
|
+
count = sectors_in(track)
|
|
58
|
+
bits = (1 << count) - 1
|
|
59
|
+
@bytes[count_offset(track)] = count
|
|
60
|
+
bitmap_size.times { |i| @bytes[bitmap_offset(track) + i] = (bits >> (8 * i)) & 0xff }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def write_at(track, sector, offset, bytes)
|
|
64
|
+
@bytes[sector_offset(track, sector) + offset, bytes.length] = bytes
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -51,6 +51,39 @@ module Badline
|
|
|
51
51
|
files.length
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Gives a file a new name in its directory entry. A missing file
|
|
55
|
+
# fails as FILE NOT FOUND, and a new name already on the disk as
|
|
56
|
+
# FILE EXISTS.
|
|
57
|
+
def rename(old_name, new_name)
|
|
58
|
+
entry = existing_entry(old_name)
|
|
59
|
+
raise WriteError, WriteError::FILE_EXISTS if entry_named(new_name)
|
|
60
|
+
|
|
61
|
+
update_image { @bytes[entry[:offset] + 5, Directory::NAME_LENGTH] = encode_name(new_name) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Writes a new file holding the files named, one after another, as
|
|
65
|
+
# the type of the first. A missing file fails as FILE NOT FOUND, and
|
|
66
|
+
# a new name already on the disk as FILE EXISTS.
|
|
67
|
+
def copy(new_name, names)
|
|
68
|
+
sources = names.map { |name| existing_entry(name) }
|
|
69
|
+
raise WriteError, WriteError::FILE_EXISTS if entry_named(new_name)
|
|
70
|
+
|
|
71
|
+
bytes = sources.flat_map { |entry| read_chain(entry[:track], entry[:sector]) }
|
|
72
|
+
update_image { add_file(new_name, bytes, sources.first[:type]) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Adds the files named to the end of a SEQ file. A missing file fails
|
|
76
|
+
# as FILE NOT FOUND, and a file of another type as FILE TYPE
|
|
77
|
+
# MISMATCH.
|
|
78
|
+
def concat(name, names)
|
|
79
|
+
entry = existing_entry(name)
|
|
80
|
+
sources = names.map { |source| existing_entry(source) }
|
|
81
|
+
raise WriteError, WriteError::FILE_TYPE_MISMATCH unless entry[:type] == :seq
|
|
82
|
+
|
|
83
|
+
bytes = [entry, *sources].flat_map { |source| read_chain(source[:track], source[:sector]) }
|
|
84
|
+
update_image { rewrite(entry, bytes, :seq) }
|
|
85
|
+
end
|
|
86
|
+
|
|
54
87
|
# Raw block access for the DOS `U2` command. A block that the error
|
|
55
88
|
# table marks bad reads cleanly once written.
|
|
56
89
|
def write_block(track, sector, data)
|
|
@@ -81,6 +114,8 @@ module Badline
|
|
|
81
114
|
|
|
82
115
|
private
|
|
83
116
|
|
|
117
|
+
def existing_entry(name) = entry_named(name) || raise(WriteError, WriteError::FILE_NOT_FOUND)
|
|
118
|
+
|
|
84
119
|
def update_image
|
|
85
120
|
raise WriteError, WriteError::WRITE_PROTECT_ON unless writable?
|
|
86
121
|
|
|
@@ -116,8 +151,8 @@ module Badline
|
|
|
116
151
|
end
|
|
117
152
|
|
|
118
153
|
def put_sector(track, sector, data)
|
|
119
|
-
|
|
120
|
-
@bytes[
|
|
154
|
+
offset = sector_offset(track, sector)
|
|
155
|
+
SECTOR_SIZE.times { |i| @bytes[offset + i] = data[i] || 0 }
|
|
121
156
|
@errors[track_offset(track) + sector] = 1 if @errors
|
|
122
157
|
end
|
|
123
158
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "badline/storage/disk_image/bam"
|
|
4
4
|
require "badline/storage/disk_image/catalog"
|
|
5
5
|
require "badline/storage/disk_image/directory"
|
|
6
|
+
require "badline/storage/disk_image/formatting"
|
|
6
7
|
require "badline/storage/disk_image/writing"
|
|
7
8
|
|
|
8
9
|
module Badline
|
|
@@ -15,6 +16,7 @@ module Badline
|
|
|
15
16
|
include Bam
|
|
16
17
|
include Catalog
|
|
17
18
|
include Directory
|
|
19
|
+
include Formatting
|
|
18
20
|
include Writing
|
|
19
21
|
|
|
20
22
|
SECTOR_SIZE = 256
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
# A KIM-1 cassette, held as the times its signal crosses zero, in
|
|
6
|
+
# microseconds from the start of the tape, which are the KIM-1's
|
|
7
|
+
# cycles. The KIM-1 writes the tape as a square wave on U2's PB7, each
|
|
8
|
+
# bit 3,700 Hz then 2,400 Hz, and reads it back through a 565 PLL and a
|
|
9
|
+
# 311 comparator, which hold PB7 high through the 2,400 Hz tone and low
|
|
10
|
+
# through the 3,700 Hz one (User Manual, appendix E). edges gives the
|
|
11
|
+
# times PB7 changes as the tape plays.
|
|
12
|
+
#
|
|
13
|
+
# A tape reads from a .wav file, 8-bit or 16-bit PCM, its first
|
|
14
|
+
# channel only, and writes one back as a square wave.
|
|
15
|
+
class KIMTape
|
|
16
|
+
class FormatError < StandardError; end
|
|
17
|
+
|
|
18
|
+
# A period longer than this many microseconds is the 2,400 Hz tone:
|
|
19
|
+
# the split between the tones, at the geometric mean of the two.
|
|
20
|
+
SPLIT = 336
|
|
21
|
+
|
|
22
|
+
# The time constants, in seconds, of the detector's DC level and of
|
|
23
|
+
# its envelope, which sets the hysteresis.
|
|
24
|
+
DC_TIME = 0.002
|
|
25
|
+
ENVELOPE_TIME = 0.005
|
|
26
|
+
|
|
27
|
+
# The signal crosses the DC level once it swings this fraction of the
|
|
28
|
+
# envelope past it, and not for an envelope smaller than SILENCE, of
|
|
29
|
+
# full scale.
|
|
30
|
+
HYSTERESIS = 0.3
|
|
31
|
+
SILENCE = 0.03
|
|
32
|
+
|
|
33
|
+
# A gap this many microseconds long between crossings plays as
|
|
34
|
+
# silence in the .wav a tape writes.
|
|
35
|
+
GAP = 2_000
|
|
36
|
+
|
|
37
|
+
# The file the tape reads from and writes back to, or "" for none.
|
|
38
|
+
attr_reader :path, :crossings
|
|
39
|
+
|
|
40
|
+
# The tape in the .wav file at `path`.
|
|
41
|
+
def self.read(path) = new(path, crossings: from_wav(File.binread(path).bytes))
|
|
42
|
+
|
|
43
|
+
# A blank tape at `path`, written to the host file.
|
|
44
|
+
def self.create(path)
|
|
45
|
+
tape = new(path)
|
|
46
|
+
tape.save
|
|
47
|
+
tape
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The zero crossings of the .wav file in `bytes`.
|
|
51
|
+
def self.from_wav(bytes)
|
|
52
|
+
raise FormatError, "Not a RIFF WAVE file" unless bytes[0, 4] == "RIFF".bytes && bytes[8, 4] == "WAVE".bytes
|
|
53
|
+
|
|
54
|
+
format = nil
|
|
55
|
+
pos = 12
|
|
56
|
+
while pos + 8 <= bytes.length
|
|
57
|
+
id = bytes[pos, 4].pack("C*")
|
|
58
|
+
size = little_endian(bytes, pos + 4, 4)
|
|
59
|
+
format = bytes[pos + 8, 16] if id == "fmt "
|
|
60
|
+
return Detector.new(Format.new(format)).crossings(bytes, pos + 8, size) if id == "data" && format
|
|
61
|
+
|
|
62
|
+
pos += 8 + size + (size & 1)
|
|
63
|
+
end
|
|
64
|
+
raise FormatError, "No PCM data in the WAVE file"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.little_endian(bytes, pos, count)
|
|
68
|
+
value = 0
|
|
69
|
+
count.times { |i| value |= bytes[pos + i].to_i << (8 * i) }
|
|
70
|
+
value
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def initialize(path = "", crossings: [])
|
|
74
|
+
@path = path
|
|
75
|
+
@crossings = crossings
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The time of the last crossing.
|
|
79
|
+
def duration = @crossings.empty? ? 0 : @crossings.last
|
|
80
|
+
|
|
81
|
+
# Erases the tape from `time` on, as a deck recording from there
|
|
82
|
+
# does.
|
|
83
|
+
def erase_from(time)
|
|
84
|
+
@crossings.pop while !@crossings.empty? && @crossings.last >= time
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Records a crossing at `time`, after the last one.
|
|
88
|
+
def record(time)
|
|
89
|
+
@crossings << time
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The times PB7 changes as the tape plays, from high at its start: to
|
|
93
|
+
# low where a period is the 3,700 Hz tone and back to high where one
|
|
94
|
+
# is the 2,400 Hz tone. Each period runs over two crossings, and two
|
|
95
|
+
# in a row must agree before PB7 follows, so that one crossing out of
|
|
96
|
+
# place doesn't split a bit. The period that straddles the change
|
|
97
|
+
# lies between the tones, so PB7 changes half way through it.
|
|
98
|
+
def edges
|
|
99
|
+
crossings = @crossings
|
|
100
|
+
edges = []
|
|
101
|
+
high = true
|
|
102
|
+
disagree = 0
|
|
103
|
+
i = 2
|
|
104
|
+
while i < crossings.length
|
|
105
|
+
low_tone = crossings[i] - crossings[i - 2] > SPLIT
|
|
106
|
+
if low_tone == high
|
|
107
|
+
disagree = 0
|
|
108
|
+
else
|
|
109
|
+
disagree += 1
|
|
110
|
+
if disagree == 2
|
|
111
|
+
high = low_tone
|
|
112
|
+
edges << ((crossings[i - 3] + crossings[i - 2]) / 2)
|
|
113
|
+
disagree = 0
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
i += 1
|
|
117
|
+
end
|
|
118
|
+
edges
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Writes the tape back to its host file.
|
|
122
|
+
def save
|
|
123
|
+
write_wav(@path)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Writes the tape to `path` as a .wav file, a square wave of `bits`
|
|
127
|
+
# (8 or 16) at `rate` samples a second.
|
|
128
|
+
def write_wav(path, rate: 44_100, bits: 8)
|
|
129
|
+
samples = square_wave(rate, bits == 8 ? 96 : 24_000)
|
|
130
|
+
bytes = bits == 8 ? samples.map { |sample| sample + 128 } : samples.flat_map { |s| [s & 0xff, (s >> 8) & 0xff] }
|
|
131
|
+
File.binwrite(path, (wav_header(rate, bits, bytes.length) + bytes).pack("C*"))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
# The tape as samples of `amplitude`: silent up to the first
|
|
137
|
+
# crossing, after a GAP and at the end, and otherwise each crossing
|
|
138
|
+
# turning the wave over.
|
|
139
|
+
def square_wave(rate, amplitude)
|
|
140
|
+
samples = []
|
|
141
|
+
crossings = @crossings
|
|
142
|
+
count = crossings.empty? ? 0 : (((crossings.last + GAP) * rate) / 1_000_000) + 1
|
|
143
|
+
index = 0
|
|
144
|
+
level = -amplitude
|
|
145
|
+
count.times do |n|
|
|
146
|
+
time = (n * 1_000_000) / rate
|
|
147
|
+
while index < crossings.length && crossings[index] <= time
|
|
148
|
+
level = -level
|
|
149
|
+
index += 1
|
|
150
|
+
end
|
|
151
|
+
samples << (index.zero? || time - crossings[index - 1] > GAP ? 0 : level)
|
|
152
|
+
end
|
|
153
|
+
samples
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def wav_header(rate, bits, size)
|
|
157
|
+
bytes_per_sample = bits / 8
|
|
158
|
+
"RIFF".bytes + le(36 + size, 4) + "WAVEfmt ".bytes + le(16, 4) + le(1, 2) + le(1, 2) +
|
|
159
|
+
le(rate, 4) + le(rate * bytes_per_sample, 4) + le(bytes_per_sample, 2) + le(bits, 2) +
|
|
160
|
+
"data".bytes + le(size, 4)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def le(value, count) = Array.new(count) { |i| (value >> (8 * i)) & 0xff }
|
|
164
|
+
|
|
165
|
+
# A WAVE file's fmt chunk: PCM, 8-bit unsigned or 16-bit signed.
|
|
166
|
+
class Format
|
|
167
|
+
attr_reader :channels, :rate, :bits
|
|
168
|
+
|
|
169
|
+
def initialize(chunk)
|
|
170
|
+
tag = KIMTape.little_endian(chunk, 0, 2)
|
|
171
|
+
@channels = KIMTape.little_endian(chunk, 2, 2)
|
|
172
|
+
@rate = KIMTape.little_endian(chunk, 4, 4)
|
|
173
|
+
@bits = KIMTape.little_endian(chunk, 14, 2)
|
|
174
|
+
return if [1, 0xfffe].include?(tag) && [8, 16].include?(@bits) && @channels.positive?
|
|
175
|
+
|
|
176
|
+
raise FormatError, "Only 8-bit and 16-bit PCM WAVE files are supported"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def frame_size = @channels * @bits / 8
|
|
180
|
+
|
|
181
|
+
# The first channel's sample at `pos`, from -1 to 1.
|
|
182
|
+
def level_at(bytes, pos)
|
|
183
|
+
return (bytes[pos] - 128) / 128.0 if @bits == 8
|
|
184
|
+
|
|
185
|
+
value = bytes[pos] | (bytes[pos + 1] << 8)
|
|
186
|
+
(value >= 0x8000 ? value - 0x10000 : value) / 32_768.0
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Finds where a recording crosses zero. A cassette's signal wanders
|
|
191
|
+
# off its centre and fades, so the detector follows its DC level and
|
|
192
|
+
# its envelope, and counts a crossing only once the signal swings past
|
|
193
|
+
# the DC level by HYSTERESIS of the envelope, timing it where it
|
|
194
|
+
# crossed the DC level, between two samples.
|
|
195
|
+
class Detector
|
|
196
|
+
def initialize(format)
|
|
197
|
+
@format = format
|
|
198
|
+
rate = format.rate
|
|
199
|
+
@dc_step = 1.0 / (rate * DC_TIME)
|
|
200
|
+
@decay = Math.exp(-1.0 / (rate * ENVELOPE_TIME))
|
|
201
|
+
@scale = 1_000_000.0 / rate
|
|
202
|
+
@dc = @envelope = @previous = @rose = @fell = 0.0
|
|
203
|
+
@high = false
|
|
204
|
+
@found = []
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# The crossings, in microseconds, of the samples in `bytes` from
|
|
208
|
+
# `pos`, `size` bytes of them.
|
|
209
|
+
def crossings(bytes, pos, size)
|
|
210
|
+
format = @format
|
|
211
|
+
frame = format.frame_size
|
|
212
|
+
last = [pos + size, bytes.length].min - frame
|
|
213
|
+
n = 0
|
|
214
|
+
while pos <= last
|
|
215
|
+
follow(format.level_at(bytes, pos), n)
|
|
216
|
+
pos += frame
|
|
217
|
+
n += 1
|
|
218
|
+
end
|
|
219
|
+
@found
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private
|
|
223
|
+
|
|
224
|
+
# Takes the sample `sample`, the `index`th.
|
|
225
|
+
def follow(sample, index)
|
|
226
|
+
@dc += (sample - @dc) * @dc_step
|
|
227
|
+
level = sample - @dc
|
|
228
|
+
@envelope *= @decay
|
|
229
|
+
@envelope = level.abs if level.abs > @envelope
|
|
230
|
+
previous = @previous
|
|
231
|
+
if previous <= 0 && level.positive?
|
|
232
|
+
@rose = index - 1 + (previous / (previous - level))
|
|
233
|
+
elsif previous >= 0 && level.negative?
|
|
234
|
+
@fell = index - 1 + (previous / (previous - level))
|
|
235
|
+
end
|
|
236
|
+
@previous = level
|
|
237
|
+
swing(level) if @envelope > SILENCE
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Turns over at a `level` far enough past the DC level.
|
|
241
|
+
def swing(level)
|
|
242
|
+
swing = @envelope * HYSTERESIS
|
|
243
|
+
if !@high && level > swing
|
|
244
|
+
@high = true
|
|
245
|
+
@found << (@rose * @scale).round
|
|
246
|
+
elsif @high && level < -swing
|
|
247
|
+
@high = false
|
|
248
|
+
@found << (@fell * @scale).round
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
# A MOS papertape, the .ptp or .pap the KIM-1 monitor's Q command
|
|
6
|
+
# punches and its L command reads (User Manual, appendix F). Each
|
|
7
|
+
# record is a semicolon, then in hex its count of data bytes, its
|
|
8
|
+
# address, high byte first, the bytes and a 16-bit checksum, the sum
|
|
9
|
+
# of every byte before it, high byte first. The last record has a
|
|
10
|
+
# count of 0, the number of records before it where the address goes,
|
|
11
|
+
# and its checksum, which the monitor checks without counting the
|
|
12
|
+
# records. What comes between records, a line's end, NULs
|
|
13
|
+
# and the XOFF that ends a dump, the monitor skips, as this does.
|
|
14
|
+
class Papertape
|
|
15
|
+
class FormatError < StandardError; end
|
|
16
|
+
|
|
17
|
+
# One record's data: its bytes and where they go.
|
|
18
|
+
class Record
|
|
19
|
+
attr_reader :address, :bytes
|
|
20
|
+
|
|
21
|
+
def initialize(address, bytes)
|
|
22
|
+
@address = address
|
|
23
|
+
@bytes = bytes
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
HEX = "0123456789ABCDEFabcdef"
|
|
28
|
+
|
|
29
|
+
# The Records, in the tape's order.
|
|
30
|
+
attr_reader :records
|
|
31
|
+
|
|
32
|
+
# The tape's text from its start to the last record's end, which a
|
|
33
|
+
# teletype sends for the L command.
|
|
34
|
+
attr_reader :text
|
|
35
|
+
|
|
36
|
+
def self.read(path) = new(File.binread(path))
|
|
37
|
+
|
|
38
|
+
def initialize(text)
|
|
39
|
+
@records = []
|
|
40
|
+
@text = text.b
|
|
41
|
+
@position = 0
|
|
42
|
+
read_records
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Where the first record goes.
|
|
46
|
+
def start_address = @records.first.address
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def read_records
|
|
51
|
+
loop do
|
|
52
|
+
start = @text.index(";", @position)
|
|
53
|
+
raise FormatError, "no last record" if start.nil?
|
|
54
|
+
|
|
55
|
+
@position = start + 1
|
|
56
|
+
count = byte
|
|
57
|
+
address = word
|
|
58
|
+
data = Array.new(count) { byte }
|
|
59
|
+
check(count, address, data)
|
|
60
|
+
return finish if count.zero?
|
|
61
|
+
|
|
62
|
+
@records << Record.new(address, data)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def check(count, address, data)
|
|
67
|
+
sum = (count + (address >> 8) + (address & 0xff) + data.sum) & 0xffff
|
|
68
|
+
raise FormatError, "bad checksum in the record at $#{address.to_s(16).rjust(4, '0')}" unless word == sum
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def finish
|
|
72
|
+
raise FormatError, "no records" if @records.empty?
|
|
73
|
+
|
|
74
|
+
@text = @text[0, @position]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def byte
|
|
78
|
+
digits = @text[@position, 2].to_s
|
|
79
|
+
unless digits.size == 2 && HEX.include?(digits[0]) && HEX.include?(digits[1])
|
|
80
|
+
raise FormatError, "not a hex byte at offset #{@position}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
@position += 2
|
|
84
|
+
digits.to_i(16)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def word = (byte << 8) | byte
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/badline/storage.rb
CHANGED
|
@@ -5,13 +5,17 @@ require "badline/storage/p00"
|
|
|
5
5
|
require "badline/storage/host_directory"
|
|
6
6
|
require "badline/storage/disk_image"
|
|
7
7
|
require "badline/storage/d64_image"
|
|
8
|
+
require "badline/storage/d67_image"
|
|
8
9
|
require "badline/storage/d71_image"
|
|
9
10
|
require "badline/storage/d81_image"
|
|
10
11
|
require "badline/storage/d80_image"
|
|
11
12
|
require "badline/storage/d82_image"
|
|
13
|
+
require "badline/storage/d90_image"
|
|
12
14
|
require "badline/storage/g64_image"
|
|
13
15
|
require "badline/storage/t64"
|
|
14
16
|
require "badline/storage/tap"
|
|
17
|
+
require "badline/storage/papertape"
|
|
18
|
+
require "badline/storage/kim_tape"
|
|
15
19
|
require "badline/storage/crt_file"
|
|
16
20
|
require "badline/storage/unavailable"
|
|
17
21
|
|
|
@@ -24,6 +28,7 @@ module Badline
|
|
|
24
28
|
WRITE_PROTECT_ON = 26
|
|
25
29
|
FILE_NOT_FOUND = 62
|
|
26
30
|
FILE_EXISTS = 63
|
|
31
|
+
FILE_TYPE_MISMATCH = 64
|
|
27
32
|
DISK_FULL = 72
|
|
28
33
|
|
|
29
34
|
attr_reader :code
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Support
|
|
5
|
+
# A Checkpoint of a CBM-II: its CPU with both segment registers, the
|
|
6
|
+
# 2K of segment 15's RAM, the RAM board's segments, the screen RAM, the
|
|
7
|
+
# picture, the CRTC's registers and counters, the CIA, the ACIA's
|
|
8
|
+
# registers and both TPIs.
|
|
9
|
+
module CBM2Checkpoint
|
|
10
|
+
COMPONENTS = %w[cpu ram segments screen display crtc cia acia tpi1 tpi2].freeze
|
|
11
|
+
|
|
12
|
+
def self.take(machine)
|
|
13
|
+
bus = machine.bus
|
|
14
|
+
Checkpoint.new(machine.cycles, [
|
|
15
|
+
Checkpoint.fnv1a(Checkpoint.cpu_state(machine.cpu) +
|
|
16
|
+
[bus.execution_segment, bus.indirect_segment]),
|
|
17
|
+
Checkpoint.fnv1a(Checkpoint.ram(bus.ram, 0x800)),
|
|
18
|
+
Checkpoint.fnv1a(segments(bus)),
|
|
19
|
+
Checkpoint.fnv1a(Array.new(0x800) { |offset| bus.screen.peek(0xd000 + offset) }),
|
|
20
|
+
Checkpoint.fnv1a(machine.video.display),
|
|
21
|
+
Checkpoint.fnv1a(crtc_state(machine.crtc)),
|
|
22
|
+
Checkpoint.fnv1a(Checkpoint.cia_state(machine.cia)),
|
|
23
|
+
Checkpoint.fnv1a(acia_state(machine.acia)),
|
|
24
|
+
Checkpoint.fnv1a(tpi_state(machine.tpi1)),
|
|
25
|
+
Checkpoint.fnv1a(tpi_state(machine.tpi2))
|
|
26
|
+
], COMPONENTS)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.parse(line) = Checkpoint.parse(line, COMPONENTS)
|
|
30
|
+
|
|
31
|
+
# Every byte of the segments the RAM board fills, segment by segment.
|
|
32
|
+
def self.segments(bus)
|
|
33
|
+
bytes = []
|
|
34
|
+
bus.segment_ram.each { |memory| bytes.concat(Checkpoint.ram(memory, 0x10000)) if memory }
|
|
35
|
+
bytes
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.crtc_state(crtc) = crtc.registers + [crtc.column, crtc.line, crtc.row]
|
|
39
|
+
|
|
40
|
+
def self.acia_state(acia) = [acia.command, acia.control, acia.status]
|
|
41
|
+
|
|
42
|
+
def self.tpi_state(tpi)
|
|
43
|
+
[tpi.port_a_output, tpi.port_b_output, tpi.port_c_output, tpi.latches, tpi.mask, tpi.control,
|
|
44
|
+
tpi.irq? ? 1 : 0, *tpi.stack]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Support
|
|
5
|
+
# A Checkpoint of a KIM-1: its CPU, the 1K of RAM, each 6530's RAM,
|
|
6
|
+
# ports and timer, the panel's held key and what its digits show, and
|
|
7
|
+
# what the teletype has printed and its line's level.
|
|
8
|
+
module KIM1Checkpoint
|
|
9
|
+
COMPONENTS = %w[cpu ram u2 u3 panel tty].freeze
|
|
10
|
+
|
|
11
|
+
def self.take(machine)
|
|
12
|
+
ram = machine.ram
|
|
13
|
+
Checkpoint.new(machine.cycles, [
|
|
14
|
+
Checkpoint.fnv1a(Checkpoint.cpu_state(machine.cpu)),
|
|
15
|
+
Checkpoint.fnv1a(Checkpoint.ram(ram, ram.length)),
|
|
16
|
+
Checkpoint.fnv1a(rriot_state(machine.u2)),
|
|
17
|
+
Checkpoint.fnv1a(rriot_state(machine.u3)),
|
|
18
|
+
Checkpoint.fnv1a(panel_state(machine.panel)),
|
|
19
|
+
Checkpoint.fnv1a(teletype_state(machine.teletype))
|
|
20
|
+
], COMPONENTS)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.parse(line) = Checkpoint.parse(line, COMPONENTS)
|
|
24
|
+
|
|
25
|
+
def self.rriot_state(rriot) = rriot.idle_state.map { |value| bit(value) } + [rriot.timer.counter]
|
|
26
|
+
|
|
27
|
+
def self.panel_state(panel)
|
|
28
|
+
held = panel.held
|
|
29
|
+
[held.nil? ? -1 : KIM1::Panel::KEYS.index(held), *panel.display]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.teletype_state(teletype) = teletype.output.bytes + [teletype.line_high? ? 1 : 0]
|
|
33
|
+
|
|
34
|
+
def self.bit(value)
|
|
35
|
+
return value if value.is_a?(Integer)
|
|
36
|
+
|
|
37
|
+
value ? 1 : 0
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Support
|
|
5
|
+
# A Checkpoint of a P500: its CPU with both segment registers, the 2K
|
|
6
|
+
# of segment 15's RAM, segments 0 and 1, the static video matrix, the
|
|
7
|
+
# colour RAM's nibbles, the picture, the VIC's registers and beam, the
|
|
8
|
+
# SID's registers, the CIA, the ACIA's registers and both TPIs, the
|
|
9
|
+
# last three as CBM2Checkpoint digests them.
|
|
10
|
+
module P500Checkpoint
|
|
11
|
+
COMPONENTS = %w[cpu ram segments screen color_ram display vic sid cia acia tpi1 tpi2].freeze
|
|
12
|
+
|
|
13
|
+
def self.take(machine)
|
|
14
|
+
bus = machine.bus
|
|
15
|
+
Checkpoint.new(machine.cycles, [
|
|
16
|
+
Checkpoint.fnv1a(Checkpoint.cpu_state(machine.cpu) +
|
|
17
|
+
[bus.execution_segment, bus.indirect_segment]),
|
|
18
|
+
Checkpoint.fnv1a(Checkpoint.ram(bus.ram, 0x800)),
|
|
19
|
+
Checkpoint.fnv1a(CBM2Checkpoint.segments(bus)),
|
|
20
|
+
Checkpoint.fnv1a(Array.new(0x400) { |offset| bus.screen.peek(0xd000 + offset) }),
|
|
21
|
+
Checkpoint.fnv1a(Checkpoint.color_ram(machine.color_ram, 0xd400)),
|
|
22
|
+
Checkpoint.fnv1a(machine.vic.display),
|
|
23
|
+
Checkpoint.fnv1a(Checkpoint.vic_state(machine.vic)),
|
|
24
|
+
Checkpoint.fnv1a(Array.new(0x20) { |reg| machine.sid.register(reg) }),
|
|
25
|
+
Checkpoint.fnv1a(Checkpoint.cia_state(machine.cia)),
|
|
26
|
+
Checkpoint.fnv1a(CBM2Checkpoint.acia_state(machine.acia)),
|
|
27
|
+
Checkpoint.fnv1a(CBM2Checkpoint.tpi_state(machine.tpi1)),
|
|
28
|
+
Checkpoint.fnv1a(CBM2Checkpoint.tpi_state(machine.tpi2))
|
|
29
|
+
], COMPONENTS)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.parse(line) = Checkpoint.parse(line, COMPONENTS)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|