badline 0.5.0 → 0.6.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 +24 -0
- data/CLAUDE.md +23 -23
- data/CONTRIBUTING.md +5 -0
- data/README.md +59 -1
- data/doc/pinned-behaviour.md +4 -2
- data/exe/badline-ruby +2 -1
- data/lib/badline/address_bus/saved_state.rb +72 -0
- data/lib/badline/address_bus.rb +1 -0
- data/lib/badline/audio/stream.rb +6 -0
- data/lib/badline/cartridge/action_replay.rb +15 -0
- data/lib/badline/cartridge/comal80.rb +8 -0
- data/lib/badline/cartridge/dinamic.rb +8 -0
- data/lib/badline/cartridge/easy_flash.rb +20 -1
- data/lib/badline/cartridge/epyx_fastload.rb +8 -0
- data/lib/badline/cartridge/final_cartridge3.rb +12 -0
- data/lib/badline/cartridge/flash.rb +41 -0
- data/lib/badline/cartridge/fun_play.rb +8 -0
- data/lib/badline/cartridge/g_mod2.rb +12 -0
- data/lib/badline/cartridge/game_system.rb +8 -0
- data/lib/badline/cartridge/geo_ram.rb +16 -0
- data/lib/badline/cartridge/magic_desk.rb +8 -0
- data/lib/badline/cartridge/ocean.rb +8 -0
- data/lib/badline/cartridge/pagefox.rb +12 -0
- data/lib/badline/cartridge/retro_replay.rb +44 -1
- data/lib/badline/cartridge/rgcd.rb +10 -0
- data/lib/badline/cartridge/super_games.rb +10 -0
- data/lib/badline/cartridge/zaxxon.rb +16 -0
- data/lib/badline/cartridge.rb +72 -0
- data/lib/badline/cia/interrupt_register.rb +17 -0
- data/lib/badline/cia/serial.rb +44 -0
- data/lib/badline/cia/state.rb +47 -0
- data/lib/badline/cia/timer.rb +33 -0
- data/lib/badline/cia.rb +24 -2
- data/lib/badline/computer/attachments.rb +132 -0
- data/lib/badline/computer/saved_state.rb +116 -0
- data/lib/badline/computer.rb +8 -31
- data/lib/badline/cpu/microcode.rb +8 -0
- data/lib/badline/cpu/saved_state.rb +115 -0
- data/lib/badline/cpu.rb +2 -0
- data/lib/badline/datasette.rb +27 -0
- data/lib/badline/drive1541/bus.rb +89 -13
- data/lib/badline/drive1541/disk.rb +7 -2
- data/lib/badline/drive1541/disk_state.rb +80 -0
- data/lib/badline/drive1541/idle.rb +9 -57
- data/lib/badline/drive1541/mechanism.rb +2 -0
- data/lib/badline/drive1541/mechanism_state.rb +67 -0
- data/lib/badline/drive1541/orbit.rb +81 -0
- data/lib/badline/drive1541/rotation.rb +10 -0
- data/lib/badline/drive1541/serial_port.rb +10 -0
- data/lib/badline/drive1541/sleep.rb +114 -0
- data/lib/badline/drive1541.rb +52 -2
- data/lib/badline/gui/application.rb +41 -14
- data/lib/badline/gui/gamepads.rb +7 -0
- data/lib/badline/gui/snapshots.rb +67 -0
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/iec_bus.rb +16 -0
- data/lib/badline/kernal_trap/channel.rb +27 -0
- data/lib/badline/kernal_trap/drive/channels.rb +25 -0
- data/lib/badline/kernal_trap/drive/memory.rb +9 -0
- data/lib/badline/kernal_trap/drive/status.rb +8 -0
- data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
- data/lib/badline/kernal_trap/drive.rb +19 -0
- data/lib/badline/kernal_trap/save.rb +8 -0
- data/lib/badline/kernal_trap/serial.rb +16 -0
- data/lib/badline/memory.rb +11 -0
- data/lib/badline/options.rb +3 -2
- data/lib/badline/ram_expansion/plus256k.rb +16 -0
- data/lib/badline/ram_expansion/plus60k.rb +12 -0
- data/lib/badline/ram_expansion/unexpanded.rb +6 -0
- data/lib/badline/reu/dma.rb +36 -0
- data/lib/badline/reu/ram.rb +15 -0
- data/lib/badline/reu/saved_state.rb +52 -0
- data/lib/badline/reu.rb +4 -0
- data/lib/badline/sdl/events.rb +2 -0
- data/lib/badline/sid/envelope/state.rb +68 -0
- data/lib/badline/sid/envelope.rb +7 -0
- data/lib/badline/sid/filter.rb +38 -0
- data/lib/badline/sid/voice.rb +10 -0
- data/lib/badline/sid/waveform/state.rb +66 -0
- data/lib/badline/sid/waveform.rb +2 -0
- data/lib/badline/sid.rb +34 -0
- data/lib/badline/snapshot/container.rb +117 -0
- data/lib/badline/snapshot/fields.rb +54 -0
- data/lib/badline/snapshot/image.rb +70 -0
- data/lib/badline/snapshot/machine_state.rb +77 -0
- data/lib/badline/snapshot/setup.rb +58 -0
- data/lib/badline/snapshot/state.rb +178 -0
- data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
- data/lib/badline/snapshot/vice/cias.rb +89 -0
- data/lib/badline/snapshot/vice/cias_import.rb +70 -0
- data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
- data/lib/badline/snapshot/vice/peripherals.rb +47 -0
- data/lib/badline/snapshot/vice/restore.rb +125 -0
- data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
- data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
- data/lib/badline/snapshot/vice/vicii.rb +119 -0
- data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
- data/lib/badline/snapshot/vice.rb +74 -0
- data/lib/badline/snapshot.rb +52 -0
- data/lib/badline/storage/crt_file.rb +9 -3
- data/lib/badline/storage/d64_image.rb +2 -0
- data/lib/badline/storage/d71_image.rb +2 -0
- data/lib/badline/storage/d81_image.rb +2 -0
- data/lib/badline/storage/disk_image.rb +11 -2
- data/lib/badline/storage/host_directory.rb +5 -0
- data/lib/badline/storage/t64.rb +6 -0
- data/lib/badline/storage/tap.rb +10 -1
- data/lib/badline/storage/unavailable.rb +20 -0
- data/lib/badline/storage.rb +34 -0
- data/lib/badline/time_of_day.rb +56 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +15 -0
- data/lib/badline/via/fast_forward.rb +15 -0
- data/lib/badline/via/interrupt_register.rb +9 -0
- data/lib/badline/via/shift_register.rb +13 -0
- data/lib/badline/via/state.rb +42 -0
- data/lib/badline/via/timer1.rb +19 -0
- data/lib/badline/via/timer2.rb +24 -0
- data/lib/badline/via.rb +2 -0
- data/lib/badline/vic/border_mask.rb +10 -0
- data/lib/badline/vic/collisions.rb +11 -0
- data/lib/badline/vic/color_patches.rb +11 -0
- data/lib/badline/vic/display_state.rb +29 -1
- data/lib/badline/vic/graphics_shifter.rb +26 -0
- data/lib/badline/vic/register_log.rb +18 -0
- data/lib/badline/vic/registers.rb +13 -2
- data/lib/badline/vic/sequencer.rb +12 -0
- data/lib/badline/vic/sequencer_state.rb +89 -0
- data/lib/badline/vic/sprite/internal_bus.rb +11 -0
- data/lib/badline/vic/sprite/saved_state.rb +64 -0
- data/lib/badline/vic/sprite.rb +2 -0
- data/lib/badline/vic/sprites.rb +3 -0
- data/lib/badline/vic/sprites_state.rb +57 -0
- data/lib/badline/vic.rb +94 -10
- data/lib/badline.rb +1 -0
- metadata +37 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class SID
|
|
5
|
+
class Waveform
|
|
6
|
+
# A snapshot of an oscillator: its phase, the noise register and the
|
|
7
|
+
# output latch, and the same fields as reSID keeps them, for VICE's
|
|
8
|
+
# SIDEXTENDED module. The model's tables and the voices it syncs with
|
|
9
|
+
# are the SID's wiring.
|
|
10
|
+
module State
|
|
11
|
+
# The fields reSID keeps for an oscillator between cycles: the phase,
|
|
12
|
+
# the noise register, its shift pipeline and reset countdown, the
|
|
13
|
+
# floating output's countdown and the pulse level.
|
|
14
|
+
def resid_fields
|
|
15
|
+
[@accumulator, @shift_register, @shift_pipeline, @shift_register_reset, @floating_ttl, @pulse]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Takes the fields resid_fields gives from `fields`, from `at` on.
|
|
19
|
+
def restore_resid_fields(fields, at)
|
|
20
|
+
@accumulator = fields[at]
|
|
21
|
+
@shift_register = fields[at + 1]
|
|
22
|
+
@shift_pipeline = fields[at + 2]
|
|
23
|
+
@shift_register_reset = fields[at + 3]
|
|
24
|
+
@floating_ttl = fields[at + 4]
|
|
25
|
+
@pulse = fields[at + 5]
|
|
26
|
+
@stale = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def save_state(out)
|
|
30
|
+
out.int(@accumulator).int(@delayed_triangle).int(@delayed_sawtooth)
|
|
31
|
+
out.int(@shift_register).int(@shift_register_reset).int(@shift_pipeline)
|
|
32
|
+
out.int(@frequency).int(@pulse_width).int(@selected)
|
|
33
|
+
[@test, @ring, @sync, @msb_rising].each { |flag| out.boolean(flag) }
|
|
34
|
+
out.int(@floating).int(@floating_ttl).int(@pulse).int(@output).boolean(@stale)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def load_state(input)
|
|
38
|
+
@accumulator = input.int
|
|
39
|
+
@delayed_triangle = input.int
|
|
40
|
+
@delayed_sawtooth = input.int
|
|
41
|
+
@shift_register = input.int
|
|
42
|
+
@shift_register_reset = input.int
|
|
43
|
+
@shift_pipeline = input.int
|
|
44
|
+
@frequency = input.int
|
|
45
|
+
@pulse_width = input.int
|
|
46
|
+
@selected = input.int
|
|
47
|
+
load_latches(input)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def load_latches(input)
|
|
53
|
+
@test = input.boolean?
|
|
54
|
+
@ring = input.boolean?
|
|
55
|
+
@sync = input.boolean?
|
|
56
|
+
@msb_rising = input.boolean?
|
|
57
|
+
@floating = input.int
|
|
58
|
+
@floating_ttl = input.int
|
|
59
|
+
@pulse = input.int
|
|
60
|
+
@output = input.int
|
|
61
|
+
@stale = input.boolean?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/badline/sid/waveform.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "badline/sid/waveform/combined"
|
|
4
4
|
require "badline/sid/waveform/fast_forward"
|
|
5
5
|
require "badline/sid/waveform/noise_writeback"
|
|
6
|
+
require "badline/sid/waveform/state"
|
|
6
7
|
|
|
7
8
|
module Badline
|
|
8
9
|
class SID
|
|
@@ -27,6 +28,7 @@ module Badline
|
|
|
27
28
|
class Waveform
|
|
28
29
|
include FastForward
|
|
29
30
|
include NoiseWriteback
|
|
31
|
+
include State
|
|
30
32
|
|
|
31
33
|
# All bits high at power up, odd ones stored inverted (SID/oscinit).
|
|
32
34
|
POWER_ON_ACCUMULATOR = 0x555555
|
data/lib/badline/sid.rb
CHANGED
|
@@ -125,6 +125,17 @@ module Badline
|
|
|
125
125
|
@voices
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
+
# Runs the DSP up to the current cycle.
|
|
129
|
+
def catch_up! = catch_up
|
|
130
|
+
|
|
131
|
+
# The byte the data bus holds and the cycles until it fades.
|
|
132
|
+
def bus_state = [@bus_value, @bus_ttl]
|
|
133
|
+
|
|
134
|
+
def restore_bus(value, ttl)
|
|
135
|
+
@bus_value = value
|
|
136
|
+
@bus_ttl = ttl
|
|
137
|
+
end
|
|
138
|
+
|
|
128
139
|
def filter
|
|
129
140
|
catch_up
|
|
130
141
|
@filter
|
|
@@ -178,6 +189,29 @@ module Badline
|
|
|
178
189
|
# Register contents as written, for save states and debugging.
|
|
179
190
|
def register(reg) = @registers.peek(reg)
|
|
180
191
|
|
|
192
|
+
# The registers, the data bus, the voices and the filter, and the span
|
|
193
|
+
# the DSP has yet to catch up on, as it stands: saving leaves the SID
|
|
194
|
+
# as it was. Whether it synthesizes or records is the host's.
|
|
195
|
+
def save_state(out)
|
|
196
|
+
out.marker("SID")
|
|
197
|
+
@registers.save_state(out)
|
|
198
|
+
out.int(@bus_value).int(@bus_ttl).int(@pending_cycles).ints(@deferred_writes)
|
|
199
|
+
@voices.each { |voice| voice.save_state(out) }
|
|
200
|
+
@filter.save_state(out)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def load_state(input)
|
|
204
|
+
input.marker("SID")
|
|
205
|
+
@registers.load_state(input)
|
|
206
|
+
@bus_value = input.int
|
|
207
|
+
@bus_ttl = input.int
|
|
208
|
+
@pending_cycles = input.int
|
|
209
|
+
input.ints_into(@deferred_writes)
|
|
210
|
+
@voices.each { |voice| voice.load_state(input) }
|
|
211
|
+
@filter.load_state(input)
|
|
212
|
+
update_span_rules
|
|
213
|
+
end
|
|
214
|
+
|
|
181
215
|
private
|
|
182
216
|
|
|
183
217
|
def latch(value)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
module Snapshot
|
|
7
|
+
# One module of a snapshot: its name, its version and its payload,
|
|
8
|
+
# without the 22-byte module header.
|
|
9
|
+
Section = Data.define(:name, :major, :minor, :data) do
|
|
10
|
+
def version = "#{major}.#{minor}"
|
|
11
|
+
|
|
12
|
+
def to_s = "#{name} #{version}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# The .vsf container as the VICE manual describes it: a magic string,
|
|
16
|
+
# the file format version and a machine name, an optional VICE version
|
|
17
|
+
# block, then modules one after another, each a 16-byte name, a major
|
|
18
|
+
# and minor version and a little-endian length that counts the header.
|
|
19
|
+
# A gzipped file reads as the file inside it, as VICE's own does.
|
|
20
|
+
class Container
|
|
21
|
+
MAGIC = "VICE Snapshot File\x1a".b
|
|
22
|
+
VERSION_MAGIC = "VICE Version\x1a".b
|
|
23
|
+
NAME_LENGTH = 16
|
|
24
|
+
MODULE_HEADER = NAME_LENGTH + 6
|
|
25
|
+
FORMAT_MAJOR = 2
|
|
26
|
+
FORMAT_MINOR = 0
|
|
27
|
+
# x64sc names its machine C64SC, and refuses a snapshot naming another.
|
|
28
|
+
MACHINE = "C64SC"
|
|
29
|
+
GZIP_MAGIC = "\x1f\x8b".b
|
|
30
|
+
|
|
31
|
+
# The VICE version block, when VICE wrote the file: four version
|
|
32
|
+
# bytes and a revision, kept as read.
|
|
33
|
+
attr_reader :machine, :sections, :vice_version
|
|
34
|
+
|
|
35
|
+
def self.parse(bytes)
|
|
36
|
+
bytes = bytes.b
|
|
37
|
+
bytes = gunzip(bytes) if bytes.start_with?(GZIP_MAGIC)
|
|
38
|
+
raise FormatError, "not a VICE snapshot" unless bytes.start_with?(MAGIC)
|
|
39
|
+
|
|
40
|
+
Reader.new(bytes).container
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.gunzip(bytes)
|
|
44
|
+
Zlib.gunzip(bytes).b
|
|
45
|
+
rescue Zlib::Error => e
|
|
46
|
+
raise FormatError, "the gzipped snapshot is damaged: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def initialize(sections = [], machine: MACHINE, vice_version: nil)
|
|
50
|
+
@machine = machine
|
|
51
|
+
@sections = sections
|
|
52
|
+
@vice_version = vice_version
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def [](name) = @sections.find { |section| section.name == name }
|
|
56
|
+
|
|
57
|
+
def to_s
|
|
58
|
+
header = MAGIC + [FORMAT_MAJOR, FORMAT_MINOR].pack("CC") + padded(@machine)
|
|
59
|
+
header += VERSION_MAGIC + @vice_version if @vice_version
|
|
60
|
+
header + @sections.map { |section| encode(section) }.join
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def padded(name)
|
|
66
|
+
raise ArgumentError, "#{name} is longer than #{NAME_LENGTH} bytes" if name.bytesize > NAME_LENGTH
|
|
67
|
+
|
|
68
|
+
name.b.ljust(NAME_LENGTH, "\0")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def encode(section)
|
|
72
|
+
padded(section.name) + [section.major, section.minor, MODULE_HEADER + section.data.bytesize].pack("CCV") +
|
|
73
|
+
section.data.b
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Splits the bytes of a snapshot into its header and modules.
|
|
77
|
+
class Reader
|
|
78
|
+
def initialize(bytes)
|
|
79
|
+
@bytes = bytes
|
|
80
|
+
@pos = MAGIC.bytesize + 2
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def container
|
|
84
|
+
machine = take(NAME_LENGTH).delete("\0")
|
|
85
|
+
version = vice_version
|
|
86
|
+
sections = []
|
|
87
|
+
sections << section while @pos < @bytes.bytesize
|
|
88
|
+
Container.new(sections, machine:, vice_version: version)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def take(length)
|
|
94
|
+
raise FormatError, "snapshot ends early, at byte #{@bytes.bytesize}" if @pos + length > @bytes.bytesize
|
|
95
|
+
|
|
96
|
+
@bytes.byteslice(@pos, length).tap { @pos += length }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# VICE 2.4.30 and later write their version after the machine name.
|
|
100
|
+
def vice_version
|
|
101
|
+
return unless @bytes.byteslice(@pos, VERSION_MAGIC.bytesize) == VERSION_MAGIC
|
|
102
|
+
|
|
103
|
+
@pos += VERSION_MAGIC.bytesize
|
|
104
|
+
take(8)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def section
|
|
108
|
+
name = take(NAME_LENGTH).delete("\0")
|
|
109
|
+
major, minor, length = take(6).unpack("CCV")
|
|
110
|
+
raise FormatError, "module #{name} has a bad length #{length}" if length < MODULE_HEADER
|
|
111
|
+
|
|
112
|
+
Section.new(name:, major:, minor:, data: take(length - MODULE_HEADER))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
# Little-endian fields for VICE modules, written one after another as
|
|
6
|
+
# VICE's SMW_B, SMW_W, SMW_DW, SMW_QW and SMW_BA write them.
|
|
7
|
+
class FieldWriter
|
|
8
|
+
def initialize
|
|
9
|
+
@out = String.new(encoding: Encoding::BINARY)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def byte(value) = tap { @out << [value & 0xff].pack("C") }
|
|
13
|
+
def word(value) = tap { @out << [value & 0xffff].pack("v") }
|
|
14
|
+
def dword(value) = tap { @out << [value & 0xffff_ffff].pack("V") }
|
|
15
|
+
def qword(value) = tap { @out << [value & 0xffff_ffff_ffff_ffff].pack("Q<") }
|
|
16
|
+
def flag(value) = byte(value ? 1 : 0)
|
|
17
|
+
|
|
18
|
+
def bytes(values)
|
|
19
|
+
tap { @out << (values.is_a?(String) ? values.b : values.map { |v| v & 0xff }.pack("C*")) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def zeros(count) = tap { @out << ("\0".b * count) }
|
|
23
|
+
|
|
24
|
+
def section(name, major, minor) = Section.new(name:, major:, minor:, data: @out.dup)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Reads a VICE module's fields in the order they were written.
|
|
28
|
+
class FieldReader
|
|
29
|
+
def initialize(section)
|
|
30
|
+
@section = section
|
|
31
|
+
@data = section.data
|
|
32
|
+
@pos = 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def byte = take(1).unpack1("C").to_i
|
|
36
|
+
def word = take(2).unpack1("v").to_i
|
|
37
|
+
def dword = take(4).unpack1("V").to_i
|
|
38
|
+
def qword = take(8).unpack1("Q<").to_i
|
|
39
|
+
def flag? = !byte.zero?
|
|
40
|
+
def bytes(count) = take(count).bytes
|
|
41
|
+
def skip(count) = tap { take(count) }
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def take(count)
|
|
46
|
+
if @pos + count > @data.bytesize
|
|
47
|
+
raise FormatError, "#{@section.name} #{@section.version} ends early, at byte #{@data.bytesize}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@data.byteslice(@pos, count).to_s.tap { @pos += count }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
# What a restore applied and what it left out: module names, and a line
|
|
6
|
+
# for each thing left out, saying why.
|
|
7
|
+
Report = Data.define(:applied, :ignored)
|
|
8
|
+
|
|
9
|
+
# A snapshot read from a file.
|
|
10
|
+
class Image
|
|
11
|
+
attr_reader :container
|
|
12
|
+
|
|
13
|
+
def initialize(container)
|
|
14
|
+
@container = container
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def sections = @container.sections
|
|
18
|
+
|
|
19
|
+
# Whether badline wrote it, with the whole machine in a BADLINE module.
|
|
20
|
+
def badline? = !section.nil?
|
|
21
|
+
|
|
22
|
+
# The State the BADLINE module holds.
|
|
23
|
+
def state = MachineState.state(section)
|
|
24
|
+
|
|
25
|
+
# A new machine, built as the snapshot's was and restored from it. A
|
|
26
|
+
# badline snapshot restores from its BADLINE module alone, which
|
|
27
|
+
# carries everything its VICE modules do, and a VICE one through the
|
|
28
|
+
# modules badline reads. Yields a line for each thing left out, or
|
|
29
|
+
# warns it without a block.
|
|
30
|
+
def load(&)
|
|
31
|
+
computer = badline? ? opening { Computer.restored(state) } : Vice.setup(@container).build
|
|
32
|
+
tell(badline? ? state_report(computer) : Vice::Restore.apply(@container, computer), &)
|
|
33
|
+
computer
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Restores `computer` from the snapshot, and returns the Report. A
|
|
37
|
+
# snapshot that fails leaves the machine as it was.
|
|
38
|
+
def restore(computer, &)
|
|
39
|
+
report = badline? ? restore_state(computer) : Vice::Restore.import(@container, computer)
|
|
40
|
+
tell(report, &)
|
|
41
|
+
report
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def section = @container[MachineState::NAME]
|
|
47
|
+
|
|
48
|
+
def tell(report)
|
|
49
|
+
report.ignored.each { |line| block_given? ? yield(line) : warn("badline: #{line}") }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def restore_state(computer)
|
|
53
|
+
opening { computer.restore(state) }
|
|
54
|
+
state_report(computer)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def opening
|
|
58
|
+
yield
|
|
59
|
+
rescue SystemCallError => e
|
|
60
|
+
raise FormatError, "a file the snapshot names won't open: #{e.message}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def state_report(computer)
|
|
64
|
+
lost = computer.init_handlers_lost
|
|
65
|
+
ignored = lost.positive? ? ["#{lost} on_init handler(s) the saved machine had yet to run, left out"] : []
|
|
66
|
+
Report.new(applied: [MachineState::NAME], ignored:)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
# The BADLINE module: a State from Computer#snapshot, which restores the
|
|
6
|
+
# machine exactly where it was saved. VICE skips a module it doesn't
|
|
7
|
+
# know.
|
|
8
|
+
#
|
|
9
|
+
# The payload is deflated. Inside, a little-endian 32-bit length leads
|
|
10
|
+
# a run of BER-compressed integers (Ruby's pack "w"): the number of
|
|
11
|
+
# values and of strings, each string's length, then the values, each
|
|
12
|
+
# folded to a natural number (0, -1, 1, -2 ... as 0, 1, 2, 3 ...). The
|
|
13
|
+
# strings follow, one after another.
|
|
14
|
+
#
|
|
15
|
+
# A State only restores in the badline version that wrote it, since
|
|
16
|
+
# each chip writes its fields as it holds them.
|
|
17
|
+
module MachineState
|
|
18
|
+
NAME = "BADLINE"
|
|
19
|
+
MAJOR = 2
|
|
20
|
+
MINOR = 0
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def section(state)
|
|
25
|
+
Section.new(name: NAME, major: MAJOR, minor: MINOR, data: Zlib.deflate(encode(state)))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def state(section)
|
|
29
|
+
unless section.major == MAJOR
|
|
30
|
+
raise FormatError, "#{NAME} #{section.version} was written by another badline version, " \
|
|
31
|
+
"and this one reads #{MAJOR}.#{MINOR}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
decode(Zlib.inflate(section.data))
|
|
35
|
+
rescue Zlib::Error => e
|
|
36
|
+
raise FormatError, "#{NAME} is damaged: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def encode(state)
|
|
40
|
+
values = state.values
|
|
41
|
+
strings = state.strings
|
|
42
|
+
numbers = [values.length, strings.length]
|
|
43
|
+
strings.each { |string| numbers << string.bytesize }
|
|
44
|
+
values.each { |value| numbers << (value.negative? ? (-2 * value) - 1 : 2 * value) }
|
|
45
|
+
index = numbers.pack("w*")
|
|
46
|
+
[index.bytesize].pack("V") + index + strings.join
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def decode(bytes)
|
|
50
|
+
raise FormatError, "#{NAME} ends early" if bytes.bytesize < 4
|
|
51
|
+
|
|
52
|
+
length = bytes.byteslice(0, 4).unpack1("V").to_i
|
|
53
|
+
numbers = bytes.byteslice(4, length).to_s.unpack("w*").map(&:to_i)
|
|
54
|
+
value_count = numbers.fetch(0, 0)
|
|
55
|
+
string_count = numbers.fetch(1, 0)
|
|
56
|
+
raise FormatError, "#{NAME} ends early" if numbers.length != 2 + string_count + value_count
|
|
57
|
+
|
|
58
|
+
State.new(values(numbers, 2 + string_count), strings(bytes, 4 + length, numbers.slice(2, string_count)))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def values(numbers, from)
|
|
62
|
+
Array.new(numbers.length - from) do |i|
|
|
63
|
+
folded = numbers[from + i]
|
|
64
|
+
folded.odd? ? -((folded + 1) / 2) : folded / 2
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def strings(bytes, pos, lengths)
|
|
69
|
+
lengths.map do |length|
|
|
70
|
+
raise FormatError, "#{NAME} ends early" if pos + length > bytes.bytesize
|
|
71
|
+
|
|
72
|
+
bytes.byteslice(pos, length).to_s.tap { pos += length }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
# How a machine was built: its VIC, CIA and SID models, its region, its
|
|
6
|
+
# RAM expansion and the size of its REU, the arguments Computer.new
|
|
7
|
+
# takes. A State starts with them, so a machine to restore it into can
|
|
8
|
+
# be built first.
|
|
9
|
+
Setup = Data.define(:vic_model, :cia_model, :sid_model, :region, :ram_expansion, :reu)
|
|
10
|
+
|
|
11
|
+
class Setup
|
|
12
|
+
SID_MODELS = %i[mos6581 mos8580].freeze
|
|
13
|
+
REGIONS = [Region::PAL, Region::NTSC].freeze
|
|
14
|
+
RAM_EXPANSIONS = [:none, *RAMExpansion::TYPES.keys].freeze
|
|
15
|
+
|
|
16
|
+
def self.of(address_bus)
|
|
17
|
+
new(vic_model: address_bus.vic.model, cia_model: address_bus.cia1.model, sid_model: address_bus.sid.model,
|
|
18
|
+
region: address_bus.region, ram_expansion: address_bus.ram_expansion.type, reu: address_bus.reu&.size_kb)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.read(input)
|
|
22
|
+
new(vic_model: VIC::MODELS.fetch(input.int), cia_model: CIA::MODELS.fetch(input.int),
|
|
23
|
+
sid_model: SID_MODELS.fetch(input.int), region: REGIONS.fetch(input.int),
|
|
24
|
+
ram_expansion: ram_expansion(RAM_EXPANSIONS.fetch(input.int)), reu: reu(input.int))
|
|
25
|
+
rescue IndexError
|
|
26
|
+
raise FormatError, "the state names a chip model, region or RAM expansion badline doesn't know"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.ram_expansion(name) = name == :none ? nil : name
|
|
30
|
+
|
|
31
|
+
# An REU's size in K, written as 0 for none.
|
|
32
|
+
def self.reu(size_kb)
|
|
33
|
+
return nil if size_kb.zero?
|
|
34
|
+
return size_kb if REU::SIZES_KB.include?(size_kb)
|
|
35
|
+
|
|
36
|
+
raise FormatError, "the state names a #{size_kb}K REU, which badline doesn't build"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def write(out)
|
|
40
|
+
out.int(VIC::MODELS.index(vic_model)).int(CIA::MODELS.index(cia_model))
|
|
41
|
+
out.int(SID_MODELS.index(sid_model)).int(REGIONS.index(region))
|
|
42
|
+
out.int(RAM_EXPANSIONS.index(ram_expansion || :none)).int(reu || 0)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A machine built this way, at power-on.
|
|
46
|
+
def build
|
|
47
|
+
Computer.new(vic_model:, cia_model:, sid_model:, region:, ram_expansion:, reu:)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_s
|
|
51
|
+
parts = [vic_model, cia_model, sid_model, region.name]
|
|
52
|
+
parts << ram_expansion if ram_expansion
|
|
53
|
+
parts << "a #{reu}K REU" if reu
|
|
54
|
+
parts.join(", ")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
# A file that isn't a snapshot badline reads, is cut short, or holds a
|
|
6
|
+
# state that doesn't line up with the machine reading it.
|
|
7
|
+
class FormatError < StandardError; end
|
|
8
|
+
|
|
9
|
+
# A machine's state, held in memory: the Integers each chip writes in
|
|
10
|
+
# turn, and the byte strings for memories, names and paths. Booleans
|
|
11
|
+
# are written as 0 and 1. Computer#snapshot takes one and
|
|
12
|
+
# Computer#restore puts it back.
|
|
13
|
+
class State
|
|
14
|
+
# The layout the chips write their fields in. It goes up whenever a
|
|
15
|
+
# save_state writes something else, so a State in another layout
|
|
16
|
+
# fails before anything is read into a machine.
|
|
17
|
+
SCHEMA = 1
|
|
18
|
+
|
|
19
|
+
attr_reader :values, :strings
|
|
20
|
+
|
|
21
|
+
def initialize(values, strings)
|
|
22
|
+
@values = values
|
|
23
|
+
@strings = strings
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ==(other)
|
|
27
|
+
other.is_a?(State) && values == other.values && strings == other.strings
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Writes a State. Each chip's save_state writes its fields through one
|
|
32
|
+
# of these, and its load_state reads them back from a StateReader in
|
|
33
|
+
# the same order.
|
|
34
|
+
class StateWriter
|
|
35
|
+
attr_reader :state
|
|
36
|
+
|
|
37
|
+
def initialize
|
|
38
|
+
@values = []
|
|
39
|
+
@strings = []
|
|
40
|
+
@state = State.new(@values, @strings)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def int(value)
|
|
44
|
+
raise TypeError, "not an Integer: #{value.inspect}" unless value.is_a?(Integer)
|
|
45
|
+
|
|
46
|
+
@values << value.to_i
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def boolean(value)
|
|
51
|
+
@values << (value ? 1 : 0)
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def optional_int(value)
|
|
56
|
+
return boolean(false) if value.nil?
|
|
57
|
+
|
|
58
|
+
boolean(true).int(value)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ints(values)
|
|
62
|
+
int(values.length)
|
|
63
|
+
values.each { |value| int(value) }
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def booleans(values)
|
|
68
|
+
int(values.length)
|
|
69
|
+
values.each { |value| boolean(value) }
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Bytes, 0 to 255 each, kept as one string.
|
|
74
|
+
def blob(values)
|
|
75
|
+
@strings << values.pack("C*")
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def string(value)
|
|
80
|
+
@strings << value.to_s.b
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def optional_string(value)
|
|
85
|
+
return boolean(false) if value.nil?
|
|
86
|
+
|
|
87
|
+
boolean(true).string(value)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A name a reader checks for, so a state that has gone out of step
|
|
91
|
+
# fails there instead of loading the wrong fields.
|
|
92
|
+
def marker(name) = string(name)
|
|
93
|
+
|
|
94
|
+
# The State's layout and the badline version writing it, which a
|
|
95
|
+
# reader checks first (StateReader#check_stamp).
|
|
96
|
+
def stamp = int(State::SCHEMA).string(VERSION)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Reads a State back in the order a StateWriter wrote it.
|
|
100
|
+
#
|
|
101
|
+
# A detached reader restores a machine that opens no host file and
|
|
102
|
+
# writes none, for a copy the machine runs on its own: a disk image
|
|
103
|
+
# comes from the bytes the state holds, write-protected, a directory
|
|
104
|
+
# or .t64 archive device 8 serves and a tape are left out, and a true
|
|
105
|
+
# drive's disk keeps the state's tracks without its image file.
|
|
106
|
+
class StateReader
|
|
107
|
+
def initialize(state, detached: false)
|
|
108
|
+
@values = state.values
|
|
109
|
+
@strings = state.strings
|
|
110
|
+
@value = 0
|
|
111
|
+
@string = 0
|
|
112
|
+
@detached = detached
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def detached? = @detached
|
|
116
|
+
|
|
117
|
+
def int
|
|
118
|
+
raise FormatError, "the state ends early, at value #{@value}" if @value >= @values.length
|
|
119
|
+
|
|
120
|
+
value = @values[@value]
|
|
121
|
+
@value += 1
|
|
122
|
+
value
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def boolean?
|
|
126
|
+
value = int
|
|
127
|
+
raise FormatError, "expected a boolean in the state, found #{value}" unless value.between?(0, 1)
|
|
128
|
+
|
|
129
|
+
value == 1
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def optional_int = boolean? ? int : nil
|
|
133
|
+
|
|
134
|
+
def ints = Array.new(int) { int }
|
|
135
|
+
|
|
136
|
+
def booleans = Array.new(int) { boolean? }
|
|
137
|
+
|
|
138
|
+
# Reads into an existing array, in place, so whatever else holds it
|
|
139
|
+
# sees the values.
|
|
140
|
+
def ints_into(array) = array.replace(ints)
|
|
141
|
+
|
|
142
|
+
def booleans_into(array) = array.replace(booleans)
|
|
143
|
+
|
|
144
|
+
def blob = string.bytes
|
|
145
|
+
|
|
146
|
+
def blob_into(array) = array.replace(blob)
|
|
147
|
+
|
|
148
|
+
def string
|
|
149
|
+
raise FormatError, "the state ends early, at string #{@string}" if @string >= @strings.length
|
|
150
|
+
|
|
151
|
+
value = @strings[@string]
|
|
152
|
+
@string += 1
|
|
153
|
+
value
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def optional_string = boolean? ? string : nil
|
|
157
|
+
|
|
158
|
+
def marker(name)
|
|
159
|
+
found = string
|
|
160
|
+
raise FormatError, "expected #{name} in the state, found #{found[0, 20].inspect}" unless found == name
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Fails unless the State was written in this layout by this badline
|
|
164
|
+
# version.
|
|
165
|
+
def check_stamp
|
|
166
|
+
schema = int
|
|
167
|
+
version = string
|
|
168
|
+
return if schema == State::SCHEMA && version == VERSION
|
|
169
|
+
|
|
170
|
+
raise FormatError, "the state was written by badline #{version.inspect} in layout #{schema}, " \
|
|
171
|
+
"and this is badline #{VERSION}, layout #{State::SCHEMA}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Whether every value and string has been read.
|
|
175
|
+
def finished? = @value == @values.length && @string == @strings.length
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|