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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/CLAUDE.md +23 -23
  4. data/CONTRIBUTING.md +5 -0
  5. data/README.md +59 -1
  6. data/doc/pinned-behaviour.md +4 -2
  7. data/exe/badline-ruby +2 -1
  8. data/lib/badline/address_bus/saved_state.rb +72 -0
  9. data/lib/badline/address_bus.rb +1 -0
  10. data/lib/badline/audio/stream.rb +6 -0
  11. data/lib/badline/cartridge/action_replay.rb +15 -0
  12. data/lib/badline/cartridge/comal80.rb +8 -0
  13. data/lib/badline/cartridge/dinamic.rb +8 -0
  14. data/lib/badline/cartridge/easy_flash.rb +20 -1
  15. data/lib/badline/cartridge/epyx_fastload.rb +8 -0
  16. data/lib/badline/cartridge/final_cartridge3.rb +12 -0
  17. data/lib/badline/cartridge/flash.rb +41 -0
  18. data/lib/badline/cartridge/fun_play.rb +8 -0
  19. data/lib/badline/cartridge/g_mod2.rb +12 -0
  20. data/lib/badline/cartridge/game_system.rb +8 -0
  21. data/lib/badline/cartridge/geo_ram.rb +16 -0
  22. data/lib/badline/cartridge/magic_desk.rb +8 -0
  23. data/lib/badline/cartridge/ocean.rb +8 -0
  24. data/lib/badline/cartridge/pagefox.rb +12 -0
  25. data/lib/badline/cartridge/retro_replay.rb +44 -1
  26. data/lib/badline/cartridge/rgcd.rb +10 -0
  27. data/lib/badline/cartridge/super_games.rb +10 -0
  28. data/lib/badline/cartridge/zaxxon.rb +16 -0
  29. data/lib/badline/cartridge.rb +72 -0
  30. data/lib/badline/cia/interrupt_register.rb +17 -0
  31. data/lib/badline/cia/serial.rb +44 -0
  32. data/lib/badline/cia/state.rb +47 -0
  33. data/lib/badline/cia/timer.rb +33 -0
  34. data/lib/badline/cia.rb +24 -2
  35. data/lib/badline/computer/attachments.rb +132 -0
  36. data/lib/badline/computer/saved_state.rb +116 -0
  37. data/lib/badline/computer.rb +8 -31
  38. data/lib/badline/cpu/microcode.rb +8 -0
  39. data/lib/badline/cpu/saved_state.rb +115 -0
  40. data/lib/badline/cpu.rb +2 -0
  41. data/lib/badline/datasette.rb +27 -0
  42. data/lib/badline/drive1541/bus.rb +89 -13
  43. data/lib/badline/drive1541/disk.rb +7 -2
  44. data/lib/badline/drive1541/disk_state.rb +80 -0
  45. data/lib/badline/drive1541/idle.rb +9 -57
  46. data/lib/badline/drive1541/mechanism.rb +2 -0
  47. data/lib/badline/drive1541/mechanism_state.rb +67 -0
  48. data/lib/badline/drive1541/orbit.rb +81 -0
  49. data/lib/badline/drive1541/rotation.rb +10 -0
  50. data/lib/badline/drive1541/serial_port.rb +10 -0
  51. data/lib/badline/drive1541/sleep.rb +114 -0
  52. data/lib/badline/drive1541.rb +52 -2
  53. data/lib/badline/gui/application.rb +41 -14
  54. data/lib/badline/gui/gamepads.rb +7 -0
  55. data/lib/badline/gui/snapshots.rb +67 -0
  56. data/lib/badline/gui.rb +1 -0
  57. data/lib/badline/iec_bus.rb +16 -0
  58. data/lib/badline/kernal_trap/channel.rb +27 -0
  59. data/lib/badline/kernal_trap/drive/channels.rb +25 -0
  60. data/lib/badline/kernal_trap/drive/memory.rb +9 -0
  61. data/lib/badline/kernal_trap/drive/status.rb +8 -0
  62. data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
  63. data/lib/badline/kernal_trap/drive.rb +19 -0
  64. data/lib/badline/kernal_trap/save.rb +8 -0
  65. data/lib/badline/kernal_trap/serial.rb +16 -0
  66. data/lib/badline/memory.rb +11 -0
  67. data/lib/badline/options.rb +3 -2
  68. data/lib/badline/ram_expansion/plus256k.rb +16 -0
  69. data/lib/badline/ram_expansion/plus60k.rb +12 -0
  70. data/lib/badline/ram_expansion/unexpanded.rb +6 -0
  71. data/lib/badline/reu/dma.rb +36 -0
  72. data/lib/badline/reu/ram.rb +15 -0
  73. data/lib/badline/reu/saved_state.rb +52 -0
  74. data/lib/badline/reu.rb +4 -0
  75. data/lib/badline/sdl/events.rb +2 -0
  76. data/lib/badline/sid/envelope/state.rb +68 -0
  77. data/lib/badline/sid/envelope.rb +7 -0
  78. data/lib/badline/sid/filter.rb +38 -0
  79. data/lib/badline/sid/voice.rb +10 -0
  80. data/lib/badline/sid/waveform/state.rb +66 -0
  81. data/lib/badline/sid/waveform.rb +2 -0
  82. data/lib/badline/sid.rb +34 -0
  83. data/lib/badline/snapshot/container.rb +117 -0
  84. data/lib/badline/snapshot/fields.rb +54 -0
  85. data/lib/badline/snapshot/image.rb +70 -0
  86. data/lib/badline/snapshot/machine_state.rb +77 -0
  87. data/lib/badline/snapshot/setup.rb +58 -0
  88. data/lib/badline/snapshot/state.rb +178 -0
  89. data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
  90. data/lib/badline/snapshot/vice/cias.rb +89 -0
  91. data/lib/badline/snapshot/vice/cias_import.rb +70 -0
  92. data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
  93. data/lib/badline/snapshot/vice/peripherals.rb +47 -0
  94. data/lib/badline/snapshot/vice/restore.rb +125 -0
  95. data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
  96. data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
  97. data/lib/badline/snapshot/vice/vicii.rb +119 -0
  98. data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
  99. data/lib/badline/snapshot/vice.rb +74 -0
  100. data/lib/badline/snapshot.rb +52 -0
  101. data/lib/badline/storage/crt_file.rb +9 -3
  102. data/lib/badline/storage/d64_image.rb +2 -0
  103. data/lib/badline/storage/d71_image.rb +2 -0
  104. data/lib/badline/storage/d81_image.rb +2 -0
  105. data/lib/badline/storage/disk_image.rb +11 -2
  106. data/lib/badline/storage/host_directory.rb +5 -0
  107. data/lib/badline/storage/t64.rb +6 -0
  108. data/lib/badline/storage/tap.rb +10 -1
  109. data/lib/badline/storage/unavailable.rb +20 -0
  110. data/lib/badline/storage.rb +34 -0
  111. data/lib/badline/time_of_day.rb +56 -0
  112. data/lib/badline/version.rb +1 -1
  113. data/lib/badline/via/control_lines.rb +15 -0
  114. data/lib/badline/via/fast_forward.rb +15 -0
  115. data/lib/badline/via/interrupt_register.rb +9 -0
  116. data/lib/badline/via/shift_register.rb +13 -0
  117. data/lib/badline/via/state.rb +42 -0
  118. data/lib/badline/via/timer1.rb +19 -0
  119. data/lib/badline/via/timer2.rb +24 -0
  120. data/lib/badline/via.rb +2 -0
  121. data/lib/badline/vic/border_mask.rb +10 -0
  122. data/lib/badline/vic/collisions.rb +11 -0
  123. data/lib/badline/vic/color_patches.rb +11 -0
  124. data/lib/badline/vic/display_state.rb +29 -1
  125. data/lib/badline/vic/graphics_shifter.rb +26 -0
  126. data/lib/badline/vic/register_log.rb +18 -0
  127. data/lib/badline/vic/registers.rb +13 -2
  128. data/lib/badline/vic/sequencer.rb +12 -0
  129. data/lib/badline/vic/sequencer_state.rb +89 -0
  130. data/lib/badline/vic/sprite/internal_bus.rb +11 -0
  131. data/lib/badline/vic/sprite/saved_state.rb +64 -0
  132. data/lib/badline/vic/sprite.rb +2 -0
  133. data/lib/badline/vic/sprites.rb +3 -0
  134. data/lib/badline/vic/sprites_state.rb +57 -0
  135. data/lib/badline/vic.rb +94 -10
  136. data/lib/badline.rb +1 -0
  137. 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. It wakes
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
- end
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
@@ -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
- return doze if @asleep
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
- @computer = Computer.new(**machine_options(machine, media_path))
38
- Media::TrueDrive.plug(@computer) if machine[:true_drive]
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 = [ScreenPane.new(@computer)]
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 cycle_mode(event.mod.anybits?(REVERSE_MOD) ? -1 : 1) if event.sym == TOGGLE_SYM
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 [TOGGLE_SYM, MUTE_SYM].include?(event.sym)
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
@@ -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