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,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ # VIC-II 1.3 as x64sc writes it, and VIC-IISC 1.4, the same fields
7
+ # under a new name, as VICE's development versions write it: the model, the registers, the beam's
8
+ # line and cycle, the IRQ latch, the video counters and buffers, the
9
+ # light pen, the borders, colour RAM and each sprite's counters, then
10
+ # x64sc's pixel pipeline and its frame buffer.
11
+ #
12
+ # Written: what badline's VIC holds in VICE's terms. The pixel
13
+ # pipeline and frame buffer are left blank, as VICE repaints on
14
+ # loading.
15
+ #
16
+ # Read: the registers, colour RAM, the IRQ latch and collisions, the
17
+ # video counters and buffers and the sprite counters. The VIC is put
18
+ # at the start of the snapshot's line and clocked on its own to the
19
+ # snapshot's cycle, so its fetch and sprite state match the line, then
20
+ # takes the snapshot's counters. Its pixel pipeline starts empty, so
21
+ # the first pixels drawn may differ.
22
+ module VICII
23
+ NAME = "VIC-II"
24
+ TRUNK_NAME = "VIC-IISC"
25
+ MAJOR = 1
26
+ MINOR = 3
27
+ VERSIONS = ["VIC-II 1.3", "VIC-IISC 1.4"].freeze
28
+ MODELS = %i[mos6569 mos8565].freeze
29
+ FRAME_WIDTH = 384
30
+ FRAME_HEIGHT = 312
31
+ # The pixel pipeline x64sc keeps between cycles, in bytes.
32
+ PIPELINE = 16 + 32 + 6 + 32 + 8 + 3 + 24 + 0x2f + 2 + 4
33
+
34
+ module_function
35
+
36
+ def export(computer)
37
+ vic = computer.vic
38
+ fields = FieldWriter.new.byte(MODELS.index(vic.model)).bytes(vic.register_file)
39
+ beam(vic, fields)
40
+ counters(vic, fields)
41
+ borders(vic, fields)
42
+ fields.bytes(Array.new(0x400) { |offset| computer.address_bus.color_ram.nibble(0xd800 + offset) })
43
+ 8.times { |n| sprite(vic, n, fields) }
44
+ pipeline(vic, fields)
45
+ fields.dword(vic.rasterline).dword(FRAME_WIDTH).dword(FRAME_HEIGHT).dword(FRAME_WIDTH)
46
+ fields.zeros(FRAME_WIDTH * (FRAME_HEIGHT + 4))
47
+ fields.section(NAME, MAJOR, MINOR)
48
+ end
49
+
50
+ def beam(vic, fields)
51
+ registers = vic.registers
52
+ fields.dword(vic.column).dword(0).dword(vic.rasterline).byte(0)
53
+ fields.byte((registers[0x19] & 0x0f) | (vic.interrupted? ? 0x80 : 0))
54
+ fields.dword(registers.raster_target).flag(vic.latch_bits.anybits?(1))
55
+ [vic.character_buffer, vic.color_buffer].each { |buffer| fields.bytes(buffer.map { |value| value || 0 }) }
56
+ fields.byte(0).dword(0).zeros(65 * 8) # x64sc's graphics buffer and draw buffer
57
+ end
58
+
59
+ def counters(vic, fields)
60
+ registers = vic.registers
61
+ state = vic.display_state
62
+ fields.dword(registers.yscroll).flag(state.bad_lines_enabled)
63
+ fields.byte(registers[0x1e]).byte(registers[0x1f]).byte(0)
64
+ fields.dword(state.display? ? 0 : 1)
65
+ [state.vc_base, state.vc, state.rc, state.vmli].each { |counter| fields.dword(counter) }
66
+ fields.dword(state.bad_line_condition? ? 1 : 0)
67
+ fields.flag(vic.latch_bits.anybits?(2)).flag(vic.latch_bits.anybits?(4))
68
+ fields.dword(registers[0x13]).dword(registers[0x14]).dword(0).qword(0)
69
+ fields.byte(vic.fetch_d011).dword(0)
70
+ fields.dword(sprite_bits(vic, &:display_on)).byte(sprite_bits(vic, &:displaying?))
71
+ end
72
+
73
+ def borders(vic, fields)
74
+ fields.byte(0xff).byte(0).byte(0).byte(0) # last colour register and value, last bus bytes
75
+ vic.sequencer.borders.each { |flop| fields.flag(flop) }
76
+ fields.byte(0xff - (5 * vic.rasterline))
77
+ end
78
+
79
+ def sprite(vic, index, fields)
80
+ sprite = vic.sprites[index]
81
+ fields.dword(sprite.sr).byte(sprite.mc).byte(sprite.mcbase).byte(0).flag(sprite.exp_ff).dword(sprite.x)
82
+ end
83
+
84
+ # Blank but for the colour registers and the sprite priority,
85
+ # multicolour and expansion bits, which x64sc keeps copies of.
86
+ def pipeline(vic, fields)
87
+ file = vic.register_file
88
+ fields.zeros(16 + 32).bytes([file[0x1b], file[0x1c], file[0x1d], 0, 0, 0])
89
+ fields.zeros(32 + 8 + 3 + 24).bytes(file.first(0x2f)).byte(0xff).byte(0).dword(0)
90
+ end
91
+
92
+ def sprite_bits(vic)
93
+ 8.times.sum { |n| yield(vic.sprites[n]) ? 1 << n : 0 }
94
+ end
95
+
96
+ # The module a snapshot has, under either name.
97
+ def find(container) = container[NAME] || container[TRUNK_NAME]
98
+
99
+ def reads?(section) = VERSIONS.include?(section.to_s)
100
+
101
+ # VICE's model numbers: the 6569, the 8565 and the 6569R1 run PAL;
102
+ # the 6567, 8562 and 6567R56A NTSC, and the 6572 PAL-N, which badline
103
+ # doesn't run yet.
104
+ def model(section)
105
+ number = FieldReader.new(section).byte
106
+ return :mos8565 if number == 1
107
+ return :mos6569 if [0, 2].include?(number)
108
+
109
+ raise FormatError, "#{section}: an NTSC or PAL-N VIC-II (VICE model #{number}), which badline doesn't run"
110
+ end
111
+
112
+ def import(section, computer)
113
+ snapshot = Import.new(FieldReader.new(section))
114
+ snapshot.apply(computer)
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module VICII
7
+ # Reads a VIC-II module and puts it into badline's VIC.
8
+ class Import
9
+ def initialize(fields)
10
+ @fields = fields
11
+ read_beam
12
+ read_counters
13
+ read_sprites
14
+ end
15
+
16
+ def apply(computer)
17
+ vic = computer.vic
18
+ computer.address_bus.color_ram.tap do |ram|
19
+ @color_ram.each_with_index { |value, offset| ram.poke(0xd800 + offset, value) }
20
+ end
21
+ load_registers(vic)
22
+ position(vic)
23
+ load_counters(vic)
24
+ load_sprites(vic)
25
+ vic.rebuild_sprite_ba
26
+ end
27
+
28
+ private
29
+
30
+ def read_beam
31
+ f = @fields
32
+ f.skip(1)
33
+ @registers = f.bytes(0x40)
34
+ @cycle = f.dword
35
+ f.skip(4)
36
+ @line = f.dword
37
+ f.skip(1)
38
+ @irq_status = f.byte
39
+ f.skip(4)
40
+ @raster_match = f.flag?
41
+ @character_buffer = f.bytes(40)
42
+ @color_buffer = f.bytes(40)
43
+ f.skip(1 + 4 + (65 * 8) + 4)
44
+ end
45
+
46
+ def read_counters
47
+ f = @fields
48
+ @bad_lines_enabled = f.flag?
49
+ @collisions = [f.byte, f.byte]
50
+ f.skip(1)
51
+ @display = f.dword.zero?
52
+ @vc_base, @vc, @rc, @vmli = Array.new(4) { f.dword }
53
+ f.skip(4)
54
+ @lp_low = f.flag?
55
+ @lp_triggered = f.flag?
56
+ f.skip(4 + 4 + 4 + 8)
57
+ @fetch_d011 = f.byte
58
+ f.skip(4)
59
+ @display_bits = f.dword
60
+ @dma_bits = f.byte
61
+ f.skip(4)
62
+ @vertical_border = f.flag?
63
+ @vertical_armed = f.flag?
64
+ @main_border = f.flag?
65
+ f.skip(1)
66
+ @color_ram = f.bytes(0x400)
67
+ end
68
+
69
+ # Each sprite's MC and MCBASE, and its expansion flip-flop as a bit.
70
+ def read_sprites
71
+ f = @fields
72
+ @sprite_mc = Array.new(8, 0)
73
+ @sprite_mcbase = Array.new(8, 0)
74
+ @sprite_exp_ff = 0
75
+ 8.times do |n|
76
+ f.skip(4)
77
+ @sprite_mc[n] = f.byte
78
+ @sprite_mcbase[n] = f.byte
79
+ f.skip(1)
80
+ @sprite_exp_ff |= 1 << n if f.flag?
81
+ f.skip(4)
82
+ end
83
+ end
84
+
85
+ def load_registers(vic)
86
+ registers = vic.registers
87
+ bytes = registers.bytes
88
+ 0x2f.times { |reg| bytes[reg] = @registers[reg] }
89
+ bytes[0x19] = @irq_status & 0x0f
90
+ bytes[0x1a] &= 0x0f
91
+ bytes[0x1e], bytes[0x1f] = @collisions
92
+ registers.update_irq_line
93
+ end
94
+
95
+ # Clocks the VIC alone from the start of the line to the cycle,
96
+ # so the line's fetches and sprite checks have run.
97
+ def position(vic)
98
+ vic.restore_line(@line)
99
+ load_display_state(vic)
100
+ @cycle.times { vic.cycle! }
101
+ registers = vic.registers
102
+ registers.bytes[0x19] = @irq_status & 0x0f
103
+ registers.bytes[0x1e], registers.bytes[0x1f] = @collisions
104
+ registers.update_irq_line
105
+ end
106
+
107
+ def load_display_state(vic)
108
+ vic.display_state.restore_counters([@vc_base, @vc, @rc, @vmli], @display, @bad_lines_enabled)
109
+ end
110
+
111
+ def load_counters(vic)
112
+ load_display_state(vic)
113
+ vic.character_buffer.replace(@character_buffer)
114
+ vic.color_buffer.replace(@color_buffer)
115
+ vic.restore_latches((@raster_match ? 1 : 0) | (@lp_low ? 2 : 0) | (@lp_triggered ? 4 : 0), @fetch_d011)
116
+ vic.sequencer.restore_borders(@vertical_border, @vertical_armed, @main_border)
117
+ end
118
+
119
+ def load_sprites(vic)
120
+ 8.times do |n|
121
+ bit = 1 << n
122
+ vic.sprites[n].restore_counters(@sprite_mc[n], @sprite_mcbase[n], @sprite_exp_ff.anybits?(bit),
123
+ @dma_bits.anybits?(bit), @display_bits.anybits?(bit))
124
+ end
125
+ vic.sprites.update_any_dma
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/snapshot/vice/main_cpu"
4
+ require "badline/snapshot/vice/c64_mem"
5
+ require "badline/snapshot/vice/cias"
6
+ require "badline/snapshot/vice/cias_import"
7
+ require "badline/snapshot/vice/sid_registers"
8
+ require "badline/snapshot/vice/sid_extended"
9
+ require "badline/snapshot/vice/peripherals"
10
+ require "badline/snapshot/vice/vicii"
11
+ require "badline/snapshot/vice/vicii_import"
12
+ require "badline/snapshot/vice/restore"
13
+
14
+ module Badline
15
+ module Snapshot
16
+ # VICE's own modules for the chips both emulators model, written from
17
+ # and read into badline's machine. Each module's file says what maps
18
+ # and what is approximated. Reading a snapshot from VICE starts from a
19
+ # machine at power-on with nothing attached, and applies the modules it
20
+ # has, in the order x64sc writes them.
21
+ module Vice
22
+ # Cycles a copy of the machine may run to reach an instruction
23
+ # boundary: the longest instruction, stretched by the VIC's longest
24
+ # hold on the bus.
25
+ BOUNDARY_CYCLES = 100
26
+
27
+ # The modules a badline snapshot carries for VICE, in x64sc's order.
28
+ # They describe a copy of the machine restored from `state`, so saving
29
+ # leaves the machine itself alone. VICE saves between instructions,
30
+ # and resumes by fetching an opcode at the program counter, so when
31
+ # badline's CPU is inside an instruction the copy runs on to its end.
32
+ # The copy's SID catches up, so its registers and voices stand as of
33
+ # that cycle. badline writes no VICE module for an REU or an NTSC
34
+ # VIC-II yet, and fails for a machine with either.
35
+ def self.export(state)
36
+ check_exportable(Computer.setup(state))
37
+ machine = settled(state)
38
+ [MainCPU.export(machine), C64Mem.export(machine), Peripherals.cartridge_port, *CIAs.export(machine),
39
+ SIDRegisters.export(machine), SIDExtended.export(machine), Peripherals.drives, VICII.export(machine),
40
+ Peripherals.glue(machine), *Peripherals.rest]
41
+ end
42
+
43
+ def self.check_exportable(setup)
44
+ raise FormatError, "badline can't save an REU in VICE's terms yet, so can't save this machine" if setup.reu
45
+ return if setup.region == Region::PAL
46
+
47
+ raise FormatError, "badline can't save an NTSC VIC-II in VICE's terms yet, so can't save this machine"
48
+ end
49
+
50
+ # The copy is detached from the host's files (Snapshot::StateReader),
51
+ # so it reads none and its traps write none.
52
+ def self.settled(state)
53
+ copy = Computer.restored(state, detached: true)
54
+ BOUNDARY_CYCLES.times do
55
+ break if copy.cpu.boundary?
56
+
57
+ copy.cycle!
58
+ end
59
+ copy.sid.catch_up!
60
+ copy
61
+ end
62
+
63
+ # How the machine the snapshot was saved from was built. VICE doesn't
64
+ # save the CIA model; the C64C's 8565 VIC-II goes with the 6526A. An
65
+ # NTSC or PAL-N VIC-II fails, as badline runs PAL only.
66
+ def self.setup(container)
67
+ vic = VICII.find(container) ? VICII.model(VICII.find(container)) : :mos6569
68
+ sid = container[SIDRegisters::NAME] ? SIDRegisters.model(container[SIDRegisters::NAME]) : :mos6581
69
+ Setup.new(vic_model: vic, cia_model: vic == :mos8565 ? :mos6526a : :mos6526, sid_model: sid,
70
+ region: Region::PAL, ram_expansion: nil, reu: nil)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "badline/snapshot/state"
5
+ require "badline/snapshot/setup"
6
+ require "badline/snapshot/container"
7
+ require "badline/snapshot/fields"
8
+ require "badline/snapshot/machine_state"
9
+ require "badline/snapshot/vice"
10
+ require "badline/snapshot/image"
11
+
12
+ module Badline
13
+ # Saves and restores the whole machine in VICE's .vsf snapshot format.
14
+ #
15
+ # A snapshot badline writes carries VICE's modules for what both
16
+ # emulators model, and a BADLINE module holding the State
17
+ # Computer#snapshot takes, with everything badline needs to carry on
18
+ # exactly where it stopped. VICE skips modules it doesn't know. Restoring
19
+ # a badline snapshot uses the BADLINE module alone; a snapshot from VICE
20
+ # restores through its modules, as far as they map, and reports the
21
+ # modules badline doesn't take.
22
+ module Snapshot
23
+ module_function
24
+
25
+ def save(computer, path)
26
+ File.binwrite(path, dump(computer))
27
+ path
28
+ end
29
+
30
+ def dump(computer)
31
+ state = computer.snapshot
32
+ Container.new(Vice.export(state) + [MachineState.section(state)]).to_s
33
+ end
34
+
35
+ def read(path) = Image.new(Container.parse(File.binread(path)))
36
+
37
+ # A new machine, built as the snapshot's was and restored from it.
38
+ # Yields a line for each thing it leaves out.
39
+ def load(path, &) = read(path).load(&)
40
+
41
+ # Restores `computer` from the snapshot at `path`, and returns what it
42
+ # applied and left out, yielding a line for each thing left out. A
43
+ # snapshot that fails leaves the machine as it was.
44
+ def restore(computer, path, &) = read(path).restore(computer, &)
45
+ end
46
+
47
+ class Computer
48
+ def save_snapshot(path) = Snapshot.save(self, path)
49
+
50
+ def restore_snapshot(path, &) = Snapshot.restore(self, path, &)
51
+ end
52
+ end
@@ -18,6 +18,11 @@ module Badline
18
18
  Image = Data.define(:hardware_type, :subtype, :exrom, :game, :name, :chips)
19
19
 
20
20
  def self.write(path, image)
21
+ File.binwrite(path, encode(image))
22
+ end
23
+
24
+ # The CRT file for an image: a parsed CRTFile or an Image.
25
+ def self.encode(image)
21
26
  header = SIGNATURE +
22
27
  [0x40, 0x0100, image.hardware_type, image.exrom, image.game, image.subtype].pack("NnnCCCx5") +
23
28
  [image.name.to_s.b[0, 32]].pack("a32")
@@ -25,11 +30,12 @@ module Badline
25
30
  ["CHIP", CHIP_HEADER_SIZE + chip.data.length, chip.chip_type, chip.bank, chip.address,
26
31
  chip.data.length].pack("a4Nn4") + chip.data.pack("C*")
27
32
  end
28
- File.binwrite(path, header + packets.join)
33
+ header + packets.join
29
34
  end
30
35
 
31
- def initialize(path)
32
- parse(File.binread(path))
36
+ # `bytes` stands in for the file's contents.
37
+ def initialize(path = nil, bytes: nil)
38
+ parse(bytes || File.binread(path))
33
39
  end
34
40
 
35
41
  private
@@ -15,6 +15,8 @@ module Badline
15
15
  count
16
16
  end
17
17
 
18
+ def storage_kind = D64
19
+
18
20
  def sectors_in(track)
19
21
  case track
20
22
  when 1..17 then 21
@@ -5,6 +5,8 @@ module Badline
5
5
  class D71Image < D64Image
6
6
  ERROR_TABLES = { 351_062 => 1366 }.freeze
7
7
 
8
+ def storage_kind = D71
9
+
8
10
  private
9
11
 
10
12
  def error_tables = ERROR_TABLES
@@ -5,6 +5,8 @@ module Badline
5
5
  class D81Image < DiskImage
6
6
  ERROR_TABLES = { 822_400 => 3200 }.freeze
7
7
 
8
+ def storage_kind = D81
9
+
8
10
  private
9
11
 
10
12
  def error_tables = ERROR_TABLES
@@ -27,16 +27,25 @@ module Badline
27
27
  # the read, write and ID errors, 74 is DRIVE NOT READY.
28
28
  DOS_ERRORS = (2..11).to_h { |code| [code, code + 18] }.merge(15 => 74).freeze
29
29
 
30
- def initialize(path, read_only: false)
30
+ # `bytes` stands in for the host file's contents, as a snapshot
31
+ # restores the image as it was.
32
+ def initialize(path, read_only: false, bytes: nil)
31
33
  @path = path
32
34
  @read_only = read_only
33
- @bytes = File.binread(path).bytes
35
+ @bytes = bytes || File.binread(path).bytes
34
36
  @errors = split_error_table
35
37
  end
36
38
 
37
39
  # Whether the image was opened write-protected.
38
40
  def read_only? = @read_only
39
41
 
42
+ # What Storage.reopen needs to open the image again as it stands: its
43
+ # path, whether it is write-protected, and its bytes with the error
44
+ # table after them.
45
+ def save_setup(out)
46
+ out.int(storage_kind).string(::File.expand_path(@path)).boolean(@read_only).blob(@bytes + @errors.to_a)
47
+ end
48
+
40
49
  # A LOAD reads only PRG files. An OPEN names the type it wants, or
41
50
  # takes the first file of any type with a nil `type`.
42
51
  def read_file(name, type: :prg)
@@ -14,6 +14,11 @@ module Badline
14
14
  @path = path
15
15
  end
16
16
 
17
+ # What Storage.reopen needs to open the directory again.
18
+ def save_setup(out)
19
+ out.int(HOST_DIRECTORY).string(::File.expand_path(@path))
20
+ end
21
+
17
22
  def read_file(name, **)
18
23
  entry = find(name)
19
24
  return unless entry
@@ -13,10 +13,16 @@ module Badline
13
13
  ID = "T64".bytes.freeze
14
14
 
15
15
  def initialize(path)
16
+ @path = path
16
17
  @bytes = File.binread(path).bytes
17
18
  raise FormatError, "Missing T64 signature" unless @bytes[0, 3] == SIGNATURE
18
19
  end
19
20
 
21
+ # What Storage.reopen needs to open the archive again.
22
+ def save_setup(out)
23
+ out.int(T64_ARCHIVE).string(::File.expand_path(@path))
24
+ end
25
+
20
26
  def read_file(name, **)
21
27
  pattern = Storage.matcher(name)
22
28
  entry = entries.find { |e| pattern.match?(e[:name]) }
@@ -13,9 +13,12 @@ module Badline
13
13
  HEADER_SIZE = 0x14
14
14
  OVERFLOW = 0x100 * 8
15
15
 
16
- attr_reader :version
16
+ # The file it was read from, expanded, and the offset of the next
17
+ # pulse byte.
18
+ attr_reader :version, :path
17
19
 
18
20
  def initialize(path)
21
+ @path = File.expand_path(path)
19
22
  @bytes = File.binread(path).bytes
20
23
  raise FormatError, "Missing TAP signature" unless @bytes[0, SIGNATURE.length] == SIGNATURE
21
24
 
@@ -30,6 +33,12 @@ module Badline
30
33
  @pos = HEADER_SIZE
31
34
  end
32
35
 
36
+ def position = @pos
37
+
38
+ def position=(offset)
39
+ @pos = offset
40
+ end
41
+
33
42
  def end? = @pos >= @end
34
43
 
35
44
  # Cycles until the next falling edge, or nil once the tape has run out.
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ # Stands in for a directory or archive a detached restore leaves out
6
+ # (Snapshot::StateReader): it finds no files and refuses every write
7
+ # as a write-protected disk does.
8
+ class Unavailable
9
+ def read_file(_name, **) = nil
10
+
11
+ def write_file(_name, _bytes, **)
12
+ raise WriteError, WriteError::WRITE_PROTECT_ON
13
+ end
14
+
15
+ def append_file(_name, _bytes, **)
16
+ raise WriteError, WriteError::WRITE_PROTECT_ON
17
+ end
18
+ end
19
+ end
20
+ end
@@ -13,6 +13,7 @@ require "badline/storage/tap"
13
13
  require "badline/storage/crt_file"
14
14
  require "badline/storage/sid_file"
15
15
  require "badline/storage/song_lengths"
16
+ require "badline/storage/unavailable"
16
17
 
17
18
  module Badline
18
19
  module Storage
@@ -33,7 +34,29 @@ module Badline
33
34
  end
34
35
  end
35
36
 
37
+ # The storage a snapshot's device 8 serves, by the number save_setup
38
+ # writes first.
39
+ HOST_DIRECTORY = 0
40
+ D64 = 1
41
+ D71 = 2
42
+ D81 = 3
43
+ T64_ARCHIVE = 4
44
+
36
45
  class << self
46
+ # Opens the storage a save_setup wrote: the same directory or archive
47
+ # by its path, or a disk image with the bytes it held, writes and
48
+ # all. A detached reader gets the image write-protected, and
49
+ # Unavailable for a directory or archive.
50
+ def reopen(input)
51
+ kind = input.int
52
+ path = input.string
53
+ case kind
54
+ when HOST_DIRECTORY then input.detached? ? Unavailable.new : HostDirectory.new(path)
55
+ when T64_ARCHIVE then input.detached? ? Unavailable.new : T64.new(path)
56
+ else reopen_image(kind, path, input)
57
+ end
58
+ end
59
+
37
60
  # Folds shifted PETSCII letters to their ASCII equivalents.
38
61
  def ascii(bytes)
39
62
  bytes.map { |b| b.between?(0xc1, 0xda) ? b - 0x80 : b }.pack("C*")
@@ -52,6 +75,17 @@ module Badline
52
75
  [base.to_s, FILE_TYPES[type.to_s.strip[0]&.upcase]]
53
76
  end
54
77
 
78
+ def reopen_image(kind, path, input)
79
+ read_only = input.boolean? || input.detached?
80
+ bytes = input.blob
81
+ case kind
82
+ when D64 then D64Image.new(path, read_only:, bytes:)
83
+ when D71 then D71Image.new(path, read_only:, bytes:)
84
+ when D81 then D81Image.new(path, read_only:, bytes:)
85
+ else raise Snapshot::FormatError, "unknown storage #{kind} in the state"
86
+ end
87
+ end
88
+
55
89
  # CBM-style filename pattern: "*" and "?" wildcards, case-insensitive.
56
90
  # The DOS stops comparing at the first "*", so whatever follows it
57
91
  # is ignored.
@@ -7,6 +7,8 @@ module Badline
7
7
  CLOCK_HZ = Region::PAL.clock_hz
8
8
  MAINS_HZ = Region::PAL.mains_hz
9
9
 
10
+ FIELDS = %i[tenths seconds minutes hours].freeze
11
+
10
12
  def initialize(clock_hz: CLOCK_HZ, mains_hz: MAINS_HZ)
11
13
  # The accumulator advances mains_hz per cycle, so a TOD pin pulse has
12
14
  # arrived when it reaches clock_hz. Integer math keeps it exact.
@@ -28,6 +30,28 @@ module Badline
28
30
  @match = enabled ? 4 : 5
29
31
  end
30
32
 
33
+ # The clock and the alarm, field by field in FIELDS order, the latch,
34
+ # and the divider's phase.
35
+ def save_state(out)
36
+ out.int(@accumulator).int(@pulses).int(@match).boolean(@stopped).boolean(@alarm_pending)
37
+ FIELDS.each { |field| out.int(@clock[field]).int(@alarm[field]) }
38
+ out.boolean(!@latch.nil?)
39
+ FIELDS.each { |field| out.int(@latch[field]) } if @latch
40
+ end
41
+
42
+ def load_state(input)
43
+ @accumulator = input.int
44
+ @pulses = input.int
45
+ @match = input.int
46
+ @stopped = input.boolean?
47
+ @alarm_pending = input.boolean?
48
+ FIELDS.each do |field|
49
+ @clock[field] = input.int
50
+ @alarm[field] = input.int
51
+ end
52
+ @latch = input.boolean? ? FIELDS.to_h { |field| [field, input.int] } : nil
53
+ end
54
+
31
55
  def cycle!
32
56
  @accumulator += @mains_hz
33
57
  pulse! if @accumulator >= @cycles_per_pulse
@@ -51,6 +75,38 @@ module Badline
51
75
  @latch[:hours]
52
76
  end
53
77
 
78
+ attr_reader :pulses
79
+
80
+ # The clock as a read of the tenths would see it: the latch while the
81
+ # clock is latched. In FIELDS order.
82
+ def latched_fields = FIELDS.map { |field| (@latch || @clock)[field] }
83
+
84
+ # The cycles until the clock next steps a tenth, as VICE counts them.
85
+ def cycles_to_tenth
86
+ to_pulse = (@cycles_per_pulse - @accumulator + @mains_hz - 1) / @mains_hz
87
+ to_pulse + ([@match - @pulses, 0].max * @cycles_per_pulse / @mains_hz)
88
+ end
89
+
90
+ # Sets the clock, the alarm and the latch from VICE's fields, in FIELDS
91
+ # order, the latch nil while unlatched.
92
+ def restore_fields(clock, alarm, latch)
93
+ FIELDS.each_with_index do |field, i|
94
+ @clock[field] = clock[i]
95
+ @alarm[field] = alarm[i]
96
+ end
97
+ @latch = latch && FIELDS.each_with_index.to_h { |field, i| [field, latch[i]] }
98
+ end
99
+
100
+ # Sets whether the clock is stopped, the pulses counted towards the
101
+ # next tenth and the cycles until it, placing the divider so the tenth
102
+ # lands then.
103
+ def restore_divider(stopped, pulses, cycles_to_tenth)
104
+ @stopped = stopped
105
+ @pulses = pulses
106
+ to_pulse = cycles_to_tenth - ([@match - pulses, 0].max * @cycles_per_pulse / @mains_hz)
107
+ @accumulator = (@cycles_per_pulse - (to_pulse * @mains_hz)).clamp(0, @cycles_per_pulse - 1)
108
+ end
109
+
54
110
  # The clock and alarm as stored, read without latching the clock.
55
111
  def registers
56
112
  [@clock[:tenths], @clock[:seconds], @clock[:minutes], @clock[:hours],
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end