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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/CLAUDE.md +6 -4
- data/README.md +39 -4
- data/doc/library.md +15 -1
- data/doc/pinned-behaviour.md +146 -0
- data/doc/snapshots.md +17 -3
- data/lib/badline/address_bus/fittings.rb +38 -0
- data/lib/badline/address_bus/pla.rb +160 -0
- data/lib/badline/address_bus/roms.rb +12 -5
- data/lib/badline/address_bus.rb +15 -90
- data/lib/badline/chrout_trap.rb +4 -3
- data/lib/badline/computer/attachments.rb +47 -7
- data/lib/badline/computer/saved_state.rb +1 -0
- data/lib/badline/computer.rb +52 -37
- data/lib/badline/control_ports.rb +10 -3
- data/lib/badline/core.rb +3 -0
- data/lib/badline/datasette.rb +39 -1
- data/lib/badline/drive1541/serial_port.rb +5 -8
- data/lib/badline/drive1541/sleep.rb +5 -6
- data/lib/badline/drive1541.rb +3 -3
- data/lib/badline/frontend/app.rb +62 -64
- data/lib/badline/frontend/boot.rb +33 -8
- data/lib/badline/frontend/controls.rb +12 -6
- data/lib/badline/frontend/drive_led.rb +9 -10
- data/lib/badline/frontend/frame_rate.rb +4 -4
- data/lib/badline/frontend/machine_window.rb +87 -0
- data/lib/badline/frontend/menu_media.rb +49 -24
- data/lib/badline/frontend/menu_pages.rb +23 -40
- data/lib/badline/frontend/menu_ports.rb +69 -0
- data/lib/badline/frontend/pacer.rb +6 -5
- data/lib/badline/frontend/pause_menu.rb +30 -33
- data/lib/badline/frontend/player_screen.rb +1 -1
- data/lib/badline/frontend/player_window.rb +1 -1
- data/lib/badline/frontend/screen.rb +39 -37
- data/lib/badline/frontend/sid_view.rb +1 -1
- data/lib/badline/frontend/sound.rb +11 -9
- data/lib/badline/frontend/timeline.rb +36 -5
- data/lib/badline/frontend.rb +2 -0
- data/lib/badline/iec_bus.rb +16 -28
- data/lib/badline/kernal_trap/drive.rb +3 -0
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/layout.rb +112 -0
- data/lib/badline/kernal_trap/load.rb +16 -27
- data/lib/badline/kernal_trap/routine.rb +11 -27
- data/lib/badline/kernal_trap/save.rb +13 -23
- data/lib/badline/kernal_trap/serial.rb +21 -19
- data/lib/badline/kernal_trap.rb +1 -0
- data/lib/badline/keyboard.rb +28 -14
- data/lib/badline/machine.rb +60 -0
- data/lib/badline/media/vic20_basic.rb +54 -0
- data/lib/badline/media/vic20_cartridge.rb +74 -0
- data/lib/badline/media/vic20_media.rb +132 -0
- data/lib/badline/media.rb +32 -18
- data/lib/badline/model.rb +29 -12
- data/lib/badline/options/event.rb +5 -3
- data/lib/badline/options/help.rb +11 -3
- data/lib/badline/options/table.rb +12 -2
- data/lib/badline/options/validation.rb +55 -4
- data/lib/badline/options.rb +33 -33
- data/lib/badline/roms/README +51 -0
- data/lib/badline/roms/c64gs.rom +0 -0
- data/lib/badline/roms/kernal-pet64.rom +0 -0
- data/lib/badline/roms/vic20/basic.rom +0 -0
- data/lib/badline/roms/vic20/character.rom +0 -0
- data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
- data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/image.rb +15 -0
- data/lib/badline/snapshot/setup.rb +15 -10
- data/lib/badline/snapshot/state.rb +5 -3
- data/lib/badline/snapshot/vice.rb +2 -1
- data/lib/badline/snapshot.rb +10 -0
- data/lib/badline/storage/crt_file.rb +16 -5
- data/lib/badline/storage/disk_image.rb +3 -0
- data/lib/badline/storage/host_directory.rb +3 -0
- data/lib/badline/storage/t64.rb +3 -0
- data/lib/badline/storage/tap.rb +38 -4
- data/lib/badline/storage/unavailable.rb +2 -0
- data/lib/badline/timing.rb +12 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/shift_register.rb +43 -16
- data/lib/badline/vic/bank.rb +55 -17
- data/lib/badline/vic/registers.rb +20 -4
- data/lib/badline/vic.rb +48 -15
- data/lib/badline/vic20/attachments.rb +103 -0
- data/lib/badline/vic20/bus.rb +308 -0
- data/lib/badline/vic20/cpu.rb +10 -0
- data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
- data/lib/badline/vic20/port_wiring.rb +50 -0
- data/lib/badline/vic20/saved_state.rb +152 -0
- data/lib/badline/vic20/sound/output.rb +83 -0
- data/lib/badline/vic20/sound.rb +233 -0
- data/lib/badline/vic20/user_via_ports.rb +47 -0
- data/lib/badline/vic20/vic/painter.rb +239 -0
- data/lib/badline/vic20/vic/saved_state.rb +55 -0
- data/lib/badline/vic20/vic/text_window.rb +71 -0
- data/lib/badline/vic20/vic.rb +267 -0
- data/lib/badline/vic20.rb +220 -0
- metadata +31 -2
- data/lib/badline/address_bus/ultimax_pages.rb +0 -38
|
@@ -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
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Vic20
|
|
5
|
+
class VIC
|
|
6
|
+
# The text window's rows: where it opens and closes, the video matrix
|
|
7
|
+
# counter and the character line, and the registers that place the
|
|
8
|
+
# window and its memory.
|
|
9
|
+
module TextWindow
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def new_frame
|
|
13
|
+
@window_open = @window_done = false
|
|
14
|
+
@row_start = @matrix = @char_line = @text_rows = 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def open_window
|
|
18
|
+
@window_open = true
|
|
19
|
+
@char_line = 0
|
|
20
|
+
@matrix = @row_start
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Steps to the next line of the text row, or past the row's last line
|
|
24
|
+
# to the next row, closing the window after the last.
|
|
25
|
+
def next_text_line
|
|
26
|
+
@char_line += 1
|
|
27
|
+
if @char_line < (1 << @char_shift)
|
|
28
|
+
@matrix = @row_start
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@char_line = 0
|
|
33
|
+
@row_start = @matrix
|
|
34
|
+
@text_rows += 1
|
|
35
|
+
return if @text_rows < @rows
|
|
36
|
+
|
|
37
|
+
@window_open = false
|
|
38
|
+
@window_done = true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# $9000 and $9002 move the window's fetches on the line, or only where
|
|
42
|
+
# they end once they've started.
|
|
43
|
+
def window_written
|
|
44
|
+
@origin_x = @registers[0] & 0x7f
|
|
45
|
+
@columns = @registers[2] & 0x7f
|
|
46
|
+
bases_written
|
|
47
|
+
return unless @window_open
|
|
48
|
+
|
|
49
|
+
if @column < @fetch_from
|
|
50
|
+
place_fetches
|
|
51
|
+
else
|
|
52
|
+
fetches_end
|
|
53
|
+
end
|
|
54
|
+
schedule
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def rows_written(value)
|
|
58
|
+
@rows = (value >> 1) & 0x3f
|
|
59
|
+
@char_shift = value.anybits?(0x01) ? 4 : 3
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The video matrix at $9005 bits 7-4 and $9002 bit 7, address bits
|
|
63
|
+
# 13-9, and the characters at $9005 bits 3-0, bits 13-10.
|
|
64
|
+
def bases_written
|
|
65
|
+
@screen_base = ((@registers[5] & 0xf0) << 6) | ((@registers[2] & 0x80) << 2)
|
|
66
|
+
@char_base = (@registers[5] & 0x0f) << 10
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|