badline 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -0
- data/CLAUDE.md +10 -9
- data/README.md +82 -10
- data/doc/pinned-behaviour.md +131 -30
- data/exe/badline-ruby +20 -23
- data/lib/badline/address_bus/saved_state.rb +72 -0
- data/lib/badline/address_bus.rb +11 -1
- data/lib/badline/audio/cli.rb +18 -5
- data/lib/badline/audio/console.rb +2 -1
- data/lib/badline/audio.rb +0 -3
- 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 +8 -0
- data/lib/badline/computer/attachments.rb +132 -0
- data/lib/badline/computer/saved_state.rb +116 -0
- data/lib/badline/computer.rb +13 -33
- data/lib/badline/cpu/boundary.rb +54 -0
- data/lib/badline/cpu/core.rb +165 -0
- data/lib/badline/cpu/microcode.rb +8 -0
- data/lib/badline/cpu/saved_state.rb +115 -0
- data/lib/badline/cpu.rb +3 -212
- data/lib/badline/datasette.rb +27 -0
- data/lib/badline/drive1541/bus.rb +13 -0
- data/lib/badline/drive1541/cpu.rb +29 -0
- data/lib/badline/drive1541/disk.rb +7 -2
- data/lib/badline/drive1541/disk_state.rb +80 -0
- data/lib/badline/drive1541/mechanism.rb +2 -0
- data/lib/badline/drive1541/mechanism_state.rb +67 -0
- data/lib/badline/drive1541/rotation.rb +10 -0
- data/lib/badline/drive1541/serial_port.rb +10 -0
- data/lib/badline/drive1541.rb +35 -0
- data/lib/badline/ffi.rb +157 -0
- data/lib/badline/frontend/app.rb +225 -0
- data/lib/badline/frontend/audio_sink.rb +72 -0
- data/lib/badline/frontend/boot.rb +52 -0
- data/lib/badline/frontend/controls.rb +163 -0
- data/lib/badline/frontend/drive_led.rb +51 -0
- data/lib/badline/frontend/frame_rate.rb +83 -0
- data/lib/badline/frontend/gamepads.rb +76 -0
- data/lib/badline/frontend/keys.rb +48 -0
- data/lib/badline/frontend/pacer.rb +100 -0
- data/lib/badline/frontend/pad_port.rb +61 -0
- data/lib/badline/frontend/screen.rb +65 -0
- data/lib/badline/frontend/screenshot.rb +22 -0
- data/lib/badline/frontend/snapshots.rb +60 -0
- data/lib/badline/frontend/sound.rb +153 -0
- data/lib/badline/frontend/timeline.rb +171 -0
- data/lib/badline/frontend.rb +18 -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 +8 -0
- data/lib/badline/options/event.rb +94 -0
- data/lib/badline/options/help.rb +78 -0
- data/lib/badline/options/option.rb +29 -0
- data/lib/badline/options/table.rb +62 -0
- data/lib/badline/options.rb +181 -144
- 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/saved_state.rb +50 -0
- data/lib/badline/reu/dma.rb +56 -11
- data/lib/badline/reu/ram.rb +15 -0
- data/lib/badline/reu/saved_state.rb +53 -0
- data/lib/badline/reu.rb +23 -10
- data/lib/badline/sdl.rb +123 -51
- 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/noise_writeback.rb +33 -11
- data/lib/badline/sid/waveform/state.rb +66 -0
- data/lib/badline/sid/waveform.rb +4 -3
- 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/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 +13 -0
- data/lib/badline/via/timer2.rb +18 -0
- data/lib/badline/via.rb +2 -0
- data/lib/badline/vic/bank.rb +21 -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 +109 -16
- data/lib/badline.rb +1 -0
- metadata +59 -18
- data/.claude/hooks/session-start.sh +0 -57
- data/.claude/settings.json +0 -14
- data/lib/badline/audio/sdl_sink.rb +0 -70
- data/lib/badline/audio/stream.rb +0 -117
- data/lib/badline/gui/application.rb +0 -216
- data/lib/badline/gui/drive_led_pane.rb +0 -29
- data/lib/badline/gui/gamepads.rb +0 -97
- data/lib/badline/gui/joy_map.rb +0 -27
- data/lib/badline/gui/key_map.rb +0 -39
- data/lib/badline/gui/palette.rb +0 -35
- data/lib/badline/gui/pane.rb +0 -36
- data/lib/badline/gui/screen_pane.rb +0 -46
- data/lib/badline/gui/window.rb +0 -61
- data/lib/badline/gui.rb +0 -13
- data/lib/badline/sdl/events.rb +0 -47
- data/lib/badline/sdl/functions.rb +0 -74
- data/vendor/.gitkeep +0 -0
data/lib/badline/cia/serial.rb
CHANGED
|
@@ -62,6 +62,19 @@ module Badline
|
|
|
62
62
|
@sp_out = true
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# The bits shifted so far, and the byte waiting to go out after the
|
|
66
|
+
# one shifting.
|
|
67
|
+
attr_reader :steps, :pending
|
|
68
|
+
|
|
69
|
+
def shift_register = @shift
|
|
70
|
+
|
|
71
|
+
# Sets the data register and the shift register, as a VICE snapshot
|
|
72
|
+
# gives them.
|
|
73
|
+
def restore_shift(data, shift)
|
|
74
|
+
@data = data
|
|
75
|
+
@shift = shift
|
|
76
|
+
end
|
|
77
|
+
|
|
65
78
|
def output?
|
|
66
79
|
@control.serial_mode?
|
|
67
80
|
end
|
|
@@ -71,6 +84,25 @@ module Badline
|
|
|
71
84
|
@cnt = level unless output?
|
|
72
85
|
end
|
|
73
86
|
|
|
87
|
+
def save_state(out)
|
|
88
|
+
out.int(@data).int(@shift).int(@steps).optional_int(@empty_in).optional_int(@busy_in)
|
|
89
|
+
out.boolean(@busy).boolean(@abandoned).optional_int(@pending).boolean(@underflow_high).boolean(@in_flight)
|
|
90
|
+
out.int(@flight_up).int(@flight_down).boolean(@idle)
|
|
91
|
+
[@cnt, @cnt_in, @sp_in, @sp_out].each { |level| out.boolean(level) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def load_state(input)
|
|
95
|
+
@data = input.int
|
|
96
|
+
@shift = input.int
|
|
97
|
+
@steps = input.int
|
|
98
|
+
@empty_in = input.optional_int
|
|
99
|
+
@busy_in = input.optional_int
|
|
100
|
+
@busy = input.boolean?
|
|
101
|
+
@abandoned = input.boolean?
|
|
102
|
+
@pending = input.optional_int
|
|
103
|
+
load_lines(input)
|
|
104
|
+
end
|
|
105
|
+
|
|
74
106
|
def write(value)
|
|
75
107
|
@data = value
|
|
76
108
|
@pending = LOAD_DELAY
|
|
@@ -132,6 +164,18 @@ module Badline
|
|
|
132
164
|
|
|
133
165
|
private
|
|
134
166
|
|
|
167
|
+
def load_lines(input)
|
|
168
|
+
@underflow_high = input.boolean?
|
|
169
|
+
@in_flight = input.boolean?
|
|
170
|
+
@flight_up = input.int
|
|
171
|
+
@flight_down = input.int
|
|
172
|
+
@idle = input.boolean?
|
|
173
|
+
@cnt = input.boolean?
|
|
174
|
+
@cnt_in = input.boolean?
|
|
175
|
+
@sp_in = input.boolean?
|
|
176
|
+
@sp_out = input.boolean?
|
|
177
|
+
end
|
|
178
|
+
|
|
135
179
|
# Without an underflow, a register with nothing counting down and
|
|
136
180
|
# nothing in the delay lines has nothing to do on a cycle.
|
|
137
181
|
def idle?
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class CIA
|
|
5
|
+
# A snapshot of the chip: everything it holds but its wiring, the ports,
|
|
6
|
+
# the PB4 and CNT levels, the timers, the interrupt register, the serial
|
|
7
|
+
# port and the TOD clock.
|
|
8
|
+
module State
|
|
9
|
+
def save_state(out)
|
|
10
|
+
out.marker("CIA")
|
|
11
|
+
out.int(@data_port_a).int(@data_port_b).int(@data_dir_a).int(@data_dir_b)
|
|
12
|
+
out.boolean(@port_b4_high).boolean(@port_b4_driven_high).boolean(@cnt_high).boolean(@cnt_rise)
|
|
13
|
+
out.int(@control_a.value).int(@control_b.value)
|
|
14
|
+
@ta.save_state(out)
|
|
15
|
+
@tb.save_state(out)
|
|
16
|
+
@icr.save_state(out)
|
|
17
|
+
@serial.save_state(out)
|
|
18
|
+
@tod.save_state(out)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def load_state(input)
|
|
22
|
+
input.marker("CIA")
|
|
23
|
+
@data_port_a = input.int
|
|
24
|
+
@data_port_b = input.int
|
|
25
|
+
@data_dir_a = input.int
|
|
26
|
+
@data_dir_b = input.int
|
|
27
|
+
@port_b4_high = input.boolean?
|
|
28
|
+
@port_b4_driven_high = input.boolean?
|
|
29
|
+
@cnt_high = input.boolean?
|
|
30
|
+
@cnt_rise = input.boolean?
|
|
31
|
+
@control_a.value = input.int
|
|
32
|
+
@control_b.value = input.int
|
|
33
|
+
load_parts(input)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def load_parts(input)
|
|
39
|
+
@ta.load_state(input)
|
|
40
|
+
@tb.load_state(input)
|
|
41
|
+
@icr.load_state(input)
|
|
42
|
+
@serial.load_state(input)
|
|
43
|
+
@tod.load_state(input)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/badline/cia/timer.rb
CHANGED
|
@@ -42,6 +42,26 @@ module Badline
|
|
|
42
42
|
settle
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# The counter, the latch and the pipeline between them. The control
|
|
46
|
+
# register is the CIA's to save.
|
|
47
|
+
def save_state(out)
|
|
48
|
+
out.int(@counter).int(@latch).int(@pipe).int(@load_delay).boolean(@reload).boolean(@underflowed)
|
|
49
|
+
out.int(@oneshot_linger).boolean(@toggle).boolean(@settled).boolean(@empty)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def load_state(input)
|
|
53
|
+
@counter = input.int
|
|
54
|
+
@latch = input.int
|
|
55
|
+
@pipe = input.int
|
|
56
|
+
@load_delay = input.int
|
|
57
|
+
@reload = input.boolean?
|
|
58
|
+
@underflowed = input.boolean?
|
|
59
|
+
@oneshot_linger = input.int
|
|
60
|
+
@toggle = input.boolean?
|
|
61
|
+
@settled = input.boolean?
|
|
62
|
+
@empty = input.boolean?
|
|
63
|
+
end
|
|
64
|
+
|
|
45
65
|
def write_control(value)
|
|
46
66
|
@toggle = true if value.anybits?(0x01) && !started?
|
|
47
67
|
@oneshot_linger = 2 if control.run_mode? && value.nobits?(0x08)
|
|
@@ -59,6 +79,19 @@ module Badline
|
|
|
59
79
|
@counter = @latch unless started?
|
|
60
80
|
end
|
|
61
81
|
|
|
82
|
+
# Whether the timer's PB output toggle is high, and its count pipeline
|
|
83
|
+
# as two bits, the newer stage high.
|
|
84
|
+
attr_reader :toggle, :pipe
|
|
85
|
+
|
|
86
|
+
# Sets the control register without its load strobe, the toggle and
|
|
87
|
+
# the count pipeline, as a VICE snapshot gives them.
|
|
88
|
+
def restore_pipeline(control, toggle, pipe)
|
|
89
|
+
@control.value = control & ~0x10
|
|
90
|
+
@toggle = toggle
|
|
91
|
+
@pipe = pipe
|
|
92
|
+
settle
|
|
93
|
+
end
|
|
94
|
+
|
|
62
95
|
private
|
|
63
96
|
|
|
64
97
|
def started?
|
data/lib/badline/cia.rb
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
require "badline/cia/interrupt_register"
|
|
4
4
|
require "badline/cia/serial"
|
|
5
5
|
require "badline/cia/timer"
|
|
6
|
+
require "badline/cia/state"
|
|
6
7
|
|
|
7
8
|
module Badline
|
|
8
9
|
# CIA (Complex Interface Adapter) chip
|
|
9
10
|
class CIA
|
|
10
11
|
include Addressable
|
|
12
|
+
include State
|
|
11
13
|
|
|
12
14
|
# The original NMOS 6526 of the breadbin C64, and the HMOS 6526A of the
|
|
13
15
|
# C64C, which differs from it only in its interrupt control register.
|
|
@@ -19,6 +21,12 @@ module Badline
|
|
|
19
21
|
# read_a and read_b, and on CIA 1 drives PB4 through port_b4_high?.
|
|
20
22
|
attr_accessor :peripheral
|
|
21
23
|
|
|
24
|
+
# Timer A and timer B.
|
|
25
|
+
def timers = [@ta, @tb]
|
|
26
|
+
|
|
27
|
+
# Port A and B's data and direction registers.
|
|
28
|
+
def port_registers = [@data_port_a, @data_port_b, @data_dir_a, @data_dir_b]
|
|
29
|
+
|
|
22
30
|
def interrupt_status = @icr.status
|
|
23
31
|
|
|
24
32
|
def interrupt_control = @icr.mask
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Computer
|
|
5
|
+
# What plugs into the machine besides its chips, the cartridge, the
|
|
6
|
+
# disk device 8 serves through the traps, a true 1541 and an REU, and
|
|
7
|
+
# their state for a snapshot.
|
|
8
|
+
module Attachments
|
|
9
|
+
# Puts the cartridge in the expansion port and wires its clock and NMI
|
|
10
|
+
# line to the machine, leaving the machine's state as it is. A
|
|
11
|
+
# restored snapshot's cartridge comes back through here.
|
|
12
|
+
def connect_cartridge(cartridge)
|
|
13
|
+
cartridge.clock = -> { @cycles }
|
|
14
|
+
cartridge.on_nmi_change { |level| @cartridge_nmi = level }
|
|
15
|
+
address_bus.attach_cartridge(cartridge)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Puts the storage in device 8. Mounting again swaps the disk at any
|
|
19
|
+
# point while the machine runs: the drive keeps its RAM, which only a
|
|
20
|
+
# drive reset clears, and its status.
|
|
21
|
+
def mount(storage)
|
|
22
|
+
return @drive.insert(storage) if @drive
|
|
23
|
+
|
|
24
|
+
@drive = KernalTrap::Drive.new(storage)
|
|
25
|
+
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
|
|
26
|
+
cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
|
|
27
|
+
@serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive, device: serial_trap_device).install
|
|
28
|
+
save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
|
|
29
|
+
cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Takes device 8's mounted storage out, and with it the LOAD, SAVE and
|
|
33
|
+
# serial traps, so the KERNAL's routines go out over the serial bus.
|
|
34
|
+
# Mounting again starts a new drive, with its RAM cleared.
|
|
35
|
+
def unmount
|
|
36
|
+
return unless @drive
|
|
37
|
+
|
|
38
|
+
cpu.remove_trap(KernalTrap::Load::ADDRESS)
|
|
39
|
+
cpu.remove_trap(KernalTrap::Save::ADDRESS)
|
|
40
|
+
@serial_trap.device = nil
|
|
41
|
+
@serial_trap = nil
|
|
42
|
+
@save_trap = nil
|
|
43
|
+
@drive = nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Whether device 8 serves a disk or directory through the traps.
|
|
47
|
+
def mounted? = !@drive.nil?
|
|
48
|
+
|
|
49
|
+
# Unplugs the Drive1541, leaving the serial bus with nothing on it.
|
|
50
|
+
def detach_drive1541
|
|
51
|
+
return unless @drive1541
|
|
52
|
+
|
|
53
|
+
@iec_bus&.detach(@drive1541)
|
|
54
|
+
@drive1541 = nil
|
|
55
|
+
@serial_trap&.device = serial_trap_device
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def save_cartridge(out)
|
|
61
|
+
cartridge = address_bus.cartridge
|
|
62
|
+
out.boolean(!cartridge.nil?)
|
|
63
|
+
cartridge&.save_setup(out)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A cartridge comes back built afresh from the setup it was built
|
|
67
|
+
# with, before the address bus puts its state into it.
|
|
68
|
+
def load_cartridge(input)
|
|
69
|
+
if input.boolean?
|
|
70
|
+
connect_cartridge(Cartridge.from_setup(input))
|
|
71
|
+
elsif address_bus.cartridge
|
|
72
|
+
address_bus.detach_cartridge
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The disk device 8 serves through the traps, with the traps' own
|
|
77
|
+
# state: open channels, the drive's status and RAM, a SAVE under way.
|
|
78
|
+
def save_trap_drive(out)
|
|
79
|
+
out.boolean(!@drive.nil?)
|
|
80
|
+
return unless @drive
|
|
81
|
+
|
|
82
|
+
@drive.save_state(out)
|
|
83
|
+
@serial_trap.save_state(out)
|
|
84
|
+
@save_trap.save_state(out)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def load_trap_drive(input)
|
|
88
|
+
return unmount unless input.boolean?
|
|
89
|
+
|
|
90
|
+
storage = Storage.reopen(input)
|
|
91
|
+
unmount
|
|
92
|
+
mount(storage)
|
|
93
|
+
@drive.load_state(input)
|
|
94
|
+
@serial_trap.load_state(input)
|
|
95
|
+
@save_trap.load_state(input)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The serial bus, whose presence decides what CIA 2's port A reads,
|
|
99
|
+
# and a true 1541 on it.
|
|
100
|
+
def save_serial_bus(out)
|
|
101
|
+
out.boolean(!@iec_bus.nil?).boolean(!@drive1541.nil?)
|
|
102
|
+
return unless @drive1541
|
|
103
|
+
|
|
104
|
+
out.int(@drive1541.device)
|
|
105
|
+
@drive1541.save_state(out)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def load_serial_bus(input)
|
|
109
|
+
iec_bus if input.boolean?
|
|
110
|
+
return detach_drive1541 unless input.boolean?
|
|
111
|
+
|
|
112
|
+
device = input.int
|
|
113
|
+
detach_drive1541 if @drive1541 && @drive1541.device != device
|
|
114
|
+
attach_drive1541(Drive1541.new(device:)) unless @drive1541
|
|
115
|
+
@drive1541.load_state(input)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The REU, which the setup says the machine has, and whether it has
|
|
119
|
+
# asked for the bus. Its IRQ line reaches the CPU as it was.
|
|
120
|
+
def save_reu(out)
|
|
121
|
+
@reu&.save_state(out)
|
|
122
|
+
out.boolean(@dma)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def load_reu(input)
|
|
126
|
+
@reu&.load_state(input)
|
|
127
|
+
@reu_irq = @reu ? @reu.irq? : false
|
|
128
|
+
@dma = input.boolean?
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -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
|
|
@@ -249,12 +226,15 @@ module Badline
|
|
|
249
226
|
@cpu.pending_write? || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
|
|
250
227
|
end
|
|
251
228
|
|
|
229
|
+
# The REU's /DMA line takes the bus from the CPU from the cycle after
|
|
230
|
+
# it asks for it, even while it waits for BA, but RDY only halts the
|
|
231
|
+
# CPU on a read, so a write it makes then goes nowhere.
|
|
252
232
|
def dma_cycle!
|
|
253
233
|
writing = @cpu.pending_write?
|
|
254
|
-
@reu.dma_cycle!(@vic.
|
|
255
|
-
return @cpu.stall! if @reu.
|
|
234
|
+
@reu.dma_cycle!(@vic.ba_low?)
|
|
235
|
+
return writing ? @address_bus.cycle_cpu_off_bus(@cpu) : @cpu.stall! if @reu.dma?
|
|
256
236
|
|
|
257
|
-
@dma =
|
|
237
|
+
@dma = false
|
|
258
238
|
writing || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
|
|
259
239
|
end
|
|
260
240
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class CPU
|
|
5
|
+
# The steps at an instruction boundary: the opcode fetch that starts
|
|
6
|
+
# an instruction, and the ends of one.
|
|
7
|
+
module Boundary
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# The first cycle of every instruction. Starts an interrupt if one was
|
|
11
|
+
# pending when the previous instruction ended. Otherwise reads the
|
|
12
|
+
# opcode and switches to its plan.
|
|
13
|
+
def op_fetch
|
|
14
|
+
return start_interrupt if @boundary_nmi || @boundary_irq
|
|
15
|
+
|
|
16
|
+
run_traps
|
|
17
|
+
opcode = @memory.peek(@program_counter)
|
|
18
|
+
log(opcode) if @debug
|
|
19
|
+
micro = MICROCODE[opcode]
|
|
20
|
+
@operation = micro.operation
|
|
21
|
+
@plan = micro.plan
|
|
22
|
+
@writes = micro.writes
|
|
23
|
+
@optional_dummy = micro.optional_dummy
|
|
24
|
+
@index = 1
|
|
25
|
+
@boundary_crossed = false
|
|
26
|
+
@program_counter = (@program_counter + 1) & 0xffff
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def end_instruction
|
|
30
|
+
@instructions += 1
|
|
31
|
+
end_sequence
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Makes the next cycle an opcode fetch, and saves the pending interrupt
|
|
35
|
+
# flags for op_fetch. The next cycle polls before op_fetch runs, which
|
|
36
|
+
# updates the live flags.
|
|
37
|
+
def end_sequence
|
|
38
|
+
@plan = FETCH_PLAN
|
|
39
|
+
@writes = FETCH_WRITES
|
|
40
|
+
@index = 0
|
|
41
|
+
@boundary_irq = @irq_pending
|
|
42
|
+
@boundary_nmi = @nmi_pending
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def log(opcode)
|
|
46
|
+
instruction = Instruction.find(opcode)
|
|
47
|
+
puts(
|
|
48
|
+
"#{@cycles}: PC: #{format16(@program_counter)} - " \
|
|
49
|
+
"#{instruction.name.upcase} #{instruction.addressing_mode}"
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|