badline 0.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -0
- data/CLAUDE.md +6 -4
- data/README.md +60 -19
- data/doc/library.md +10 -3
- data/doc/media.md +11 -2
- data/doc/pinned-behaviour.md +199 -9
- data/doc/sid-player.md +8 -4
- data/doc/snapshots.md +79 -33
- data/lib/badline/address_bus/pla.rb +130 -0
- data/lib/badline/address_bus/roms.rb +32 -0
- data/lib/badline/address_bus.rb +13 -92
- data/lib/badline/audio/bare_player.rb +24 -0
- data/lib/badline/audio/checkpoints.rb +42 -0
- data/lib/badline/audio/jukebox.rb +11 -4
- data/lib/badline/audio/machine_player.rb +16 -0
- data/lib/badline/audio/renderer.rb +80 -9
- data/lib/badline/audio/stereo.rb +3 -5
- data/lib/badline/audio.rb +1 -0
- data/lib/badline/chrout_trap.rb +4 -3
- data/lib/badline/computer/attachments.rb +52 -12
- data/lib/badline/computer/saved_state.rb +1 -0
- data/lib/badline/computer.rb +50 -33
- data/lib/badline/control_ports.rb +10 -3
- data/lib/badline/core.rb +51 -0
- data/lib/badline/data_path.rb +37 -0
- data/lib/badline/datasette.rb +63 -10
- data/lib/badline/drive1541/disk.rb +3 -3
- data/lib/badline/drive1541/disk_state.rb +14 -5
- 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 +63 -64
- data/lib/badline/frontend/boot.rb +34 -4
- data/lib/badline/frontend/confirmation.rb +17 -11
- 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_dialogs.rb +52 -14
- data/lib/badline/frontend/menu_media.rb +70 -16
- data/lib/badline/frontend/menu_pages.rb +47 -42
- data/lib/badline/frontend/menu_ports.rb +69 -0
- data/lib/badline/frontend/name_field.rb +88 -0
- data/lib/badline/frontend/pacer.rb +6 -5
- data/lib/badline/frontend/pause_menu.rb +52 -37
- data/lib/badline/frontend/player_screen.rb +1 -1
- data/lib/badline/frontend/player_window.rb +4 -4
- data/lib/badline/frontend/screen.rb +39 -37
- data/lib/badline/frontend/sid_view.rb +1 -1
- data/lib/badline/frontend/snapshots.rb +145 -17
- data/lib/badline/frontend/sound.rb +17 -9
- data/lib/badline/frontend/timeline.rb +36 -5
- data/lib/badline/frontend.rb +3 -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 +24 -25
- data/lib/badline/kernal_trap/routine.rb +17 -5
- data/lib/badline/kernal_trap/save.rb +13 -23
- data/lib/badline/kernal_trap/serial.rb +38 -24
- data/lib/badline/kernal_trap.rb +1 -0
- data/lib/badline/keyboard.rb +17 -14
- data/lib/badline/machine.rb +60 -0
- data/lib/badline/media/disk_list.rb +75 -0
- data/lib/badline/media/disk_set.rb +13 -3
- 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 +39 -20
- data/lib/badline/model.rb +62 -0
- data/lib/badline/options/event.rb +10 -6
- data/lib/badline/options/help.rb +25 -13
- data/lib/badline/options/table.rb +15 -1
- data/lib/badline/options/validation.rb +63 -3
- data/lib/badline/options.rb +37 -34
- data/lib/badline/region.rb +16 -4
- data/lib/badline/reu/ram.rb +27 -0
- data/lib/badline/reu/saved_state.rb +27 -0
- data/lib/badline/roms/README +37 -0
- data/lib/badline/roms/kernal-sx64.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/sid/decimator.rb +13 -0
- data/lib/badline/sid/filter.rb +6 -3
- data/lib/badline/sid.rb +19 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/fields.rb +1 -0
- data/lib/badline/snapshot/image.rb +15 -0
- data/lib/badline/snapshot/setup.rb +20 -8
- data/lib/badline/snapshot/state.rb +15 -7
- data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
- data/lib/badline/snapshot/vice/peripherals.rb +12 -2
- data/lib/badline/snapshot/vice/restore.rb +3 -1
- data/lib/badline/snapshot/vice/reu1764.rb +55 -0
- data/lib/badline/snapshot/vice/vicii.rb +59 -13
- data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
- data/lib/badline/snapshot/vice.rb +25 -23
- 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 +10 -4
- data/lib/badline/storage/tap.rb +44 -8
- data/lib/badline/storage/unavailable.rb +3 -1
- data/lib/badline/storage.rb +5 -5
- 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 +53 -16
- data/lib/badline/vic/registers.rb +20 -4
- data/lib/badline/vic/sprite/timing.rb +5 -9
- data/lib/badline/vic/sprite.rb +4 -3
- data/lib/badline/vic.rb +34 -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
- data/lib/badline.rb +2 -38
- metadata +37 -2
- data/lib/badline/address_bus/ultimax_pages.rb +0 -38
|
@@ -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
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/vic20/vic/painter"
|
|
4
|
+
require "badline/vic20/vic/text_window"
|
|
5
|
+
require "badline/vic20/vic/saved_state"
|
|
6
|
+
|
|
7
|
+
module Badline
|
|
8
|
+
class Vic20
|
|
9
|
+
# The VIC-I, the 6561 on PAL machines: its sixteen registers, the
|
|
10
|
+
# raster, the text window's fetches over the V-bus and the picture. The
|
|
11
|
+
# writes to the sound's registers, $900A-$900E, go on to the Sound
|
|
12
|
+
# attached as #sound.
|
|
13
|
+
#
|
|
14
|
+
# The registers repeat through $9000-$90FF. $9003 bit 7 and $9004 read
|
|
15
|
+
# the raster line, its bit 0 and bits 8-1. $9006 and $9007 read the
|
|
16
|
+
# light pen, which nothing triggers yet, and $9008 and $9009 the two
|
|
17
|
+
# pots, which read $FF with nothing plugged in. The other registers
|
|
18
|
+
# read back what was written to them.
|
|
19
|
+
#
|
|
20
|
+
# The text window opens on the line where the raster's bits 8-1 match
|
|
21
|
+
# $9001, and closes after the number of text rows in $9003, each 8 lines
|
|
22
|
+
# of a character, or 16 with $9003 bit 0 set. On each of its lines the
|
|
23
|
+
# VIC fetches two bytes a character, $9002's count of them: the
|
|
24
|
+
# character code from the video matrix, with the colour nibble beside it
|
|
25
|
+
# on the colour bus, then the character's pattern byte for the line.
|
|
26
|
+
# The first code is fetched four cycles after the column in $9000, so
|
|
27
|
+
# the window starts 16 cycles into the line with the KERNAL's origin of
|
|
28
|
+
# 12, and none is fetched past the end of the line. The fetches go to a
|
|
29
|
+
# 14-bit address, the VIC's own (see Bus#video_fetch), and the byte each
|
|
30
|
+
# one reads is the V-bus's last byte. Outside the window the 6561E
|
|
31
|
+
# fetches nothing and the V-bus keeps the CPU's last byte there.
|
|
32
|
+
#
|
|
33
|
+
# The video matrix counter steps once a character. It starts each line
|
|
34
|
+
# of a text row where the row started, and the next row starts where
|
|
35
|
+
# the last line of the row left it. It starts the frame at zero.
|
|
36
|
+
#
|
|
37
|
+
# The picture is 4 pixels a cycle, so 284 a line. A character's 8
|
|
38
|
+
# pixels come out in the two cycles after its pattern fetch. The
|
|
39
|
+
# display's lines start 6 cycles after the raster count moves, as
|
|
40
|
+
# xvic's view of the frame does, so the character fetched 16 cycles
|
|
41
|
+
# into the line lands 48 pixels in. Lines 0-27 are blanked.
|
|
42
|
+
class VIC
|
|
43
|
+
include TextWindow
|
|
44
|
+
include SavedState
|
|
45
|
+
|
|
46
|
+
# What a VIC-I sets for the machine: the clock, the raster's cycles
|
|
47
|
+
# per line and lines per frame, and how many lines at the top of the
|
|
48
|
+
# frame it blanks. The 6560 of NTSC machines, with its half lines and
|
|
49
|
+
# the interlace of $9000 bit 7, would be another.
|
|
50
|
+
Profile = Data.define(:clock_hz, :cycles_per_line, :lines_per_frame, :blanked_lines)
|
|
51
|
+
|
|
52
|
+
# The 6561: 71 cycles by 312 lines, lines 0-27 blanked (Marko
|
|
53
|
+
# Mäkelä's VIC-I.txt).
|
|
54
|
+
PAL = Profile.new(clock_hz: 1_108_405, cycles_per_line: 71, lines_per_frame: 312, blanked_lines: 28)
|
|
55
|
+
|
|
56
|
+
PIXELS_PER_CYCLE = 4
|
|
57
|
+
|
|
58
|
+
# The column whose pixels start a line of the display.
|
|
59
|
+
FIRST_PIXEL_COLUMN = 6
|
|
60
|
+
|
|
61
|
+
# How many cycles after the column in $9000 the first code is fetched.
|
|
62
|
+
FETCH_DELAY = 4
|
|
63
|
+
|
|
64
|
+
# The colours, as RGB, each from its luma and chroma: luma in five
|
|
65
|
+
# levels from black to white, and chroma at one strength in a phase
|
|
66
|
+
# that steps by 22.5 degrees from blue's. The levels and phases are
|
|
67
|
+
# the VIC-I's table on Wikipedia's "MOS Technology VIC" (Y, Pb, Pr),
|
|
68
|
+
# turned into RGB with the BT.601 matrix and the chroma scaled by 0.2.
|
|
69
|
+
PALETTE = [
|
|
70
|
+
0x000000, 0xffffff, 0x82251d, 0x7ddae2, 0xb259bf, 0x4da640, 0x402e9a, 0xbfd165,
|
|
71
|
+
0xb27240, 0xf2b27f, 0xc2655d, 0xbdffff, 0xf299ff, 0x8de57f, 0x806eda, 0xffffa5
|
|
72
|
+
].freeze
|
|
73
|
+
|
|
74
|
+
attr_reader :rasterline, :column, :profile
|
|
75
|
+
attr_accessor :sound
|
|
76
|
+
|
|
77
|
+
def initialize(profile = PAL)
|
|
78
|
+
@profile = profile
|
|
79
|
+
@cycles_per_line = profile.cycles_per_line
|
|
80
|
+
@lines_per_frame = profile.lines_per_frame
|
|
81
|
+
@painter = Painter.new(@lines_per_frame, @cycles_per_line * PIXELS_PER_CYCLE, profile.blanked_lines)
|
|
82
|
+
@patterns = @painter.patterns
|
|
83
|
+
@colors = @painter.colors
|
|
84
|
+
@counts = @painter.counts
|
|
85
|
+
@starts = @painter.starts
|
|
86
|
+
@stale = @painter.stale
|
|
87
|
+
@bus = nil
|
|
88
|
+
@sound = nil
|
|
89
|
+
@color_cells = Array.new(0x400, 0)
|
|
90
|
+
power_on!
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The bus the VIC fetches from, the machine's, and the colour RAM
|
|
94
|
+
# whose cells come in on the colour bus beside each code.
|
|
95
|
+
def connect(bus)
|
|
96
|
+
@bus = bus
|
|
97
|
+
@color_cells = bus.color_ram.cells
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The power-on state: the registers cleared and the raster at the top
|
|
101
|
+
# of the frame. The VIC has no RES pin, so only power reaches it.
|
|
102
|
+
def power_on!
|
|
103
|
+
@registers = Array.new(16, 0)
|
|
104
|
+
@rasterline = 0
|
|
105
|
+
@column = 0
|
|
106
|
+
@origin_x = @origin_y = @columns = @rows = 0
|
|
107
|
+
@char_shift = 3
|
|
108
|
+
@screen_base = @char_base = 0
|
|
109
|
+
new_frame
|
|
110
|
+
@fetch_from = @fetch_to = 0
|
|
111
|
+
@fetched = @slot = 0
|
|
112
|
+
@code = 0
|
|
113
|
+
@painter.power_on!(@lines_per_frame - 1)
|
|
114
|
+
schedule
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def cycle!
|
|
118
|
+
column = @column + 1
|
|
119
|
+
@column = column
|
|
120
|
+
return unless column == @next_event
|
|
121
|
+
|
|
122
|
+
if column > FIRST_PIXEL_COLUMN && column < @fetch_to && column >= @fetch_from
|
|
123
|
+
access(column)
|
|
124
|
+
column += 1
|
|
125
|
+
@next_event = column < @fetch_to ? column : @cycles_per_line
|
|
126
|
+
else
|
|
127
|
+
event(column)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The sixteen registers as written.
|
|
132
|
+
def register_file = @registers.dup
|
|
133
|
+
|
|
134
|
+
# The display, a palette index a pixel, 284 to a line and every line
|
|
135
|
+
# of the frame.
|
|
136
|
+
def display = @painter.display
|
|
137
|
+
|
|
138
|
+
def width = @painter.width
|
|
139
|
+
|
|
140
|
+
def height = @lines_per_frame
|
|
141
|
+
|
|
142
|
+
# Whether each line of the display has changed since the front end
|
|
143
|
+
# last cleared them.
|
|
144
|
+
def dirty_lines = @painter.dirty_lines
|
|
145
|
+
|
|
146
|
+
def clear_dirty_lines! = @painter.clear_dirty_lines!
|
|
147
|
+
|
|
148
|
+
def palette = PALETTE
|
|
149
|
+
|
|
150
|
+
# Whether the VIC paints its display. It fetches either way.
|
|
151
|
+
def render = @painter.render
|
|
152
|
+
|
|
153
|
+
def render=(value)
|
|
154
|
+
@painter.render = value
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def peek(addr)
|
|
158
|
+
register = addr & 0x0f
|
|
159
|
+
case register
|
|
160
|
+
when 0x03 then ((@rasterline & 0x01) << 7) | (@registers[3] & 0x7f)
|
|
161
|
+
when 0x04 then @rasterline >> 1
|
|
162
|
+
when 0x06, 0x07 then 0
|
|
163
|
+
when 0x08, 0x09 then 0xff
|
|
164
|
+
else @registers[register]
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def poke(addr, value)
|
|
169
|
+
register = addr & 0x0f
|
|
170
|
+
@registers[register] = value
|
|
171
|
+
case register
|
|
172
|
+
when 0x00, 0x02 then window_written
|
|
173
|
+
when 0x01 then @origin_y = value
|
|
174
|
+
when 0x03 then rows_written(value)
|
|
175
|
+
when 0x05 then bases_written
|
|
176
|
+
when 0x0a, 0x0b, 0x0c, 0x0d then @sound&.write(register, value)
|
|
177
|
+
when 0x0e
|
|
178
|
+
@painter.aux_written(pixel_position, value >> 4)
|
|
179
|
+
@sound&.write(register, value)
|
|
180
|
+
when 0x0f then @painter.colors_written(pixel_position, value)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def event(column)
|
|
187
|
+
if column == @cycles_per_line
|
|
188
|
+
column = @column = 0
|
|
189
|
+
start_line
|
|
190
|
+
end
|
|
191
|
+
@painter.finish_line(@rasterline) if column == FIRST_PIXEL_COLUMN
|
|
192
|
+
access(column) if column >= @fetch_from && column < @fetch_to
|
|
193
|
+
schedule
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# The next column with something to do: the end of the line, the
|
|
197
|
+
# start of a display line, or a fetch.
|
|
198
|
+
def schedule
|
|
199
|
+
column = @column
|
|
200
|
+
upcoming = column < FIRST_PIXEL_COLUMN ? FIRST_PIXEL_COLUMN : @cycles_per_line
|
|
201
|
+
fetch = column < @fetch_from ? @fetch_from : column + 1
|
|
202
|
+
upcoming = fetch if fetch < @fetch_to && fetch < upcoming
|
|
203
|
+
@next_event = upcoming
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def start_line
|
|
207
|
+
line = @rasterline + 1
|
|
208
|
+
if line == @lines_per_frame
|
|
209
|
+
line = 0
|
|
210
|
+
new_frame
|
|
211
|
+
end
|
|
212
|
+
@rasterline = line
|
|
213
|
+
next_text_line if @window_open
|
|
214
|
+
open_window if !@window_open && !@window_done && (line >> 1) == @origin_y && @rows.positive?
|
|
215
|
+
place_fetches
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def place_fetches
|
|
219
|
+
line = @rasterline
|
|
220
|
+
@slot = line * Painter::SLOTS
|
|
221
|
+
@fetched = @counts[line] = 0
|
|
222
|
+
@starts[line] = @origin_x * PIXELS_PER_CYCLE
|
|
223
|
+
unless @window_open
|
|
224
|
+
@fetch_from = @fetch_to = 0
|
|
225
|
+
return
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
@fetch_from = @origin_x + FETCH_DELAY
|
|
229
|
+
fetches_end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def fetches_end
|
|
233
|
+
last = @fetch_from + (2 * @columns)
|
|
234
|
+
@fetch_to = [last, @cycles_per_line].min
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def access(column)
|
|
238
|
+
slot = @slot + @fetched
|
|
239
|
+
if (column - @fetch_from).even?
|
|
240
|
+
address = (@screen_base + @matrix) & 0x3fff
|
|
241
|
+
@code = @bus.video_fetch(address)
|
|
242
|
+
color = @color_cells[address & 0x3ff]
|
|
243
|
+
return if @colors[slot] == color
|
|
244
|
+
|
|
245
|
+
@colors[slot] = color
|
|
246
|
+
else
|
|
247
|
+
pattern = @bus.video_fetch((@char_base + (@code << @char_shift) + @char_line) & 0x3fff)
|
|
248
|
+
@matrix += 1
|
|
249
|
+
@fetched += 1
|
|
250
|
+
@counts[@rasterline] = @fetched
|
|
251
|
+
return if @patterns[slot] == pattern
|
|
252
|
+
|
|
253
|
+
@patterns[slot] = pattern
|
|
254
|
+
end
|
|
255
|
+
@stale[@rasterline] = true
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# The pixel of the display line a register write in this cycle takes
|
|
259
|
+
# hold from: the second of the cycle's four.
|
|
260
|
+
def pixel_position
|
|
261
|
+
column = @column - FIRST_PIXEL_COLUMN
|
|
262
|
+
column += @cycles_per_line if column.negative?
|
|
263
|
+
(column * PIXELS_PER_CYCLE) + 1
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|