badline 0.9.0 → 0.10.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +39 -4
  5. data/doc/library.md +15 -1
  6. data/doc/pinned-behaviour.md +146 -0
  7. data/doc/snapshots.md +17 -3
  8. data/lib/badline/address_bus/fittings.rb +38 -0
  9. data/lib/badline/address_bus/pla.rb +160 -0
  10. data/lib/badline/address_bus/roms.rb +12 -5
  11. data/lib/badline/address_bus.rb +15 -90
  12. data/lib/badline/chrout_trap.rb +4 -3
  13. data/lib/badline/computer/attachments.rb +47 -7
  14. data/lib/badline/computer/saved_state.rb +1 -0
  15. data/lib/badline/computer.rb +52 -37
  16. data/lib/badline/control_ports.rb +10 -3
  17. data/lib/badline/core.rb +3 -0
  18. data/lib/badline/datasette.rb +39 -1
  19. data/lib/badline/drive1541/serial_port.rb +5 -8
  20. data/lib/badline/drive1541/sleep.rb +5 -6
  21. data/lib/badline/drive1541.rb +3 -3
  22. data/lib/badline/frontend/app.rb +62 -64
  23. data/lib/badline/frontend/boot.rb +33 -8
  24. data/lib/badline/frontend/controls.rb +12 -6
  25. data/lib/badline/frontend/drive_led.rb +9 -10
  26. data/lib/badline/frontend/frame_rate.rb +4 -4
  27. data/lib/badline/frontend/machine_window.rb +87 -0
  28. data/lib/badline/frontend/menu_media.rb +49 -24
  29. data/lib/badline/frontend/menu_pages.rb +23 -40
  30. data/lib/badline/frontend/menu_ports.rb +69 -0
  31. data/lib/badline/frontend/pacer.rb +6 -5
  32. data/lib/badline/frontend/pause_menu.rb +30 -33
  33. data/lib/badline/frontend/player_screen.rb +1 -1
  34. data/lib/badline/frontend/player_window.rb +1 -1
  35. data/lib/badline/frontend/screen.rb +39 -37
  36. data/lib/badline/frontend/sid_view.rb +1 -1
  37. data/lib/badline/frontend/sound.rb +11 -9
  38. data/lib/badline/frontend/timeline.rb +36 -5
  39. data/lib/badline/frontend.rb +2 -0
  40. data/lib/badline/iec_bus.rb +16 -28
  41. data/lib/badline/kernal_trap/drive.rb +3 -0
  42. data/lib/badline/kernal_trap/file.rb +1 -1
  43. data/lib/badline/kernal_trap/layout.rb +112 -0
  44. data/lib/badline/kernal_trap/load.rb +16 -27
  45. data/lib/badline/kernal_trap/routine.rb +11 -27
  46. data/lib/badline/kernal_trap/save.rb +13 -23
  47. data/lib/badline/kernal_trap/serial.rb +21 -19
  48. data/lib/badline/kernal_trap.rb +1 -0
  49. data/lib/badline/keyboard.rb +28 -14
  50. data/lib/badline/machine.rb +60 -0
  51. data/lib/badline/media/vic20_basic.rb +54 -0
  52. data/lib/badline/media/vic20_cartridge.rb +74 -0
  53. data/lib/badline/media/vic20_media.rb +132 -0
  54. data/lib/badline/media.rb +32 -18
  55. data/lib/badline/model.rb +29 -12
  56. data/lib/badline/options/event.rb +5 -3
  57. data/lib/badline/options/help.rb +11 -3
  58. data/lib/badline/options/table.rb +12 -2
  59. data/lib/badline/options/validation.rb +55 -4
  60. data/lib/badline/options.rb +33 -33
  61. data/lib/badline/roms/README +51 -0
  62. data/lib/badline/roms/c64gs.rom +0 -0
  63. data/lib/badline/roms/kernal-pet64.rom +0 -0
  64. data/lib/badline/roms/vic20/basic.rom +0 -0
  65. data/lib/badline/roms/vic20/character.rom +0 -0
  66. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  67. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  68. data/lib/badline/snapshot/container.rb +2 -0
  69. data/lib/badline/snapshot/image.rb +15 -0
  70. data/lib/badline/snapshot/setup.rb +15 -10
  71. data/lib/badline/snapshot/state.rb +5 -3
  72. data/lib/badline/snapshot/vice.rb +2 -1
  73. data/lib/badline/snapshot.rb +10 -0
  74. data/lib/badline/storage/crt_file.rb +16 -5
  75. data/lib/badline/storage/disk_image.rb +3 -0
  76. data/lib/badline/storage/host_directory.rb +3 -0
  77. data/lib/badline/storage/t64.rb +3 -0
  78. data/lib/badline/storage/tap.rb +38 -4
  79. data/lib/badline/storage/unavailable.rb +2 -0
  80. data/lib/badline/timing.rb +12 -0
  81. data/lib/badline/version.rb +1 -1
  82. data/lib/badline/via/shift_register.rb +43 -16
  83. data/lib/badline/vic/bank.rb +55 -17
  84. data/lib/badline/vic/registers.rb +20 -4
  85. data/lib/badline/vic.rb +48 -15
  86. data/lib/badline/vic20/attachments.rb +103 -0
  87. data/lib/badline/vic20/bus.rb +308 -0
  88. data/lib/badline/vic20/cpu.rb +10 -0
  89. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  90. data/lib/badline/vic20/port_wiring.rb +50 -0
  91. data/lib/badline/vic20/saved_state.rb +152 -0
  92. data/lib/badline/vic20/sound/output.rb +83 -0
  93. data/lib/badline/vic20/sound.rb +233 -0
  94. data/lib/badline/vic20/user_via_ports.rb +47 -0
  95. data/lib/badline/vic20/vic/painter.rb +239 -0
  96. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  97. data/lib/badline/vic20/vic/text_window.rb +71 -0
  98. data/lib/badline/vic20/vic.rb +267 -0
  99. data/lib/badline/vic20.rb +220 -0
  100. metadata +31 -2
  101. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
@@ -0,0 +1,308 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ # The CPU's bus:
6
+ #
7
+ # $0000-$03FF - 1K of RAM
8
+ # $0400-$0FFF - RAM1-3, 3K of expansion RAM, or nothing
9
+ # $1000-$1FFF - 4K of RAM
10
+ # $2000-$7FFF - BLK1, BLK2 and BLK3, 8K of expansion RAM each, or nothing
11
+ # $8000-$8FFF - the character ROM
12
+ # $9000-$93FF - I/O 0: the VIC and both VIAs (see IO0)
13
+ # $9400-$97FF - colour RAM, 1K by four bits
14
+ # $9800-$9FFF - I/O 2 and I/O 3, for cartridges
15
+ # $A000-$BFFF - BLK5, 8K of expansion RAM, or nothing
16
+ # $C000-$DFFF - the BASIC ROM
17
+ # $E000-$FFFF - the KERNAL ROM
18
+ #
19
+ # The VIC reaches $0000-$1FFF and $8000-$9FFF, the pages where A13 and
20
+ # A14 are both low, over the V-bus, and the CPU reaches them through it
21
+ # too. Elsewhere the CPU is on its own side of the bus.
22
+ #
23
+ # Each side's data lines hold the last byte they carried, so a read
24
+ # from an empty spot finds that byte: the V-bus's in the 3K hole, and
25
+ # the CPU's own in the empty blocks, I/O 2 and I/O 3. Colour RAM drives
26
+ # the low four lines only, and the V-bus's high four come through.
27
+ # Writes to ROM and to empty spots go nowhere.
28
+ class Bus
29
+ include Addressable
30
+
31
+ # The pages each expansion block covers, as [first page, count].
32
+ BLOCKS = {
33
+ ram123: [0x04, 0x0c], blk1: [0x20, 0x20], blk2: [0x40, 0x20], blk3: [0x60, 0x20], blk5: [0xa0, 0x20]
34
+ }.freeze
35
+
36
+ # The blocks each of the usual RAM expansions fills, and every block,
37
+ # which is what xvic's -memory all fits.
38
+ RAM_CONFIGURATIONS = {
39
+ unexpanded: [],
40
+ "3k": %i[ram123],
41
+ "8k": %i[blk1],
42
+ "16k": %i[blk1 blk2],
43
+ "24k": %i[blk1 blk2 blk3],
44
+ "32k": %i[blk1 blk2 blk3 blk5],
45
+ all: %i[ram123 blk1 blk2 blk3 blk5]
46
+ }.freeze
47
+
48
+ # RAM, internal and expansion alike, powers on in alternating $FF
49
+ # and $00 bytes, $FF at the even addresses, the alternation most of
50
+ # the machines in VIC20/raminitpattern/readme.txt show. Colour RAM
51
+ # powers on at zero. See doc/pinned-behaviour.md.
52
+ RAM_POWER_ON = Array.new(0xc000) { |addr| addr.even? ? 0xff : 0x00 }.freeze
53
+
54
+ # The last byte on the V-bus's data lines, which an empty spot on
55
+ # the VIC's side reads back.
56
+ class VideoOpenBus
57
+ def initialize(bus)
58
+ @bus = bus
59
+ end
60
+
61
+ def peek(_addr) = @bus.video_data
62
+ def poke(_addr, _value); end
63
+ end
64
+
65
+ # The last byte the CPU moved, which an empty spot on the CPU's side
66
+ # reads back.
67
+ class OpenBus
68
+ def initialize(bus)
69
+ @bus = bus
70
+ end
71
+
72
+ def peek(_addr) = @bus.data
73
+ def poke(_addr, _value); end
74
+ end
75
+
76
+ # Colour RAM: four bits a cell, under the V-bus's high four lines.
77
+ class ColorRAM
78
+ # The 1,024 cells, which the VIC reads its colour nibbles from.
79
+ attr_reader :cells
80
+
81
+ def initialize(bus)
82
+ @bus = bus
83
+ @cells = Array.new(0x400, 0)
84
+ end
85
+
86
+ def peek(addr) = (@bus.video_data & 0xf0) | @cells[addr & 0x3ff]
87
+
88
+ def poke(addr, value)
89
+ @cells[addr & 0x3ff] = value & 0x0f
90
+ end
91
+
92
+ # The four bits the cell at +addr+ stores.
93
+ def nibble(addr) = @cells[addr & 0x3ff]
94
+ end
95
+
96
+ # I/O 0, $9000-$93FF. The VIC answers in $9000-$90FF, its sixteen
97
+ # registers repeated. The VIAs answer all through I/O 0, on the
98
+ # address lines alone: A4 selects VIA 1 and A5 selects VIA 2. Where
99
+ # more than one chip answers a read, each pulls the lines it drives
100
+ # low, so the CPU reads the AND of them, and a write reaches them
101
+ # all. Where none answers, the CPU reads the V-bus's last byte: the
102
+ # VIC's fetch in a cycle it fetches in, as the real 6561E's dump of
103
+ # VIC20/split-tests/timing shows at $9100 and $9200.
104
+ class IO0
105
+ def initialize(bus, vic:, via1:, via2:)
106
+ @bus = bus
107
+ @vic = vic
108
+ @via1 = via1
109
+ @via2 = via2
110
+ end
111
+
112
+ def peek(addr)
113
+ vic = addr < 0x9100
114
+ return @bus.video_data unless vic || addr.anybits?(0x30)
115
+
116
+ value = vic ? @vic.peek(addr) : 0xff
117
+ value &= @via1.peek(addr) if addr.anybits?(0x10)
118
+ value &= @via2.peek(addr) if addr.anybits?(0x20)
119
+ value
120
+ end
121
+
122
+ def poke(addr, value)
123
+ @vic.poke(addr, value) if addr < 0x9100
124
+ return unless addr.anybits?(0x30)
125
+
126
+ @via1.poke(addr, value) if addr.anybits?(0x10)
127
+ @via2.poke(addr, value) if addr.anybits?(0x20)
128
+ @bus.via_written
129
+ end
130
+ end
131
+
132
+ # VICE's debug cartridge register at $910F, a spot in I/O 0 that
133
+ # nothing answers. A write there calls the handler with the byte, the
134
+ # exit code the VICE testprogs report. Every other access falls
135
+ # through to I/O 0.
136
+ class DebugRegister
137
+ ADDRESS = 0x910f
138
+
139
+ def initialize(io0, &handler)
140
+ @io0 = io0
141
+ @handler = handler
142
+ end
143
+
144
+ def peek(addr) = @io0.peek(addr)
145
+
146
+ def poke(addr, value)
147
+ return @handler.call(value) if addr == ADDRESS
148
+
149
+ @io0.poke(addr, value)
150
+ end
151
+ end
152
+
153
+ attr_reader :ram, :color_ram, :blocks
154
+
155
+ # The last byte the CPU read or wrote.
156
+ attr_reader :data
157
+
158
+ # The last byte on the V-bus.
159
+ attr_reader :video_data
160
+
161
+ # +vic+, +via1+ and +via2+ are the chips in I/O 0, each answering
162
+ # peek and poke with the CPU's address. +blocks+ names the expansion
163
+ # blocks that hold RAM (BLOCKS), and +kernal+ the KERNAL ROM's file.
164
+ def initialize(vic:, via1:, via2:, blocks: [], kernal: "vic20/kernal-pal.rom")
165
+ addressable_at(0, length: 2**16)
166
+ @ram = Memory.new(RAM_POWER_ON, length: 0xc000, start: 0)
167
+ @color_ram = ColorRAM.new(self)
168
+ @character_rom = ROM.load("vic20/character.rom", 0x8000)
169
+ @basic_rom = ROM.load("vic20/basic.rom", 0xc000)
170
+ @kernal_rom = ROM.load(kernal, 0xe000)
171
+ @io0 = IO0.new(self, vic:, via1:, via2:)
172
+ @open_bus = OpenBus.new(self)
173
+ @video_open_bus = VideoOpenBus.new(self)
174
+ @data = @video_data = 0
175
+ @read_pages = Array.new(256, @open_bus)
176
+ @write_pages = Array.new(256, @open_bus)
177
+ @roms = []
178
+ @debug_register = nil
179
+ @via_write_handler = nil
180
+ self.blocks = blocks
181
+ end
182
+
183
+ # Puts a cartridge's ROM chip of +bytes+, whole pages of them, at
184
+ # +address+. It answers reads on the pages it covers in place of
185
+ # whatever was there, and writes to them go nowhere.
186
+ def map_rom(address, bytes)
187
+ @roms << ROM.new(bytes, length: bytes.length, start: address)
188
+ map_pages
189
+ end
190
+
191
+ # Calls the block after each write to a VIA, which may have moved the
192
+ # lines its ports drive.
193
+ def on_via_write(&handler)
194
+ @via_write_handler = handler
195
+ end
196
+
197
+ def via_written
198
+ @via_write_handler&.call
199
+ end
200
+
201
+ # Calls the block with each byte written to $910F (DebugRegister).
202
+ def install_debug_register(&)
203
+ @debug_register = DebugRegister.new(@io0, &)
204
+ map_pages
205
+ end
206
+
207
+ # Fills the expansion blocks named in +names+ with RAM and empties
208
+ # the rest.
209
+ def blocks=(names)
210
+ @blocks = names.dup.freeze
211
+ map_pages
212
+ end
213
+
214
+ def power_on!
215
+ @ram.clear!(RAM_POWER_ON)
216
+ end
217
+
218
+ # The RAM, the colour RAM, the byte each side's data lines hold and the
219
+ # cartridge's ROM chips, for a snapshot. The expansion blocks are how
220
+ # the machine was built.
221
+ def save_state(out)
222
+ out.marker("VIC20 BUS")
223
+ @ram.save_state(out)
224
+ out.blob(@color_ram.cells).int(@data).int(@video_data).int(@roms.length)
225
+ @roms.each do |rom|
226
+ out.int(rom.start)
227
+ rom.save_state(out)
228
+ end
229
+ end
230
+
231
+ def load_state(input)
232
+ input.marker("VIC20 BUS")
233
+ @ram.load_state(input)
234
+ input.blob_into(@color_ram.cells)
235
+ @data = input.int
236
+ @video_data = input.int
237
+ chips = Array.new(input.int) { [input.int, input.blob] }
238
+ @roms = []
239
+ chips.each { |chip| @roms << ROM.new(chip[1], length: chip[1].length, start: chip[0]) }
240
+ map_pages
241
+ end
242
+
243
+ # Whether +block+ (BLOCKS) holds RAM.
244
+ def ram?(block) = @blocks.include?(block)
245
+
246
+ # The VIC's fetch from +address+, its own 14-bit address, which leaves
247
+ # the byte on the V-bus. The VIC's A13 is the CPU's A15 inverted, so
248
+ # $2000-$3FFF are the CPU's $0000-$1FFF and $0000-$1FFF its
249
+ # $8000-$9FFF. It reaches the internal RAM and the character ROM, and
250
+ # no expansion RAM. Elsewhere nothing drives the V-bus's data lines,
251
+ # and the VIC reads the byte they hold, as does a fetch from colour
252
+ # RAM, which drives only the colour bus while the VIC fetches.
253
+ def video_fetch(address)
254
+ value =
255
+ if address >= 0x2000
256
+ internal = address & 0x1fff
257
+ internal < 0x0400 || internal >= 0x1000 ? @ram.peek(internal) : @video_data
258
+ elsif address < 0x1000
259
+ @character_rom.peek(address | 0x8000)
260
+ else
261
+ @video_data
262
+ end
263
+ @video_data = value
264
+ end
265
+
266
+ def peek(addr)
267
+ value = @read_pages[addr >> 8].peek(addr)
268
+ @video_data = value if addr.nobits?(0x6000)
269
+ @data = value
270
+ end
271
+
272
+ def poke(addr, value)
273
+ @data = value
274
+ @video_data = value if addr.nobits?(0x6000)
275
+ @write_pages[addr >> 8].poke(addr, value)
276
+ end
277
+
278
+ private
279
+
280
+ def map_pages
281
+ map(@open_bus, 0x00, 0x100)
282
+ @read_pages.fill(@video_open_bus, 0x04, 0x0c)
283
+ map(@ram, 0x00, 0x04)
284
+ map(@ram, 0x10, 0x10)
285
+ BLOCKS.each { |block, pages| map(@ram, pages[0], pages[1]) if ram?(block) }
286
+ @read_pages.fill(@character_rom, 0x80, 0x10)
287
+ map(@io0, 0x90, 0x04)
288
+ map(@color_ram, 0x94, 0x04)
289
+ @read_pages.fill(@basic_rom, 0xc0, 0x20)
290
+ @read_pages.fill(@kernal_rom, 0xe0, 0x20)
291
+ @roms.each { |rom| map_rom_pages(rom) }
292
+ @write_pages[0x91] = @debug_register if @debug_register
293
+ end
294
+
295
+ def map_rom_pages(rom)
296
+ first = rom.start >> 8
297
+ count = rom.length >> 8
298
+ @read_pages.fill(rom, first, count)
299
+ @write_pages.fill(@open_bus, first, count)
300
+ end
301
+
302
+ def map(chip, first, count)
303
+ @read_pages.fill(chip, first, count)
304
+ @write_pages.fill(chip, first, count)
305
+ end
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ # The VIC-20's 6502, on the Vic20::Bus.
6
+ class CPU
7
+ include Badline::CPU::Core
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ # What VIA 2's ports read from outside the chip: the key matrix, with
6
+ # port B driving its columns and port A reading its rows. The KERNAL's
7
+ # SCNKEY ($EB1E) writes a column pattern to $9120 and reads the rows
8
+ # back from $9121, and its key table ($EC5E) puts each key at column
9
+ # times 8 plus row.
10
+ #
11
+ # PB7 is also the joystick's right switch. A held switch pulls that
12
+ # column line low before the matrix settles, so it reads as a key down
13
+ # in every row whose key in column 7 is held too. PB3 doubles as the
14
+ # cassette write line, which only the datasette listens to.
15
+ class KeyboardVIAPorts
16
+ # One row of keys per port A line, in port B column order. The keys
17
+ # are the C64's, under the same names, wired to other lines.
18
+ MATRIX = [
19
+ %i[1 left control run_stop space cbm q 2],
20
+ %i[3 w a lshift z s e 4],
21
+ %i[5 r d x c f t 6],
22
+ %i[7 y g v b h u 8],
23
+ %i[9 i j n m k o 0],
24
+ %i[+ p l , . : @ -],
25
+ %i[£ * ; / rshift = up clr_home],
26
+ %i[delete return cursor_h cursor_v f1 f3 f5 f7]
27
+ ].freeze
28
+
29
+ attr_reader :keyboard, :joystick
30
+
31
+ def initialize(keyboard:, joystick:)
32
+ @keyboard = keyboard
33
+ @joystick = joystick
34
+ @via = nil
35
+ end
36
+
37
+ # The VIA these ports hang off, whose other port's output the matrix
38
+ # settles against.
39
+ def connect(via)
40
+ @via = via
41
+ end
42
+
43
+ def read_a(lines) = @keyboard.scan(lines, columns(@via.port_b_output)).first
44
+
45
+ def read_b(lines) = @keyboard.scan(@via.port_a_output, columns(lines)).last
46
+
47
+ private
48
+
49
+ # The joystick's right switch is bit 3 of its port bits, and pulls PB7.
50
+ def columns(lines) = lines & (0x7f | ((@joystick.port_bits & 0x08) << 4))
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ # The datasette and the serial bus, wired to the VIAs.
6
+ #
7
+ # The serial bus's lines go out through inverters, so an output that
8
+ # drives high pulls its line low: ATN from VIA 1's PA7, the clock from
9
+ # VIA 2's CA2 and the data line from VIA 2's CB2. An output left as an
10
+ # input floats high, and pulls its line too, as after a reset. CLK and
11
+ # DATA come back on VIA 1's PA0 and PA1 (UserVIAPorts). The machine
12
+ # pushes the lines it pulls into the bus, as the C64's CIA 2 port A
13
+ # bits, after each write to a VIA that moves them.
14
+ #
15
+ # The datasette's read line pulses VIA 2's CA1, its sense switch reads
16
+ # on VIA 1's PA6, its motor runs while VIA 1's CA2 is low, and VIA 2's
17
+ # PB3 drives its write line, the same pin as a keyboard column.
18
+ module PortWiring
19
+ # The lines the machine pulls, as IECBus host lines.
20
+ def serial_lines
21
+ lines = 0
22
+ lines |= IECBus::HOST_ATN_OUT if @via1.port_a_output.anybits?(0x80)
23
+ lines |= IECBus::HOST_CLK_OUT if @via2.ca2_output
24
+ lines |= IECBus::HOST_DATA_OUT if @via2.cb2_output
25
+ lines
26
+ end
27
+
28
+ private
29
+
30
+ def wire_ports
31
+ @datasette.on_flag { tape_pulse }
32
+ @bus.on_via_write { via_written }
33
+ via_written
34
+ end
35
+
36
+ def via_written
37
+ lines = serial_lines
38
+ @iec_bus.host_lines = lines unless lines == @iec_bus.host_lines
39
+ @datasette.motor = !@via1.ca2_output
40
+ @datasette.write_line = @via2.port_b_output.anybits?(0x08)
41
+ end
42
+
43
+ # A falling edge on the read line, and the line back up.
44
+ def tape_pulse
45
+ @via2.ca1 = false
46
+ @via2.ca1 = true
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ MARKER = "VIC20"
6
+
7
+ # The RAM expansion of the machine a State from #snapshot was taken
8
+ # of, a key of Bus::RAM_CONFIGURATIONS.
9
+ def self.setup(state)
10
+ input = Snapshot::StateReader.new(state)
11
+ input.marker(MARKER)
12
+ input.check_stamp
13
+ SavedState.read_ram(input)
14
+ end
15
+
16
+ # A new machine, built as the one a State from #snapshot was taken of,
17
+ # at that state.
18
+ def self.restored(state) = new(ram: setup(state)).apply_state(state)
19
+
20
+ # The whole machine's state for a snapshot, as Computer's: the
21
+ # machine's own latches, the bus with its RAM and cartridge ROM, the
22
+ # VIC with its sound, both VIAs, the CPU, the datasette and its tape,
23
+ # the disk device 8 serves through the traps, and a true 1541. The
24
+ # serial lines go back into the bus from the restored VIAs. What the
25
+ # host holds stays out: the keyboard, the joystick, RESTORE, the traps
26
+ # and callbacks, whether the display renders, whether the sound
27
+ # records, and whether the datasette records.
28
+ module SavedState
29
+ RAM_CONFIGURATIONS = Bus::RAM_CONFIGURATIONS.keys.freeze
30
+
31
+ def self.read_ram(input)
32
+ RAM_CONFIGURATIONS.fetch(input.int)
33
+ rescue IndexError
34
+ raise Snapshot::FormatError, "the state names a VIC-20 RAM expansion badline doesn't know"
35
+ end
36
+
37
+ # How many more on_init handlers the machine a State was taken of had
38
+ # yet to run than this one has. Nil until the machine is restored.
39
+ attr_reader :init_handlers_lost
40
+
41
+ def snapshot
42
+ out = Snapshot::StateWriter.new
43
+ save_state(out)
44
+ out.state
45
+ end
46
+
47
+ # Takes the machine back to a State from #snapshot, trying it on a new
48
+ # machine first, so one that fails leaves this machine as it was.
49
+ # Raises Snapshot::FormatError for a state of another machine or RAM
50
+ # expansion.
51
+ def restore(state)
52
+ check_ram(Vic20.setup(state))
53
+ Vic20.restored(state)
54
+ apply_state(state)
55
+ end
56
+
57
+ # Puts a State into the machine without trying it on a new one first.
58
+ def apply_state(state)
59
+ input = Snapshot::StateReader.new(state)
60
+ load_state(input)
61
+ raise Snapshot::FormatError, "the state goes on past the machine" unless input.finished?
62
+
63
+ self
64
+ end
65
+
66
+ def save_state(out)
67
+ out.marker(MARKER).stamp
68
+ out.int(RAM_CONFIGURATIONS.index(@ram_configuration))
69
+ out.int(@cycles).boolean(@nmi_asserted).boolean(!@pending_keys.nil?)
70
+ out.ints(@pending_keys) if @pending_keys
71
+ out.int(@cycles <= @init_threshold ? @init_handlers.length : 0)
72
+ @bus.save_state(out)
73
+ @vic.save_state(out)
74
+ @sound.save_state(out)
75
+ @via1.save_state(out)
76
+ @via2.save_state(out)
77
+ @cpu.save_state(out)
78
+ @datasette.save_state(out)
79
+ save_trap_drive(out)
80
+ save_drive1541(out)
81
+ end
82
+
83
+ def load_state(input)
84
+ input.marker(MARKER)
85
+ input.check_stamp
86
+ check_ram(SavedState.read_ram(input))
87
+ @cycles = input.int
88
+ @nmi_asserted = input.boolean?
89
+ @pending_keys = nil
90
+ @pending_keys = input.ints if input.boolean?
91
+ @init_handlers_lost = [input.int - @init_handlers.length, 0].max
92
+ @bus.load_state(input)
93
+ @vic.load_state(input)
94
+ @sound.load_state(input)
95
+ @via1.load_state(input)
96
+ @via2.load_state(input)
97
+ @cpu.load_state(input)
98
+ via_written
99
+ @datasette.load_state(input)
100
+ load_trap_drive(input)
101
+ load_drive1541(input)
102
+ end
103
+
104
+ private
105
+
106
+ # The disk device 8 serves through the traps, with its channels.
107
+ def save_trap_drive(out)
108
+ out.boolean(!@drive.nil?)
109
+ return unless @drive
110
+
111
+ @drive.save_state(out)
112
+ @serial_trap.save_state(out)
113
+ @save_trap.save_state(out)
114
+ end
115
+
116
+ def load_trap_drive(input)
117
+ return unmount unless input.boolean?
118
+
119
+ storage = Storage.reopen(input)
120
+ unmount
121
+ mount(storage)
122
+ @drive.load_state(input)
123
+ @serial_trap.load_state(input)
124
+ @save_trap.load_state(input)
125
+ end
126
+
127
+ # A true 1541 on the serial bus, with its device number.
128
+ def save_drive1541(out)
129
+ out.boolean(!@drive1541.nil?)
130
+ return unless @drive1541
131
+
132
+ out.int(@drive1541.device)
133
+ @drive1541.save_state(out)
134
+ end
135
+
136
+ def load_drive1541(input)
137
+ return detach_drive1541 unless input.boolean?
138
+
139
+ device = input.int
140
+ detach_drive1541 if @drive1541 && @drive1541.device != device
141
+ attach_drive1541(Drive1541.new(device:)) unless @drive1541
142
+ @drive1541.load_state(input)
143
+ end
144
+
145
+ def check_ram(ram)
146
+ return if ram == @ram_configuration
147
+
148
+ raise Snapshot::FormatError, "the state is of a VIC-20 with #{ram} RAM, not #{@ram_configuration}"
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ class Sound
6
+ # The sound's level averaged over each sample of the audio rate, then
7
+ # through the machine's output stage: a one-pole low-pass at 1,420 Hz
8
+ # and a one-pole high-pass at 158 Hz, both run at the audio rate.
9
+ #
10
+ # The two corners are fitted to xvic's step response: a voice turned
11
+ # on at volume 15 rises and decays back to the line as those two RC
12
+ # stages do, within 6 parts in 11,000.
13
+ class Output
14
+ LOW_PASS_HZ = 1_420
15
+ HIGH_PASS_HZ = 158
16
+
17
+ # Scales the loudest level, four voices high at volume 15, to the
18
+ # top of a signed 16-bit sample.
19
+ SCALE = 32_767.0 / 570
20
+
21
+ attr_reader :samples
22
+
23
+ # `level` is the sound's level as the recording starts, which the
24
+ # stage takes as settled.
25
+ def initialize(clock_hz:, rate:, level:)
26
+ @clock_hz = clock_hz
27
+ @rate = rate
28
+ @remainder = 0
29
+ @window = next_window
30
+ @left = @window
31
+ @sum = 0
32
+ @low = @drift = level.to_f
33
+ @low_pass = 1.0 - Math.exp(-2.0 * Math::PI * LOW_PASS_HZ / rate)
34
+ @high_pass = 1.0 - Math.exp(-2.0 * Math::PI * HIGH_PASS_HZ / rate)
35
+ @samples = []
36
+ end
37
+
38
+ # Adds `cycles` cycles at `level`, closing every sample they finish.
39
+ def hold(level, cycles)
40
+ left = @left
41
+ while cycles >= left
42
+ @sum += level * left
43
+ cycles -= left
44
+ close
45
+ left = @left
46
+ end
47
+ @sum += level * cycles
48
+ @left = left - cycles
49
+ end
50
+
51
+ # The samples closed since the last drain.
52
+ def drain
53
+ samples = @samples
54
+ @samples = []
55
+ samples
56
+ end
57
+
58
+ private
59
+
60
+ def close
61
+ @low += @low_pass * (@sum.fdiv(@window) - @low)
62
+ @drift += @high_pass * (@low - @drift)
63
+ sample = ((@low - @drift) * SCALE).round
64
+ sample = 32_767 if sample > 32_767
65
+ sample = -32_768 if sample < -32_768
66
+ @samples << sample
67
+ @sum = 0
68
+ @window = next_window
69
+ @left = @window
70
+ end
71
+
72
+ # Each window is a whole number of cycles, and they add up to the
73
+ # clock's cycles over a second.
74
+ def next_window
75
+ @remainder += @clock_hz
76
+ cycles = @remainder / @rate
77
+ @remainder -= cycles * @rate
78
+ cycles
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end