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
|
@@ -27,7 +27,8 @@ module Badline
|
|
|
27
27
|
#
|
|
28
28
|
# Such a pass runs the same way again from where it ended, for as
|
|
29
29
|
# long as ATN holds still and no counter sets a flag, so the drive
|
|
30
|
-
# sleeps: host_cycle! only counts the host cycles that go by
|
|
30
|
+
# sleeps: host_cycle! only counts the host cycles that go by (see
|
|
31
|
+
# Sleep). It wakes
|
|
31
32
|
# when ATN moves, when a counter is due to set a flag, or when something
|
|
32
33
|
# outside reads or changes the drive (the readers in Drive1541 call
|
|
33
34
|
# settle!). Waking counts off the whole passes it owes in bulk,
|
|
@@ -41,6 +42,9 @@ module Badline
|
|
|
41
42
|
# A pass starts at $EBFF, where the CPU always comes from the loop's
|
|
42
43
|
# last instruction, a JMP, so what that leaves in the CPU's working
|
|
43
44
|
# registers is the same each time round.
|
|
45
|
+
#
|
|
46
|
+
# The timer's interrupts come round too, and once they do, the drive
|
|
47
|
+
# sleeps on through them (see Orbit).
|
|
44
48
|
module Idle
|
|
45
49
|
# Where the DOS 2.6 idle loop starts over.
|
|
46
50
|
IDLE_LOOP = 0xebff
|
|
@@ -54,38 +58,15 @@ module Badline
|
|
|
54
58
|
@idle_skip = on
|
|
55
59
|
end
|
|
56
60
|
|
|
57
|
-
# Whether the drive is skipping its idle loop right now.
|
|
58
|
-
def asleep? = @asleep
|
|
59
|
-
|
|
60
|
-
# Brings the drive up to the present, cycle for cycle, and drops the
|
|
61
|
-
# pass it was recording, since whoever called may change it. Every
|
|
62
|
-
# reader of the drive's parts calls this.
|
|
63
|
-
def settle!
|
|
64
|
-
if @asleep
|
|
65
|
-
wake!
|
|
66
|
-
@serial_port.latch_host
|
|
67
|
-
end
|
|
68
|
-
stop_recording if @recording
|
|
69
|
-
end
|
|
70
|
-
|
|
71
61
|
private
|
|
72
62
|
|
|
73
|
-
# A host cycle asleep.
|
|
74
|
-
def doze
|
|
75
|
-
@slept += 1
|
|
76
|
-
settle! if @slept == @wake_at || @serial_port.atn_moved?
|
|
77
|
-
end
|
|
78
|
-
|
|
79
63
|
def init_idle(debug)
|
|
80
64
|
@idle_skip = !debug
|
|
81
|
-
@asleep = false
|
|
82
65
|
@recording = false
|
|
83
|
-
@owed = 0
|
|
84
|
-
@budget = 0
|
|
85
|
-
@slept = 0
|
|
86
|
-
@wake_at = 0
|
|
87
66
|
@pass_cycles = 0
|
|
88
67
|
@pass_instructions = 0
|
|
68
|
+
init_sleep
|
|
69
|
+
init_orbits
|
|
89
70
|
end
|
|
90
71
|
|
|
91
72
|
# The CPU is at $EBFF. At an instruction boundary, the drive sleeps if
|
|
@@ -130,39 +111,10 @@ module Badline
|
|
|
130
111
|
@pass_cycles = @cycles - @record_cycles
|
|
131
112
|
@pass_instructions = @cpu.instructions - @record_instructions
|
|
132
113
|
@asleep = true
|
|
114
|
+
@host_still = false
|
|
133
115
|
@owed = 0
|
|
134
116
|
@budget = quiet_cycles
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
# Asleep, host_cycle! counts host cycles, and leaves it to wake! to
|
|
138
|
-
# turn them into drive cycles through the phase accumulator. This sets
|
|
139
|
-
# the host cycle that brings the drive cycles owed up to the budget.
|
|
140
|
-
def plan_wake
|
|
141
|
-
@slept = 0
|
|
142
|
-
needed = ((@budget - @owed) * @host_clock_hz) - @phase
|
|
143
|
-
@wake_at = needed.positive? ? (needed + CLOCK_HZ - 1) / CLOCK_HZ : 1
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Runs the cycles owed: whole passes in bulk, up to the budget, and
|
|
147
|
-
# the rest one by one.
|
|
148
|
-
def wake!
|
|
149
|
-
@asleep = false
|
|
150
|
-
phase = @phase + (@slept * CLOCK_HZ)
|
|
151
|
-
@phase = phase % @host_clock_hz
|
|
152
|
-
owed = @owed + (phase / @host_clock_hz)
|
|
153
|
-
@owed = 0
|
|
154
|
-
@slept = 0
|
|
155
|
-
span = [owed, @budget].min
|
|
156
|
-
passes = span / @pass_cycles
|
|
157
|
-
if passes.positive?
|
|
158
|
-
cycles = passes * @pass_cycles
|
|
159
|
-
@cpu.fast_forward(cycles, passes * @pass_instructions)
|
|
160
|
-
@via1.fast_forward(cycles)
|
|
161
|
-
@via2.fast_forward(cycles)
|
|
162
|
-
@cycles += cycles
|
|
163
|
-
owed -= cycles
|
|
164
|
-
end
|
|
165
|
-
owed.times { step }
|
|
117
|
+
anchor
|
|
166
118
|
end
|
|
167
119
|
|
|
168
120
|
def quiet_cycles
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "badline/drive1541/rotation"
|
|
4
|
+
require "badline/drive1541/mechanism_state"
|
|
4
5
|
|
|
5
6
|
module Badline
|
|
6
7
|
class Drive1541
|
|
@@ -65,6 +66,7 @@ module Badline
|
|
|
65
66
|
# half track after it.
|
|
66
67
|
class Mechanism
|
|
67
68
|
include Rotation
|
|
69
|
+
include MechanismState
|
|
68
70
|
|
|
69
71
|
MOTOR = 0x04
|
|
70
72
|
LED = 0x08
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# Saving and restoring the Mechanism for a snapshot.
|
|
6
|
+
module MechanismState
|
|
7
|
+
# The disk (Disk#save_state) and everything the head, the motor, the
|
|
8
|
+
# stepper and the read and write electronics hold, down to the cell
|
|
9
|
+
# under the head and the read clock's phase.
|
|
10
|
+
def save_state(out)
|
|
11
|
+
out.boolean(!@disk.nil?)
|
|
12
|
+
@disk&.save_state(out)
|
|
13
|
+
[@half_track, @slip, @zone, @clock, @time, @clock_at, @clocks, @shift, @ones, @bits, @write_shift,
|
|
14
|
+
@index, @mask, @cell_end, @width, @next].each { |value| out.int(value) }
|
|
15
|
+
[@motor, @led, @sync, @byte_ready, @writing, @write_gate, @protected, @track_written].each do |flag|
|
|
16
|
+
out.boolean(flag)
|
|
17
|
+
end
|
|
18
|
+
out.optional_int(@write_index)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Puts the state back, reusing the disk in when it's the one the state
|
|
22
|
+
# names (Disk::State.load). A disk taken out is flushed, as insert
|
|
23
|
+
# does.
|
|
24
|
+
def load_state(input)
|
|
25
|
+
disk = input.boolean? ? Disk::State.load(input, @disk) : nil
|
|
26
|
+
flush unless disk.equal?(@disk)
|
|
27
|
+
@disk = disk
|
|
28
|
+
load_head(input)
|
|
29
|
+
load_flags(input)
|
|
30
|
+
@write_index = input.optional_int
|
|
31
|
+
head_on_track
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def load_head(input)
|
|
37
|
+
@half_track = input.int
|
|
38
|
+
@slip = input.int
|
|
39
|
+
@zone = input.int
|
|
40
|
+
@clock = input.int
|
|
41
|
+
@time = input.int
|
|
42
|
+
@clock_at = input.int
|
|
43
|
+
@clocks = input.int
|
|
44
|
+
@shift = input.int
|
|
45
|
+
@ones = input.int
|
|
46
|
+
@bits = input.int
|
|
47
|
+
@write_shift = input.int
|
|
48
|
+
@index = input.int
|
|
49
|
+
@mask = input.int
|
|
50
|
+
@cell_end = input.int
|
|
51
|
+
@width = input.int
|
|
52
|
+
@next = input.int
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def load_flags(input)
|
|
56
|
+
@motor = input.boolean?
|
|
57
|
+
@led = input.boolean?
|
|
58
|
+
@sync = input.boolean?
|
|
59
|
+
@byte_ready = input.boolean?
|
|
60
|
+
@writing = input.boolean?
|
|
61
|
+
@write_gate = input.boolean?
|
|
62
|
+
@protected = input.boolean?
|
|
63
|
+
@track_written = input.boolean?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# Sleeping through the DOS's timer interrupts, exactly, once they come
|
|
6
|
+
# round.
|
|
7
|
+
#
|
|
8
|
+
# VIA 2's timer 1 interrupts the idle loop for the job loop, which
|
|
9
|
+
# finds no job, restarts the timer and returns into the pass it cut
|
|
10
|
+
# into, 14,998 cycles and more apart. The interrupts land at a
|
|
11
|
+
# different point in the pass each time, until, six of them on, the
|
|
12
|
+
# whole drive is back where it was: an orbit of 90,044 cycles.
|
|
13
|
+
#
|
|
14
|
+
# Each place the drive falls asleep (see Idle) is an anchor, and the
|
|
15
|
+
# drive keeps everything it holds at each one: RAM, the counters of
|
|
16
|
+
# the timers that are armed or that it read or wrote since, and the
|
|
17
|
+
# rest as Idle compares a pass. It keeps them only while it keeps to
|
|
18
|
+
# itself, with Bus#guard! checking that it doesn't read the serial
|
|
19
|
+
# bus, write VIA 1, turn the motor on or change the LED. ATN moving,
|
|
20
|
+
# or a reader settling the drive, forgets them.
|
|
21
|
+
#
|
|
22
|
+
# Coming to an anchor it kept before closes an orbit, which runs the
|
|
23
|
+
# same way again from here for as long as ATN holds still, so the
|
|
24
|
+
# drive sleeps on without waking for its counters. Waking counts off
|
|
25
|
+
# the whole orbits it owes in bulk, moving on only the counters it
|
|
26
|
+
# never touched, and runs the rest as Idle does.
|
|
27
|
+
module Orbit
|
|
28
|
+
# How many anchors the drive keeps while it looks for an orbit.
|
|
29
|
+
ANCHORS = 32
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def init_orbits
|
|
34
|
+
@anchors = {}
|
|
35
|
+
@orbit_cycles = nil
|
|
36
|
+
@orbit_instructions = 0
|
|
37
|
+
@bus.guard!
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The drive falls asleep at an anchor. One it came to before closes
|
|
41
|
+
# an orbit: everything from there to here runs the same way again.
|
|
42
|
+
def anchor
|
|
43
|
+
forget_orbits if @bus.tainted || @anchors.size >= ANCHORS
|
|
44
|
+
key = anchor_state
|
|
45
|
+
if (seen = @anchors[key])
|
|
46
|
+
@orbit_cycles = @cycles - seen[0]
|
|
47
|
+
@orbit_instructions = @cpu.instructions - seen[1]
|
|
48
|
+
else
|
|
49
|
+
@anchors[key] = [@cycles, @cpu.instructions]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def forget_orbits
|
|
54
|
+
@anchors.clear
|
|
55
|
+
@orbit_cycles = nil
|
|
56
|
+
@bus.guard!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Runs whole orbits, which bring the drive back to the anchor it
|
|
60
|
+
# sleeps at but for the counters it never touched. Returns the
|
|
61
|
+
# cycles.
|
|
62
|
+
def skip_orbits(orbits)
|
|
63
|
+
cycles = orbits * @orbit_cycles
|
|
64
|
+
touched = @bus.touched_timers
|
|
65
|
+
@cpu.fast_forward(cycles, orbits * @orbit_instructions)
|
|
66
|
+
@via1.skip_orbits(cycles, touched & 0x03)
|
|
67
|
+
@via2.skip_orbits(cycles, touched >> 2)
|
|
68
|
+
@cycles += cycles
|
|
69
|
+
cycles
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Everything the drive holds at an anchor, but for the counters of
|
|
73
|
+
# the unarmed timers it hasn't touched since it was last tainted.
|
|
74
|
+
def anchor_state
|
|
75
|
+
touched = @bus.touched_timers
|
|
76
|
+
[*idle_state, *@via1.counter_state(touched & 0x03), *@via2.counter_state(touched >> 2),
|
|
77
|
+
@bus.ram.snapshot]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -92,6 +92,16 @@ module Badline
|
|
|
92
92
|
@writing ? write_bit(at) : read_bit(one)
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
# The track under the head and its layout, leaving the head's place
|
|
96
|
+
# on it as it stands.
|
|
97
|
+
def head_on_track
|
|
98
|
+
@track = @disk&.track(@half_track) || BLANK
|
|
99
|
+
@bytes = @track.bytes
|
|
100
|
+
@widths = @track.widths
|
|
101
|
+
@length = @track.length
|
|
102
|
+
@last = @length - 1
|
|
103
|
+
end
|
|
104
|
+
|
|
95
105
|
# The track under the head, from the cell that's under it +time+
|
|
96
106
|
# into the turn.
|
|
97
107
|
def load_track(time = @time)
|
|
@@ -44,6 +44,16 @@ module Badline
|
|
|
44
44
|
@host = @bus ? @bus.host_lines : 0
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# The C64's lines as last latched. The jumpers and the bus are the
|
|
48
|
+
# drive's wiring.
|
|
49
|
+
def save_state(out)
|
|
50
|
+
out.int(@host)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def load_state(input)
|
|
54
|
+
@host = input.int
|
|
55
|
+
end
|
|
56
|
+
|
|
47
57
|
# Whether the C64 has moved ATN since the last latch_host.
|
|
48
58
|
def atn_moved?
|
|
49
59
|
lines = @bus ? @bus.host_lines : 0
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# The drive asleep, as Idle and Orbit let it: host_cycle! counts the
|
|
6
|
+
# host cycles that go by, and waking turns them into drive cycles
|
|
7
|
+
# through the phase accumulator and runs them. Whole orbits and whole
|
|
8
|
+
# passes go in bulk, and the rest cycle by cycle, which may find the
|
|
9
|
+
# idle loop and put the drive back to sleep.
|
|
10
|
+
#
|
|
11
|
+
# The drive looks at ATN only when the C64 may have moved it, on a
|
|
12
|
+
# bus that says when (IECBus#notifies?), and every host cycle on one
|
|
13
|
+
# that doesn't.
|
|
14
|
+
module Sleep
|
|
15
|
+
# Whether the drive is skipping its idle loop right now.
|
|
16
|
+
def asleep? = @asleep
|
|
17
|
+
|
|
18
|
+
# Brings the drive up to the present, cycle for cycle, and drops the
|
|
19
|
+
# pass it was recording and the orbits it found, since whoever called
|
|
20
|
+
# may change it. Every reader of the drive's parts calls this.
|
|
21
|
+
def settle!
|
|
22
|
+
if @asleep
|
|
23
|
+
wake!
|
|
24
|
+
@serial_port.latch_host
|
|
25
|
+
@asleep = false
|
|
26
|
+
@orbit_cycles = nil
|
|
27
|
+
end
|
|
28
|
+
stop_recording if @recording
|
|
29
|
+
forget_orbits
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def init_sleep
|
|
35
|
+
@asleep = false
|
|
36
|
+
@host_still = false
|
|
37
|
+
@owed = 0
|
|
38
|
+
@budget = 0
|
|
39
|
+
@slept = 0
|
|
40
|
+
@wake_at = 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A host cycle asleep that may wake the drive. The bus says whether
|
|
44
|
+
# the next ones need a look at ATN.
|
|
45
|
+
def doze
|
|
46
|
+
@host_still = @serial_bus.notifies?
|
|
47
|
+
if @serial_port.atn_moved?
|
|
48
|
+
settle!
|
|
49
|
+
elsif @slept == @wake_at
|
|
50
|
+
wake!
|
|
51
|
+
@serial_port.latch_host
|
|
52
|
+
plan_wake if @asleep
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Asleep, host_cycle! counts host cycles, and leaves it to wake! to
|
|
57
|
+
# turn them into drive cycles through the phase accumulator. This sets
|
|
58
|
+
# the host cycle that brings the drive cycles owed up to the budget.
|
|
59
|
+
# On an orbit the drive sleeps until something outside wakes it:
|
|
60
|
+
# @slept counts from 1, so it never comes to 0.
|
|
61
|
+
def plan_wake
|
|
62
|
+
@slept = 0
|
|
63
|
+
return @wake_at = 0 if @orbit_cycles
|
|
64
|
+
|
|
65
|
+
needed = ((@budget - @owed) * @host_clock_hz) - @phase
|
|
66
|
+
@wake_at = needed.positive? ? (needed + CLOCK_HZ - 1) / CLOCK_HZ : 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Runs the cycles owed.
|
|
70
|
+
def wake!
|
|
71
|
+
phase = @phase + (@slept * CLOCK_HZ)
|
|
72
|
+
@phase = phase % @host_clock_hz
|
|
73
|
+
owed = @owed + (phase / @host_clock_hz)
|
|
74
|
+
@owed = 0
|
|
75
|
+
@slept = 0
|
|
76
|
+
catch_up(owed)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Runs +owed+ drive cycles: whole orbits and whole passes in bulk,
|
|
80
|
+
# passes only up to the budget, and the rest cycle by cycle, which
|
|
81
|
+
# may put the drive back to sleep.
|
|
82
|
+
def catch_up(owed)
|
|
83
|
+
while owed.positive?
|
|
84
|
+
if @asleep
|
|
85
|
+
if @orbit_cycles && owed >= @orbit_cycles
|
|
86
|
+
owed -= skip_orbits(owed / @orbit_cycles)
|
|
87
|
+
next
|
|
88
|
+
end
|
|
89
|
+
@orbit_cycles = nil
|
|
90
|
+
passes = [owed, @budget].min / @pass_cycles
|
|
91
|
+
if passes.positive?
|
|
92
|
+
owed -= skip_passes(passes)
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
@asleep = false
|
|
96
|
+
end
|
|
97
|
+
run_cycle
|
|
98
|
+
owed -= 1
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Runs whole passes, moving the counters on. Returns the cycles.
|
|
103
|
+
def skip_passes(passes)
|
|
104
|
+
cycles = passes * @pass_cycles
|
|
105
|
+
@cpu.fast_forward(cycles, passes * @pass_instructions)
|
|
106
|
+
@via1.fast_forward(cycles)
|
|
107
|
+
@via2.fast_forward(cycles)
|
|
108
|
+
@cycles += cycles
|
|
109
|
+
@budget -= cycles
|
|
110
|
+
cycles
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/badline/drive1541.rb
CHANGED
|
@@ -7,7 +7,9 @@ require "badline/drive1541/track"
|
|
|
7
7
|
require "badline/drive1541/disk"
|
|
8
8
|
require "badline/drive1541/mechanism"
|
|
9
9
|
require "badline/drive1541/disk_via"
|
|
10
|
+
require "badline/drive1541/sleep"
|
|
10
11
|
require "badline/drive1541/idle"
|
|
12
|
+
require "badline/drive1541/orbit"
|
|
11
13
|
|
|
12
14
|
module Badline
|
|
13
15
|
# The 1541 disk drive as a machine of its own: a 6502 running the DOS
|
|
@@ -27,9 +29,12 @@ module Badline
|
|
|
27
29
|
# While the DOS idles, host_cycle! skips the drive's cycles and catches
|
|
28
30
|
# up on them later, exactly (see Idle). The readers of the drive's parts
|
|
29
31
|
# catch up first, so they find the drive as running every cycle would
|
|
30
|
-
# have left it.
|
|
32
|
+
# have left it. Once the DOS's timer interrupts come round, the drive
|
|
33
|
+
# sleeps through them too (see Orbit).
|
|
31
34
|
class Drive1541
|
|
35
|
+
include Sleep
|
|
32
36
|
include Idle
|
|
37
|
+
include Orbit
|
|
33
38
|
|
|
34
39
|
CLOCK_HZ = 1_000_000
|
|
35
40
|
|
|
@@ -143,8 +148,14 @@ module Badline
|
|
|
143
148
|
# The host has run this cycle already, and the drive sees what it did
|
|
144
149
|
# from the next one on (see SerialPort). Asleep, the drive owes the
|
|
145
150
|
# cycles instead, until ATN moves or its counters are due (see Idle).
|
|
151
|
+
# It looks at ATN only after the C64 wrote CIA 2's port A, on a bus
|
|
152
|
+
# that says so (see host_written!).
|
|
146
153
|
def host_cycle!
|
|
147
|
-
|
|
154
|
+
if @asleep
|
|
155
|
+
return if (@slept += 1) != @wake_at && @host_still
|
|
156
|
+
|
|
157
|
+
return doze
|
|
158
|
+
end
|
|
148
159
|
|
|
149
160
|
phase = @phase + CLOCK_HZ
|
|
150
161
|
while phase >= @host_clock_hz
|
|
@@ -157,6 +168,11 @@ module Badline
|
|
|
157
168
|
plan_wake if @asleep
|
|
158
169
|
end
|
|
159
170
|
|
|
171
|
+
# The C64 wrote CIA 2's port A, so ATN may have moved.
|
|
172
|
+
def host_written!
|
|
173
|
+
@host_still = false
|
|
174
|
+
end
|
|
175
|
+
|
|
160
176
|
# Runs one drive cycle, catching up first on any the drive owes (see
|
|
161
177
|
# step).
|
|
162
178
|
def cycle!
|
|
@@ -164,6 +180,40 @@ module Badline
|
|
|
164
180
|
step
|
|
165
181
|
end
|
|
166
182
|
|
|
183
|
+
# The drive's whole state: its clock phase, the CPU, RAM, both VIAs,
|
|
184
|
+
# the serial port's latch of the C64's lines and the mechanism with its
|
|
185
|
+
# disk. The drive settles first, so it is awake and not recording a
|
|
186
|
+
# pass, and a restored drive starts the same way, its orbits forgotten
|
|
187
|
+
# from the state it takes: Idle's and Orbit's bookkeeping
|
|
188
|
+
# isn't stored. The device number, the host's clock rate and whether
|
|
189
|
+
# the drive may sleep are the machine's wiring.
|
|
190
|
+
def save_state(out)
|
|
191
|
+
settle!
|
|
192
|
+
out.marker("DRIVE1541")
|
|
193
|
+
out.int(@phase).int(@cycles).boolean(@so_pending)
|
|
194
|
+
@serial_port.save_state(out)
|
|
195
|
+
@via1.save_state(out)
|
|
196
|
+
@via2.save_state(out)
|
|
197
|
+
@bus.save_state(out)
|
|
198
|
+
@cpu.save_state(out)
|
|
199
|
+
@mechanism.save_state(out)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def load_state(input)
|
|
203
|
+
settle!
|
|
204
|
+
input.marker("DRIVE1541")
|
|
205
|
+
@phase = input.int
|
|
206
|
+
@cycles = input.int
|
|
207
|
+
@so_pending = input.boolean?
|
|
208
|
+
@serial_port.load_state(input)
|
|
209
|
+
@via1.load_state(input)
|
|
210
|
+
@via2.load_state(input)
|
|
211
|
+
@bus.load_state(input)
|
|
212
|
+
@cpu.load_state(input)
|
|
213
|
+
@mechanism.load_state(input)
|
|
214
|
+
forget_orbits
|
|
215
|
+
end
|
|
216
|
+
|
|
167
217
|
def inspect
|
|
168
218
|
"#<#{self.class.name} cycles=#{cycles} cpu=(#{cpu.inspect})>"
|
|
169
219
|
end
|
|
@@ -6,6 +6,9 @@ module Badline
|
|
|
6
6
|
TITLE = "Badline"
|
|
7
7
|
TOGGLE_SYM = SDL::KEY_TAB
|
|
8
8
|
MUTE_SYM = SDL::KEY_F10
|
|
9
|
+
SAVE_SYM = SDL::KEY_F11
|
|
10
|
+
RESTORE_SYM = SDL::KEY_F12
|
|
11
|
+
HOST_SYMS = [TOGGLE_SYM, MUTE_SYM, SAVE_SYM, RESTORE_SYM].freeze
|
|
9
12
|
REVERSE_MOD = SDL::KMOD_SHIFT
|
|
10
13
|
|
|
11
14
|
SHARED_KEYS = %i[cursor_up cursor_left cursor_h cursor_v space w a s d lshift].freeze
|
|
@@ -34,14 +37,12 @@ module Badline
|
|
|
34
37
|
# options (autostart:, song:, disk:) go to Media.attach.
|
|
35
38
|
def initialize(media_path: nil, machine: {}, sound: false, verbose: false, **media)
|
|
36
39
|
@verbose = verbose
|
|
37
|
-
@
|
|
38
|
-
|
|
39
|
-
puts Media.attach(@computer, media_path, **media) if media_path
|
|
40
|
+
@snapshots = Snapshots.new
|
|
41
|
+
@computer = @snapshots.boot(media_path, machine, media)
|
|
40
42
|
|
|
41
43
|
@mode = :keyboard
|
|
42
44
|
@pot_device = nil
|
|
43
|
-
@panes =
|
|
44
|
-
@panes << DriveLedPane.new(@computer.drive1541, @panes.first) if @computer.drive1541
|
|
45
|
+
@panes = panes
|
|
45
46
|
@stream = open_stream if sound
|
|
46
47
|
@paced = ENV["NOVSYNC"].nil?
|
|
47
48
|
@window = Window.new(
|
|
@@ -94,8 +95,7 @@ module Badline
|
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
def handle_key_down(event)
|
|
97
|
-
return
|
|
98
|
-
return toggle_mute if event.sym == MUTE_SYM
|
|
98
|
+
return host_key(event) if HOST_SYMS.include?(event.sym)
|
|
99
99
|
|
|
100
100
|
port, dir = JoyMap.parse(event) if @mode == :joystick
|
|
101
101
|
if port
|
|
@@ -105,6 +105,16 @@ module Badline
|
|
|
105
105
|
end
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
# Tab, F10, F11 and F12 drive the front end, not the machine.
|
|
109
|
+
def host_key(event)
|
|
110
|
+
case event.sym
|
|
111
|
+
when TOGGLE_SYM then cycle_mode(event.mod.anybits?(REVERSE_MOD) ? -1 : 1)
|
|
112
|
+
when MUTE_SYM then toggle_mute
|
|
113
|
+
when SAVE_SYM then @snapshots.save(@computer) unless event.repeat
|
|
114
|
+
else restore_snapshot unless event.repeat
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
108
118
|
# RESTORE isn't in the key matrix, so it goes to the machine instead,
|
|
109
119
|
# once per press as the real key's one-shot fires.
|
|
110
120
|
def press_key(key, repeat)
|
|
@@ -115,7 +125,7 @@ module Badline
|
|
|
115
125
|
end
|
|
116
126
|
|
|
117
127
|
def handle_key_up(event)
|
|
118
|
-
return if
|
|
128
|
+
return if HOST_SYMS.include?(event.sym)
|
|
119
129
|
|
|
120
130
|
port, dir = JoyMap.parse(event) if @mode == :joystick
|
|
121
131
|
if port
|
|
@@ -155,6 +165,29 @@ module Badline
|
|
|
155
165
|
update_title
|
|
156
166
|
end
|
|
157
167
|
|
|
168
|
+
# Runs the restored machine in place of the one before: the screen,
|
|
169
|
+
# the drive LED, the gamepads and the sound go over to it. The input
|
|
170
|
+
# mode stays the host's, so a mouse or paddles go back in their port.
|
|
171
|
+
def restore_snapshot
|
|
172
|
+
computer = @snapshots.restore
|
|
173
|
+
return unless computer
|
|
174
|
+
|
|
175
|
+
release_inputs
|
|
176
|
+
@computer = computer
|
|
177
|
+
@panes = panes
|
|
178
|
+
@gamepads.computer = computer
|
|
179
|
+
@stream&.sid = computer.sid
|
|
180
|
+
attach_pot_device if POT_DEVICES.key?(@mode)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# The screen, and the drive LED in its border when a true drive is
|
|
184
|
+
# plugged in.
|
|
185
|
+
def panes
|
|
186
|
+
screen = ScreenPane.new(@computer)
|
|
187
|
+
drive = @computer.drive1541
|
|
188
|
+
drive ? [screen, DriveLedPane.new(drive, screen)] : [screen]
|
|
189
|
+
end
|
|
190
|
+
|
|
158
191
|
def update_title
|
|
159
192
|
tags = [MODES[@mode], @stream&.muted? && "MUTED"].select(&:itself)
|
|
160
193
|
@window.title = [TITLE, *tags.map { |tag| "[#{tag}]" }].join(" ")
|
|
@@ -169,12 +202,6 @@ module Badline
|
|
|
169
202
|
warn "badline: no sound, can't open the audio device: #{e.message}"
|
|
170
203
|
end
|
|
171
204
|
|
|
172
|
-
def machine_options(machine, media_path)
|
|
173
|
-
options = { sid_model: machine[:sid_model] || Media.sid_model(media_path), **machine.slice(:reu) }
|
|
174
|
-
options[:region] = Region::NTSC if machine[:ntsc]
|
|
175
|
-
options
|
|
176
|
-
end
|
|
177
|
-
|
|
178
205
|
def fit_frame
|
|
179
206
|
rate = @window.refresh_rate
|
|
180
207
|
clock_hz = @computer.region.clock_hz
|
data/lib/badline/gui/gamepads.rb
CHANGED
|
@@ -53,6 +53,13 @@ module Badline
|
|
|
53
53
|
.reject(&:null?)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# Wires the controllers to another machine's joysticks, which take
|
|
57
|
+
# what they hold on the next poll.
|
|
58
|
+
def computer=(computer)
|
|
59
|
+
@computer = computer
|
|
60
|
+
@pressed.map! { [] }
|
|
61
|
+
end
|
|
62
|
+
|
|
56
63
|
def poll
|
|
57
64
|
@controllers.each_with_index { |controller, index| apply(controller, index) }
|
|
58
65
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module GUI
|
|
5
|
+
# The window's snapshots: a .vsf opened as the media, F11 saving the
|
|
6
|
+
# machine to a new badline-<date>-<time>.vsf in the working directory,
|
|
7
|
+
# and F12 going back to the snapshot last saved or opened.
|
|
8
|
+
class Snapshots
|
|
9
|
+
def self.snapshot?(path) = !path.nil? && File.extname(path).casecmp?(".vsf")
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@last = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# The machine the window starts with: for a .vsf the machine it holds,
|
|
16
|
+
# built with that machine's chip models, which F12 goes back to, and
|
|
17
|
+
# otherwise one built from the machine options (Application.new) with
|
|
18
|
+
# the media attached.
|
|
19
|
+
def boot(media_path, machine, media)
|
|
20
|
+
return load(media_path) if Snapshots.snapshot?(media_path)
|
|
21
|
+
|
|
22
|
+
Computer.new(**machine_options(machine, media_path)).tap do |computer|
|
|
23
|
+
Media::TrueDrive.plug(computer) if machine[:true_drive]
|
|
24
|
+
puts Media.attach(computer, media_path, **media) if media_path
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A new machine, built as the snapshot's was and restored from it.
|
|
29
|
+
def load(path)
|
|
30
|
+
computer = Snapshot.load(path) { |line| puts line }
|
|
31
|
+
@last = path
|
|
32
|
+
puts "Restored #{path}"
|
|
33
|
+
computer
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Saves the machine, and warns when it can't.
|
|
37
|
+
def save(computer)
|
|
38
|
+
path = "badline-#{Time.now.strftime('%Y%m%d-%H%M%S')}.vsf"
|
|
39
|
+
computer.save_snapshot(path)
|
|
40
|
+
@last = path
|
|
41
|
+
puts "Saved #{path}, F12 restores it"
|
|
42
|
+
rescue Snapshot::FormatError, SystemCallError => e
|
|
43
|
+
warn "badline: #{path}: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A new machine restored from the snapshot last saved or opened, or
|
|
47
|
+
# nil when there is none or it fails, which it warns about.
|
|
48
|
+
def restore
|
|
49
|
+
return load(@last) if @last
|
|
50
|
+
|
|
51
|
+
puts "No snapshot to restore, F11 saves one"
|
|
52
|
+
nil
|
|
53
|
+
rescue Snapshot::FormatError, SystemCallError => e
|
|
54
|
+
warn "badline: #{@last}: #{e.message}"
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def machine_options(machine, media_path)
|
|
61
|
+
options = { sid_model: machine[:sid_model] || Media.sid_model(media_path), **machine.slice(:reu) }
|
|
62
|
+
options[:region] = Region::NTSC if machine[:ntsc]
|
|
63
|
+
options
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|