badline 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/CLAUDE.md +10 -9
  4. data/README.md +82 -10
  5. data/doc/pinned-behaviour.md +131 -30
  6. data/exe/badline-ruby +20 -23
  7. data/lib/badline/address_bus/saved_state.rb +72 -0
  8. data/lib/badline/address_bus.rb +11 -1
  9. data/lib/badline/audio/cli.rb +18 -5
  10. data/lib/badline/audio/console.rb +2 -1
  11. data/lib/badline/audio.rb +0 -3
  12. data/lib/badline/cartridge/action_replay.rb +15 -0
  13. data/lib/badline/cartridge/comal80.rb +8 -0
  14. data/lib/badline/cartridge/dinamic.rb +8 -0
  15. data/lib/badline/cartridge/easy_flash.rb +20 -1
  16. data/lib/badline/cartridge/epyx_fastload.rb +8 -0
  17. data/lib/badline/cartridge/final_cartridge3.rb +12 -0
  18. data/lib/badline/cartridge/flash.rb +41 -0
  19. data/lib/badline/cartridge/fun_play.rb +8 -0
  20. data/lib/badline/cartridge/g_mod2.rb +12 -0
  21. data/lib/badline/cartridge/game_system.rb +8 -0
  22. data/lib/badline/cartridge/geo_ram.rb +16 -0
  23. data/lib/badline/cartridge/magic_desk.rb +8 -0
  24. data/lib/badline/cartridge/ocean.rb +8 -0
  25. data/lib/badline/cartridge/pagefox.rb +12 -0
  26. data/lib/badline/cartridge/retro_replay.rb +44 -1
  27. data/lib/badline/cartridge/rgcd.rb +10 -0
  28. data/lib/badline/cartridge/super_games.rb +10 -0
  29. data/lib/badline/cartridge/zaxxon.rb +16 -0
  30. data/lib/badline/cartridge.rb +72 -0
  31. data/lib/badline/cia/interrupt_register.rb +17 -0
  32. data/lib/badline/cia/serial.rb +44 -0
  33. data/lib/badline/cia/state.rb +47 -0
  34. data/lib/badline/cia/timer.rb +33 -0
  35. data/lib/badline/cia.rb +8 -0
  36. data/lib/badline/computer/attachments.rb +132 -0
  37. data/lib/badline/computer/saved_state.rb +116 -0
  38. data/lib/badline/computer.rb +13 -33
  39. data/lib/badline/cpu/boundary.rb +54 -0
  40. data/lib/badline/cpu/core.rb +165 -0
  41. data/lib/badline/cpu/microcode.rb +8 -0
  42. data/lib/badline/cpu/saved_state.rb +115 -0
  43. data/lib/badline/cpu.rb +3 -212
  44. data/lib/badline/datasette.rb +27 -0
  45. data/lib/badline/drive1541/bus.rb +13 -0
  46. data/lib/badline/drive1541/cpu.rb +29 -0
  47. data/lib/badline/drive1541/disk.rb +7 -2
  48. data/lib/badline/drive1541/disk_state.rb +80 -0
  49. data/lib/badline/drive1541/mechanism.rb +2 -0
  50. data/lib/badline/drive1541/mechanism_state.rb +67 -0
  51. data/lib/badline/drive1541/rotation.rb +10 -0
  52. data/lib/badline/drive1541/serial_port.rb +10 -0
  53. data/lib/badline/drive1541.rb +35 -0
  54. data/lib/badline/ffi.rb +157 -0
  55. data/lib/badline/frontend/app.rb +225 -0
  56. data/lib/badline/frontend/audio_sink.rb +72 -0
  57. data/lib/badline/frontend/boot.rb +52 -0
  58. data/lib/badline/frontend/controls.rb +163 -0
  59. data/lib/badline/frontend/drive_led.rb +51 -0
  60. data/lib/badline/frontend/frame_rate.rb +83 -0
  61. data/lib/badline/frontend/gamepads.rb +76 -0
  62. data/lib/badline/frontend/keys.rb +48 -0
  63. data/lib/badline/frontend/pacer.rb +100 -0
  64. data/lib/badline/frontend/pad_port.rb +61 -0
  65. data/lib/badline/frontend/screen.rb +65 -0
  66. data/lib/badline/frontend/screenshot.rb +22 -0
  67. data/lib/badline/frontend/snapshots.rb +60 -0
  68. data/lib/badline/frontend/sound.rb +153 -0
  69. data/lib/badline/frontend/timeline.rb +171 -0
  70. data/lib/badline/frontend.rb +18 -0
  71. data/lib/badline/kernal_trap/channel.rb +27 -0
  72. data/lib/badline/kernal_trap/drive/channels.rb +25 -0
  73. data/lib/badline/kernal_trap/drive/memory.rb +9 -0
  74. data/lib/badline/kernal_trap/drive/status.rb +8 -0
  75. data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
  76. data/lib/badline/kernal_trap/drive.rb +19 -0
  77. data/lib/badline/kernal_trap/save.rb +8 -0
  78. data/lib/badline/kernal_trap/serial.rb +16 -0
  79. data/lib/badline/memory.rb +8 -0
  80. data/lib/badline/options/event.rb +94 -0
  81. data/lib/badline/options/help.rb +78 -0
  82. data/lib/badline/options/option.rb +29 -0
  83. data/lib/badline/options/table.rb +62 -0
  84. data/lib/badline/options.rb +181 -144
  85. data/lib/badline/ram_expansion/plus256k.rb +16 -0
  86. data/lib/badline/ram_expansion/plus60k.rb +12 -0
  87. data/lib/badline/ram_expansion/unexpanded.rb +6 -0
  88. data/lib/badline/reu/dma/saved_state.rb +50 -0
  89. data/lib/badline/reu/dma.rb +56 -11
  90. data/lib/badline/reu/ram.rb +15 -0
  91. data/lib/badline/reu/saved_state.rb +53 -0
  92. data/lib/badline/reu.rb +23 -10
  93. data/lib/badline/sdl.rb +123 -51
  94. data/lib/badline/sid/envelope/state.rb +68 -0
  95. data/lib/badline/sid/envelope.rb +7 -0
  96. data/lib/badline/sid/filter.rb +38 -0
  97. data/lib/badline/sid/voice.rb +10 -0
  98. data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
  99. data/lib/badline/sid/waveform/state.rb +66 -0
  100. data/lib/badline/sid/waveform.rb +4 -3
  101. data/lib/badline/sid.rb +34 -0
  102. data/lib/badline/snapshot/container.rb +117 -0
  103. data/lib/badline/snapshot/fields.rb +54 -0
  104. data/lib/badline/snapshot/image.rb +70 -0
  105. data/lib/badline/snapshot/machine_state.rb +77 -0
  106. data/lib/badline/snapshot/setup.rb +58 -0
  107. data/lib/badline/snapshot/state.rb +178 -0
  108. data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
  109. data/lib/badline/snapshot/vice/cias.rb +89 -0
  110. data/lib/badline/snapshot/vice/cias_import.rb +70 -0
  111. data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
  112. data/lib/badline/snapshot/vice/peripherals.rb +47 -0
  113. data/lib/badline/snapshot/vice/restore.rb +125 -0
  114. data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
  115. data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
  116. data/lib/badline/snapshot/vice/vicii.rb +119 -0
  117. data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
  118. data/lib/badline/snapshot/vice.rb +74 -0
  119. data/lib/badline/snapshot.rb +52 -0
  120. data/lib/badline/storage/crt_file.rb +9 -3
  121. data/lib/badline/storage/d64_image.rb +2 -0
  122. data/lib/badline/storage/d71_image.rb +2 -0
  123. data/lib/badline/storage/d81_image.rb +2 -0
  124. data/lib/badline/storage/disk_image.rb +11 -2
  125. data/lib/badline/storage/host_directory.rb +5 -0
  126. data/lib/badline/storage/t64.rb +6 -0
  127. data/lib/badline/storage/tap.rb +10 -1
  128. data/lib/badline/storage/unavailable.rb +20 -0
  129. data/lib/badline/storage.rb +34 -0
  130. data/lib/badline/time_of_day.rb +56 -0
  131. data/lib/badline/version.rb +1 -1
  132. data/lib/badline/via/control_lines.rb +15 -0
  133. data/lib/badline/via/interrupt_register.rb +9 -0
  134. data/lib/badline/via/shift_register.rb +13 -0
  135. data/lib/badline/via/state.rb +42 -0
  136. data/lib/badline/via/timer1.rb +13 -0
  137. data/lib/badline/via/timer2.rb +18 -0
  138. data/lib/badline/via.rb +2 -0
  139. data/lib/badline/vic/bank.rb +21 -0
  140. data/lib/badline/vic/border_mask.rb +10 -0
  141. data/lib/badline/vic/collisions.rb +11 -0
  142. data/lib/badline/vic/color_patches.rb +11 -0
  143. data/lib/badline/vic/display_state.rb +29 -1
  144. data/lib/badline/vic/graphics_shifter.rb +26 -0
  145. data/lib/badline/vic/register_log.rb +18 -0
  146. data/lib/badline/vic/registers.rb +13 -2
  147. data/lib/badline/vic/sequencer.rb +12 -0
  148. data/lib/badline/vic/sequencer_state.rb +89 -0
  149. data/lib/badline/vic/sprite/internal_bus.rb +11 -0
  150. data/lib/badline/vic/sprite/saved_state.rb +64 -0
  151. data/lib/badline/vic/sprite.rb +2 -0
  152. data/lib/badline/vic/sprites.rb +3 -0
  153. data/lib/badline/vic/sprites_state.rb +57 -0
  154. data/lib/badline/vic.rb +109 -16
  155. data/lib/badline.rb +1 -0
  156. metadata +59 -18
  157. data/.claude/hooks/session-start.sh +0 -57
  158. data/.claude/settings.json +0 -14
  159. data/lib/badline/audio/sdl_sink.rb +0 -70
  160. data/lib/badline/audio/stream.rb +0 -117
  161. data/lib/badline/gui/application.rb +0 -216
  162. data/lib/badline/gui/drive_led_pane.rb +0 -29
  163. data/lib/badline/gui/gamepads.rb +0 -97
  164. data/lib/badline/gui/joy_map.rb +0 -27
  165. data/lib/badline/gui/key_map.rb +0 -39
  166. data/lib/badline/gui/palette.rb +0 -35
  167. data/lib/badline/gui/pane.rb +0 -36
  168. data/lib/badline/gui/screen_pane.rb +0 -46
  169. data/lib/badline/gui/window.rb +0 -61
  170. data/lib/badline/gui.rb +0 -13
  171. data/lib/badline/sdl/events.rb +0 -47
  172. data/lib/badline/sdl/functions.rb +0 -74
  173. data/vendor/.gitkeep +0 -0
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/cpu/microcode"
4
+ require "badline/cpu/addressing"
5
+ require "badline/cpu/operations"
6
+ require "badline/cpu/stack_operations"
7
+ require "badline/cpu/boundary"
8
+ require "badline/cpu/saved_state"
9
+
10
+ module Badline
11
+ class CPU
12
+ STATUS_FLAGS = [:carry, :zero, :interrupt, :decimal, :break, 1, :overflow, :negative].freeze
13
+
14
+ # The 6502 core, on whatever bus +memory+ is. The C64's 6510 (CPU) and
15
+ # the 1541's 6502 (Drive1541::CPU) each include it.
16
+ module Core
17
+ include IntegerHelper
18
+ include InstructionSet
19
+ include Interrupts
20
+ include Traps
21
+ include Addressing
22
+ include Operations
23
+ include StackOperations
24
+ include Boundary
25
+ include SavedState
26
+
27
+ attr_reader :memory, :instructions, :boundary_crossed, :cycles, :pending_write
28
+ attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y, :nmi, :irq
29
+
30
+ alias pending_write? pending_write
31
+
32
+ # +ane_constant+ is ANE's magic constant, which varies from chip to
33
+ # chip. The default is the C64 6510's.
34
+ def initialize(memory, debug: false, ane_constant: 0xef)
35
+ @debug = debug
36
+ @ane_constant = ane_constant
37
+ @memory = memory
38
+ @status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
39
+ reset_registers
40
+
41
+ @nmi = @irq = false
42
+ @irq_sample = @irq_pending = false
43
+ @nmi_sample = @nmi_pending = false
44
+ @skip_poll = false
45
+ @boundary_crossed = false
46
+ @interrupt = nil
47
+ @brk = false
48
+ @pending_write = false
49
+ @stalled_at = nil
50
+ @so_high = true
51
+
52
+ @cycles = 0
53
+ @instructions = 0
54
+ @traps = nil
55
+ end_sequence
56
+ end
57
+
58
+ def reset!
59
+ status.interrupt = true
60
+ reset_registers
61
+ @nmi = false
62
+ @irq_sample = @irq_pending = false
63
+ @nmi_sample = @nmi_pending = false
64
+ @skip_poll = false
65
+ @pending_write = false
66
+ end_sequence
67
+ end
68
+
69
+ def p
70
+ status.value
71
+ end
72
+
73
+ def p=(new_value)
74
+ status.value = new_value
75
+ end
76
+
77
+ def cycle!
78
+ poll # the interrupt lines
79
+
80
+ # Run the next step
81
+ index = @index
82
+ @index = index + 1
83
+ send(@plan[index])
84
+ @cycles += 1
85
+
86
+ # Record if the next step writes to memory
87
+ @pending_write = @writes[@index]
88
+ nil
89
+ end
90
+
91
+ # Called instead of #cycle! on a cycle the VIC holds the CPU through
92
+ # BA. Records which cycle the CPU was stalled before, and keeps
93
+ # sampling the interrupt lines.
94
+ def stall!
95
+ @stalled_at = @cycles
96
+ sample_while_stalled
97
+ end
98
+
99
+ # The 6502's SO (set overflow) pin, which the 6510 leaves out, so the
100
+ # C64 never drives it. A falling edge sets V and a held level does
101
+ # nothing more.
102
+ def so=(high)
103
+ @status.overflow = true if @so_high && !high
104
+ @so_high = high
105
+ end
106
+
107
+ # Pulses SO: pulls it low, which sets V, and releases it.
108
+ def so!
109
+ @status.overflow = true
110
+ @so_high = true
111
+ end
112
+
113
+ def jammed?
114
+ @plan.equal?(JAMMED_PLAN)
115
+ end
116
+
117
+ # Runs to the end of the instruction, or until the CPU jams.
118
+ def step!
119
+ cycle!
120
+ cycle! until @plan.equal?(FETCH_PLAN) || jammed?
121
+ nil
122
+ end
123
+
124
+ # Whether the next cycle fetches an opcode.
125
+ def boundary? = @index.zero? && @plan.equal?(FETCH_PLAN)
126
+
127
+ def trapped? = !@traps.nil?
128
+
129
+ def inspect
130
+ "Cycles: #{@cycles}, PC: #{format16(program_counter)}, " \
131
+ "SP: #{format8(stack_pointer)}, A: #{format8(a)}, X: #{format8(x)}, " \
132
+ "Y: #{format8(y)}, P: #{format8(p)}"
133
+ end
134
+
135
+ private
136
+
137
+ # True when the CPU was stalled right before this cycle.
138
+ def stalled_before_this_cycle?
139
+ @stalled_at == @cycles
140
+ end
141
+
142
+ # True when the CPU was stalled right before the cycle that preceded
143
+ # this one.
144
+ def stalled_before_previous_cycle?
145
+ @stalled_at == @cycles - 1
146
+ end
147
+
148
+ def write_byte(addr, value)
149
+ @memory.poke(addr, value)
150
+ end
151
+
152
+ def stack_address(offset = 0)
153
+ 0x0100 | ((@stack_pointer + offset) & 0xff)
154
+ end
155
+
156
+ def reset_registers
157
+ # The program counter is initialized from the reset vector
158
+ @program_counter = @memory.peek16(0xfffc)
159
+ # Stack pointer starts at 0x01ff and grows down
160
+ @stack_pointer = 0xff
161
+ @a = @x = @y = 0x0
162
+ end
163
+ end
164
+ end
165
+ end
@@ -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
@@ -1,219 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "badline/cpu/microcode"
4
- require "badline/cpu/addressing"
5
- require "badline/cpu/operations"
6
- require "badline/cpu/stack_operations"
3
+ require "badline/cpu/core"
7
4
 
8
5
  module Badline
6
+ # The C64's 6510, on the AddressBus.
9
7
  class CPU
10
- STATUS_FLAGS = [:carry, :zero, :interrupt, :decimal, :break, 1, :overflow, :negative].freeze
11
-
12
- include IntegerHelper
13
- include InstructionSet
14
- include Interrupts
15
- include Traps
16
- include Addressing
17
- include Operations
18
- include StackOperations
19
-
20
- attr_reader :memory, :instructions, :boundary_crossed, :cycles, :pending_write
21
- attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y, :nmi, :irq
22
-
23
- alias pending_write? pending_write
24
-
25
- # +ane_constant+ is ANE's magic constant, which varies from chip to
26
- # chip. The default is the C64 6510's.
27
- def initialize(memory, debug: false, ane_constant: 0xef)
28
- @debug = debug
29
- @ane_constant = ane_constant
30
- @memory = memory
31
- @status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
32
- reset_registers
33
-
34
- @nmi = @irq = false
35
- @irq_sample = @irq_pending = false
36
- @nmi_sample = @nmi_pending = false
37
- @skip_poll = false
38
- @boundary_crossed = false
39
- @interrupt = nil
40
- @brk = false
41
- @pending_write = false
42
- @stalled_at = nil
43
- @so_high = true
44
-
45
- @cycles = 0
46
- @instructions = 0
47
- @traps = nil
48
- end_sequence
49
- end
50
-
51
- def reset!
52
- status.interrupt = true
53
- reset_registers
54
- @nmi = false
55
- @irq_sample = @irq_pending = false
56
- @nmi_sample = @nmi_pending = false
57
- @skip_poll = false
58
- @pending_write = false
59
- end_sequence
60
- end
61
-
62
- def p
63
- status.value
64
- end
65
-
66
- def p=(new_value)
67
- status.value = new_value
68
- end
69
-
70
- def cycle!
71
- poll # the interrupt lines
72
-
73
- # Run the next step
74
- index = @index
75
- @index = index + 1
76
- send(@plan[index])
77
- @cycles += 1
78
-
79
- # Record if the next step writes to memory
80
- @pending_write = @writes[@index]
81
- nil
82
- end
83
-
84
- # Called instead of #cycle! on a cycle the VIC holds the CPU through
85
- # BA. Records which cycle the CPU was stalled before, and keeps
86
- # sampling the interrupt lines.
87
- def stall!
88
- @stalled_at = @cycles
89
- sample_while_stalled
90
- end
91
-
92
- # The 6502's SO (set overflow) pin, which the 6510 leaves out, so the
93
- # C64 never drives it. A falling edge sets V and a held level does
94
- # nothing more.
95
- def so=(high)
96
- @status.overflow = true if @so_high && !high
97
- @so_high = high
98
- end
99
-
100
- # Pulses SO: pulls it low, which sets V, and releases it.
101
- def so!
102
- @status.overflow = true
103
- @so_high = true
104
- end
105
-
106
- def jammed?
107
- @plan.equal?(JAMMED_PLAN)
108
- end
109
-
110
- # Runs to the end of the instruction, or until the CPU jams.
111
- def step!
112
- cycle!
113
- cycle! until @plan.equal?(FETCH_PLAN) || jammed?
114
- nil
115
- end
116
-
117
- # Whether the next cycle fetches an opcode.
118
- def boundary? = @index.zero? && @plan.equal?(FETCH_PLAN)
119
-
120
- def trapped? = !@traps.nil?
121
-
122
- # Everything the CPU holds but its cycle and instruction counts: the
123
- # registers, the interrupt lines and pipeline, and what the last
124
- # instruction left in its working registers. Drive1541::Idle compares
125
- # it at two instruction boundaries.
126
- def idle_state
127
- [@program_counter, @stack_pointer, @a, @x, @y, @status.value,
128
- @irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
129
- @boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
130
- @operation, @optional_dummy, @boundary_crossed, @branch_taken,
131
- @address, @pointer, @dummy_address, @value, @rmw_result]
132
- end
133
-
134
- # Counts +cycles+ cycles and +instructions+ instructions that ran
135
- # without changing anything idle_state holds.
136
- def fast_forward(cycles, instructions)
137
- @cycles += cycles
138
- @instructions += instructions
139
- end
140
-
141
- def inspect
142
- "Cycles: #{@cycles}, PC: #{format16(program_counter)}, " \
143
- "SP: #{format8(stack_pointer)}, A: #{format8(a)}, X: #{format8(x)}, " \
144
- "Y: #{format8(y)}, P: #{format8(p)}"
145
- end
146
-
147
- private
148
-
149
- # The first cycle of every instruction. Starts an interrupt if one was
150
- # pending when the previous instruction ended. Otherwise reads the
151
- # opcode and switches to its plan.
152
- def op_fetch
153
- return start_interrupt if @boundary_nmi || @boundary_irq
154
-
155
- run_traps
156
- opcode = @memory.peek(@program_counter)
157
- log(opcode) if @debug
158
- micro = MICROCODE[opcode]
159
- @operation = micro.operation
160
- @plan = micro.plan
161
- @writes = micro.writes
162
- @optional_dummy = micro.optional_dummy
163
- @index = 1
164
- @boundary_crossed = false
165
- @program_counter = (@program_counter + 1) & 0xffff
166
- end
167
-
168
- def end_instruction
169
- @instructions += 1
170
- end_sequence
171
- end
172
-
173
- # Makes the next cycle an opcode fetch, and saves the pending interrupt
174
- # flags for op_fetch. The next cycle polls before op_fetch runs, which
175
- # updates the live flags.
176
- def end_sequence
177
- @plan = FETCH_PLAN
178
- @writes = FETCH_WRITES
179
- @index = 0
180
- @boundary_irq = @irq_pending
181
- @boundary_nmi = @nmi_pending
182
- end
183
-
184
- # True when the CPU was stalled right before this cycle.
185
- def stalled_before_this_cycle?
186
- @stalled_at == @cycles
187
- end
188
-
189
- # True when the CPU was stalled right before the cycle that preceded
190
- # this one.
191
- def stalled_before_previous_cycle?
192
- @stalled_at == @cycles - 1
193
- end
194
-
195
- def write_byte(addr, value)
196
- @memory.poke(addr, value)
197
- end
198
-
199
- def stack_address(offset = 0)
200
- 0x0100 | ((@stack_pointer + offset) & 0xff)
201
- end
202
-
203
- def reset_registers
204
- # The program counter is initialized from the reset vector
205
- @program_counter = @memory.peek16(0xfffc)
206
- # Stack pointer starts at 0x01ff and grows down
207
- @stack_pointer = 0xff
208
- @a = @x = @y = 0x0
209
- end
210
-
211
- def log(opcode)
212
- instruction = Instruction.find(opcode)
213
- puts(
214
- "#{@cycles}: PC: #{format16(@program_counter)} - " \
215
- "#{instruction.name.upcase} #{instruction.addressing_mode}"
216
- )
217
- end
8
+ include Core
218
9
  end
219
10
  end
@@ -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
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1541
5
+ # The drive's 6502, on the drive's Bus.
6
+ class CPU
7
+ include Badline::CPU::Core
8
+
9
+ # Everything the CPU holds but its cycle and instruction counts: the
10
+ # registers, the interrupt lines and pipeline, and what the last
11
+ # instruction left in its working registers. Drive1541::Idle compares
12
+ # it at two instruction boundaries.
13
+ def idle_state
14
+ [@program_counter, @stack_pointer, @a, @x, @y, @status.value,
15
+ @irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
16
+ @boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
17
+ @operation, @optional_dummy, @boundary_crossed, @branch_taken,
18
+ @address, @pointer, @dummy_address, @value, @rmw_result]
19
+ end
20
+
21
+ # Counts +cycles+ cycles and +instructions+ instructions that ran
22
+ # without changing anything idle_state holds.
23
+ def fast_forward(cycles, instructions)
24
+ @cycles += cycles
25
+ @instructions += instructions
26
+ end
27
+ end
28
+ end
29
+ 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.