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,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Computer
|
|
5
|
+
# The keyword arguments Computer.new takes to build the machine a
|
|
6
|
+
# State from #snapshot was taken of.
|
|
7
|
+
def self.setup(state)
|
|
8
|
+
input = Snapshot::StateReader.new(state)
|
|
9
|
+
input.marker("COMPUTER")
|
|
10
|
+
input.check_stamp
|
|
11
|
+
Snapshot::Setup.read(input)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# A new machine, built as the one a State from #snapshot was taken of,
|
|
15
|
+
# at that state. `detached` builds it without the host's files
|
|
16
|
+
# (Snapshot::StateReader).
|
|
17
|
+
def self.restored(state, detached: false) = setup(state).build.apply_state(state, detached:)
|
|
18
|
+
|
|
19
|
+
# The whole machine's state for a snapshot: the machine's own latches,
|
|
20
|
+
# the bus and its chips, the CPU, and what is attached to it
|
|
21
|
+
# (Attachments).
|
|
22
|
+
module SavedState
|
|
23
|
+
# How many more on_init handlers the machine a State was taken of had
|
|
24
|
+
# yet to run than this one has: they are the host's blocks, which a
|
|
25
|
+
# State can't hold. Nil until the machine is restored.
|
|
26
|
+
attr_reader :init_handlers_lost
|
|
27
|
+
|
|
28
|
+
# The machine's whole state, in memory. #restore takes the machine, or
|
|
29
|
+
# another built with the same chip models, region and RAM expansion,
|
|
30
|
+
# back to it. The cartridge, the disk in device 8, a true 1541 and its
|
|
31
|
+
# disk and the tape go in the state too. What the host holds stays
|
|
32
|
+
# out: the keyboard, the joysticks and the pot devices, the traps and
|
|
33
|
+
# callbacks, whether the display renders and whether the SID records.
|
|
34
|
+
def snapshot
|
|
35
|
+
out = Snapshot::StateWriter.new
|
|
36
|
+
save_state(out)
|
|
37
|
+
out.state
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Takes the machine back to a State from #snapshot. The machine keeps
|
|
41
|
+
# its chips, its traps and its callbacks, and puts in the cartridge,
|
|
42
|
+
# drives and tape the state has, taking out any it hasn't. Raises
|
|
43
|
+
# Snapshot::FormatError for a state of a machine built another way.
|
|
44
|
+
# The state goes into a new machine first, so one that fails leaves
|
|
45
|
+
# this machine as it was.
|
|
46
|
+
def restore(state)
|
|
47
|
+
check_setup(Computer.setup(state))
|
|
48
|
+
Computer.restored(state)
|
|
49
|
+
apply_state(state)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Puts a State into the machine without trying it on a new one
|
|
53
|
+
# first, for a machine built for it (Computer.restored): a state that
|
|
54
|
+
# fails leaves the machine part restored. `detached` restores it
|
|
55
|
+
# without the host's files (Snapshot::StateReader).
|
|
56
|
+
def apply_state(state, detached: false)
|
|
57
|
+
input = Snapshot::StateReader.new(state, detached:)
|
|
58
|
+
load_state(input)
|
|
59
|
+
raise Snapshot::FormatError, "the state goes on past the machine" unless input.finished?
|
|
60
|
+
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Sets the machine's cycle count, for a snapshot from VICE, which
|
|
65
|
+
# counts from its own clock.
|
|
66
|
+
def resume_at(cycles)
|
|
67
|
+
@cycles = cycles
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def save_state(out)
|
|
71
|
+
out.marker("COMPUTER").stamp
|
|
72
|
+
Snapshot::Setup.of(address_bus).write(out)
|
|
73
|
+
out.int(@cycles).boolean(@nmi_asserted).boolean(@cartridge_nmi).boolean(@restore_pulse).boolean(@freezing)
|
|
74
|
+
out.int(@freeze_writes).boolean(!@pending_keys.nil?)
|
|
75
|
+
out.ints(@pending_keys) if @pending_keys
|
|
76
|
+
out.int(booting? ? @init_handlers.length : 0)
|
|
77
|
+
save_cartridge(out)
|
|
78
|
+
address_bus.save_state(out)
|
|
79
|
+
@cpu.save_state(out)
|
|
80
|
+
save_trap_drive(out)
|
|
81
|
+
save_serial_bus(out)
|
|
82
|
+
save_reu(out)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def load_state(input)
|
|
86
|
+
input.marker("COMPUTER")
|
|
87
|
+
input.check_stamp
|
|
88
|
+
check_setup(Snapshot::Setup.read(input))
|
|
89
|
+
@cycles = input.int
|
|
90
|
+
@nmi_asserted = input.boolean?
|
|
91
|
+
@cartridge_nmi = input.boolean?
|
|
92
|
+
@restore_pulse = input.boolean?
|
|
93
|
+
@freezing = input.boolean?
|
|
94
|
+
@freeze_writes = input.int
|
|
95
|
+
@pending_keys = nil
|
|
96
|
+
@pending_keys = input.ints if input.boolean?
|
|
97
|
+
@init_handlers_lost = [input.int - @init_handlers.length, 0].max
|
|
98
|
+
load_cartridge(input)
|
|
99
|
+
address_bus.load_state(input)
|
|
100
|
+
@cpu.load_state(input)
|
|
101
|
+
load_trap_drive(input)
|
|
102
|
+
load_serial_bus(input)
|
|
103
|
+
load_reu(input)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def check_setup(setup)
|
|
109
|
+
ours = Snapshot::Setup.of(address_bus)
|
|
110
|
+
return if setup == ours
|
|
111
|
+
|
|
112
|
+
raise Snapshot::FormatError, "the state is of a machine with #{setup}, not #{ours}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/badline/computer.rb
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/computer/attachments"
|
|
4
|
+
require "badline/computer/saved_state"
|
|
5
|
+
|
|
3
6
|
module Badline
|
|
4
7
|
class Computer
|
|
5
8
|
include IntegerHelper
|
|
6
9
|
include KeyboardBuffer
|
|
10
|
+
include Attachments
|
|
11
|
+
include SavedState
|
|
7
12
|
|
|
8
13
|
attr_reader :address_bus, :cpu, :cycles, :drive1541
|
|
9
14
|
|
|
@@ -59,6 +64,7 @@ module Badline
|
|
|
59
64
|
@pending_keys = nil
|
|
60
65
|
@drive = nil
|
|
61
66
|
@serial_trap = nil
|
|
67
|
+
@save_trap = nil
|
|
62
68
|
@drive1541 = nil
|
|
63
69
|
@iec_bus = nil
|
|
64
70
|
@reu = reu ? plug_reu(reu) : nil
|
|
@@ -95,9 +101,7 @@ module Badline
|
|
|
95
101
|
end
|
|
96
102
|
|
|
97
103
|
def attach_cartridge(cartridge)
|
|
98
|
-
cartridge
|
|
99
|
-
cartridge.on_nmi_change { |level| @cartridge_nmi = level }
|
|
100
|
-
address_bus.attach_cartridge(cartridge)
|
|
104
|
+
connect_cartridge(cartridge)
|
|
101
105
|
power_cycle!
|
|
102
106
|
end
|
|
103
107
|
|
|
@@ -151,33 +155,6 @@ module Badline
|
|
|
151
155
|
@restore_pulse = true
|
|
152
156
|
end
|
|
153
157
|
|
|
154
|
-
# Puts the storage in device 8. Mounting again swaps the disk at any
|
|
155
|
-
# point while the machine runs: the drive keeps its RAM, which only a
|
|
156
|
-
# drive reset clears, and its status.
|
|
157
|
-
def mount(storage)
|
|
158
|
-
return @drive.insert(storage) if @drive
|
|
159
|
-
|
|
160
|
-
@drive = KernalTrap::Drive.new(storage)
|
|
161
|
-
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
|
|
162
|
-
cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
|
|
163
|
-
@serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive, device: serial_trap_device).install
|
|
164
|
-
save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
|
|
165
|
-
cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
# Takes device 8's mounted storage out, and with it the LOAD, SAVE and
|
|
169
|
-
# serial traps, so the KERNAL's routines go out over the serial bus.
|
|
170
|
-
# Mounting again starts a new drive, with its RAM cleared.
|
|
171
|
-
def unmount
|
|
172
|
-
return unless @drive
|
|
173
|
-
|
|
174
|
-
cpu.remove_trap(KernalTrap::Load::ADDRESS)
|
|
175
|
-
cpu.remove_trap(KernalTrap::Save::ADDRESS)
|
|
176
|
-
@serial_trap.device = nil
|
|
177
|
-
@serial_trap = nil
|
|
178
|
-
@drive = nil
|
|
179
|
-
end
|
|
180
|
-
|
|
181
158
|
# Plugs in a Drive1541, which then runs alongside the C64 on its own
|
|
182
159
|
# clock and talks to it over the serial bus. The serial traps stop
|
|
183
160
|
# answering the drive's device number, so the KERNAL's TALK, LISTEN and
|
|
@@ -195,7 +172,7 @@ module Badline
|
|
|
195
172
|
# for, which plugging in a drive does. Until then port A's serial
|
|
196
173
|
# inputs float high.
|
|
197
174
|
def iec_bus
|
|
198
|
-
@iec_bus ||= IECBus.new(host: @cia2).tap
|
|
175
|
+
@iec_bus ||= IECBus.new(host: @cia2).tap(&:plug_in!)
|
|
199
176
|
end
|
|
200
177
|
|
|
201
178
|
def capture_output
|
|
@@ -165,5 +165,13 @@ module Badline
|
|
|
165
165
|
INTERRUPT_WRITES = Microcode.write_mask(INTERRUPT_PLAN)
|
|
166
166
|
JAMMED_WRITES = Microcode.write_mask(JAMMED_PLAN)
|
|
167
167
|
MICROCODE = Microcode.table
|
|
168
|
+
|
|
169
|
+
# The instruction methods, in a fixed order a snapshot names them by.
|
|
170
|
+
OPERATIONS = MICROCODE.map(&:operation).uniq.freeze
|
|
171
|
+
|
|
172
|
+
# The plans a snapshot names that aren't an opcode's.
|
|
173
|
+
FETCH = -1
|
|
174
|
+
INTERRUPT = -2
|
|
175
|
+
JAMMED = -3
|
|
168
176
|
end
|
|
169
177
|
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class CPU
|
|
5
|
+
# The CPU's state for a snapshot: the registers, the interrupt lines and
|
|
6
|
+
# their pipeline, and the step of the instruction in flight, named by
|
|
7
|
+
# where its plan comes from.
|
|
8
|
+
module SavedState
|
|
9
|
+
# Writes everything the CPU holds, down to the step of the instruction
|
|
10
|
+
# it is in, for load_state to put back. The interrupt lines, BA stalls
|
|
11
|
+
# and SO are state too; the traps and the debug flag belong to the host.
|
|
12
|
+
def save_state(out)
|
|
13
|
+
out.marker("CPU")
|
|
14
|
+
out.int(@program_counter).int(@stack_pointer).int(@a).int(@x).int(@y).int(@status.value)
|
|
15
|
+
[@irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll, @boundary_irq,
|
|
16
|
+
@boundary_nmi, @boundary_crossed, @brk, @pending_write, @so_high, @optional_dummy,
|
|
17
|
+
@branch_taken].each { |flag| out.boolean(flag) }
|
|
18
|
+
out.optional_int(@interrupt).optional_int(@stalled_at).int(@cycles).int(@instructions)
|
|
19
|
+
out.int(plan_source).int(@index).int(OPERATIONS.index(@operation) || -1)
|
|
20
|
+
[@address, @dummy_address, @value, @rmw_result, @pointer].each { |register| out.optional_int(register) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def load_state(input)
|
|
24
|
+
input.marker("CPU")
|
|
25
|
+
@program_counter = input.int
|
|
26
|
+
@stack_pointer = input.int
|
|
27
|
+
@a = input.int
|
|
28
|
+
@x = input.int
|
|
29
|
+
@y = input.int
|
|
30
|
+
@status.value = input.int
|
|
31
|
+
load_flags(input)
|
|
32
|
+
@interrupt = input.optional_int
|
|
33
|
+
@stalled_at = input.optional_int
|
|
34
|
+
@cycles = input.int
|
|
35
|
+
@instructions = input.int
|
|
36
|
+
load_plan(input.int)
|
|
37
|
+
@index = input.int
|
|
38
|
+
operation = input.int
|
|
39
|
+
@operation = operation.negative? ? nil : OPERATIONS.fetch(operation)
|
|
40
|
+
load_working_registers(input)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Drops the instruction or interrupt sequence in flight and the
|
|
44
|
+
# interrupt lines' pipeline, so the next cycle fetches an opcode at the
|
|
45
|
+
# program counter, and sets the cycle count. A snapshot taken between
|
|
46
|
+
# instructions, as VICE's are, restores through here.
|
|
47
|
+
def resume(cycles)
|
|
48
|
+
@irq_sample = @irq_pending = false
|
|
49
|
+
@nmi_sample = @nmi_pending = false
|
|
50
|
+
@skip_poll = false
|
|
51
|
+
@interrupt = nil
|
|
52
|
+
@brk = false
|
|
53
|
+
@stalled_at = nil
|
|
54
|
+
@cycles = cycles
|
|
55
|
+
end_sequence
|
|
56
|
+
@pending_write = false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Where the plan running comes from: an opcode's microcode, or FETCH,
|
|
62
|
+
# INTERRUPT or JAMMED below it.
|
|
63
|
+
def plan_source
|
|
64
|
+
return FETCH if @plan.equal?(FETCH_PLAN)
|
|
65
|
+
return INTERRUPT if @plan.equal?(INTERRUPT_PLAN)
|
|
66
|
+
return JAMMED if @plan.equal?(JAMMED_PLAN)
|
|
67
|
+
|
|
68
|
+
MICROCODE.index { |micro| micro.plan.equal?(@plan) && micro.operation == @operation } ||
|
|
69
|
+
MICROCODE.index { |micro| micro.plan.equal?(@plan) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def load_plan(source)
|
|
73
|
+
case source
|
|
74
|
+
when FETCH then use_plan(FETCH_PLAN, FETCH_WRITES)
|
|
75
|
+
when INTERRUPT then use_plan(INTERRUPT_PLAN, INTERRUPT_WRITES)
|
|
76
|
+
when JAMMED then use_plan(JAMMED_PLAN, JAMMED_WRITES)
|
|
77
|
+
else
|
|
78
|
+
micro = MICROCODE.fetch(source)
|
|
79
|
+
use_plan(micro.plan, micro.writes)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def use_plan(plan, writes)
|
|
84
|
+
@plan = plan
|
|
85
|
+
@writes = writes
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def load_flags(input)
|
|
89
|
+
@irq = input.boolean?
|
|
90
|
+
@nmi = input.boolean?
|
|
91
|
+
@irq_sample = input.boolean?
|
|
92
|
+
@irq_pending = input.boolean?
|
|
93
|
+
@nmi_sample = input.boolean?
|
|
94
|
+
@nmi_pending = input.boolean?
|
|
95
|
+
@skip_poll = input.boolean?
|
|
96
|
+
@boundary_irq = input.boolean?
|
|
97
|
+
@boundary_nmi = input.boolean?
|
|
98
|
+
@boundary_crossed = input.boolean?
|
|
99
|
+
@brk = input.boolean?
|
|
100
|
+
@pending_write = input.boolean?
|
|
101
|
+
@so_high = input.boolean?
|
|
102
|
+
@optional_dummy = input.boolean?
|
|
103
|
+
@branch_taken = input.boolean?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def load_working_registers(input)
|
|
107
|
+
@address = input.optional_int
|
|
108
|
+
@dummy_address = input.optional_int
|
|
109
|
+
@value = input.optional_int
|
|
110
|
+
@rmw_result = input.optional_int
|
|
111
|
+
@pointer = input.optional_int
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/badline/cpu.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "badline/cpu/microcode"
|
|
|
4
4
|
require "badline/cpu/addressing"
|
|
5
5
|
require "badline/cpu/operations"
|
|
6
6
|
require "badline/cpu/stack_operations"
|
|
7
|
+
require "badline/cpu/saved_state"
|
|
7
8
|
|
|
8
9
|
module Badline
|
|
9
10
|
class CPU
|
|
@@ -16,6 +17,7 @@ module Badline
|
|
|
16
17
|
include Addressing
|
|
17
18
|
include Operations
|
|
18
19
|
include StackOperations
|
|
20
|
+
include SavedState
|
|
19
21
|
|
|
20
22
|
attr_reader :memory, :instructions, :boundary_crossed, :cycles, :pending_write
|
|
21
23
|
attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y, :nmi, :irq
|
data/lib/badline/datasette.rb
CHANGED
|
@@ -54,6 +54,33 @@ module Badline
|
|
|
54
54
|
|
|
55
55
|
def running? = @playing && @motor && !@tape.nil?
|
|
56
56
|
|
|
57
|
+
# The keys, the motor, the countdown to the next pulse and the tape: its
|
|
58
|
+
# path and how far it has played.
|
|
59
|
+
def save_state(out)
|
|
60
|
+
out.marker("DATASETTE")
|
|
61
|
+
out.boolean(@playing).boolean(@motor).int(@countdown).boolean(!@tape.nil?)
|
|
62
|
+
return unless @tape
|
|
63
|
+
|
|
64
|
+
out.string(@tape.path).int(@tape.position)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A tape from another file goes in from its path, and a detached
|
|
68
|
+
# reader leaves the tape out. The sense and flag handlers don't fire.
|
|
69
|
+
def load_state(input)
|
|
70
|
+
input.marker("DATASETTE")
|
|
71
|
+
@playing = input.boolean?
|
|
72
|
+
@motor = input.boolean?
|
|
73
|
+
@countdown = input.int
|
|
74
|
+
return @tape = nil unless input.boolean?
|
|
75
|
+
|
|
76
|
+
path = input.string
|
|
77
|
+
position = input.int
|
|
78
|
+
return @tape = nil if input.detached?
|
|
79
|
+
|
|
80
|
+
@tape = Storage::TAP.new(path) unless @tape&.path&.b == path
|
|
81
|
+
@tape.position = position
|
|
82
|
+
end
|
|
83
|
+
|
|
57
84
|
def cycle!
|
|
58
85
|
return unless running?
|
|
59
86
|
|
|
@@ -19,7 +19,8 @@ module Badline
|
|
|
19
19
|
# both on a real 1541.
|
|
20
20
|
#
|
|
21
21
|
# While Idle records a pass of the DOS's idle loop, the bus watches it
|
|
22
|
-
# (see watch!).
|
|
22
|
+
# (see watch!). While Idle looks for an orbit, the bus guards the drive
|
|
23
|
+
# (see guard!).
|
|
23
24
|
class Bus
|
|
24
25
|
include Addressable
|
|
25
26
|
|
|
@@ -31,6 +32,10 @@ module Badline
|
|
|
31
32
|
VOLATILE_VIA2_READS = 0x0730
|
|
32
33
|
VOLATILE_VIA2_WRITES = 0x0ff0
|
|
33
34
|
|
|
35
|
+
# The timer each VIA register reaches, by offset, as bits: 1 for
|
|
36
|
+
# timer 1 and 2 for timer 2.
|
|
37
|
+
TIMER_REGISTERS = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 0, 0, 0, 0, 0, 0].freeze
|
|
38
|
+
|
|
34
39
|
attr_reader :ram, :rom, :via1, :via2, :data
|
|
35
40
|
|
|
36
41
|
# Whether a watched stretch did something that makes it unrepeatable.
|
|
@@ -40,6 +45,14 @@ module Badline
|
|
|
40
45
|
# from before the first access.
|
|
41
46
|
attr_reader :touched
|
|
42
47
|
|
|
48
|
+
# Whether the drive did something since guard! that reached outside
|
|
49
|
+
# it, or took something in from outside.
|
|
50
|
+
attr_reader :tainted
|
|
51
|
+
|
|
52
|
+
# The timers the drive read or wrote since guard!, as bits: VIA 1's
|
|
53
|
+
# timer 1 and timer 2 in bits 0 and 1, and VIA 2's in bits 2 and 3.
|
|
54
|
+
attr_reader :touched_timers
|
|
55
|
+
|
|
43
56
|
def initialize(rom:, via1:, via2:)
|
|
44
57
|
addressable_at(0, length: 2**16)
|
|
45
58
|
@ram = Memory.new([], length: 0x0800, start: 0)
|
|
@@ -47,20 +60,50 @@ module Badline
|
|
|
47
60
|
@via1 = via1
|
|
48
61
|
@via2 = via2
|
|
49
62
|
@data = 0
|
|
50
|
-
@watching = false
|
|
51
|
-
@volatile = false
|
|
63
|
+
@recording = @guarding = @watching = false
|
|
64
|
+
@volatile = @tainted = false
|
|
52
65
|
@touched = {}
|
|
66
|
+
@touched_timers = 0
|
|
67
|
+
@led = 0
|
|
53
68
|
end
|
|
54
69
|
|
|
55
70
|
# Starts watching: see volatile and touched. The bus stops watching
|
|
56
71
|
# once the stretch turns volatile.
|
|
57
72
|
def watch!
|
|
58
|
-
@
|
|
73
|
+
@recording = true
|
|
59
74
|
@volatile = false
|
|
60
75
|
@touched = {}
|
|
76
|
+
watching!
|
|
61
77
|
end
|
|
62
78
|
|
|
63
79
|
def unwatch!
|
|
80
|
+
@recording = false
|
|
81
|
+
watching!
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Starts guarding: see tainted and touched_timers. Reading VIA 1's
|
|
85
|
+
# port B, which reads the serial bus, taints the drive, and so do
|
|
86
|
+
# any write to VIA 1, whose port B drives the serial bus, a write to
|
|
87
|
+
# VIA 2's shift register or ACR, and VIA 2 turning the motor on or
|
|
88
|
+
# changing the LED. The bus stops guarding once the drive is tainted.
|
|
89
|
+
def guard!
|
|
90
|
+
@guarding = true
|
|
91
|
+
@tainted = false
|
|
92
|
+
@touched_timers = 0
|
|
93
|
+
@led = @via2.port_b_output & Mechanism::LED
|
|
94
|
+
watching!
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The last byte on the data bus and the RAM. What a watched stretch
|
|
98
|
+
# touched is Idle's, and a restored bus isn't watching.
|
|
99
|
+
def save_state(out)
|
|
100
|
+
out.int(@data)
|
|
101
|
+
@ram.save_state(out)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def load_state(input)
|
|
105
|
+
@data = input.int
|
|
106
|
+
@ram.load_state(input)
|
|
64
107
|
@watching = false
|
|
65
108
|
end
|
|
66
109
|
|
|
@@ -86,7 +129,7 @@ module Badline
|
|
|
86
129
|
|
|
87
130
|
addr &= 0x1fff
|
|
88
131
|
if addr < 0x0800
|
|
89
|
-
touch(addr) if @
|
|
132
|
+
touch(addr) if @recording
|
|
90
133
|
@ram.poke(addr, value)
|
|
91
134
|
elsif addr >= 0x1c00
|
|
92
135
|
@via2.poke(addr, value)
|
|
@@ -98,36 +141,69 @@ module Badline
|
|
|
98
141
|
|
|
99
142
|
private
|
|
100
143
|
|
|
144
|
+
def watching!
|
|
145
|
+
@watching = @recording || @guarding
|
|
146
|
+
end
|
|
147
|
+
|
|
101
148
|
def touch(addr)
|
|
102
149
|
@touched[addr] = @ram.peek(addr) unless @touched.key?(addr)
|
|
103
150
|
end
|
|
104
151
|
|
|
105
152
|
def watch_read(addr)
|
|
106
153
|
if addr < 0x0800
|
|
107
|
-
touch(addr)
|
|
154
|
+
touch(addr) if @recording
|
|
108
155
|
elsif addr >= 0x1800
|
|
109
|
-
|
|
156
|
+
via1 = addr < 0x1c00
|
|
157
|
+
watch(via1 ? VOLATILE_VIA1_READS : VOLATILE_VIA2_READS, addr) if @recording
|
|
158
|
+
guard_read(via1, addr & 0x0f) if @guarding
|
|
110
159
|
end
|
|
111
160
|
end
|
|
112
161
|
|
|
113
|
-
# Any write to VIA 1 makes the stretch volatile, and so does a write
|
|
114
|
-
# to VIA 2 that leaves the motor on.
|
|
115
162
|
def watch_write(addr)
|
|
116
163
|
if addr >= 0x1c00
|
|
117
|
-
|
|
118
|
-
|
|
164
|
+
watch_via2_write(addr) if @recording
|
|
165
|
+
guard_via2_write(addr & 0x0f) if @guarding
|
|
119
166
|
elsif addr >= 0x1800
|
|
120
|
-
volatile!
|
|
167
|
+
volatile! if @recording
|
|
168
|
+
taint! if @guarding
|
|
121
169
|
end
|
|
122
170
|
end
|
|
123
171
|
|
|
172
|
+
# Any write to VIA 1 makes the stretch volatile (watch_write), and so
|
|
173
|
+
# does a write to VIA 2 that leaves the motor on.
|
|
174
|
+
def watch_via2_write(addr)
|
|
175
|
+
watch(VOLATILE_VIA2_WRITES, addr)
|
|
176
|
+
volatile! if @recording && @via2.port_b_output.anybits?(Mechanism::MOTOR)
|
|
177
|
+
end
|
|
178
|
+
|
|
124
179
|
def watch(volatile, addr)
|
|
125
180
|
volatile! if volatile.anybits?(1 << (addr & 0x0f))
|
|
126
181
|
end
|
|
127
182
|
|
|
128
183
|
def volatile!
|
|
129
184
|
@volatile = true
|
|
130
|
-
@
|
|
185
|
+
@recording = false
|
|
186
|
+
watching!
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def guard_read(via1, offset)
|
|
190
|
+
return taint! if via1 && offset.zero?
|
|
191
|
+
|
|
192
|
+
@touched_timers |= via1 ? TIMER_REGISTERS[offset] : TIMER_REGISTERS[offset] << 2
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def guard_via2_write(offset)
|
|
196
|
+
return taint! if offset.between?(0x0a, 0x0b)
|
|
197
|
+
|
|
198
|
+
@touched_timers |= TIMER_REGISTERS[offset] << 2
|
|
199
|
+
lines = @via2.port_b_output
|
|
200
|
+
taint! if lines.anybits?(Mechanism::MOTOR) || (lines & Mechanism::LED) != @led
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def taint!
|
|
204
|
+
@tainted = true
|
|
205
|
+
@guarding = false
|
|
206
|
+
watching!
|
|
131
207
|
end
|
|
132
208
|
end
|
|
133
209
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "badline/drive1541/sector_reader"
|
|
4
|
+
require "badline/drive1541/disk_state"
|
|
4
5
|
|
|
5
6
|
module Badline
|
|
6
7
|
class Drive1541
|
|
@@ -16,6 +17,8 @@ module Badline
|
|
|
16
17
|
# host file. The disk is write-protected when the image won't take
|
|
17
18
|
# writes.
|
|
18
19
|
class Disk
|
|
20
|
+
include State
|
|
21
|
+
|
|
19
22
|
MAX_HALF_TRACK = 84
|
|
20
23
|
|
|
21
24
|
# GCR bytes around a track in each speed zone: 200 ms of rotation at
|
|
@@ -52,9 +55,9 @@ module Badline
|
|
|
52
55
|
# anything else as a .d64, formatted. `read_only` opens the image
|
|
53
56
|
# write-protected.
|
|
54
57
|
def self.open(path, read_only: false)
|
|
55
|
-
return from_g64(Storage::G64Image.new(path, read_only:))
|
|
58
|
+
return from_g64(Storage::G64Image.new(path, read_only:)).opened(path, read_only) if State.g64?(path)
|
|
56
59
|
|
|
57
|
-
from_d64(Storage::D64Image.new(path, read_only:))
|
|
60
|
+
from_d64(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
# A disk from a G64 image, each half track as the image stores it,
|
|
@@ -145,6 +148,8 @@ module Badline
|
|
|
145
148
|
@image = image
|
|
146
149
|
@tracks = Array.new(MAX_HALF_TRACK + 1)
|
|
147
150
|
@written = {}
|
|
151
|
+
@path = nil
|
|
152
|
+
@read_only = false
|
|
148
153
|
end
|
|
149
154
|
|
|
150
155
|
# The Track at the half track, or nil where there's no data.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
class Disk
|
|
6
|
+
# Saving and restoring a Disk for a snapshot: the image by its path,
|
|
7
|
+
# and every half track as the head sees it, with those written since
|
|
8
|
+
# the last flush. The image itself is read again from its host file.
|
|
9
|
+
module State
|
|
10
|
+
# The image's path, expanded, and whether it was opened read-only,
|
|
11
|
+
# for a disk from Disk.open. Nil for a disk made another way.
|
|
12
|
+
attr_reader :path, :read_only
|
|
13
|
+
|
|
14
|
+
# A .g64 image by its name, and anything else a .d64.
|
|
15
|
+
def self.g64?(path) = File.extname(path).casecmp?(".g64")
|
|
16
|
+
|
|
17
|
+
# A disk for the image at the path, its tracks left for the state.
|
|
18
|
+
def self.reopen(path, read_only)
|
|
19
|
+
if g64?(path)
|
|
20
|
+
Disk.new(Storage::G64Image.new(path, read_only:)).opened(path, read_only)
|
|
21
|
+
else
|
|
22
|
+
Disk.new(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The disk a state from save_state describes: +current+ when it was
|
|
27
|
+
# opened from the same image the same way, and otherwise a disk for
|
|
28
|
+
# the image at the path the state names, or one without an image,
|
|
29
|
+
# as a detached reader always gets. Either way its tracks come from
|
|
30
|
+
# the state.
|
|
31
|
+
def self.load(input, current)
|
|
32
|
+
path = input.optional_string
|
|
33
|
+
read_only = input.boolean?
|
|
34
|
+
disk = current if current && !path.nil? && current.path == path && current.read_only == read_only
|
|
35
|
+
disk ||= path && !input.detached? ? reopen(path, read_only) : Disk.new
|
|
36
|
+
disk.load_tracks(input)
|
|
37
|
+
disk
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Notes the image's path and how it was opened.
|
|
41
|
+
def opened(path, read_only)
|
|
42
|
+
@path = File.expand_path(path)
|
|
43
|
+
@read_only = read_only
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def save_state(out)
|
|
48
|
+
out.optional_string(@path).boolean(@read_only)
|
|
49
|
+
@tracks.each do |track|
|
|
50
|
+
out.boolean(!track.nil?)
|
|
51
|
+
save_track(track, out) if track
|
|
52
|
+
end
|
|
53
|
+
out.ints(@written.keys)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load_tracks(input)
|
|
57
|
+
@tracks.each_index do |half_track|
|
|
58
|
+
@tracks[half_track] = input.boolean? ? load_track(input) : nil
|
|
59
|
+
end
|
|
60
|
+
@written.clear
|
|
61
|
+
input.ints.each { |half_track| @written[half_track] = true }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def save_track(track, out)
|
|
67
|
+
out.int(track.zone).boolean(!track.speeds.nil?)
|
|
68
|
+
out.ints(track.speeds) if track.speeds
|
|
69
|
+
out.blob(track.bytes)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def load_track(input)
|
|
73
|
+
zone = input.int
|
|
74
|
+
speeds = input.boolean? ? input.ints : nil
|
|
75
|
+
Track.new(input.blob, zone, speeds)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|