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,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ # C64MEM 0.1: the CPU port, the cartridge's EXROM and GAME lines, and
7
+ # the 64K of RAM. The lines are written inactive, to match the empty
8
+ # cartridge port the snapshot shows VICE, and aren't read: badline
9
+ # takes them from its own cartridge.
10
+ #
11
+ # The floating port bits 6 and 7 carry their charge both ways. VICE's
12
+ # clocks for when the charge drains are written as zero and not read:
13
+ # the charge lasts as long as badline holds it.
14
+ module C64Mem
15
+ NAME = "C64MEM"
16
+ MAJOR = 0
17
+ MINOR = 1
18
+ FLOATING = [0x40, 0x80].freeze
19
+
20
+ module_function
21
+
22
+ def reads?(section) = section.major == MAJOR && section.minor == MINOR
23
+
24
+ def export(computer)
25
+ bus = computer.address_bus
26
+ ddr, port_out, floating = bus.port_state
27
+ fields = FieldWriter.new.byte(port_out).byte(ddr).byte(0).byte(0)
28
+ fields.bytes(Array.new(0x10000) { |addr| computer.ram.peek(addr) })
29
+ fields.byte((port_out & ddr) | (floating & ~ddr)).byte(bus.peek(1)).byte(ddr)
30
+ fields.dword(0).dword(0)
31
+ charged = FLOATING.map { |bit| floating.anybits?(bit) }
32
+ draining = FLOATING.map { |bit| floating.anybits?(bit) && ddr.nobits?(bit) }
33
+ (charged + draining).each { |set| fields.flag(set) }
34
+ fields.section(NAME, MAJOR, MINOR)
35
+ end
36
+
37
+ # The port is set without a bus write, which would leave the VIC's
38
+ # phi1 byte in the RAM under $00 and $01.
39
+ def import(section, computer)
40
+ fields = FieldReader.new(section)
41
+ data = fields.byte
42
+ ddr = fields.byte
43
+ fields.skip(2) # EXROM and GAME
44
+ computer.ram.write(0, fields.bytes(0x10000))
45
+ fields.skip(3 + 8)
46
+ charge = FLOATING.sum { |bit| fields.flag? ? bit : 0 }
47
+ driven = ddr & AddressBus::PORT_FLOATING
48
+ computer.address_bus.restore_port(ddr, data, (data & driven) | (charge & ~driven & 0xff))
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ # CIA1 and CIA2 2.3: the registers, both timers' counters and
7
+ # latches, the interrupt flags and mask, the TOD clock, its alarm and
8
+ # latch, and the shift register.
9
+ #
10
+ # Each timer's VICE state word carries its start, one-shot and input
11
+ # bits and whether its count pipeline is full, which is what badline's
12
+ # pipeline holds between loads. A load or one-shot stop in flight,
13
+ # the TOD divider's phase within a tenth and the shift register's
14
+ # progress through a byte are approximations both ways.
15
+ module CIAs
16
+ MAJOR = 2
17
+ MINOR = 3
18
+ NAMES = %w[CIA1 CIA2].freeze
19
+
20
+ # VICE's timer state bits.
21
+ START = 0x001
22
+ COUNT2 = 0x002
23
+ ONESHOT = 0x008
24
+ PHI2IN = 0x020
25
+ COUNT3 = 0x040
26
+ COUNT = 0x800
27
+
28
+ module_function
29
+
30
+ def export(computer)
31
+ [computer.cia1, computer.cia2].each_with_index.map do |cia, i|
32
+ fields = FieldWriter.new
33
+ registers(cia, fields)
34
+ time_of_day(cia.time_of_day, fields)
35
+ internals(cia, fields)
36
+ fields.section(NAMES[i], MAJOR, MINOR)
37
+ end
38
+ end
39
+
40
+ def registers(cia, fields)
41
+ cia.port_registers.each { |value| fields.byte(value) }
42
+ fields.word(cia.timer_a).word(cia.timer_b)
43
+ cia.time_of_day.registers.first(4).each { |value| fields.byte(value) }
44
+ fields.byte(cia.serial.data).byte(cia.interrupt_control.value)
45
+ fields.byte(cia.control_a.value).byte(cia.control_b.value)
46
+ fields.word(cia.timer_a_latch).word(cia.timer_b_latch).byte(cia.interrupt_status.value)
47
+ ta, tb = cia.timers
48
+ fields.byte((ta.toggle ? 0x40 : 0) | (tb.toggle ? 0x80 : 0) |
49
+ (ta.underflowed ? 0x04 : 0) | (tb.underflowed ? 0x08 : 0))
50
+ fields.byte(cia.serial.steps.clamp(0, 0xff))
51
+ end
52
+
53
+ def time_of_day(tod, fields)
54
+ registers = tod.registers
55
+ registers[4, 4].each { |value| fields.byte(value) }
56
+ fields.byte(0) # cycles since the ICR was read
57
+ fields.byte(registers[9] | (registers[8] << 1)) # latched, stopped
58
+ tod.latched_fields.each { |value| fields.byte(value) }
59
+ fields.qword(tod.cycles_to_tenth)
60
+ end
61
+
62
+ def internals(cia, fields)
63
+ cia.timers.each { |timer| fields.word(timer_state(timer)) }
64
+ serial = cia.serial
65
+ shift = serial.shift_register
66
+ fields.byte(shift & 0xff).flag(!serial.pending.nil?).flag(cia.interrupted?)
67
+ fields.byte(cia.time_of_day.pulses)
68
+ fields.byte(shift >> 8).byte(0)
69
+ fields.byte((serial.sp_in ? 0x80 : 0) | (serial.cnt_in ? 0x40 : 0))
70
+ end
71
+
72
+ def timer_state(timer)
73
+ control = timer.control.value
74
+ pipe = timer.pipe
75
+ (control & (START | ONESHOT)) | (control.nobits?(0x60) ? PHI2IN : 0) |
76
+ (pipe.anybits?(0b10) ? COUNT2 : 0) | (pipe.anybits?(0b01) ? COUNT3 | COUNT : 0)
77
+ end
78
+
79
+ # Later minor versions add fields at the end.
80
+ def reads?(section) = section.major == MAJOR && section.minor >= MINOR
81
+
82
+ def import(section, computer)
83
+ cia = section.name == NAMES.first ? computer.cia1 : computer.cia2
84
+ Import.new(FieldReader.new(section)).apply(cia)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module CIAs
7
+ # Reads a CIA module and puts it into one of badline's CIAs.
8
+ class Import
9
+ def initialize(fields)
10
+ @fields = fields
11
+ f = fields
12
+ @ports = f.bytes(4)
13
+ @counters = [f.word, f.word]
14
+ @clock = f.bytes(4)
15
+ @sdr, @mask, @control_a, @control_b = f.bytes(4)
16
+ @latches = [f.word, f.word]
17
+ @status, @toggles = f.bytes(2)
18
+ read_time_of_day
19
+ read_internals
20
+ end
21
+
22
+ def apply(cia)
23
+ @ports.each_with_index { |value, port| cia.poke(cia.start + port, value) }
24
+ cia.timer_a, cia.timer_b = @counters
25
+ cia.timer_a_latch, cia.timer_b_latch = @latches
26
+ cia.interrupt_control.value = @mask & 0x1f
27
+ cia.interrupt_status.value = @status
28
+ cia.serial.restore_shift(@sdr, @shift)
29
+ load_timers(cia)
30
+ cia.time_of_day.fifty_hz = @control_a.anybits?(0x80)
31
+ cia.time_of_day.restore_fields(@clock, @alarm, @latch)
32
+ cia.time_of_day.restore_divider(@stopped, @pulses, @ticks)
33
+ end
34
+
35
+ private
36
+
37
+ def read_time_of_day
38
+ f = @fields
39
+ f.skip(1) # the shift register's half-bits
40
+ @alarm = f.bytes(4)
41
+ f.skip(1) # cycles since the ICR was read
42
+ state = f.byte
43
+ @stopped = state.anybits?(2)
44
+ latch = f.bytes(4)
45
+ @latch = state.anybits?(1) ? latch : nil
46
+ @ticks = f.qword
47
+ end
48
+
49
+ def read_internals
50
+ f = @fields
51
+ @states = [f.word, f.word]
52
+ @shift = f.byte
53
+ f.skip(2) # whether the SDR holds a byte to send, the IRQ line
54
+ @pulses = f.byte.clamp(0, 5)
55
+ end
56
+
57
+ # Each timer takes its control register without the load strobe,
58
+ # its output toggle, and whether its count pipeline is full.
59
+ def load_timers(cia)
60
+ [@control_a, @control_b].zip(cia.timers).each_with_index do |(control, timer), i|
61
+ state = @states[i]
62
+ pipe = (state.anybits?(COUNT2) ? 0b10 : 0) | (state.anybits?(COUNT3) ? 0b01 : 0)
63
+ timer.restore_pipeline(control, @toggles.anybits?(0x40 << i), pipe)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ # The 6510 as x64sc saves it: MAINCPU 1.4 from VICE 3.8 on, 1.2
7
+ # before, and MAINC64CPU 1.5, the same fields under a new name, in
8
+ # VICE's development versions. The clock, the registers, the last
9
+ # opcode, the ANE and LXA log levels, whether the CPU is jammed and the
10
+ # BA low flags, then the interrupt bookkeeping: the IRQ, NMI and
11
+ # pending-IRQ clocks, the stolen cycles and when they were stolen, the
12
+ # IRQ and NMI source counts, the pending interrupts and the IRQ and NMI
13
+ # delays. 1.2 lacks the log levels and the jammed flag.
14
+ #
15
+ # Written: the registers as they stand, which between instructions is
16
+ # the whole CPU. VICE's interrupt clocks and delays are written as long
17
+ # past, with the IRQ and NMI source counts and a pending NMI.
18
+ #
19
+ # Read: the clock and the registers. The CPU starts on an instruction
20
+ # boundary, as VICE's snapshot is taken on one, and a pending NMI is
21
+ # taken there.
22
+ module MainCPU
23
+ NAME = "MAINCPU"
24
+ TRUNK_NAME = "MAINC64CPU"
25
+ MAJOR = 1
26
+ MINOR = 4
27
+ # The versions read, by name, and whether each has the log levels
28
+ # and the jammed flag.
29
+ VERSIONS = { "MAINCPU 1.2" => false, "MAINCPU 1.4" => true, "MAINC64CPU 1.5" => true }.freeze
30
+ IK_NMI = 0x01
31
+ IK_IRQ = 0x02
32
+
33
+ module_function
34
+
35
+ def export(computer)
36
+ cpu = computer.cpu
37
+ fields = FieldWriter.new.qword(computer.cycles)
38
+ [cpu.a, cpu.x, cpu.y, cpu.stack_pointer].each { |register| fields.byte(register) }
39
+ fields.word(cpu.program_counter).byte(cpu.p)
40
+ fields.dword(0).dword(0).dword(0).dword(cpu.jammed? ? 1 : 0).dword(0)
41
+ 5.times { fields.qword(0) }
42
+ interrupts(computer, fields)
43
+ fields.qword(0).qword(0)
44
+ fields.section(NAME, MAJOR, MINOR)
45
+ end
46
+
47
+ def interrupts(computer, fields)
48
+ irqs = [computer.vic.interrupted?, computer.cia1.interrupted?].count(true)
49
+ nmis = computer.cia2.interrupted? ? 1 : 0
50
+ pending = (irqs.positive? ? IK_IRQ : 0) | (computer.cpu.nmi ? IK_NMI : 0)
51
+ fields.dword(irqs).dword(nmis).dword(pending)
52
+ end
53
+
54
+ # The module a snapshot has, under either name.
55
+ def find(container) = container[NAME] || container[TRUNK_NAME]
56
+
57
+ def reads?(section) = VERSIONS.key?(section.to_s)
58
+
59
+ def import(section, computer)
60
+ fields = FieldReader.new(section)
61
+ clock = fields.qword
62
+ cpu = computer.cpu
63
+ cpu.a, cpu.x, cpu.y, cpu.stack_pointer = Array.new(4) { fields.byte }
64
+ cpu.program_counter = fields.word
65
+ cpu.p = fields.byte
66
+ fields.skip(VERSIONS.fetch(section.to_s) ? 20 : 8)
67
+ fields.skip(40 + 8)
68
+ pending = fields.dword
69
+ cpu.resume(clock)
70
+ cpu.nmi = pending.anybits?(IK_NMI)
71
+ computer.resume_at(clock)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ # The modules x64sc refuses a snapshot without, for the parts badline
7
+ # doesn't model in VICE's terms: the cartridge port, the file system
8
+ # drives, the glue logic, memory expansions, the tape port, the
9
+ # control ports and the user port. Each is written as a machine with
10
+ # nothing attached leaves it, with the joysticks released and the
11
+ # glue logic's VIC bank taken from CIA 2. A cartridge or tape badline
12
+ # has attached is carried by the BADLINE module only.
13
+ module Peripherals
14
+ # The glue logic's VIC bank, and its pending bank change.
15
+ GLUE = "GLUE"
16
+
17
+ FIXED = [
18
+ ["C64CART", 0, 1, [0x00]], # no cartridge
19
+ ["FSDRIVE", 0, 0, Array.new(258, 0)] # no file system drive open
20
+ ].freeze
21
+
22
+ AFTER_VIC = [
23
+ ["C64MEMHACKS", 0, 0, [0x00]], # no memory expansion hack
24
+ ["TAPEPORT", 1, 0, [0x01, 0x01]], # tape port on, datasette in it
25
+ ["JOYPORT0", 0, 0, [0x01]], ["JOYSTICK0", 1, 2, [0x00, 0x00]], # joysticks, released
26
+ ["JOYPORT1", 0, 0, [0x01]], ["JOYSTICK1", 1, 2, [0x00, 0x00]],
27
+ ["USERPORT", 1, 0, [0x01, 0x00]] # user port on, nothing in it
28
+ ].freeze
29
+
30
+ module_function
31
+
32
+ def cartridge_port = section(*FIXED[0])
33
+
34
+ def drives = section(*FIXED[1])
35
+
36
+ def glue(computer)
37
+ bank = ~computer.cia2.port_a_lines & 0x03
38
+ FieldWriter.new.byte(0).byte(bank).byte(0).section(GLUE, 1, 0)
39
+ end
40
+
41
+ def rest = AFTER_VIC.map { |entry| section(*entry) }
42
+
43
+ def section(name, major, minor, bytes) = FieldWriter.new.bytes(bytes).section(name, major, minor)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ # Restores a machine from a snapshot VICE wrote, through the modules
7
+ # badline reads.
8
+ module Restore
9
+ # The modules badline reads, in the order it applies them: the VIC
10
+ # comes last, as it is clocked to its cycle through memory and CIA 2.
11
+ # MAINCPU and C64MEM hold the machine itself, so a version badline
12
+ # doesn't read fails the restore; the others are left out and
13
+ # reported.
14
+ READ = [MainCPU::NAME, C64Mem::NAME, "CIA1", "CIA2", SIDRegisters::NAME, SIDExtended::NAME,
15
+ VICII::NAME].freeze
16
+ REQUIRED = [MainCPU::NAME, C64Mem::NAME].freeze
17
+
18
+ module_function
19
+
20
+ # Applies the modules badline reads to `computer`, and returns a
21
+ # Report naming what it applied and, with a reason, what it left out.
22
+ # The snapshot goes into a new machine first, so one that fails leaves
23
+ # `computer` as it was, and one of a machine built another way fails
24
+ # too. Then the machine is taken to power-on with its cartridge out,
25
+ # so nothing it was doing carries into the snapshot's machine.
26
+ def import(container, computer)
27
+ setup = Vice.setup(container)
28
+ ours = Setup.of(computer.address_bus)
29
+ raise FormatError, "the snapshot is of a machine with #{setup}, not #{ours}" unless setup == ours
30
+
31
+ apply(container, setup.build)
32
+ prepare(computer)
33
+ report = apply(container, computer)
34
+ Report.new(applied: report.applied, ignored: report.ignored + drives(computer))
35
+ end
36
+
37
+ # Applies the modules badline reads to a machine built as the
38
+ # snapshot's was (setup), and returns the Report.
39
+ def apply(container, computer)
40
+ applied = []
41
+ ignored = []
42
+ READ.each do |name|
43
+ section = find(name, container)
44
+ next unless section
45
+
46
+ reason = refusal(name, section, container)
47
+ if reason
48
+ ignored << "#{section}: #{reason}, left out"
49
+ else
50
+ import_module(name, section, computer)
51
+ applied << section.name
52
+ end
53
+ end
54
+ Report.new(applied:, ignored: ignored + left_out(container, applied))
55
+ end
56
+
57
+ # The module a snapshot has for one badline reads, under any of its
58
+ # names.
59
+ def find(name, container)
60
+ case name
61
+ when MainCPU::NAME then MainCPU.find(container)
62
+ when VICII::NAME then VICII.find(container)
63
+ else container[name]
64
+ end
65
+ end
66
+
67
+ def prepare(computer)
68
+ computer.address_bus.detach_cartridge if computer.address_bus.cartridge
69
+ computer.power_cycle!
70
+ end
71
+
72
+ # Why badline leaves the module out, or nil when it reads it.
73
+ def refusal(name, section, container)
74
+ reason = if !reads?(name, section) then "badline doesn't read this version"
75
+ elsif name == SIDExtended::NAME && !resid?(container) then "another SID engine wrote it"
76
+ end
77
+ raise FormatError, "#{section}: #{reason}" if reason && REQUIRED.include?(name)
78
+
79
+ reason
80
+ end
81
+
82
+ def reads?(name, section)
83
+ case name
84
+ when MainCPU::NAME then MainCPU.reads?(section)
85
+ when C64Mem::NAME then C64Mem.reads?(section)
86
+ when SIDRegisters::NAME then SIDRegisters.reads?(section)
87
+ when SIDExtended::NAME then SIDExtended.reads?(section)
88
+ when VICII::NAME then VICII.reads?(section)
89
+ else CIAs.reads?(section)
90
+ end
91
+ end
92
+
93
+ def import_module(name, section, computer)
94
+ case name
95
+ when MainCPU::NAME then MainCPU.import(section, computer)
96
+ when C64Mem::NAME then C64Mem.import(section, computer)
97
+ when SIDRegisters::NAME then SIDRegisters.import(section, computer)
98
+ when SIDExtended::NAME then SIDExtended.import(section, computer)
99
+ when VICII::NAME then VICII.import(section, computer)
100
+ else CIAs.import(section, computer)
101
+ end
102
+ end
103
+
104
+ # SIDEXTENDED holds the engine's own state, and badline reads reSID's.
105
+ def resid?(container)
106
+ sid = container[SIDRegisters::NAME]
107
+ !sid.nil? && SIDRegisters.engine(sid) == SIDRegisters::RESID
108
+ end
109
+
110
+ def left_out(container, applied)
111
+ read = READ + [MainCPU::TRUNK_NAME, VICII::TRUNK_NAME]
112
+ container.sections.reject { |section| applied.include?(section.name) || read.include?(section.name) }
113
+ .map { |section| "#{section}: badline doesn't read this module, left out" }
114
+ end
115
+
116
+ def drives(computer)
117
+ lines = []
118
+ lines << "the disk mounted in device 8 stays mounted" if computer.mounted?
119
+ lines << "the 1541 stays on the serial bus, reset" if computer.drive1541
120
+ lines
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ # SIDEXTENDED 1.4 for the reSID engine: the state reSID keeps between
7
+ # cycles, voice by voice. badline's SID follows reSID's model, so its
8
+ # oscillators, noise registers and envelopes map across field by
9
+ # field. x64sc won't load a snapshot without this module when it runs
10
+ # reSID. A module another engine wrote has another layout, and isn't
11
+ # read.
12
+ #
13
+ # After the 32 registers, the data bus byte and its countdown, each
14
+ # field is written for the three voices in turn: the phase, the noise
15
+ # register, the envelope's rate and exponential counters, its level,
16
+ # state and hold-at-zero flag, its rate and exponential periods and
17
+ # pipeline, then the noise register's shift pipeline and reset
18
+ # countdown, the floating output's countdown and the pulse level. The
19
+ # voice mask and two bytes before it end the module.
20
+ module SIDExtended
21
+ NAME = "SIDEXTENDED"
22
+ MAJOR = 1
23
+ MINOR = 4
24
+ WAVEFORM_FIELDS = 6
25
+ ENVELOPE_FIELDS = 8
26
+ # What VICE's own snapshots carry for reSID's voice mask.
27
+ VOICE_MASK = 0xf7
28
+
29
+ # Each field's width, and where it sits among a voice's oscillator
30
+ # fields (Waveform#resid_fields, :waveform) or envelope fields
31
+ # (Envelope#resid_fields, :envelope).
32
+ FIELDS = [
33
+ [:dword, :waveform, 0], [:dword, :waveform, 1], [:word, :envelope, 0], [:word, :envelope, 1],
34
+ [:byte, :envelope, 2], [:byte, :envelope, 3], [:byte, :envelope, 4], [:word, :envelope, 5],
35
+ [:word, :envelope, 6], [:byte, :envelope, 7], [:byte, :waveform, 2], [:dword, :waveform, 3],
36
+ [:dword, :waveform, 4], [:word, :waveform, 5]
37
+ ].freeze
38
+
39
+ module_function
40
+
41
+ def reads?(section) = section.major == MAJOR && section.minor == MINOR
42
+
43
+ # `computer`'s SID must have caught up.
44
+ def export(computer)
45
+ sid = computer.sid
46
+ fields = FieldWriter.new.bytes(Array.new(0x20) { |reg| sid.register(reg) })
47
+ value, ttl = sid.bus_state
48
+ fields.byte(value).dword(ttl)
49
+ voices = sid.voices.map { |voice| [voice.waveform.resid_fields, voice.envelope.resid_fields] }
50
+ FIELDS.each do |width, part, index|
51
+ voices.each { |waveform, envelope| write(fields, width, (part == :waveform ? waveform : envelope)[index]) }
52
+ end
53
+ fields.byte(0).byte(0).byte(VOICE_MASK).section(NAME, MAJOR, MINOR)
54
+ end
55
+
56
+ def write(fields, width, value)
57
+ case width
58
+ when :byte then fields.byte(value)
59
+ when :word then fields.word(value)
60
+ else fields.dword(value)
61
+ end
62
+ end
63
+
64
+ def import(section, computer)
65
+ sid = computer.sid
66
+ voices = sid.voices
67
+ fields = FieldReader.new(section).skip(0x20)
68
+ sid.restore_bus(fields.byte, fields.dword)
69
+ waveforms = Array.new(voices.length * WAVEFORM_FIELDS, 0)
70
+ envelopes = Array.new(voices.length * ENVELOPE_FIELDS, 0)
71
+ FIELDS.each do |width, part, index|
72
+ voices.length.times do |n|
73
+ if part == :waveform
74
+ waveforms[(n * WAVEFORM_FIELDS) + index] = value(fields, width)
75
+ else
76
+ envelopes[(n * ENVELOPE_FIELDS) + index] = value(fields, width)
77
+ end
78
+ end
79
+ end
80
+ voices.each_with_index do |voice, n|
81
+ voice.waveform.restore_resid_fields(waveforms, n * WAVEFORM_FIELDS)
82
+ voice.envelope.restore_resid_fields(envelopes, n * ENVELOPE_FIELDS)
83
+ end
84
+ end
85
+
86
+ def value(fields, width)
87
+ case width
88
+ when :byte then fields.byte
89
+ when :word then fields.word
90
+ else fields.dword
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ # SID 1.5, the module every VICE SID engine writes: the number of
7
+ # extra SIDs, whether sound is on, the engine, the model and the 32
8
+ # registers as last written. The engine's own state goes in
9
+ # SIDEXTENDED, which is reSID's and badline doesn't read.
10
+ #
11
+ # Read: the registers are written to the SID in order, so the voices
12
+ # start again from the gates as they stand, with their envelopes and
13
+ # oscillators from zero.
14
+ module SIDRegisters
15
+ NAME = "SID"
16
+ MAJOR = 1
17
+ MINOR = 5
18
+ RESID = 1
19
+ MODELS = %i[mos6581 mos8580].freeze
20
+
21
+ module_function
22
+
23
+ def export(computer)
24
+ sid = computer.sid
25
+ fields = FieldWriter.new.byte(0).byte(1).byte(RESID).byte(MODELS.index(sid.model))
26
+ fields.bytes(Array.new(0x20) { |reg| sid.register(reg) })
27
+ fields.section(NAME, MAJOR, MINOR)
28
+ end
29
+
30
+ # 1.4 added the registers as last written, which badline reads.
31
+ def reads?(section) = section.major == MAJOR && section.minor >= 4
32
+
33
+ def engine(section) = FieldReader.new(section).skip(2).byte
34
+
35
+ def model(section)
36
+ MODELS.fetch(FieldReader.new(section).skip(3).byte, :mos6581)
37
+ end
38
+
39
+ def import(section, computer)
40
+ registers = FieldReader.new(section).skip(4).bytes(0x20)
41
+ registers.first(0x19).each_with_index { |value, reg| computer.sid.poke(0xd400 + reg, value) }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end