badline 0.1.0 → 0.2.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 +185 -0
- data/CLAUDE.md +217 -0
- data/CODE_OF_CONDUCT.md +92 -0
- data/CONTRIBUTING.md +51 -0
- data/README.md +224 -31
- data/doc/pinned-behaviour.md +1117 -0
- data/exe/badline +22 -2
- data/exe/badline-sid +34 -0
- data/lib/badline/address_bus.rb +96 -25
- data/lib/badline/audio/aiff.rb +36 -0
- data/lib/badline/audio/bare_player.rb +129 -0
- data/lib/badline/audio/cli.rb +133 -0
- data/lib/badline/audio/console.rb +75 -0
- data/lib/badline/audio/jukebox.rb +82 -0
- data/lib/badline/audio/machine_player.rb +57 -0
- data/lib/badline/audio/options.rb +137 -0
- data/lib/badline/audio/pcm_writer.rb +57 -0
- data/lib/badline/audio/playback.rb +108 -0
- data/lib/badline/audio/renderer.rb +85 -0
- data/lib/badline/audio/sdl_sink.rb +102 -0
- data/lib/badline/audio/wav.rb +23 -0
- data/lib/badline/audio.rb +14 -0
- data/lib/badline/cartridge/action_replay.rb +111 -0
- data/lib/badline/cartridge/atomic_power.rb +24 -0
- data/lib/badline/cartridge/bank.rb +81 -0
- data/lib/badline/cartridge/comal80.rb +48 -0
- data/lib/badline/cartridge/dinamic.rb +34 -0
- data/lib/badline/cartridge/easy_flash.rb +122 -0
- data/lib/badline/cartridge/epyx_fastload.rb +71 -0
- data/lib/badline/cartridge/final_cartridge3.rb +77 -0
- data/lib/badline/cartridge/flash.rb +265 -0
- data/lib/badline/cartridge/freezer.rb +31 -0
- data/lib/badline/cartridge/fun_play.rb +38 -0
- data/lib/badline/cartridge/g_mod2.rb +62 -0
- data/lib/badline/cartridge/game_system.rb +40 -0
- data/lib/badline/cartridge/kcs_power.rb +72 -0
- data/lib/badline/cartridge/mach5.rb +34 -0
- data/lib/badline/cartridge/magic_desk.rb +6 -3
- data/lib/badline/cartridge/ocean.rb +11 -12
- data/lib/badline/cartridge/pagefox.rb +56 -0
- data/lib/badline/cartridge/retro_replay.rb +276 -0
- data/lib/badline/cartridge/rex_utility.rb +33 -0
- data/lib/badline/cartridge/rgcd.rb +48 -0
- data/lib/badline/cartridge/simons_basic.rb +40 -0
- data/lib/badline/cartridge/standard.rb +3 -19
- data/lib/badline/cartridge/super_games.rb +36 -0
- data/lib/badline/cartridge/westermann.rb +34 -0
- data/lib/badline/cartridge/zaxxon.rb +50 -0
- data/lib/badline/cartridge.rb +132 -16
- data/lib/badline/cia/interrupt_register.rb +102 -0
- data/lib/badline/cia/serial.rb +219 -0
- data/lib/badline/cia/timer.rb +43 -31
- data/lib/badline/cia.rb +138 -88
- data/lib/badline/color_memory.rb +14 -5
- data/lib/badline/computer.rb +97 -12
- data/lib/badline/control_ports.rb +54 -9
- data/lib/badline/cpu/addressing.rb +131 -0
- data/lib/badline/cpu/microcode.rb +169 -0
- data/lib/badline/cpu/operations.rb +88 -0
- data/lib/badline/cpu/stack_operations.rb +82 -0
- data/lib/badline/cpu.rb +109 -168
- data/lib/badline/cycleable.rb +3 -0
- data/lib/badline/datasette.rb +81 -0
- data/lib/badline/debug_register.rb +28 -0
- data/lib/badline/gui/application.rb +79 -18
- data/lib/badline/gui/gamepads.rb +97 -0
- data/lib/badline/gui/joy_map.rb +15 -7
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/input/mouse1351.rb +41 -0
- data/lib/badline/input/paddles.rb +36 -0
- data/lib/badline/input.rb +4 -0
- data/lib/badline/instruction_set/arithmetic.rb +16 -21
- data/lib/badline/instruction_set/bitwise.rb +17 -22
- data/lib/badline/instruction_set/branch.rb +20 -19
- data/lib/badline/instruction_set/flag.rb +7 -7
- data/lib/badline/instruction_set/illegal.rb +14 -22
- data/lib/badline/instruction_set/inc_dec.rb +8 -10
- data/lib/badline/instruction_set/stack.rb +5 -57
- data/lib/badline/instruction_set/transfer.rb +9 -9
- data/lib/badline/instruction_set.rb +9 -31
- data/lib/badline/interrupts.rb +104 -0
- data/lib/badline/joystick.rb +9 -9
- data/lib/badline/kernal_trap/channel.rb +51 -0
- data/lib/badline/kernal_trap/drive/status.rb +49 -0
- data/lib/badline/kernal_trap/drive.rb +277 -0
- data/lib/badline/kernal_trap/file.rb +5 -25
- data/lib/badline/kernal_trap/load.rb +86 -24
- data/lib/badline/kernal_trap/routine.rb +51 -0
- data/lib/badline/kernal_trap/save.rb +46 -14
- data/lib/badline/kernal_trap/serial.rb +171 -0
- data/lib/badline/kernal_trap.rb +4 -0
- data/lib/badline/keyboard.rb +40 -16
- data/lib/badline/media.rb +48 -10
- data/lib/badline/memory.rb +5 -0
- data/lib/badline/roms/eapi/LICENSE.md +15 -0
- data/lib/badline/roms/eapi/README +41 -0
- data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
- data/lib/badline/sid/decimator.rb +46 -0
- data/lib/badline/sid/envelope.rb +232 -0
- data/lib/badline/sid/filter.rb +213 -0
- data/lib/badline/sid/voice.rb +61 -0
- data/lib/badline/sid/waveform/combined.rb +126 -0
- data/lib/badline/sid/waveform/fast_forward.rb +105 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
- data/lib/badline/sid/waveform.rb +289 -0
- data/lib/badline/sid.rb +274 -7
- data/lib/badline/status.rb +251 -23
- data/lib/badline/storage/crt_file.rb +18 -2
- data/lib/badline/storage/d64_image.rb +6 -0
- data/lib/badline/storage/d71_image.rb +4 -0
- data/lib/badline/storage/d81_image.rb +4 -0
- data/lib/badline/storage/disk_image.rb +99 -13
- data/lib/badline/storage/host_directory.rb +30 -4
- data/lib/badline/storage/sid_file.rb +274 -0
- data/lib/badline/storage/song_lengths.rb +65 -0
- data/lib/badline/storage/t64.rb +67 -0
- data/lib/badline/storage/tap.rb +68 -0
- data/lib/badline/storage.rb +19 -0
- data/lib/badline/time_of_day.rb +105 -52
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +14 -6
- data/lib/badline/vic/border_mask.rb +76 -0
- data/lib/badline/vic/collisions.rb +104 -0
- data/lib/badline/vic/color_patches.rb +33 -0
- data/lib/badline/vic/display_state.rb +92 -29
- data/lib/badline/vic/graphics_mode.rb +78 -53
- data/lib/badline/vic/graphics_shifter.rb +140 -0
- data/lib/badline/vic/register_log.rb +63 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic/sequencer.rb +163 -137
- data/lib/badline/vic/sequencer_output.rb +116 -0
- data/lib/badline/vic/sprite/internal_bus.rb +49 -0
- data/lib/badline/vic/sprite/shifter.rb +129 -0
- data/lib/badline/vic/sprite.rb +275 -65
- data/lib/badline/vic/sprites.rb +220 -58
- data/lib/badline/vic.rb +389 -55
- data/lib/badline.rb +5 -0
- data/vendor/.gitkeep +0 -0
- metadata +104 -4
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/vic/border_mask"
|
|
4
|
+
require "badline/vic/color_patches"
|
|
3
5
|
require "badline/vic/graphics_mode"
|
|
6
|
+
require "badline/vic/graphics_shifter"
|
|
7
|
+
require "badline/vic/sequencer_output"
|
|
4
8
|
|
|
5
9
|
module Badline
|
|
6
10
|
class VIC < Cycleable
|
|
@@ -8,6 +12,8 @@ module Badline
|
|
|
8
12
|
#
|
|
9
13
|
# Turns fetched graphics data into output pixels.
|
|
10
14
|
class Sequencer
|
|
15
|
+
include Output
|
|
16
|
+
|
|
11
17
|
DISPLAY_X_BOUNDS = [
|
|
12
18
|
[135, 438].freeze,
|
|
13
19
|
[128, 447].freeze
|
|
@@ -28,28 +34,16 @@ module Badline
|
|
|
28
34
|
[51, 251].freeze
|
|
29
35
|
].freeze
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
# window groups and bulk-fill full border groups.
|
|
33
|
-
BORDER_FULL = 0
|
|
34
|
-
BORDER_NONE = 1
|
|
35
|
-
BORDER_MIXED = 2
|
|
36
|
-
|
|
37
|
-
NULL_MODE = GraphicsMode::Null.new
|
|
38
|
-
MODES = [
|
|
39
|
-
GraphicsMode::Text.new, # 000 standard text
|
|
40
|
-
GraphicsMode::MulticolorText.new, # 001 multicolour text
|
|
41
|
-
GraphicsMode::Bitmap.new, # 010 standard bitmap
|
|
42
|
-
GraphicsMode::MulticolorBitmap.new, # 011 multicolour bitmap
|
|
43
|
-
GraphicsMode::ExtendedBackgroundText.new, # 100 ECM text
|
|
44
|
-
NULL_MODE, # 101 invalid
|
|
45
|
-
NULL_MODE, # 110 invalid
|
|
46
|
-
NULL_MODE # 111 invalid
|
|
47
|
-
].freeze
|
|
37
|
+
MODES = GraphicsMode::MODES
|
|
48
38
|
|
|
49
|
-
attr_reader :colors, :fg, :
|
|
39
|
+
attr_reader :colors, :fg, :registers, :bank, :cur_colors,
|
|
40
|
+
:color_patches
|
|
50
41
|
# The fg masks are shared frozen patterns assigned by reference, never
|
|
51
42
|
# mutated in place.
|
|
52
43
|
attr_accessor :cur_fg
|
|
44
|
+
# False leaves the line's colours unpainted and keeps only the
|
|
45
|
+
# foreground mask the sprite collisions read.
|
|
46
|
+
attr_writer :render
|
|
53
47
|
|
|
54
48
|
def initialize(width, registers, bank)
|
|
55
49
|
@width = width
|
|
@@ -57,180 +51,212 @@ module Badline
|
|
|
57
51
|
@bank = bank
|
|
58
52
|
@colors = Array.new(width, 0)
|
|
59
53
|
@fg = Array.new(width, false)
|
|
60
|
-
@
|
|
61
|
-
|
|
54
|
+
@border_mask = BorderMask.new(width)
|
|
55
|
+
# Hot-path aliases: the pixel loops write coverage straight into the
|
|
56
|
+
# mask's arrays.
|
|
57
|
+
@border = @border_mask.mask
|
|
58
|
+
@border_groups = @border_mask.groups
|
|
62
59
|
@cur_colors = Array.new(8, 0)
|
|
63
60
|
@cur_fg = GraphicsMode::NO_FG
|
|
64
61
|
@prev_colors = Array.new(8, 0)
|
|
65
62
|
@prev_fg = GraphicsMode::NO_FG
|
|
66
63
|
@vertical_border = true
|
|
64
|
+
@vertical_armed = true
|
|
67
65
|
@main_border = true
|
|
66
|
+
@color_patches = ColorPatches.new(self)
|
|
67
|
+
@shifter = GraphicsShifter.new(registers)
|
|
68
|
+
@xscroll = 0
|
|
69
|
+
@last_shift = 0
|
|
70
|
+
@last_mode = 0
|
|
71
|
+
@settling = false
|
|
72
|
+
@ring_data = Array.new(4, 0)
|
|
73
|
+
@ring_char = Array.new(4, 0)
|
|
74
|
+
@ring_color = Array.new(4, 1)
|
|
75
|
+
@prev_fresh = false
|
|
76
|
+
@check = true
|
|
77
|
+
@render = true
|
|
68
78
|
new_line(0)
|
|
69
79
|
end
|
|
70
80
|
|
|
71
|
-
# Reset the line buffers at the start of a rasterline
|
|
72
|
-
# vertical border flip-flop.
|
|
81
|
+
# Reset the line buffers at the start of a rasterline.
|
|
73
82
|
def new_line(line)
|
|
74
|
-
|
|
83
|
+
@line = line
|
|
84
|
+
@left_vertical_border = nil
|
|
85
|
+
@first_csel = @registers.csel?
|
|
75
86
|
@colors.fill(@registers.border)
|
|
76
87
|
@fg.fill(false)
|
|
77
|
-
@
|
|
88
|
+
@border_mask.reset
|
|
78
89
|
@prev_colors.fill(@registers.background)
|
|
79
90
|
@prev_fg = GraphicsMode::NO_FG
|
|
91
|
+
@prev_fresh = false
|
|
92
|
+
@check = true
|
|
93
|
+
@color_patches.clear
|
|
80
94
|
end
|
|
81
95
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
end
|
|
91
|
-
group += 1
|
|
92
|
-
end
|
|
96
|
+
# The load point for the next group's byte: XSCROLL as a g-access
|
|
97
|
+
# column with the border open sees it, a column before the group that
|
|
98
|
+
# uses it (VICE x64sc `xscroll_pipe`).
|
|
99
|
+
def latch_xscroll(xscroll = @registers.xscroll)
|
|
100
|
+
return if xscroll == @xscroll
|
|
101
|
+
|
|
102
|
+
@xscroll = xscroll
|
|
103
|
+
@check = true
|
|
93
104
|
end
|
|
94
105
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
# A write to a register the painted colours depend on: the byte kept
|
|
107
|
+
# from the previous group is painted again before its pixels are shown.
|
|
108
|
+
def colors_changed!
|
|
109
|
+
@prev_fresh = false
|
|
110
|
+
@check = true
|
|
99
111
|
end
|
|
100
112
|
|
|
101
|
-
def
|
|
102
|
-
@
|
|
103
|
-
@cur_fg = GraphicsMode::NO_FG
|
|
104
|
-
output(col)
|
|
105
|
-
roll
|
|
113
|
+
def apply_color_patches
|
|
114
|
+
@color_patches.apply(@colors, @fg)
|
|
106
115
|
end
|
|
107
116
|
|
|
108
|
-
|
|
117
|
+
# The vertical border compares run in every cycle of their line (VICE
|
|
118
|
+
# x64sc). The top one resets the flip-flop at once when DEN is set. The
|
|
119
|
+
# bottom one only arms it, and the armed state takes hold at the line's
|
|
120
|
+
# first cycle and at the left window edge (Bauer §3.9 rules 2-5). The
|
|
121
|
+
# VIC calls this after each $d011 write.
|
|
122
|
+
def compare_vertical_border(line)
|
|
123
|
+
top, bottom = BORDER_Y_BOUNDS[@registers.rsel? ? 1 : 0]
|
|
124
|
+
@vertical_armed = true if line == bottom
|
|
125
|
+
@vertical_armed = @vertical_border = false if line == top && @registers.display_enabled?
|
|
126
|
+
end
|
|
109
127
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
x_pos = (col + 16) * 8
|
|
116
|
-
win_lo, right_compare = WINDOW_COMPARES[@registers.csel? ? 1 : 0]
|
|
117
|
-
|
|
118
|
-
if boundary_group?(x_pos, win_lo, right_compare)
|
|
119
|
-
output_boundary(col, x_pos, win_lo, right_compare)
|
|
120
|
-
elsif @main_border || @vertical_border
|
|
121
|
-
output_border(x_pos)
|
|
122
|
-
else
|
|
123
|
-
output_window(col, x_pos)
|
|
124
|
-
end
|
|
128
|
+
# The line's first cycle, Bauer's cycle 1, which is the last column of
|
|
129
|
+
# the line before it.
|
|
130
|
+
def start_vertical_border(line)
|
|
131
|
+
compare_vertical_border(line)
|
|
132
|
+
@vertical_border = @vertical_armed
|
|
125
133
|
end
|
|
126
134
|
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
hi_delta = right_compare - x_pos
|
|
132
|
-
(lo_delta >= 0 && lo_delta < 8) || (hi_delta >= 0 && hi_delta < 8)
|
|
135
|
+
# The 40-column left compare, a column before the column that draws its
|
|
136
|
+
# pixel. The 38-column one runs in the drawing column.
|
|
137
|
+
def left_compare_vertical_border
|
|
138
|
+
@left_vertical_border = start_vertical_border(@line) if @registers.csel?
|
|
133
139
|
end
|
|
134
140
|
|
|
135
|
-
def
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
def border_at?(pixel_x) = @border_mask.at?(pixel_x)
|
|
142
|
+
|
|
143
|
+
# True while the vertical border flip-flop is set and stays set for the
|
|
144
|
+
# rest of the line: it can only clear mid-line on a top compare line
|
|
145
|
+
# with DEN set.
|
|
146
|
+
def vertical_closed?
|
|
147
|
+
return false unless @vertical_border
|
|
148
|
+
return true unless @line == 51 || @line == 55
|
|
149
|
+
|
|
150
|
+
top, = BORDER_Y_BOUNDS[@registers.rsel? ? 1 : 0]
|
|
151
|
+
!(@line == top && @registers.display_enabled?)
|
|
139
152
|
end
|
|
140
153
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
154
|
+
# Snapshot the finished line so apply_border can restore the border
|
|
155
|
+
# pixels sprites were composited over, keeping mid-line border splits.
|
|
156
|
+
def snapshot_line = @border_mask.snapshot(@colors)
|
|
157
|
+
|
|
158
|
+
# Repaint the border over the composited line, hiding the sprites.
|
|
159
|
+
def apply_border = @border_mask.restore(@colors)
|
|
160
|
+
|
|
161
|
+
# The g-accesses the VIC hands over, as a ring the column's slot
|
|
162
|
+
# indexes: the byte read, the screen byte and the colour nibble. The
|
|
163
|
+
# slot before is the byte the previous group loaded.
|
|
164
|
+
attr_reader :ring_data, :ring_char, :ring_color
|
|
165
|
+
|
|
166
|
+
# Paints the byte in the slot through the current mode. An idle access
|
|
167
|
+
# latches 0/0 and one that made no access passes on zero data with the
|
|
168
|
+
# kept values; under a closed border neither has foreground. A group
|
|
169
|
+
# where the mode or the load point changes, and the one after it, run
|
|
170
|
+
# the shift register pixel by pixel.
|
|
171
|
+
def emit(slot, col, display)
|
|
172
|
+
mode = @registers.mode
|
|
173
|
+
return emit_checked(slot, col, display, mode) if @check || mode != @last_mode
|
|
144
174
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
175
|
+
paint_slot(slot, mode, display)
|
|
176
|
+
output(col, @last_shift)
|
|
177
|
+
roll
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
# After a mode, XSCROLL or colour change: groups paint whole bytes
|
|
183
|
+
# again once the change has settled, and run pixel by pixel until then.
|
|
184
|
+
def emit_checked(slot, col, display, mode)
|
|
185
|
+
shift = @xscroll
|
|
186
|
+
if mode == @last_mode && shift == @last_shift && !@settling
|
|
187
|
+
emit_settled(slot, col, display, mode, shift)
|
|
152
188
|
else
|
|
153
|
-
|
|
189
|
+
emit_pixels(slot, col, mode, shift, !display && border_hidden?)
|
|
190
|
+
@settling = mode != @last_mode || shift != @last_shift
|
|
191
|
+
@last_mode = mode
|
|
192
|
+
@last_shift = shift
|
|
154
193
|
end
|
|
155
194
|
end
|
|
156
195
|
|
|
157
|
-
def
|
|
158
|
-
|
|
159
|
-
|
|
196
|
+
def emit_settled(slot, col, display, mode, shift)
|
|
197
|
+
repaint_previous(mode, (slot - 1) & 3) unless @prev_fresh || shift.zero?
|
|
198
|
+
paint_slot(slot, mode, display)
|
|
199
|
+
output(col, shift)
|
|
200
|
+
roll
|
|
201
|
+
@prev_fresh = true
|
|
202
|
+
@check = false
|
|
203
|
+
end
|
|
160
204
|
|
|
161
|
-
|
|
162
|
-
if
|
|
163
|
-
@
|
|
205
|
+
def paint_slot(slot, mode, display)
|
|
206
|
+
if !display && border_hidden?
|
|
207
|
+
@cur_fg = GraphicsMode::NO_FG
|
|
164
208
|
else
|
|
165
|
-
|
|
209
|
+
paint(slot, mode)
|
|
166
210
|
end
|
|
167
|
-
output_shifted_fg(col, x_pos, in_gfx, shift, keep)
|
|
168
211
|
end
|
|
169
212
|
|
|
170
|
-
def
|
|
171
|
-
|
|
213
|
+
def repaint_previous(mode, slot)
|
|
214
|
+
roll
|
|
215
|
+
paint(slot, mode)
|
|
216
|
+
roll
|
|
217
|
+
end
|
|
172
218
|
|
|
173
|
-
|
|
174
|
-
if
|
|
175
|
-
@
|
|
219
|
+
def paint(slot, mode)
|
|
220
|
+
if @render
|
|
221
|
+
MODES[mode].paint(@ring_data[slot], @ring_char[slot], @ring_color[slot], self)
|
|
176
222
|
else
|
|
177
|
-
@fg
|
|
223
|
+
@cur_fg = MODES[mode].fg(@ring_data[slot], @ring_char[slot], @ring_color[slot])
|
|
178
224
|
end
|
|
179
225
|
end
|
|
180
226
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
@
|
|
188
|
-
|
|
189
|
-
i = 0
|
|
190
|
-
while i < 8
|
|
191
|
-
src = i - shift
|
|
192
|
-
if src >= 0
|
|
193
|
-
pixel = @cur_colors[src]
|
|
194
|
-
mask = @cur_fg[src]
|
|
195
|
-
elsif bleed
|
|
196
|
-
pixel = @prev_colors[8 + src]
|
|
197
|
-
mask = @prev_fg[8 + src]
|
|
198
|
-
else
|
|
199
|
-
pixel = bg
|
|
200
|
-
mask = false
|
|
201
|
-
end
|
|
202
|
-
x = x_pos + i
|
|
203
|
-
shown = pixel_shown?(x, win_lo, right_compare)
|
|
204
|
-
@colors[x] = shown ? pixel : border
|
|
205
|
-
@border[x] = !shown
|
|
206
|
-
@fg[x] = x >= GFX_X_START && x < GFX_X_END ? mask : false
|
|
207
|
-
i += 1
|
|
208
|
-
end
|
|
227
|
+
def emit_pixels(slot, col, mode, shift, hidden)
|
|
228
|
+
prime_shifter((slot - 1) & 3) unless @settling
|
|
229
|
+
@shifter.draw(hidden ? 0 : @ring_data[slot], @ring_char[slot], @ring_color[slot], shift, mode)
|
|
230
|
+
@cur_colors[0, 8] = @shifter.colors
|
|
231
|
+
@cur_fg = @shifter.fg
|
|
232
|
+
output(col, 0)
|
|
233
|
+
@prev_fresh = false
|
|
234
|
+
@check = true
|
|
209
235
|
end
|
|
210
236
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
@colors[x_pos + i] = color if @border[x_pos + i]
|
|
215
|
-
i += 1
|
|
216
|
-
end
|
|
237
|
+
# Picks up from a group that painted whole bytes.
|
|
238
|
+
def prime_shifter(slot)
|
|
239
|
+
@shifter.prime(@ring_data[slot], @ring_char[slot], @ring_color[slot], @last_shift, @last_mode)
|
|
217
240
|
end
|
|
218
241
|
|
|
242
|
+
# The group is border throughout when the main flip-flop is set and the
|
|
243
|
+
# vertical one keeps it from clearing.
|
|
244
|
+
def border_hidden? = @main_border && vertical_closed?
|
|
245
|
+
|
|
219
246
|
def pixel_shown?(pixel_x, left_compare, right_compare)
|
|
220
247
|
@main_border = true if pixel_x == right_compare
|
|
221
|
-
@main_border = false if pixel_x == left_compare &&
|
|
222
|
-
|
|
248
|
+
@main_border = false if pixel_x == left_compare && !left_vertical_border
|
|
249
|
+
!@main_border
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def left_vertical_border
|
|
253
|
+
@left_vertical_border.nil? ? start_vertical_border(@line) : @left_vertical_border
|
|
223
254
|
end
|
|
224
255
|
|
|
225
256
|
def roll
|
|
226
257
|
@prev_colors, @cur_colors = @cur_colors, @prev_colors
|
|
227
258
|
@prev_fg, @cur_fg = @cur_fg, @prev_fg
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def update_vertical_border(line)
|
|
231
|
-
top, bottom = BORDER_Y_BOUNDS[@registers.rsel? ? 1 : 0]
|
|
232
|
-
@vertical_border = true if line == bottom
|
|
233
|
-
@vertical_border = false if line == top && @registers.display_enabled?
|
|
259
|
+
nil
|
|
234
260
|
end
|
|
235
261
|
end
|
|
236
262
|
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIC < Cycleable
|
|
5
|
+
class Sequencer
|
|
6
|
+
# Writes a column's 8-pixel group into the line buffers, through the
|
|
7
|
+
# main border flip-flop and XSCROLL.
|
|
8
|
+
module Output
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# Write the 8-pixel group for a column into the line buffers. The main
|
|
12
|
+
# border flip-flop only changes state in the groups containing the
|
|
13
|
+
# window edge compares, so all other groups take a branch-free bulk
|
|
14
|
+
# path: fully border or fully window. A group's first pixel, where the
|
|
15
|
+
# 40-column compares fall, still sees CSEL as the column before had it.
|
|
16
|
+
def output(col, shift)
|
|
17
|
+
x_pos = (col + 16) * 8
|
|
18
|
+
csel = @registers.csel?
|
|
19
|
+
first = WINDOW_COMPARES[@first_csel ? 1 : 0]
|
|
20
|
+
@first_csel = csel
|
|
21
|
+
compares = WINDOW_COMPARES[csel ? 1 : 0]
|
|
22
|
+
win_lo, right_compare = compares
|
|
23
|
+
|
|
24
|
+
if !first.equal?(compares) || boundary_group?(x_pos, win_lo, right_compare)
|
|
25
|
+
output_boundary(x_pos, first, compares, shift)
|
|
26
|
+
elsif @main_border
|
|
27
|
+
output_border(x_pos)
|
|
28
|
+
else
|
|
29
|
+
output_window(x_pos, shift)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# True if the group contains a window edge, where the main border
|
|
34
|
+
# flip-flop can change state.
|
|
35
|
+
def boundary_group?(x_pos, win_lo, right_compare)
|
|
36
|
+
lo_delta = win_lo - x_pos
|
|
37
|
+
hi_delta = right_compare - x_pos
|
|
38
|
+
(lo_delta >= 0 && lo_delta < 8) || (hi_delta >= 0 && hi_delta < 8)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def output_border(x_pos)
|
|
42
|
+
@colors.fill(@registers.border, x_pos, 8) if @render
|
|
43
|
+
@border_groups[x_pos >> 3] = BorderMask::FULL
|
|
44
|
+
@fg.fill(false, x_pos, 8)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def output_window(x_pos, shift)
|
|
48
|
+
in_gfx = x_pos >= GFX_X_START && x_pos < GFX_X_END
|
|
49
|
+
@border_groups[x_pos >> 3] = BorderMask::NONE
|
|
50
|
+
|
|
51
|
+
if shift.zero?
|
|
52
|
+
@colors[x_pos, 8] = @cur_colors if @render
|
|
53
|
+
if in_gfx
|
|
54
|
+
@fg[x_pos, 8] = @cur_fg
|
|
55
|
+
else
|
|
56
|
+
@fg.fill(false, x_pos, 8)
|
|
57
|
+
end
|
|
58
|
+
else
|
|
59
|
+
output_window_shifted(x_pos, in_gfx, shift)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def output_window_shifted(x_pos, in_gfx, shift)
|
|
64
|
+
keep = 8 - shift
|
|
65
|
+
|
|
66
|
+
if @render
|
|
67
|
+
copy(@cur_colors, 0, @colors, x_pos + shift, keep)
|
|
68
|
+
copy(@prev_colors, keep, @colors, x_pos, shift)
|
|
69
|
+
end
|
|
70
|
+
output_shifted_fg(x_pos, in_gfx, shift, keep)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def output_shifted_fg(x_pos, in_gfx, shift, keep)
|
|
74
|
+
return @fg.fill(false, x_pos, 8) unless in_gfx
|
|
75
|
+
|
|
76
|
+
copy(@cur_fg, 0, @fg, x_pos + shift, keep)
|
|
77
|
+
copy(@prev_fg, keep, @fg, x_pos, shift)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def copy(src, from, dest, to, count)
|
|
81
|
+
i = 0
|
|
82
|
+
while i < count
|
|
83
|
+
dest[to + i] = src[from + i]
|
|
84
|
+
i += 1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Slow path for the groups where the border flip-flop can change state.
|
|
89
|
+
def output_boundary(x_pos, first, compares, shift)
|
|
90
|
+
border = @registers.border
|
|
91
|
+
win_lo, right_compare = first
|
|
92
|
+
@border_groups[x_pos >> 3] = BorderMask::MIXED
|
|
93
|
+
|
|
94
|
+
i = 0
|
|
95
|
+
while i < 8
|
|
96
|
+
src = i - shift
|
|
97
|
+
if src >= 0
|
|
98
|
+
pixel = @cur_colors[src]
|
|
99
|
+
mask = @cur_fg[src]
|
|
100
|
+
else
|
|
101
|
+
pixel = @prev_colors[8 + src]
|
|
102
|
+
mask = @prev_fg[8 + src]
|
|
103
|
+
end
|
|
104
|
+
x = x_pos + i
|
|
105
|
+
shown = pixel_shown?(x, win_lo, right_compare)
|
|
106
|
+
@colors[x] = shown ? pixel : border
|
|
107
|
+
@border[x] = !shown
|
|
108
|
+
@fg[x] = x >= GFX_X_START && x < GFX_X_END ? mask : false
|
|
109
|
+
win_lo, right_compare = compares if i.zero?
|
|
110
|
+
i += 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIC < Cycleable
|
|
5
|
+
class Sprite
|
|
6
|
+
# = InternalBus
|
|
7
|
+
#
|
|
8
|
+
# What a sprite's s-accesses read while its DMA is off. The first and
|
|
9
|
+
# third fall in phi2, where the VIC's internal bus floats to $ff unless
|
|
10
|
+
# the CPU reads or writes a VIC register in that cycle, and the middle
|
|
11
|
+
# one is the idle fetch at $3fff (sbsprf24's readme).
|
|
12
|
+
class InternalBus
|
|
13
|
+
def initialize(bank, columns)
|
|
14
|
+
@bank = bank
|
|
15
|
+
@lines = 0
|
|
16
|
+
@ghost = 0xff
|
|
17
|
+
@value = Array.new(columns, 0xff)
|
|
18
|
+
@line = Array.new(columns, -1)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Sprites 3-7 fetch in Bauer cycles 1-10, so the idle byte is taken
|
|
22
|
+
# as the line starts.
|
|
23
|
+
def start_line(sprites_enabled)
|
|
24
|
+
@lines += 1
|
|
25
|
+
@ghost = @bank.peek(0x3fff) if sprites_enabled
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The VIC has advanced to `column`, so the access runs in Bauer cycle
|
|
29
|
+
# `column + 1`, and column 0 is the first cycle of the line about to
|
|
30
|
+
# start.
|
|
31
|
+
def access(column, value)
|
|
32
|
+
@value[column] = value
|
|
33
|
+
@line[column] = column.zero? ? @lines + 1 : @lines
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The row sprite `index` (3-7) fetched this line, in Bauer cycles
|
|
37
|
+
# 2n-5 and 2n-4.
|
|
38
|
+
def row(index)
|
|
39
|
+
column = (2 * index) - 6
|
|
40
|
+
(phi2(column) << 16) | (@ghost << 8) | phi2(column + 1)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def phi2(column) = @line[column] == @lines ? @value[column] : 0xff
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIC < Cycleable
|
|
5
|
+
class Sprite
|
|
6
|
+
# The X comparator and the 24-bit shift register behind it: where a
|
|
7
|
+
# line fires the sprite, and the pixels each firing shifts out.
|
|
8
|
+
module Shifter
|
|
9
|
+
EMPTY = [].freeze
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# The first pixel of every comparator match along the line. Writes to
|
|
14
|
+
# $d000/$d010 move the compare value mid-line, so a sprite can miss
|
|
15
|
+
# its match entirely and pick up a later one, a second one, or none.
|
|
16
|
+
def comparator_hits(log)
|
|
17
|
+
unless log
|
|
18
|
+
match = compare_x(@registers)
|
|
19
|
+
return match ? [(match + 1) % @width] : EMPTY
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
hits = []
|
|
23
|
+
log.rewind
|
|
24
|
+
log.advance(0)
|
|
25
|
+
from = 0
|
|
26
|
+
while from < @width
|
|
27
|
+
upto = [log.next_x, @width].min
|
|
28
|
+
match = compare_x(log)
|
|
29
|
+
hits << ((match + 1) % @width) if match && match >= from && match < upto
|
|
30
|
+
break if upto >= @width
|
|
31
|
+
|
|
32
|
+
from = upto
|
|
33
|
+
log.advance(from)
|
|
34
|
+
end
|
|
35
|
+
hits
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The VIC's X counter only runs to 503, so the eight coordinates above
|
|
39
|
+
# it never match and the sprite stays dark — the gap the spritegap
|
|
40
|
+
# tests find at $1f8.
|
|
41
|
+
def compare_x(view)
|
|
42
|
+
msb = view[0x10].anybits?(@bit) ? 0x100 : 0
|
|
43
|
+
xpos = msb | view[index * 2]
|
|
44
|
+
return if xpos >= @width
|
|
45
|
+
|
|
46
|
+
(xpos + COMPARE_OFFSET) % @width
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Pixels from the X match to the next reload.
|
|
50
|
+
def reload_cut(start) = (start < @reload_x ? @reload_x : @reload_x + @width) - start
|
|
51
|
+
|
|
52
|
+
# A run that reaches the next reload is cut there, holding its last
|
|
53
|
+
# pixel through K+6. Returns the span that is left.
|
|
54
|
+
def cut_at_reload(cut, span, codes)
|
|
55
|
+
return span if cut > span || cut >= MAX_SPAN
|
|
56
|
+
|
|
57
|
+
RELOAD_HOLD.times { |i| codes[cut + i] = codes[cut - 1] }
|
|
58
|
+
cut + RELOAD_HOLD
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Shift one firing out into `codes`, up to the sprite's end, `cut`
|
|
62
|
+
# pixels from the reload.
|
|
63
|
+
def run(log, start, codes, bits, cut)
|
|
64
|
+
@sr = bits
|
|
65
|
+
log&.rewind
|
|
66
|
+
pos = start
|
|
67
|
+
count = 0
|
|
68
|
+
boundary = log ? 0 : Float::INFINITY
|
|
69
|
+
mc = multicolor?
|
|
70
|
+
expanded = x_expanded?
|
|
71
|
+
while count < MAX_SPAN
|
|
72
|
+
if pos >= boundary
|
|
73
|
+
log.advance(pos)
|
|
74
|
+
mc = log[0x1c].anybits?(@bit)
|
|
75
|
+
expanded = log[0x1d].anybits?(@bit)
|
|
76
|
+
boundary = log.next_x
|
|
77
|
+
end
|
|
78
|
+
count.zero? ? prime(mc) : step(mc, expanded, count == cut - 1)
|
|
79
|
+
break if @latch.zero? && @sr.zero?
|
|
80
|
+
|
|
81
|
+
codes[count] = @latch
|
|
82
|
+
count += 1
|
|
83
|
+
pos += 1
|
|
84
|
+
end
|
|
85
|
+
count
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The X match loads the shift register and arms both flip-flops, so
|
|
89
|
+
# the sprite's first pixel comes straight off the fetched row.
|
|
90
|
+
def prime(multicolor)
|
|
91
|
+
@mc_flop = false
|
|
92
|
+
@xe_flop = false
|
|
93
|
+
@latch = (@sr >> 22) & (multicolor ? 3 : 2)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# One pixel of the sequencer. An unexpanded sprite shifts every pixel;
|
|
97
|
+
# an expanded one shifts every other, and the multicolor latch reloads
|
|
98
|
+
# every other shift, so its pairs stretch with the expansion. Both
|
|
99
|
+
# flip-flops sit idle while their register bit is clear, which is why
|
|
100
|
+
# the first pixel after a mid-sprite change repeats the last one.
|
|
101
|
+
def step(multicolor, expanded, last)
|
|
102
|
+
shift = true
|
|
103
|
+
if expanded
|
|
104
|
+
shift = @xe_flop
|
|
105
|
+
@xe_flop = !@xe_flop
|
|
106
|
+
else
|
|
107
|
+
@xe_flop = false
|
|
108
|
+
end
|
|
109
|
+
@mc_flop = false unless multicolor
|
|
110
|
+
return unless shift
|
|
111
|
+
|
|
112
|
+
@sr = (@sr << 1) & 0xffffff
|
|
113
|
+
load_latch(multicolor, last)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A multicolor pair loaded on the last pixel before the reload keeps
|
|
117
|
+
# only its high bit, as a hi-res pixel does (spritefetchbug).
|
|
118
|
+
def load_latch(multicolor, last)
|
|
119
|
+
if multicolor
|
|
120
|
+
@latch = (@sr >> 22) & (last ? 2 : 3) if @mc_flop
|
|
121
|
+
@mc_flop = !@mc_flop
|
|
122
|
+
else
|
|
123
|
+
@latch = (@sr >> 22) & 2
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|