badline 0.9.0 → 0.10.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +39 -4
  5. data/doc/pinned-behaviour.md +146 -0
  6. data/doc/snapshots.md +13 -0
  7. data/lib/badline/address_bus/pla.rb +130 -0
  8. data/lib/badline/address_bus.rb +9 -88
  9. data/lib/badline/chrout_trap.rb +4 -3
  10. data/lib/badline/computer/attachments.rb +47 -7
  11. data/lib/badline/computer/saved_state.rb +1 -0
  12. data/lib/badline/computer.rb +44 -29
  13. data/lib/badline/control_ports.rb +10 -3
  14. data/lib/badline/core.rb +3 -0
  15. data/lib/badline/datasette.rb +39 -1
  16. data/lib/badline/drive1541/serial_port.rb +5 -8
  17. data/lib/badline/drive1541/sleep.rb +5 -6
  18. data/lib/badline/drive1541.rb +3 -3
  19. data/lib/badline/frontend/app.rb +62 -64
  20. data/lib/badline/frontend/boot.rb +33 -8
  21. data/lib/badline/frontend/controls.rb +12 -6
  22. data/lib/badline/frontend/drive_led.rb +9 -10
  23. data/lib/badline/frontend/frame_rate.rb +4 -4
  24. data/lib/badline/frontend/machine_window.rb +87 -0
  25. data/lib/badline/frontend/menu_media.rb +49 -24
  26. data/lib/badline/frontend/menu_pages.rb +23 -40
  27. data/lib/badline/frontend/menu_ports.rb +69 -0
  28. data/lib/badline/frontend/pacer.rb +6 -5
  29. data/lib/badline/frontend/pause_menu.rb +30 -33
  30. data/lib/badline/frontend/player_screen.rb +1 -1
  31. data/lib/badline/frontend/player_window.rb +1 -1
  32. data/lib/badline/frontend/screen.rb +39 -37
  33. data/lib/badline/frontend/sid_view.rb +1 -1
  34. data/lib/badline/frontend/sound.rb +11 -9
  35. data/lib/badline/frontend/timeline.rb +36 -5
  36. data/lib/badline/frontend.rb +2 -0
  37. data/lib/badline/iec_bus.rb +16 -28
  38. data/lib/badline/kernal_trap/drive.rb +3 -0
  39. data/lib/badline/kernal_trap/file.rb +1 -1
  40. data/lib/badline/kernal_trap/layout.rb +112 -0
  41. data/lib/badline/kernal_trap/load.rb +16 -27
  42. data/lib/badline/kernal_trap/routine.rb +11 -27
  43. data/lib/badline/kernal_trap/save.rb +13 -23
  44. data/lib/badline/kernal_trap/serial.rb +21 -19
  45. data/lib/badline/kernal_trap.rb +1 -0
  46. data/lib/badline/keyboard.rb +17 -14
  47. data/lib/badline/machine.rb +60 -0
  48. data/lib/badline/media/vic20_basic.rb +54 -0
  49. data/lib/badline/media/vic20_cartridge.rb +74 -0
  50. data/lib/badline/media/vic20_media.rb +132 -0
  51. data/lib/badline/media.rb +32 -18
  52. data/lib/badline/options/event.rb +5 -3
  53. data/lib/badline/options/help.rb +11 -3
  54. data/lib/badline/options/table.rb +11 -1
  55. data/lib/badline/options/validation.rb +55 -4
  56. data/lib/badline/options.rb +33 -33
  57. data/lib/badline/roms/README +27 -0
  58. data/lib/badline/roms/vic20/basic.rom +0 -0
  59. data/lib/badline/roms/vic20/character.rom +0 -0
  60. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  61. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  62. data/lib/badline/snapshot/container.rb +2 -0
  63. data/lib/badline/snapshot/image.rb +15 -0
  64. data/lib/badline/snapshot/state.rb +4 -3
  65. data/lib/badline/snapshot.rb +10 -0
  66. data/lib/badline/storage/crt_file.rb +16 -5
  67. data/lib/badline/storage/disk_image.rb +3 -0
  68. data/lib/badline/storage/host_directory.rb +3 -0
  69. data/lib/badline/storage/t64.rb +3 -0
  70. data/lib/badline/storage/tap.rb +38 -4
  71. data/lib/badline/storage/unavailable.rb +2 -0
  72. data/lib/badline/timing.rb +12 -0
  73. data/lib/badline/version.rb +1 -1
  74. data/lib/badline/via/shift_register.rb +43 -16
  75. data/lib/badline/vic/bank.rb +53 -16
  76. data/lib/badline/vic/registers.rb +20 -4
  77. data/lib/badline/vic.rb +34 -15
  78. data/lib/badline/vic20/attachments.rb +103 -0
  79. data/lib/badline/vic20/bus.rb +308 -0
  80. data/lib/badline/vic20/cpu.rb +10 -0
  81. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  82. data/lib/badline/vic20/port_wiring.rb +50 -0
  83. data/lib/badline/vic20/saved_state.rb +152 -0
  84. data/lib/badline/vic20/sound/output.rb +83 -0
  85. data/lib/badline/vic20/sound.rb +233 -0
  86. data/lib/badline/vic20/user_via_ports.rb +47 -0
  87. data/lib/badline/vic20/vic/painter.rb +239 -0
  88. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  89. data/lib/badline/vic20/vic/text_window.rb +71 -0
  90. data/lib/badline/vic20/vic.rb +267 -0
  91. data/lib/badline/vic20.rb +220 -0
  92. metadata +28 -2
  93. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
@@ -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
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/vic20/sound/output"
4
+
5
+ module Badline
6
+ class Vic20
7
+ # The VIC-I's sound: three square-wave voices, bass, alto and soprano at
8
+ # $900A-$900C, a noise voice at $900D, and the volume in $900E bits 0-3.
9
+ #
10
+ # Each voice has a 7-bit counter, ticked every 16, 8, 4 or 2 cycles from
11
+ # bass to noise. It counts up from the low 7 bits of its register, and
12
+ # on the tick it would reach 127 it reloads from the register instead.
13
+ # So a voice's counter comes round every 128 - ((value + 1) & 127)
14
+ # ticks, and the register's new value takes hold at the next reload.
15
+ #
16
+ # A tone voice's counter shifts an 8-bit shift register at each reload,
17
+ # bit 7 out and back into bit 0 inverted while bit 7 of the register
18
+ # enables the voice, and a zero while it doesn't. Bit 0 is the voice's
19
+ # output: a square wave of 16 shifts while the voice is on, which a
20
+ # voice turned off shifts out to silence.
21
+ #
22
+ # The noise voice's counter shifts a 16-bit LFSR instead, bit 0 in from
23
+ # the XOR of bits 3, 12, 14 and 15 while the voice is on and a one
24
+ # while it's off. Each time the LFSR's bit 0 rises, it shifts the noise
25
+ # voice's own 8-bit shift register, bit 7 back into bit 0, inverted
26
+ # while the voice is on. Its bit 0 is the voice's output while the voice
27
+ # is on, and it is silent while it's off.
28
+ #
29
+ # The output is the voices that are high, plus a little more, times the
30
+ # volume, so a volume write steps the output even with every voice off.
31
+ #
32
+ # The voices only run while #record collects the output: nothing the
33
+ # CPU reads depends on them. Until then a write only sets the register.
34
+ # While recording, the voices catch up to the current cycle whenever a
35
+ # write lands or the samples are drained, one shift at a time.
36
+ class Sound
37
+ VOICES = 4
38
+ NOISE = 3
39
+
40
+ # The cycles between ticks of each voice's counter.
41
+ DIVIDERS = [16, 8, 4, 2].freeze
42
+
43
+ # What a voice that is high adds to the output's level, and what the
44
+ # output carries with every voice low, before the volume.
45
+ VOICE_LEVEL = 9
46
+ IDLE_LEVEL = 2
47
+
48
+ attr_reader :volume
49
+
50
+ # `clock` answers the machine's cycles, `clock_hz` the rate they run at.
51
+ def initialize(clock, clock_hz)
52
+ @clock = clock
53
+ @clock_hz = clock_hz
54
+ @output = nil
55
+ @time = 0
56
+ @shift_registers = Array.new(VOICES, 0)
57
+ @next_shifts = Array.new(VOICES, 0)
58
+ @frequencies = Array.new(VOICES, 0)
59
+ @enabled = Array.new(VOICES, false)
60
+ power_on!
61
+ end
62
+
63
+ def model = :mos6561
64
+
65
+ # The power-on state: every register clear and the tone voices' shift
66
+ # registers empty. The noise voice's LFSR and shift register start full
67
+ # of ones, as a noise voice left off fills them.
68
+ def power_on!
69
+ now = @clock.cycles
70
+ run_to(now)
71
+ @volume = 0
72
+ VOICES.times do |voice|
73
+ @frequencies[voice] = 0
74
+ @enabled[voice] = false
75
+ @shift_registers[voice] = 0
76
+ divider = DIVIDERS[voice]
77
+ @next_shifts[voice] = ((now / divider) + 128) * divider
78
+ end
79
+ @shift_registers[NOISE] = 0xff
80
+ @lfsr = 0xffff
81
+ @level = level_now
82
+ end
83
+
84
+ # Starts collecting the output, averaged down to `rate` samples a
85
+ # second, for #drain_samples. `clock_hz` is the rate the cycles are
86
+ # taken to run at.
87
+ def record(rate:, clock_hz: @clock_hz)
88
+ now = @clock.cycles
89
+ run_to(now)
90
+ resume(now)
91
+ @output = Output.new(clock_hz:, rate:, level: @level)
92
+ end
93
+
94
+ def recording? = !@output.nil?
95
+
96
+ # The samples recorded since the last drain.
97
+ def drain_samples
98
+ output = @output
99
+ return [] if output.nil?
100
+
101
+ run_to(@clock.cycles)
102
+ output.drain
103
+ end
104
+
105
+ # A write to $900A-$900E, which takes hold from the next cycle.
106
+ def write(register, value)
107
+ run_to(@clock.cycles + 1)
108
+ if register == 0x0e
109
+ @volume = value & 0x0f
110
+ else
111
+ voice = register - 0x0a
112
+ @frequencies[voice] = value & 0x7f
113
+ @enabled[voice] = value >= 0x80
114
+ end
115
+ @level = level_now
116
+ end
117
+
118
+ # The registers and the voices as they last ran, for a snapshot.
119
+ # Recording is the host's, and starts the voices again from the
120
+ # cycle it starts at.
121
+ def save_state(out)
122
+ run_to(@clock.cycles)
123
+ out.marker("VIC20 SOUND").int(@volume).ints(@frequencies).booleans(@enabled)
124
+ out.ints(@shift_registers).ints(@next_shifts).int(@lfsr).int(@level).int(@time)
125
+ end
126
+
127
+ def load_state(input)
128
+ input.marker("VIC20 SOUND")
129
+ @volume = input.int
130
+ input.ints_into(@frequencies)
131
+ input.booleans_into(@enabled)
132
+ input.ints_into(@shift_registers)
133
+ input.ints_into(@next_shifts)
134
+ @lfsr = input.int
135
+ @level = input.int
136
+ @time = input.int
137
+ resume(@clock.cycles) if recording?
138
+ end
139
+
140
+ # A voice's shift register as of the current cycle, while recording.
141
+ def shift_register(voice)
142
+ run_to(@clock.cycles)
143
+ @shift_registers[voice]
144
+ end
145
+
146
+ def lfsr
147
+ run_to(@clock.cycles)
148
+ @lfsr
149
+ end
150
+
151
+ private
152
+
153
+ # Moves each shift that the cycles since the voices last ran have
154
+ # passed to its next turn from `now`, keeping its phase.
155
+ def resume(now)
156
+ @time = now
157
+ VOICES.times do |voice|
158
+ due = @next_shifts[voice]
159
+ next if due >= now
160
+
161
+ interval = period(voice) * DIVIDERS[voice]
162
+ @next_shifts[voice] = due + ((now - due + interval - 1) / interval * interval)
163
+ end
164
+ end
165
+
166
+ # Runs the voices up to the start of cycle `target`, the output holding
167
+ # each level for the cycles it lasts.
168
+ def run_to(target)
169
+ time = @time
170
+ output = @output
171
+ return if output.nil? || target <= time
172
+
173
+ following = next_shift
174
+ while following < target
175
+ output.hold(@level, following - time)
176
+ time = following
177
+ shift_due(time)
178
+ @level = level_now
179
+ following = next_shift
180
+ end
181
+ output.hold(@level, target - time)
182
+ @time = target
183
+ end
184
+
185
+ def next_shift
186
+ shifts = @next_shifts
187
+ following = shifts[0]
188
+ following = shifts[1] if shifts[1] < following
189
+ following = shifts[2] if shifts[2] < following
190
+ following = shifts[3] if shifts[3] < following
191
+ following
192
+ end
193
+
194
+ def shift_due(time)
195
+ shifts = @next_shifts
196
+ VOICES.times do |voice|
197
+ next unless shifts[voice] == time
198
+
199
+ voice == NOISE ? step_noise : shift_tone(voice)
200
+ shifts[voice] = time + (period(voice) * DIVIDERS[voice])
201
+ end
202
+ end
203
+
204
+ def shift_tone(voice)
205
+ bits = @shift_registers[voice]
206
+ fill = @enabled[voice] && bits < 0x80 ? 1 : 0
207
+ @shift_registers[voice] = ((bits << 1) | fill) & 0xff
208
+ end
209
+
210
+ def step_noise
211
+ lfsr = @lfsr
212
+ enabled = @enabled[NOISE]
213
+ fill = enabled ? ((lfsr >> 3) ^ (lfsr >> 12) ^ (lfsr >> 14) ^ (lfsr >> 15)) & 1 : 1
214
+ @lfsr = ((lfsr << 1) | fill) & 0xffff
215
+ return unless fill == 1 && lfsr.even?
216
+
217
+ bits = @shift_registers[NOISE]
218
+ fill = (bits >> 7) ^ (enabled ? 1 : 0)
219
+ @shift_registers[NOISE] = ((bits << 1) | fill) & 0xff
220
+ end
221
+
222
+ # The ticks between two reloads of a voice's counter.
223
+ def period(voice) = 128 - ((@frequencies[voice] + 1) & 0x7f)
224
+
225
+ def level_now
226
+ registers = @shift_registers
227
+ high = (registers[0] & 1) + (registers[1] & 1) + (registers[2] & 1)
228
+ high += registers[NOISE] & 1 if @enabled[NOISE]
229
+ ((high * VOICE_LEVEL) + IDLE_LEVEL) * @volume
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ # What VIA 1's ports read from outside the chip. Port A carries the
6
+ # serial bus's CLK and DATA on PA0 and PA1, which read 1 while a line
7
+ # is released, the joystick's up, down and left switches on PA2-PA4,
8
+ # its fire button on PA5, the line the light pen shares, and the
9
+ # cassette sense switch on PA6, low while a key is down on the deck.
10
+ # The right switch is on VIA 2 (see KeyboardVIAPorts). PA7 drives ATN
11
+ # out and reads back what it drives. Port B is the user port, with
12
+ # nothing plugged in.
13
+ #
14
+ # CA2 drives the cassette motor, which runs while CA2 is low, and the
15
+ # RESTORE key pulls CA1 (see Vic20#press_restore).
16
+ class UserVIAPorts
17
+ attr_reader :joystick
18
+
19
+ def initialize(joystick:, serial_bus:, datasette:)
20
+ @joystick = joystick
21
+ @serial_bus = serial_bus
22
+ @datasette = datasette
23
+ end
24
+
25
+ # The joystick's up, down and left bits (0-2) move up two, and its
26
+ # fire bit (4) up one.
27
+ def read_a(_lines)
28
+ bits = @joystick.port_bits
29
+ value = 0x80 | ((bits & 0x07) << 2) | ((bits & 0x10) << 1)
30
+ value |= 0x40 unless @datasette.sense_low?
31
+ value | serial_inputs
32
+ end
33
+
34
+ def read_b(_lines) = 0xff
35
+
36
+ private
37
+
38
+ def serial_inputs
39
+ low = @serial_bus.low_lines
40
+ value = 0x03
41
+ value &= ~0x01 if low.anybits?(IECBus::CLK)
42
+ value &= ~0x02 if low.anybits?(IECBus::DATA)
43
+ value
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ class VIC
6
+ # Paints the VIC-I's display a line at a time from what the VIC
7
+ # fetched for it: each character's pattern byte and colour nibble, the
8
+ # pixel its first character starts at and how many it fetched. Each
9
+ # line keeps its own, so the next line's fetches, which can start
10
+ # before this line's last pixels are out, don't overwrite them, and a
11
+ # fetch that reads what the same line read a frame ago changes
12
+ # nothing.
13
+ #
14
+ # A line is painted when its last pixel is out, unless a colour
15
+ # register was written while it was going out: then the pixels up to
16
+ # the write are painted first, with the old colours. Border,
17
+ # background and auxiliary colour take hold from the pixel the write
18
+ # lands on, and the reverse bit two pixels later. A line with the
19
+ # same fetches and colours as the frame before is left alone.
20
+ #
21
+ # A character's colour nibble picks its mode. Below 8 it's a hires
22
+ # character in that colour: a set bit is the character's colour and a
23
+ # clear one the background, or the other way round with $900F bit 3
24
+ # clear. From 8 up it's multicolour, two bits to a double-width pixel:
25
+ # 00 the background, 01 the border, 10 the character's colour (the
26
+ # nibble's low three bits) and 11 the auxiliary colour, whatever
27
+ # $900F bit 3 says (MOS 6560/6561 datasheet, "Color operating modes").
28
+ class Painter
29
+ # The characters a line holds room for. A line fetches at most 34.
30
+ SLOTS = 40
31
+
32
+ # What the VIC fetched for each line, SLOTS characters a line, and
33
+ # whether any of it differs from what the line fetched a frame ago.
34
+ attr_reader :patterns, :colors, :counts, :starts, :stale
35
+
36
+ attr_reader :display, :width, :dirty_lines, :render
37
+
38
+ # +blanked+ is the number of lines at the top of the frame that are
39
+ # never painted.
40
+ def initialize(lines, width, blanked)
41
+ @width = width
42
+ @blanked = blanked
43
+ @display = Array.new(width * lines, 0)
44
+ @dirty_lines = Array.new(lines, true)
45
+ @patterns = Array.new(lines * SLOTS, 0)
46
+ @colors = Array.new(lines * SLOTS, 0)
47
+ @counts = Array.new(lines, 0)
48
+ @starts = Array.new(lines, 0)
49
+ @stale = Array.new(lines, true)
50
+ @memo_keys = Array.new(lines, -1)
51
+ @render = true
52
+ end
53
+
54
+ # Clears the display and starts painting +row+, with the colour
55
+ # registers at zero.
56
+ def power_on!(row)
57
+ @border = @background = @aux = 0
58
+ @reverse = true
59
+ @row = row
60
+ @painted = 0
61
+ @written = false
62
+ @counts.fill(0)
63
+ @stale.fill(true)
64
+ @memo_keys.fill(-1)
65
+ @display.fill(0)
66
+ @dirty_lines.fill(true)
67
+ end
68
+
69
+ def clear_dirty_lines!
70
+ @dirty_lines.fill(false)
71
+ end
72
+
73
+ # The colours, the line being painted and how far, what each line
74
+ # fetched, and the display, for a snapshot. Whether it renders is
75
+ # the host's, and a restored display is all changed.
76
+ def save_state(out)
77
+ out.int(@border).int(@background).int(@aux).boolean(@reverse)
78
+ out.int(@row).int(@painted).boolean(@written)
79
+ out.blob(@display).blob(@patterns).blob(@colors)
80
+ out.ints(@counts).ints(@starts).booleans(@stale).ints(@memo_keys)
81
+ end
82
+
83
+ def load_state(input)
84
+ @border = input.int
85
+ @background = input.int
86
+ @aux = input.int
87
+ @reverse = input.boolean?
88
+ @row = input.int
89
+ @painted = input.int
90
+ @written = input.boolean?
91
+ input.blob_into(@display)
92
+ input.blob_into(@patterns)
93
+ input.blob_into(@colors)
94
+ input.ints_into(@counts)
95
+ input.ints_into(@starts)
96
+ input.booleans_into(@stale)
97
+ input.ints_into(@memo_keys)
98
+ @dirty_lines.fill(true)
99
+ end
100
+
101
+ # Painting can be turned off, and once it's back on every line is
102
+ # painted afresh.
103
+ def render=(value)
104
+ @memo_keys.fill(-1)
105
+ @render = value
106
+ end
107
+
108
+ # $900F written while +position+ of the line is going out.
109
+ def colors_written(position, value)
110
+ @written = true
111
+ paint_to(position)
112
+ @border = value & 0x07
113
+ @background = value >> 4
114
+ paint_to(position + 2)
115
+ @reverse = value.nobits?(0x08)
116
+ end
117
+
118
+ # The auxiliary colour, $900E bits 7-4, written while +position+ of
119
+ # the line is going out.
120
+ def aux_written(position, aux)
121
+ @written = true
122
+ paint_to(position)
123
+ @aux = aux
124
+ end
125
+
126
+ # The current line's last pixel is out: paints what's left of it,
127
+ # then starts +next_row+.
128
+ def finish_line(next_row)
129
+ row = @row
130
+ if @render && row >= @blanked
131
+ key = memo_key(row)
132
+ if @written || @stale[row] || key != @memo_keys[row]
133
+ paint_to(@width)
134
+ @memo_keys[row] = @written ? -1 : key
135
+ @dirty_lines[row] = true
136
+ end
137
+ end
138
+ @stale[row] = false
139
+ @painted = 0
140
+ @written = false
141
+ @row = next_row
142
+ end
143
+
144
+ private
145
+
146
+ # The colours and the window's place on +row+, packed into one
147
+ # Integer.
148
+ def memo_key(row)
149
+ count = @counts[row]
150
+ start = count.zero? ? 0 : @starts[row]
151
+ @border | (@background << 3) | (@aux << 7) | (@reverse ? 0x800 : 0) | (start << 12) | (count << 24)
152
+ end
153
+
154
+ # Paints the current line's pixels from where it got to up to
155
+ # +stop+.
156
+ def paint_to(stop)
157
+ return unless @render && @row >= @blanked
158
+
159
+ x = @painted
160
+ return if x >= stop
161
+
162
+ @painted = stop
163
+ base = @row * @width
164
+ border_to = @starts[@row]
165
+ border_to = stop if border_to > stop
166
+ x = paint_border(base, x, border_to)
167
+ x = paint_characters(base, x, stop)
168
+ paint_border(base, x, stop)
169
+ end
170
+
171
+ # Paints the border from +from+ up to +to+, and returns where it
172
+ # got to.
173
+ def paint_border(base, from, to)
174
+ return from if from >= to
175
+
176
+ @display.fill(@border, base + from, to - from)
177
+ to
178
+ end
179
+
180
+ # Paints the characters from +from+ up to +stop+, and returns where it
181
+ # got to: +stop+, or the end of the line's characters.
182
+ def paint_characters(base, from, stop)
183
+ x = from
184
+ row = @row
185
+ start = @starts[row]
186
+ finish = start + (@counts[row] * 8)
187
+ slot = row * SLOTS
188
+ while x < stop && x < finish
189
+ first = x - start
190
+ cell = first & ~7
191
+ last = stop - start - cell
192
+ last = 8 if last > 8
193
+ paint_character(base + start + cell, slot + (cell >> 3), first & 7, last)
194
+ x = start + cell + last
195
+ end
196
+ x
197
+ end
198
+
199
+ # Paints pixels +first+ up to +last+ of the character in +slot+,
200
+ # whose first pixel is at +at+.
201
+ def paint_character(at, slot, first, last)
202
+ pattern = @patterns[slot]
203
+ color = @colors[slot]
204
+ if color < 8
205
+ paint_hires(at, pattern, color, first, last)
206
+ else
207
+ paint_multicolor(at, pattern, color & 0x07, first, last)
208
+ end
209
+ end
210
+
211
+ def paint_hires(at, pattern, color, first, last)
212
+ display = @display
213
+ set = @reverse ? @background : color
214
+ clear = @reverse ? color : @background
215
+ i = first
216
+ while i < last
217
+ display[at + i] = pattern.anybits?(0x80 >> i) ? set : clear
218
+ i += 1
219
+ end
220
+ end
221
+
222
+ def paint_multicolor(at, pattern, color, first, last)
223
+ display = @display
224
+ i = first
225
+ while i < last
226
+ pair = (pattern >> (6 - (i & 6))) & 0x03
227
+ display[at + i] =
228
+ if pair.zero? then @background
229
+ elsif pair == 1 then @border
230
+ elsif pair == 2 then color
231
+ else @aux
232
+ end
233
+ i += 1
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ class VIC
6
+ # The VIC's state for a snapshot: the registers and what they set, the
7
+ # raster, the text window and its fetches, and the picture with what
8
+ # each line fetched. The sound saves its own.
9
+ module SavedState
10
+ def save_state(out)
11
+ out.marker("VIC20 VIC")
12
+ out.ints(@registers).int(@rasterline).int(@column).int(@next_event)
13
+ [@origin_x, @origin_y, @columns, @rows, @char_shift, @screen_base, @char_base].each { |value| out.int(value) }
14
+ out.boolean(@window_open).boolean(@window_done)
15
+ [@row_start, @matrix, @char_line, @text_rows].each { |value| out.int(value) }
16
+ [@fetch_from, @fetch_to, @fetched, @slot, @code].each { |value| out.int(value) }
17
+ @painter.save_state(out)
18
+ end
19
+
20
+ def load_state(input)
21
+ input.marker("VIC20 VIC")
22
+ input.ints_into(@registers)
23
+ @rasterline = input.int
24
+ @column = input.int
25
+ @next_event = input.int
26
+ load_window(input)
27
+ @fetch_from = input.int
28
+ @fetch_to = input.int
29
+ @fetched = input.int
30
+ @slot = input.int
31
+ @code = input.int
32
+ @painter.load_state(input)
33
+ end
34
+
35
+ private
36
+
37
+ def load_window(input)
38
+ @origin_x = input.int
39
+ @origin_y = input.int
40
+ @columns = input.int
41
+ @rows = input.int
42
+ @char_shift = input.int
43
+ @screen_base = input.int
44
+ @char_base = input.int
45
+ @window_open = input.boolean?
46
+ @window_done = input.boolean?
47
+ @row_start = input.int
48
+ @matrix = input.int
49
+ @char_line = input.int
50
+ @text_rows = input.int
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end