badline 0.5.1 → 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 (129) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +58 -0
  4. data/exe/badline-ruby +2 -1
  5. data/lib/badline/address_bus/saved_state.rb +72 -0
  6. data/lib/badline/address_bus.rb +1 -0
  7. data/lib/badline/audio/stream.rb +6 -0
  8. data/lib/badline/cartridge/action_replay.rb +15 -0
  9. data/lib/badline/cartridge/comal80.rb +8 -0
  10. data/lib/badline/cartridge/dinamic.rb +8 -0
  11. data/lib/badline/cartridge/easy_flash.rb +20 -1
  12. data/lib/badline/cartridge/epyx_fastload.rb +8 -0
  13. data/lib/badline/cartridge/final_cartridge3.rb +12 -0
  14. data/lib/badline/cartridge/flash.rb +41 -0
  15. data/lib/badline/cartridge/fun_play.rb +8 -0
  16. data/lib/badline/cartridge/g_mod2.rb +12 -0
  17. data/lib/badline/cartridge/game_system.rb +8 -0
  18. data/lib/badline/cartridge/geo_ram.rb +16 -0
  19. data/lib/badline/cartridge/magic_desk.rb +8 -0
  20. data/lib/badline/cartridge/ocean.rb +8 -0
  21. data/lib/badline/cartridge/pagefox.rb +12 -0
  22. data/lib/badline/cartridge/retro_replay.rb +44 -1
  23. data/lib/badline/cartridge/rgcd.rb +10 -0
  24. data/lib/badline/cartridge/super_games.rb +10 -0
  25. data/lib/badline/cartridge/zaxxon.rb +16 -0
  26. data/lib/badline/cartridge.rb +72 -0
  27. data/lib/badline/cia/interrupt_register.rb +17 -0
  28. data/lib/badline/cia/serial.rb +44 -0
  29. data/lib/badline/cia/state.rb +47 -0
  30. data/lib/badline/cia/timer.rb +33 -0
  31. data/lib/badline/cia.rb +8 -0
  32. data/lib/badline/computer/attachments.rb +132 -0
  33. data/lib/badline/computer/saved_state.rb +116 -0
  34. data/lib/badline/computer.rb +7 -30
  35. data/lib/badline/cpu/microcode.rb +8 -0
  36. data/lib/badline/cpu/saved_state.rb +115 -0
  37. data/lib/badline/cpu.rb +2 -0
  38. data/lib/badline/datasette.rb +27 -0
  39. data/lib/badline/drive1541/bus.rb +13 -0
  40. data/lib/badline/drive1541/disk.rb +7 -2
  41. data/lib/badline/drive1541/disk_state.rb +80 -0
  42. data/lib/badline/drive1541/mechanism.rb +2 -0
  43. data/lib/badline/drive1541/mechanism_state.rb +67 -0
  44. data/lib/badline/drive1541/rotation.rb +10 -0
  45. data/lib/badline/drive1541/serial_port.rb +10 -0
  46. data/lib/badline/drive1541.rb +34 -0
  47. data/lib/badline/gui/application.rb +41 -14
  48. data/lib/badline/gui/gamepads.rb +7 -0
  49. data/lib/badline/gui/snapshots.rb +67 -0
  50. data/lib/badline/gui.rb +1 -0
  51. data/lib/badline/kernal_trap/channel.rb +27 -0
  52. data/lib/badline/kernal_trap/drive/channels.rb +25 -0
  53. data/lib/badline/kernal_trap/drive/memory.rb +9 -0
  54. data/lib/badline/kernal_trap/drive/status.rb +8 -0
  55. data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
  56. data/lib/badline/kernal_trap/drive.rb +19 -0
  57. data/lib/badline/kernal_trap/save.rb +8 -0
  58. data/lib/badline/kernal_trap/serial.rb +16 -0
  59. data/lib/badline/memory.rb +8 -0
  60. data/lib/badline/options.rb +3 -2
  61. data/lib/badline/ram_expansion/plus256k.rb +16 -0
  62. data/lib/badline/ram_expansion/plus60k.rb +12 -0
  63. data/lib/badline/ram_expansion/unexpanded.rb +6 -0
  64. data/lib/badline/reu/dma.rb +36 -0
  65. data/lib/badline/reu/ram.rb +15 -0
  66. data/lib/badline/reu/saved_state.rb +52 -0
  67. data/lib/badline/reu.rb +4 -0
  68. data/lib/badline/sdl/events.rb +2 -0
  69. data/lib/badline/sid/envelope/state.rb +68 -0
  70. data/lib/badline/sid/envelope.rb +7 -0
  71. data/lib/badline/sid/filter.rb +38 -0
  72. data/lib/badline/sid/voice.rb +10 -0
  73. data/lib/badline/sid/waveform/state.rb +66 -0
  74. data/lib/badline/sid/waveform.rb +2 -0
  75. data/lib/badline/sid.rb +34 -0
  76. data/lib/badline/snapshot/container.rb +117 -0
  77. data/lib/badline/snapshot/fields.rb +54 -0
  78. data/lib/badline/snapshot/image.rb +70 -0
  79. data/lib/badline/snapshot/machine_state.rb +77 -0
  80. data/lib/badline/snapshot/setup.rb +58 -0
  81. data/lib/badline/snapshot/state.rb +178 -0
  82. data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
  83. data/lib/badline/snapshot/vice/cias.rb +89 -0
  84. data/lib/badline/snapshot/vice/cias_import.rb +70 -0
  85. data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
  86. data/lib/badline/snapshot/vice/peripherals.rb +47 -0
  87. data/lib/badline/snapshot/vice/restore.rb +125 -0
  88. data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
  89. data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
  90. data/lib/badline/snapshot/vice/vicii.rb +119 -0
  91. data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
  92. data/lib/badline/snapshot/vice.rb +74 -0
  93. data/lib/badline/snapshot.rb +52 -0
  94. data/lib/badline/storage/crt_file.rb +9 -3
  95. data/lib/badline/storage/d64_image.rb +2 -0
  96. data/lib/badline/storage/d71_image.rb +2 -0
  97. data/lib/badline/storage/d81_image.rb +2 -0
  98. data/lib/badline/storage/disk_image.rb +11 -2
  99. data/lib/badline/storage/host_directory.rb +5 -0
  100. data/lib/badline/storage/t64.rb +6 -0
  101. data/lib/badline/storage/tap.rb +10 -1
  102. data/lib/badline/storage/unavailable.rb +20 -0
  103. data/lib/badline/storage.rb +34 -0
  104. data/lib/badline/time_of_day.rb +56 -0
  105. data/lib/badline/version.rb +1 -1
  106. data/lib/badline/via/control_lines.rb +15 -0
  107. data/lib/badline/via/interrupt_register.rb +9 -0
  108. data/lib/badline/via/shift_register.rb +13 -0
  109. data/lib/badline/via/state.rb +42 -0
  110. data/lib/badline/via/timer1.rb +13 -0
  111. data/lib/badline/via/timer2.rb +18 -0
  112. data/lib/badline/via.rb +2 -0
  113. data/lib/badline/vic/border_mask.rb +10 -0
  114. data/lib/badline/vic/collisions.rb +11 -0
  115. data/lib/badline/vic/color_patches.rb +11 -0
  116. data/lib/badline/vic/display_state.rb +29 -1
  117. data/lib/badline/vic/graphics_shifter.rb +26 -0
  118. data/lib/badline/vic/register_log.rb +18 -0
  119. data/lib/badline/vic/registers.rb +13 -2
  120. data/lib/badline/vic/sequencer.rb +12 -0
  121. data/lib/badline/vic/sequencer_state.rb +89 -0
  122. data/lib/badline/vic/sprite/internal_bus.rb +11 -0
  123. data/lib/badline/vic/sprite/saved_state.rb +64 -0
  124. data/lib/badline/vic/sprite.rb +2 -0
  125. data/lib/badline/vic/sprites.rb +3 -0
  126. data/lib/badline/vic/sprites_state.rb +57 -0
  127. data/lib/badline/vic.rb +94 -10
  128. data/lib/badline.rb +1 -0
  129. metadata +35 -1
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CPU
5
+ # The CPU's state for a snapshot: the registers, the interrupt lines and
6
+ # their pipeline, and the step of the instruction in flight, named by
7
+ # where its plan comes from.
8
+ module SavedState
9
+ # Writes everything the CPU holds, down to the step of the instruction
10
+ # it is in, for load_state to put back. The interrupt lines, BA stalls
11
+ # and SO are state too; the traps and the debug flag belong to the host.
12
+ def save_state(out)
13
+ out.marker("CPU")
14
+ out.int(@program_counter).int(@stack_pointer).int(@a).int(@x).int(@y).int(@status.value)
15
+ [@irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll, @boundary_irq,
16
+ @boundary_nmi, @boundary_crossed, @brk, @pending_write, @so_high, @optional_dummy,
17
+ @branch_taken].each { |flag| out.boolean(flag) }
18
+ out.optional_int(@interrupt).optional_int(@stalled_at).int(@cycles).int(@instructions)
19
+ out.int(plan_source).int(@index).int(OPERATIONS.index(@operation) || -1)
20
+ [@address, @dummy_address, @value, @rmw_result, @pointer].each { |register| out.optional_int(register) }
21
+ end
22
+
23
+ def load_state(input)
24
+ input.marker("CPU")
25
+ @program_counter = input.int
26
+ @stack_pointer = input.int
27
+ @a = input.int
28
+ @x = input.int
29
+ @y = input.int
30
+ @status.value = input.int
31
+ load_flags(input)
32
+ @interrupt = input.optional_int
33
+ @stalled_at = input.optional_int
34
+ @cycles = input.int
35
+ @instructions = input.int
36
+ load_plan(input.int)
37
+ @index = input.int
38
+ operation = input.int
39
+ @operation = operation.negative? ? nil : OPERATIONS.fetch(operation)
40
+ load_working_registers(input)
41
+ end
42
+
43
+ # Drops the instruction or interrupt sequence in flight and the
44
+ # interrupt lines' pipeline, so the next cycle fetches an opcode at the
45
+ # program counter, and sets the cycle count. A snapshot taken between
46
+ # instructions, as VICE's are, restores through here.
47
+ def resume(cycles)
48
+ @irq_sample = @irq_pending = false
49
+ @nmi_sample = @nmi_pending = false
50
+ @skip_poll = false
51
+ @interrupt = nil
52
+ @brk = false
53
+ @stalled_at = nil
54
+ @cycles = cycles
55
+ end_sequence
56
+ @pending_write = false
57
+ end
58
+
59
+ private
60
+
61
+ # Where the plan running comes from: an opcode's microcode, or FETCH,
62
+ # INTERRUPT or JAMMED below it.
63
+ def plan_source
64
+ return FETCH if @plan.equal?(FETCH_PLAN)
65
+ return INTERRUPT if @plan.equal?(INTERRUPT_PLAN)
66
+ return JAMMED if @plan.equal?(JAMMED_PLAN)
67
+
68
+ MICROCODE.index { |micro| micro.plan.equal?(@plan) && micro.operation == @operation } ||
69
+ MICROCODE.index { |micro| micro.plan.equal?(@plan) }
70
+ end
71
+
72
+ def load_plan(source)
73
+ case source
74
+ when FETCH then use_plan(FETCH_PLAN, FETCH_WRITES)
75
+ when INTERRUPT then use_plan(INTERRUPT_PLAN, INTERRUPT_WRITES)
76
+ when JAMMED then use_plan(JAMMED_PLAN, JAMMED_WRITES)
77
+ else
78
+ micro = MICROCODE.fetch(source)
79
+ use_plan(micro.plan, micro.writes)
80
+ end
81
+ end
82
+
83
+ def use_plan(plan, writes)
84
+ @plan = plan
85
+ @writes = writes
86
+ end
87
+
88
+ def load_flags(input)
89
+ @irq = input.boolean?
90
+ @nmi = input.boolean?
91
+ @irq_sample = input.boolean?
92
+ @irq_pending = input.boolean?
93
+ @nmi_sample = input.boolean?
94
+ @nmi_pending = input.boolean?
95
+ @skip_poll = input.boolean?
96
+ @boundary_irq = input.boolean?
97
+ @boundary_nmi = input.boolean?
98
+ @boundary_crossed = input.boolean?
99
+ @brk = input.boolean?
100
+ @pending_write = input.boolean?
101
+ @so_high = input.boolean?
102
+ @optional_dummy = input.boolean?
103
+ @branch_taken = input.boolean?
104
+ end
105
+
106
+ def load_working_registers(input)
107
+ @address = input.optional_int
108
+ @dummy_address = input.optional_int
109
+ @value = input.optional_int
110
+ @rmw_result = input.optional_int
111
+ @pointer = input.optional_int
112
+ end
113
+ end
114
+ end
115
+ end
data/lib/badline/cpu.rb CHANGED
@@ -4,6 +4,7 @@ require "badline/cpu/microcode"
4
4
  require "badline/cpu/addressing"
5
5
  require "badline/cpu/operations"
6
6
  require "badline/cpu/stack_operations"
7
+ require "badline/cpu/saved_state"
7
8
 
8
9
  module Badline
9
10
  class CPU
@@ -16,6 +17,7 @@ module Badline
16
17
  include Addressing
17
18
  include Operations
18
19
  include StackOperations
20
+ include SavedState
19
21
 
20
22
  attr_reader :memory, :instructions, :boundary_crossed, :cycles, :pending_write
21
23
  attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y, :nmi, :irq
@@ -54,6 +54,33 @@ module Badline
54
54
 
55
55
  def running? = @playing && @motor && !@tape.nil?
56
56
 
57
+ # The keys, the motor, the countdown to the next pulse and the tape: its
58
+ # path and how far it has played.
59
+ def save_state(out)
60
+ out.marker("DATASETTE")
61
+ out.boolean(@playing).boolean(@motor).int(@countdown).boolean(!@tape.nil?)
62
+ return unless @tape
63
+
64
+ out.string(@tape.path).int(@tape.position)
65
+ end
66
+
67
+ # A tape from another file goes in from its path, and a detached
68
+ # reader leaves the tape out. The sense and flag handlers don't fire.
69
+ def load_state(input)
70
+ input.marker("DATASETTE")
71
+ @playing = input.boolean?
72
+ @motor = input.boolean?
73
+ @countdown = input.int
74
+ return @tape = nil unless input.boolean?
75
+
76
+ path = input.string
77
+ position = input.int
78
+ return @tape = nil if input.detached?
79
+
80
+ @tape = Storage::TAP.new(path) unless @tape&.path&.b == path
81
+ @tape.position = position
82
+ end
83
+
57
84
  def cycle!
58
85
  return unless running?
59
86
 
@@ -94,6 +94,19 @@ module Badline
94
94
  watching!
95
95
  end
96
96
 
97
+ # The last byte on the data bus and the RAM. What a watched stretch
98
+ # touched is Idle's, and a restored bus isn't watching.
99
+ def save_state(out)
100
+ out.int(@data)
101
+ @ram.save_state(out)
102
+ end
103
+
104
+ def load_state(input)
105
+ @data = input.int
106
+ @ram.load_state(input)
107
+ @watching = false
108
+ end
109
+
97
110
  def peek(addr)
98
111
  return @data = @rom.peek(addr | 0x4000) if addr >= 0x8000
99
112
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/drive1541/sector_reader"
4
+ require "badline/drive1541/disk_state"
4
5
 
5
6
  module Badline
6
7
  class Drive1541
@@ -16,6 +17,8 @@ module Badline
16
17
  # host file. The disk is write-protected when the image won't take
17
18
  # writes.
18
19
  class Disk
20
+ include State
21
+
19
22
  MAX_HALF_TRACK = 84
20
23
 
21
24
  # GCR bytes around a track in each speed zone: 200 ms of rotation at
@@ -52,9 +55,9 @@ module Badline
52
55
  # anything else as a .d64, formatted. `read_only` opens the image
53
56
  # write-protected.
54
57
  def self.open(path, read_only: false)
55
- return from_g64(Storage::G64Image.new(path, read_only:)) if File.extname(path).casecmp?(".g64")
58
+ return from_g64(Storage::G64Image.new(path, read_only:)).opened(path, read_only) if State.g64?(path)
56
59
 
57
- from_d64(Storage::D64Image.new(path, read_only:))
60
+ from_d64(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
58
61
  end
59
62
 
60
63
  # A disk from a G64 image, each half track as the image stores it,
@@ -145,6 +148,8 @@ module Badline
145
148
  @image = image
146
149
  @tracks = Array.new(MAX_HALF_TRACK + 1)
147
150
  @written = {}
151
+ @path = nil
152
+ @read_only = false
148
153
  end
149
154
 
150
155
  # The Track at the half track, or nil where there's no data.
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1541
5
+ class Disk
6
+ # Saving and restoring a Disk for a snapshot: the image by its path,
7
+ # and every half track as the head sees it, with those written since
8
+ # the last flush. The image itself is read again from its host file.
9
+ module State
10
+ # The image's path, expanded, and whether it was opened read-only,
11
+ # for a disk from Disk.open. Nil for a disk made another way.
12
+ attr_reader :path, :read_only
13
+
14
+ # A .g64 image by its name, and anything else a .d64.
15
+ def self.g64?(path) = File.extname(path).casecmp?(".g64")
16
+
17
+ # A disk for the image at the path, its tracks left for the state.
18
+ def self.reopen(path, read_only)
19
+ if g64?(path)
20
+ Disk.new(Storage::G64Image.new(path, read_only:)).opened(path, read_only)
21
+ else
22
+ Disk.new(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
23
+ end
24
+ end
25
+
26
+ # The disk a state from save_state describes: +current+ when it was
27
+ # opened from the same image the same way, and otherwise a disk for
28
+ # the image at the path the state names, or one without an image,
29
+ # as a detached reader always gets. Either way its tracks come from
30
+ # the state.
31
+ def self.load(input, current)
32
+ path = input.optional_string
33
+ read_only = input.boolean?
34
+ disk = current if current && !path.nil? && current.path == path && current.read_only == read_only
35
+ disk ||= path && !input.detached? ? reopen(path, read_only) : Disk.new
36
+ disk.load_tracks(input)
37
+ disk
38
+ end
39
+
40
+ # Notes the image's path and how it was opened.
41
+ def opened(path, read_only)
42
+ @path = File.expand_path(path)
43
+ @read_only = read_only
44
+ self
45
+ end
46
+
47
+ def save_state(out)
48
+ out.optional_string(@path).boolean(@read_only)
49
+ @tracks.each do |track|
50
+ out.boolean(!track.nil?)
51
+ save_track(track, out) if track
52
+ end
53
+ out.ints(@written.keys)
54
+ end
55
+
56
+ def load_tracks(input)
57
+ @tracks.each_index do |half_track|
58
+ @tracks[half_track] = input.boolean? ? load_track(input) : nil
59
+ end
60
+ @written.clear
61
+ input.ints.each { |half_track| @written[half_track] = true }
62
+ end
63
+
64
+ private
65
+
66
+ def save_track(track, out)
67
+ out.int(track.zone).boolean(!track.speeds.nil?)
68
+ out.ints(track.speeds) if track.speeds
69
+ out.blob(track.bytes)
70
+ end
71
+
72
+ def load_track(input)
73
+ zone = input.int
74
+ speeds = input.boolean? ? input.ints : nil
75
+ Track.new(input.blob, zone, speeds)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -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
@@ -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
@@ -180,6 +180,40 @@ module Badline
180
180
  step
181
181
  end
182
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
+
183
217
  def inspect
184
218
  "#<#{self.class.name} cycles=#{cycles} cpu=(#{cpu.inspect})>"
185
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
data/lib/badline/gui.rb CHANGED
@@ -10,4 +10,5 @@ require "badline/gui/pane"
10
10
  require "badline/gui/screen_pane"
11
11
  require "badline/gui/drive_led_pane"
12
12
  require "badline/gui/window"
13
+ require "badline/gui/snapshots"
13
14
  require "badline/gui/application"