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