badline 0.3.0 → 0.4.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/.claude/hooks/session-start.sh +57 -0
- data/.claude/settings.json +14 -0
- data/CHANGELOG.md +58 -0
- data/CLAUDE.md +45 -16
- data/CONTRIBUTING.md +95 -3
- data/README.md +111 -61
- data/SECURITY.md +9 -0
- data/doc/pinned-behaviour.md +275 -33
- data/exe/badline-ruby +59 -0
- data/lib/badline/address_bus.rb +22 -10
- data/lib/badline/audio/aiff.rb +8 -3
- data/lib/badline/audio/cli.rb +17 -12
- data/lib/badline/audio/console.rb +12 -58
- data/lib/badline/audio/playback.rb +3 -0
- data/lib/badline/audio/sdl_sink.rb +20 -52
- data/lib/badline/audio/terminal.rb +90 -0
- data/lib/badline/audio.rb +2 -2
- data/lib/badline/cartridge/geo_ram.rb +63 -0
- data/lib/badline/cartridge.rb +1 -0
- data/lib/badline/cia/interrupt_register.rb +34 -10
- data/lib/badline/cia.rb +15 -8
- data/lib/badline/computer.rb +28 -13
- data/lib/badline/gui/application.rb +33 -21
- data/lib/badline/gui/gamepads.rb +22 -22
- data/lib/badline/gui/joy_map.rb +1 -1
- data/lib/badline/gui/key_map.rb +6 -1
- data/lib/badline/gui/pane.rb +7 -6
- data/lib/badline/gui/screen_pane.rb +9 -13
- data/lib/badline/gui/window.rb +35 -20
- data/lib/badline/gui.rb +1 -1
- data/lib/badline/input/mouse1351.rb +21 -6
- data/lib/badline/input/paddles.rb +11 -6
- data/lib/badline/kernal_trap/channel.rb +3 -0
- data/lib/badline/kernal_trap/drive/block_commands.rb +33 -4
- data/lib/badline/kernal_trap/drive/status.rb +3 -0
- data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
- data/lib/badline/kernal_trap/drive/writes.rb +92 -0
- data/lib/badline/kernal_trap/drive.rb +51 -51
- data/lib/badline/kernal_trap/file.rb +6 -9
- data/lib/badline/kernal_trap/save.rb +9 -4
- data/lib/badline/keyboard.rb +19 -3
- data/lib/badline/media.rb +42 -11
- data/lib/badline/options.rb +209 -0
- data/lib/badline/ram_expansion/banking.rb +30 -0
- data/lib/badline/ram_expansion/plus256k.rb +38 -0
- data/lib/badline/ram_expansion/plus60k.rb +31 -0
- data/lib/badline/ram_expansion/unexpanded.rb +22 -0
- data/lib/badline/ram_expansion.rb +21 -0
- data/lib/badline/region.rb +51 -0
- data/lib/badline/sdl/events.rb +47 -0
- data/lib/badline/sdl/functions.rb +73 -0
- data/lib/badline/sdl.rb +67 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
- data/lib/badline/sid/waveform.rb +1 -1
- data/lib/badline/sid.rb +3 -1
- data/lib/badline/storage/d64_image.rb +10 -0
- data/lib/badline/storage/d71_image.rb +12 -0
- data/lib/badline/storage/d81_image.rb +9 -0
- data/lib/badline/storage/disk_image/bam.rb +111 -0
- data/lib/badline/storage/disk_image/directory.rb +79 -0
- data/lib/badline/storage/disk_image/writing.rb +110 -0
- data/lib/badline/storage/disk_image.rb +27 -4
- data/lib/badline/storage/host_directory.rb +2 -1
- data/lib/badline/storage.rb +15 -0
- data/lib/badline/time_of_day.rb +4 -2
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +104 -0
- data/lib/badline/via/interrupt_register.rb +58 -0
- data/lib/badline/via/shift_register.rb +135 -0
- data/lib/badline/via/timer1.rb +82 -0
- data/lib/badline/via/timer2.rb +84 -0
- data/lib/badline/via.rb +262 -0
- data/lib/badline/vic/bank.rb +4 -3
- data/lib/badline/vic/color_patches.rb +6 -3
- data/lib/badline/vic/display_state.rb +4 -0
- data/lib/badline/vic/graphics_shifter.rb +42 -7
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic/sequencer.rb +18 -18
- data/lib/badline/vic/sequencer_output.rb +4 -4
- data/lib/badline/vic/sprite/internal_bus.rb +4 -0
- data/lib/badline/vic/sprite/shifter.rb +25 -3
- data/lib/badline/vic/sprite.rb +5 -4
- data/lib/badline/vic/sprites.rb +8 -3
- data/lib/badline/vic.rb +93 -44
- data/lib/badline.rb +4 -0
- data/packaging/homebrew/badline.rb +34 -0
- metadata +30 -20
- data/exe/badline +0 -78
- data/exe/badline-sid +0 -34
- data/lib/badline/audio/options.rb +0 -137
|
@@ -16,16 +16,27 @@ module Badline
|
|
|
16
16
|
# ECM+MCM mode is a pixel later on both counts: the lookup changes at
|
|
17
17
|
# pixel 5 and the read at the next group's pixel 0. The mode bits are
|
|
18
18
|
# the sequencer's: ECM 4, BMM 2, MCM 1.
|
|
19
|
+
#
|
|
20
|
+
# The 8565 has no colour latency: ECM and BMM take hold, rising or
|
|
21
|
+
# falling, at the next group's pixel 0, and an MCM that falls out of
|
|
22
|
+
# ECM+MCM does so on time. Where that pixel 0 takes the group out of an
|
|
23
|
+
# invalid mode, it is still black into hi-res text, and so is a
|
|
24
|
+
# background pixel into multicolour bitmap.
|
|
19
25
|
class GraphicsShifter
|
|
26
|
+
ECM = 0b100
|
|
20
27
|
ECM_BMM = 0b110
|
|
28
|
+
MULTICOLOR_BITMAP = 0b011
|
|
21
29
|
ECM_MCM = 0b101
|
|
22
30
|
BMM = 0b010
|
|
23
31
|
|
|
24
32
|
# The group #draw painted, and which of its pixels are foreground.
|
|
25
33
|
attr_reader :colors, :fg
|
|
26
34
|
|
|
27
|
-
def initialize(registers)
|
|
35
|
+
def initialize(registers, model: :mos6569)
|
|
28
36
|
@registers = registers
|
|
37
|
+
@latency = model != :mos8565
|
|
38
|
+
@next_ecm_bmm = 0
|
|
39
|
+
@black_below = 0
|
|
29
40
|
@colors = Array.new(8, 0)
|
|
30
41
|
@fg = Array.new(8, false)
|
|
31
42
|
@gbuf = 0
|
|
@@ -43,6 +54,7 @@ module Badline
|
|
|
43
54
|
# the byte loaded at pixel shift under an unchanging mode.
|
|
44
55
|
def prime(data, screencode, color, shift, mode)
|
|
45
56
|
@mode_lookup = mode
|
|
57
|
+
@next_ecm_bmm = mode & ECM_BMM
|
|
46
58
|
@mode_read = mode & 1
|
|
47
59
|
@late_read = false
|
|
48
60
|
load(data, screencode, color)
|
|
@@ -56,32 +68,55 @@ module Badline
|
|
|
56
68
|
step_mode(i, mode)
|
|
57
69
|
load(data, screencode, color) if i == shift
|
|
58
70
|
pixel = read_pixel
|
|
59
|
-
@colors[i] = lookup(pixel)
|
|
71
|
+
@colors[i] = pixel < @black_below ? 0 : lookup(pixel)
|
|
72
|
+
@black_below = 0
|
|
60
73
|
@fg[i] = pixel >= 2
|
|
61
74
|
i += 1
|
|
62
75
|
end
|
|
76
|
+
@next_ecm_bmm = mode & ECM_BMM
|
|
63
77
|
end
|
|
64
78
|
|
|
65
79
|
private
|
|
66
80
|
|
|
67
81
|
def step_mode(pixel, mode)
|
|
68
82
|
case pixel
|
|
69
|
-
when 0 then
|
|
83
|
+
when 0 then start_group
|
|
70
84
|
when 4 then step_lookup(mode)
|
|
71
85
|
when 5 then step_early_fall(mode)
|
|
72
|
-
when 6 then @mode_lookup &= mode | 1
|
|
86
|
+
when 6 then @mode_lookup &= mode | 1 if @latency
|
|
73
87
|
when 7 then step_read
|
|
74
88
|
end
|
|
75
89
|
end
|
|
76
90
|
|
|
91
|
+
def start_group
|
|
92
|
+
end_late_read
|
|
93
|
+
return if @latency
|
|
94
|
+
|
|
95
|
+
invalid = @mode_lookup > ECM
|
|
96
|
+
@mode_lookup = (@mode_lookup & ~ECM_BMM) | @next_ecm_bmm
|
|
97
|
+
@black_below = black_pixels_out_of_invalid if invalid
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The pixel values below which pixel 0 stays black as the group leaves
|
|
101
|
+
# an invalid mode: all four into hi-res text, the background ones into
|
|
102
|
+
# multicolour bitmap.
|
|
103
|
+
def black_pixels_out_of_invalid
|
|
104
|
+
case @mode_lookup
|
|
105
|
+
when 0 then 4
|
|
106
|
+
when MULTICOLOR_BITMAP then 2
|
|
107
|
+
else 0
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
77
111
|
def step_lookup(mode)
|
|
78
|
-
@late_mcm = @mode_lookup.allbits?(ECM_MCM) && mode.nobits?(1)
|
|
79
|
-
|
|
112
|
+
@late_mcm = @latency && @mode_lookup.allbits?(ECM_MCM) && mode.nobits?(1)
|
|
113
|
+
rising = @latency ? mode & ECM_BMM : 0
|
|
114
|
+
@mode_lookup = (@mode_lookup & ~1) | (mode & 1) | (@late_mcm ? 1 : 0) | rising
|
|
80
115
|
end
|
|
81
116
|
|
|
82
117
|
def step_early_fall(mode)
|
|
83
118
|
@mode_lookup &= ~1 if @late_mcm
|
|
84
|
-
@mode_lookup &= mode | ~BMM
|
|
119
|
+
@mode_lookup &= mode | ~BMM if @latency && @mode_lookup != BMM
|
|
85
120
|
end
|
|
86
121
|
|
|
87
122
|
def step_read
|
|
@@ -91,6 +91,8 @@ module Badline
|
|
|
91
91
|
|
|
92
92
|
def read(reg)
|
|
93
93
|
case reg
|
|
94
|
+
when 0x16 then @bytes[reg] | 0xc0 # bits 7-6 unused, read 1
|
|
95
|
+
when 0x18 then @bytes[reg] | 0x01 # bit 0 unused, reads 1
|
|
94
96
|
when 0x1a, 0x20..0x2e then @bytes[reg] | 0xf0
|
|
95
97
|
when 0x1e, 0x1f then read_clear(reg) # collision registers clear on read
|
|
96
98
|
when 0x2f..0x3f then 0xff
|
|
@@ -106,6 +108,7 @@ module Badline
|
|
|
106
108
|
when 0x1a # only the four latch bits
|
|
107
109
|
@bytes[reg] = value & 0x0f
|
|
108
110
|
update_irq_line
|
|
111
|
+
when 0x1e, 0x1f then nil # the collision registers are read-only
|
|
109
112
|
else @bytes[reg] = value
|
|
110
113
|
end
|
|
111
114
|
end
|
|
@@ -14,21 +14,6 @@ module Badline
|
|
|
14
14
|
class Sequencer
|
|
15
15
|
include Output
|
|
16
16
|
|
|
17
|
-
DISPLAY_X_BOUNDS = [
|
|
18
|
-
[135, 438].freeze,
|
|
19
|
-
[128, 447].freeze
|
|
20
|
-
].freeze
|
|
21
|
-
|
|
22
|
-
# The graphics window always spans the full 40 columns, ignoring CSEL.
|
|
23
|
-
GFX_X_START = DISPLAY_X_BOUNDS[1][0]
|
|
24
|
-
GFX_X_END = DISPLAY_X_BOUNDS[1][1] + 1
|
|
25
|
-
|
|
26
|
-
# [left compare, right compare] for the border flip-flop per CSEL state.
|
|
27
|
-
WINDOW_COMPARES = [
|
|
28
|
-
[DISPLAY_X_BOUNDS[0][0], DISPLAY_X_BOUNDS[0][1] + 1].freeze,
|
|
29
|
-
[GFX_X_START, GFX_X_END].freeze
|
|
30
|
-
].freeze
|
|
31
|
-
|
|
32
17
|
BORDER_Y_BOUNDS = [
|
|
33
18
|
[55, 247].freeze,
|
|
34
19
|
[51, 251].freeze
|
|
@@ -45,8 +30,9 @@ module Badline
|
|
|
45
30
|
# foreground mask the sprite collisions read.
|
|
46
31
|
attr_writer :render
|
|
47
32
|
|
|
48
|
-
def initialize(width, registers, bank)
|
|
33
|
+
def initialize(width, registers, bank, model: :mos6569, region: Region::PAL)
|
|
49
34
|
@width = width
|
|
35
|
+
window_bounds(region.display_x_bounds)
|
|
50
36
|
@registers = registers
|
|
51
37
|
@bank = bank
|
|
52
38
|
@colors = Array.new(width, 0)
|
|
@@ -63,8 +49,8 @@ module Badline
|
|
|
63
49
|
@vertical_border = true
|
|
64
50
|
@vertical_armed = true
|
|
65
51
|
@main_border = true
|
|
66
|
-
@color_patches = ColorPatches.new(self)
|
|
67
|
-
@shifter = GraphicsShifter.new(registers)
|
|
52
|
+
@color_patches = ColorPatches.new(self, grey_dots: model == :mos8565)
|
|
53
|
+
@shifter = GraphicsShifter.new(registers, model:)
|
|
68
54
|
@xscroll = 0
|
|
69
55
|
@last_shift = 0
|
|
70
56
|
@last_mode = 0
|
|
@@ -179,6 +165,20 @@ module Badline
|
|
|
179
165
|
|
|
180
166
|
private
|
|
181
167
|
|
|
168
|
+
# The display window's edges per CSEL state, as the region gives them.
|
|
169
|
+
# The graphics window always spans the full 40 columns, ignoring CSEL,
|
|
170
|
+
# and the border flip-flop takes [left compare, right compare] per
|
|
171
|
+
# CSEL state.
|
|
172
|
+
def window_bounds(bounds)
|
|
173
|
+
narrow, wide = bounds
|
|
174
|
+
@gfx_x_start = wide[0]
|
|
175
|
+
@gfx_x_end = wide[1] + 1
|
|
176
|
+
@window_compares = [
|
|
177
|
+
[narrow[0], narrow[1] + 1].freeze,
|
|
178
|
+
[@gfx_x_start, @gfx_x_end].freeze
|
|
179
|
+
].freeze
|
|
180
|
+
end
|
|
181
|
+
|
|
182
182
|
# After a mode, XSCROLL or colour change: groups paint whole bytes
|
|
183
183
|
# again once the change has settled, and run pixel by pixel until then.
|
|
184
184
|
def emit_checked(slot, col, display, mode)
|
|
@@ -16,9 +16,9 @@ module Badline
|
|
|
16
16
|
def output(col, shift)
|
|
17
17
|
x_pos = (col + 16) * 8
|
|
18
18
|
csel = @registers.csel?
|
|
19
|
-
first =
|
|
19
|
+
first = @window_compares[@first_csel ? 1 : 0]
|
|
20
20
|
@first_csel = csel
|
|
21
|
-
compares =
|
|
21
|
+
compares = @window_compares[csel ? 1 : 0]
|
|
22
22
|
win_lo, right_compare = compares
|
|
23
23
|
|
|
24
24
|
if !first.equal?(compares) || boundary_group?(x_pos, win_lo, right_compare)
|
|
@@ -45,7 +45,7 @@ module Badline
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def output_window(x_pos, shift)
|
|
48
|
-
in_gfx = x_pos >=
|
|
48
|
+
in_gfx = x_pos >= @gfx_x_start && x_pos < @gfx_x_end
|
|
49
49
|
@border_groups[x_pos >> 3] = BorderMask::NONE
|
|
50
50
|
|
|
51
51
|
if shift.zero?
|
|
@@ -105,7 +105,7 @@ module Badline
|
|
|
105
105
|
shown = pixel_shown?(x, win_lo, right_compare)
|
|
106
106
|
@colors[x] = shown ? pixel : border
|
|
107
107
|
@border[x] = !shown
|
|
108
|
-
@fg[x] = x >=
|
|
108
|
+
@fg[x] = x >= @gfx_x_start && x < @gfx_x_end ? mask : false
|
|
109
109
|
win_lo, right_compare = compares if i.zero?
|
|
110
110
|
i += 1
|
|
111
111
|
end
|
|
@@ -10,8 +10,12 @@ module Badline
|
|
|
10
10
|
# the CPU reads or writes a VIC register in that cycle, and the middle
|
|
11
11
|
# one is the idle fetch at $3fff (sbsprf24's readme).
|
|
12
12
|
class InternalBus
|
|
13
|
+
# The columns of a raster line.
|
|
14
|
+
attr_reader :columns
|
|
15
|
+
|
|
13
16
|
def initialize(bank, columns)
|
|
14
17
|
@bank = bank
|
|
18
|
+
@columns = columns
|
|
15
19
|
@lines = 0
|
|
16
20
|
@ghost = 0xff
|
|
17
21
|
@value = Array.new(columns, 0xff)
|
|
@@ -71,8 +71,8 @@ module Badline
|
|
|
71
71
|
while count < MAX_SPAN
|
|
72
72
|
if pos >= boundary
|
|
73
73
|
log.advance(pos)
|
|
74
|
-
mc = log[0x1c].anybits?(@bit)
|
|
75
74
|
expanded = log[0x1d].anybits?(@bit)
|
|
75
|
+
mc = logged_multicolor(log, mc, expanded, count)
|
|
76
76
|
boundary = log.next_x
|
|
77
77
|
end
|
|
78
78
|
count.zero? ? prime(mc) : step(mc, expanded, count == cut - 1)
|
|
@@ -88,11 +88,29 @@ module Badline
|
|
|
88
88
|
# The X match loads the shift register and arms both flip-flops, so
|
|
89
89
|
# the sprite's first pixel comes straight off the fetched row.
|
|
90
90
|
def prime(multicolor)
|
|
91
|
-
@mc_flop =
|
|
91
|
+
@mc_flop = @new_chip && !multicolor
|
|
92
92
|
@xe_flop = false
|
|
93
93
|
@latch = (@sr >> 22) & (multicolor ? 3 : 2)
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
# The multicolor bit as the log has it at the pixel. A change after
|
|
97
|
+
# the first pixel reaches the 8565's flip-flop.
|
|
98
|
+
def logged_multicolor(log, was, expanded, count)
|
|
99
|
+
multicolor = log[0x1c].anybits?(@bit)
|
|
100
|
+
switch_multicolor(multicolor, expanded) if @new_chip && multicolor != was && count.positive?
|
|
101
|
+
multicolor
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# On the 8565 a multicolor change works on the flip-flop instead of
|
|
105
|
+
# the next pixels. It stays as it is unless the change lands between
|
|
106
|
+
# the two halves of an expanded pixel, where it flips, or is set by a
|
|
107
|
+
# switch to hi-res.
|
|
108
|
+
def switch_multicolor(multicolor, expanded)
|
|
109
|
+
return unless expanded && !@xe_flop
|
|
110
|
+
|
|
111
|
+
@mc_flop = multicolor ? !@mc_flop : true
|
|
112
|
+
end
|
|
113
|
+
|
|
96
114
|
# One pixel of the sequencer. An unexpanded sprite shifts every pixel;
|
|
97
115
|
# an expanded one shifts every other, and the multicolor latch reloads
|
|
98
116
|
# every other shift, so its pairs stretch with the expansion. Both
|
|
@@ -106,7 +124,7 @@ module Badline
|
|
|
106
124
|
else
|
|
107
125
|
@xe_flop = false
|
|
108
126
|
end
|
|
109
|
-
@mc_flop = false unless multicolor
|
|
127
|
+
@mc_flop = false unless multicolor || @new_chip
|
|
110
128
|
return unless shift
|
|
111
129
|
|
|
112
130
|
@sr = (@sr << 1) & 0xffffff
|
|
@@ -115,10 +133,14 @@ module Badline
|
|
|
115
133
|
|
|
116
134
|
# A multicolor pair loaded on the last pixel before the reload keeps
|
|
117
135
|
# only its high bit, as a hi-res pixel does (spritefetchbug).
|
|
136
|
+
# On the 8565 a hi-res pixel with the multicolor flip-flop clear
|
|
137
|
+
# sets it and loads nothing, so the latch repeats its last pixel.
|
|
118
138
|
def load_latch(multicolor, last)
|
|
119
139
|
if multicolor
|
|
120
140
|
@latch = (@sr >> 22) & (last ? 2 : 3) if @mc_flop
|
|
121
141
|
@mc_flop = !@mc_flop
|
|
142
|
+
elsif @new_chip && !@mc_flop
|
|
143
|
+
@mc_flop = true
|
|
122
144
|
else
|
|
123
145
|
@latch = (@sr >> 22) & 2
|
|
124
146
|
end
|
data/lib/badline/vic/sprite.rb
CHANGED
|
@@ -56,12 +56,13 @@ module Badline
|
|
|
56
56
|
attr_reader :index, :leftmost, :span, :codes,
|
|
57
57
|
:reload_leftmost, :reload_span, :reload_codes
|
|
58
58
|
|
|
59
|
-
def initialize(index, registers, bank,
|
|
59
|
+
def initialize(index, registers, bank, bus, model: :mos6569)
|
|
60
60
|
@index = index
|
|
61
|
+
@new_chip = model == :mos8565
|
|
61
62
|
@registers = registers
|
|
62
63
|
@bank = bank
|
|
63
64
|
@bus = bus
|
|
64
|
-
@width =
|
|
65
|
+
@width = bus.columns * 8
|
|
65
66
|
@bit = 1 << index
|
|
66
67
|
@dma = false
|
|
67
68
|
@display_on = false
|
|
@@ -77,8 +78,8 @@ module Badline
|
|
|
77
78
|
@reload_codes = Array.new(MAX_SPAN + RELOAD_HOLD, 0)
|
|
78
79
|
@reload_leftmost = 0
|
|
79
80
|
@reload_span = 0
|
|
80
|
-
@reload_x = (RELOAD_X + (RELOAD_STEP * index)) % width
|
|
81
|
-
@reload_next_line = RELOAD_X + (RELOAD_STEP * index) < width
|
|
81
|
+
@reload_x = (RELOAD_X + (RELOAD_STEP * index)) % @width
|
|
82
|
+
@reload_next_line = RELOAD_X + (RELOAD_STEP * index) < @width
|
|
82
83
|
@sr = 0
|
|
83
84
|
@latch = 0
|
|
84
85
|
@mc_flop = false
|
data/lib/badline/vic/sprites.rb
CHANGED
|
@@ -27,12 +27,17 @@ module Badline
|
|
|
27
27
|
[0x25, 0x26, *0x27..0x2e].each { |reg| delays[reg] = COLOR_DELAY }
|
|
28
28
|
end.freeze
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
# The 8565's sprite sequencers take a multicolor change a pixel
|
|
31
|
+
# sooner than the 6569's.
|
|
32
|
+
WRITE_DELAY_8565 = WRITE_DELAY.dup.tap { |delays| delays[0x1c] = SEQUENCER_DELAY - 1 }.freeze
|
|
33
|
+
|
|
34
|
+
def initialize(registers, bank, width, model: :mos6569)
|
|
31
35
|
@registers = registers
|
|
36
|
+
@write_delay = model == :mos8565 ? WRITE_DELAY_8565 : WRITE_DELAY
|
|
32
37
|
@bank = bank
|
|
33
38
|
@width = width
|
|
34
39
|
@bus = Sprite::InternalBus.new(bank, width / 8)
|
|
35
|
-
@sprites = Array.new(8) { |i| Sprite.new(i, registers, bank,
|
|
40
|
+
@sprites = Array.new(8) { |i| Sprite.new(i, registers, bank, @bus, model:) }
|
|
36
41
|
@collisions = Collisions.new(registers, width)
|
|
37
42
|
@hits = @collisions.hits
|
|
38
43
|
@win_color = Array.new(width, 0)
|
|
@@ -123,7 +128,7 @@ module Badline
|
|
|
123
128
|
# Record a mid-line write at the pixel where it becomes visible.
|
|
124
129
|
def log_change(reg, old, value, beam_x)
|
|
125
130
|
clear_y_expansion(old & ~value, beam_x) if reg == 0x17
|
|
126
|
-
delay =
|
|
131
|
+
delay = @write_delay[reg]
|
|
127
132
|
return unless delay && active?
|
|
128
133
|
|
|
129
134
|
@log.log(beam_x + delay, reg, old, value)
|
data/lib/badline/vic.rb
CHANGED
|
@@ -11,14 +11,22 @@ module Badline
|
|
|
11
11
|
include Addressable
|
|
12
12
|
include IntegerHelper
|
|
13
13
|
|
|
14
|
+
# The 6569 of the breadbin C64, and the HMOS 8565 of the C64C.
|
|
15
|
+
MODELS = %i[mos6569 mos8565].freeze
|
|
16
|
+
|
|
14
17
|
attr_reader :address_bus, :display, :width, :height, :vic_bank, :column,
|
|
15
|
-
:rasterline, :dirty_lines
|
|
18
|
+
:rasterline, :dirty_lines, :model, :region
|
|
16
19
|
|
|
17
20
|
# Returns the byte the CPU's halted read would see, for the c-accesses
|
|
18
21
|
# that run before AEC. Without it they read colour RAM.
|
|
19
22
|
attr_writer :open_bus
|
|
20
23
|
|
|
21
24
|
LIGHTPEN_IRQ = 0x08 # $D019 latch bit
|
|
25
|
+
COLOR_REGISTERS = 0x20..0x2e
|
|
26
|
+
|
|
27
|
+
# What the 8565 shows on the pixel before a color register write takes
|
|
28
|
+
# hold, where the 6569 shows the old color: the grey dot.
|
|
29
|
+
GREY_DOT = 0x0f
|
|
22
30
|
|
|
23
31
|
# A g-access reaches the pixel output this many columns after it runs.
|
|
24
32
|
GRAPHICS_DELAY = 2
|
|
@@ -28,17 +36,19 @@ module Badline
|
|
|
28
36
|
G_IDLE = 1
|
|
29
37
|
G_DISPLAY = 2
|
|
30
38
|
|
|
39
|
+
# The idle g-access in the column a mid-row bad line opens display
|
|
40
|
+
# state reads here on a 6569.
|
|
41
|
+
DMA_DELAY_IDLE_ADDRESS = 0x38ff
|
|
42
|
+
|
|
31
43
|
# The $d011 mode bits a g-access still sees for a cycle after they fall:
|
|
32
44
|
# BMM always, and ECM too when the access it leaves read the character
|
|
33
45
|
# ROM.
|
|
34
46
|
FETCH_HOLD = 0x20
|
|
35
47
|
FETCH_HOLD_ROM = 0x60
|
|
36
48
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
[14, 15, 53, 54, 57, 62].include?(column)
|
|
41
|
-
end.freeze
|
|
49
|
+
# The columns carrying a hook, besides the line's last column, which
|
|
50
|
+
# starts the vertical border for the line after it.
|
|
51
|
+
HOOKS = [14, 15, 53, 54, 57].freeze
|
|
42
52
|
|
|
43
53
|
# BA falls three columns ahead of each sprite's pair of s-accesses, and
|
|
44
54
|
# the five-column windows step two columns apart from sprite 0 at column
|
|
@@ -46,19 +56,29 @@ module Badline
|
|
|
46
56
|
# split: the tail columns fall on the line whose cycle 55/56 compare
|
|
47
57
|
# started the fetch, the head columns on the line after it.
|
|
48
58
|
SPRITE_BA_WINDOWS = Array.new(8) { |n| (54 + (2 * n))..(58 + (2 * n)) }.freeze
|
|
49
|
-
SPRITE_BA_TAIL = SPRITE_BA_WINDOWS.map { |w| w.select { |c| c < 63 } }.freeze
|
|
50
|
-
SPRITE_BA_HEAD = SPRITE_BA_WINDOWS.map { |w| w.filter_map { |c| c - 63 if c >= 63 } }.freeze
|
|
51
59
|
|
|
52
|
-
def initialize(address_bus = nil, debug: false)
|
|
60
|
+
def initialize(address_bus = nil, debug: false, model: :mos6569, region: Region::PAL)
|
|
61
|
+
raise ArgumentError, "unknown VIC-II model #{model}" unless MODELS.include?(model)
|
|
62
|
+
|
|
53
63
|
addressable_at(0xd000, length: 2**10)
|
|
64
|
+
@model = model
|
|
65
|
+
@region = region
|
|
66
|
+
@lightpen_extra = model == :mos8565 ? 1 : 2
|
|
67
|
+
@grey_dots = model == :mos8565
|
|
68
|
+
@delayed_fetch = model == :mos8565
|
|
69
|
+
@dma_delay_idle = model == :mos6569
|
|
54
70
|
@address_bus = address_bus || AddressBus.new
|
|
55
71
|
@vic_bank = VIC::Bank.new(@address_bus)
|
|
56
72
|
@debug = debug
|
|
57
73
|
|
|
58
|
-
@
|
|
59
|
-
@
|
|
60
|
-
@
|
|
74
|
+
@columns_per_line = region.cycles_per_line
|
|
75
|
+
@width = @columns_per_line * 8
|
|
76
|
+
@height = region.lines_per_frame
|
|
77
|
+
@last_column = @columns_per_line - 1
|
|
61
78
|
@last_line = @height - 1
|
|
79
|
+
layout_columns
|
|
80
|
+
@blank_columns = Array.new(@columns_per_line) { |column| Region.blanked?(column, region.hblank) }.freeze
|
|
81
|
+
@blank_lines = Array.new(@height) { |line| Region.blanked?(line, region.vblank) }.freeze
|
|
62
82
|
@display = Array.new(@width * @height, 0)
|
|
63
83
|
@lines = Array.new(@height) { Array.new(@width, 0) }
|
|
64
84
|
@dirty_lines = Array.new(@height, true)
|
|
@@ -75,9 +95,9 @@ module Badline
|
|
|
75
95
|
@registers = VIC::Registers.new
|
|
76
96
|
@register_bytes = @registers.bytes
|
|
77
97
|
@display_state = VIC::DisplayState.new(@registers)
|
|
78
|
-
@sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank)
|
|
98
|
+
@sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank, model: @model, region: @region)
|
|
79
99
|
@sequencer.render = @render
|
|
80
|
-
@sprites = VIC::Sprites.new(@registers, @vic_bank, @width)
|
|
100
|
+
@sprites = VIC::Sprites.new(@registers, @vic_bank, @width, model: @model)
|
|
81
101
|
@display.fill(0)
|
|
82
102
|
@lines.each { |line| line.fill(0) }
|
|
83
103
|
@dirty_lines.fill(true)
|
|
@@ -108,11 +128,11 @@ module Badline
|
|
|
108
128
|
# The g-access runs in the first half of the cycle, ahead of the bad
|
|
109
129
|
# line compare, and the c-access in the second half, after it.
|
|
110
130
|
draw!
|
|
111
|
-
@display_state.cycle(@rasterline, @column)
|
|
131
|
+
dma_delay_idle_access if @display_state.cycle(@rasterline, @column)
|
|
112
132
|
|
|
113
133
|
fetch_character_data! if dma_active?
|
|
114
134
|
|
|
115
|
-
column_hooks if
|
|
135
|
+
column_hooks if @hook_columns[@column]
|
|
116
136
|
|
|
117
137
|
@column += 1
|
|
118
138
|
if @column == @columns_per_line
|
|
@@ -140,7 +160,7 @@ module Badline
|
|
|
140
160
|
when 53 then check_sprite_dma
|
|
141
161
|
when 54 then check_dma_and_toggle_expansion
|
|
142
162
|
when 57 then @sprites.check_display(@rasterline)
|
|
143
|
-
when
|
|
163
|
+
when @last_column then @sequencer.start_vertical_border(@rasterline == @last_line ? 0 : @rasterline + 1)
|
|
144
164
|
end
|
|
145
165
|
end
|
|
146
166
|
|
|
@@ -197,9 +217,10 @@ module Badline
|
|
|
197
217
|
end
|
|
198
218
|
|
|
199
219
|
# The first trigger per frame latches the beam position into LPX/LPY and
|
|
200
|
-
# raises the LP IRQ. The latch happens one cycle after the edge, with
|
|
201
|
-
#
|
|
202
|
-
# without latching unless it lands
|
|
220
|
+
# raises the LP IRQ. The latch happens one cycle after the edge, with
|
|
221
|
+
# the model's extra half-pixels, two on the 6569 and one on the 8565; a
|
|
222
|
+
# trigger on the last line is consumed without latching unless it lands
|
|
223
|
+
# on the line's first cycle.
|
|
203
224
|
def trigger_lightpen
|
|
204
225
|
return if @lp_triggered
|
|
205
226
|
|
|
@@ -213,7 +234,7 @@ module Badline
|
|
|
213
234
|
return if line == @last_line && column.positive?
|
|
214
235
|
|
|
215
236
|
vic_x = ((column * 8) - Sprite::X_OFFSET) % @width
|
|
216
|
-
latch_lightpen((vic_x >> 1) +
|
|
237
|
+
latch_lightpen((vic_x >> 1) + @lightpen_extra, line)
|
|
217
238
|
end
|
|
218
239
|
|
|
219
240
|
# The byte the VIC fetched in the phi1 half of the cycle the CPU is
|
|
@@ -224,17 +245,9 @@ module Badline
|
|
|
224
245
|
vic_bank.peek(phi1_address)
|
|
225
246
|
end
|
|
226
247
|
|
|
227
|
-
def hblank?
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def vblank?
|
|
232
|
-
@rasterline < 16 || @rasterline > 299
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def blanking?
|
|
236
|
-
@rasterline < 16 || @rasterline > 299 || @column < 10 || @column > 60
|
|
237
|
-
end
|
|
248
|
+
def hblank? = @blank_columns[@column]
|
|
249
|
+
def vblank? = @blank_lines[@rasterline]
|
|
250
|
+
def blanking? = @blank_lines[@rasterline] || @blank_columns[@column]
|
|
238
251
|
|
|
239
252
|
def render? = @render
|
|
240
253
|
|
|
@@ -265,19 +278,30 @@ module Badline
|
|
|
265
278
|
# Mid-line writes to the color and sprite registers are logged against
|
|
266
279
|
# the cycle after the write (the CPU runs after the VIC within a machine
|
|
267
280
|
# cycle, so @column already points there); each register adds its own
|
|
268
|
-
# pixel delay on top.
|
|
281
|
+
# pixel delay on top. On the 8565 a color register write that leaves
|
|
282
|
+
# the value as it was still shows its grey dot.
|
|
269
283
|
def log_register_change(reg, value)
|
|
270
284
|
old = @registers[reg]
|
|
271
|
-
return if old == value
|
|
285
|
+
return if old == value && !grey_dot?(reg)
|
|
272
286
|
|
|
273
287
|
if (0x20..0x24).cover?(reg)
|
|
274
288
|
@sequencer.colors_changed! unless reg == 0x20
|
|
275
289
|
@sequencer.color_patches.log(reg, old, value, @column * 8) if @render && !blanking?
|
|
276
290
|
else
|
|
277
|
-
|
|
291
|
+
log_sprite_change(reg, old, value)
|
|
278
292
|
end
|
|
279
293
|
end
|
|
280
294
|
|
|
295
|
+
def grey_dot?(reg) = @grey_dots && COLOR_REGISTERS.cover?(reg)
|
|
296
|
+
|
|
297
|
+
# A sprite color shows its grey dot as a write of light grey on the
|
|
298
|
+
# pixel before the new color.
|
|
299
|
+
def log_sprite_change(reg, old, value)
|
|
300
|
+
beam_x = @column * 8
|
|
301
|
+
@sprites.log_change(reg, old, GREY_DOT, beam_x - 1) if grey_dot?(reg)
|
|
302
|
+
@sprites.log_change(reg, old, value, beam_x)
|
|
303
|
+
end
|
|
304
|
+
|
|
281
305
|
# Bauer cycles 1-10 and 58-63 are sprite p- and s-accesses, 11-15
|
|
282
306
|
# refresh, 16-55 g-accesses and 56-57 idle. @column has already
|
|
283
307
|
# advanced, so it is the Bauer cycle less one.
|
|
@@ -311,11 +335,12 @@ module Badline
|
|
|
311
335
|
graphics_address(@registers[0x11], @display_state.vmli - 1, (@display_state.vc - 1) & 0x3ff)
|
|
312
336
|
end
|
|
313
337
|
|
|
314
|
-
# A $d011/$d012 write is compared in the next column. A write
|
|
315
|
-
# column
|
|
338
|
+
# A $d011/$d012 write is compared in the next column. A write in the
|
|
339
|
+
# line's second last column is left to its last, which compares the
|
|
340
|
+
# next line.
|
|
316
341
|
def compare_raster_writes(reg)
|
|
317
342
|
return unless (0x11..0x12).cover?(reg)
|
|
318
|
-
return if @column == @
|
|
343
|
+
return if @column == @last_column
|
|
319
344
|
|
|
320
345
|
check_raster_irq!
|
|
321
346
|
@sequencer.compare_vertical_border(@rasterline) if reg == 0x11
|
|
@@ -358,11 +383,21 @@ module Badline
|
|
|
358
383
|
8.times do |n|
|
|
359
384
|
next unless @sprites[n].displaying?
|
|
360
385
|
|
|
361
|
-
|
|
362
|
-
|
|
386
|
+
@sprite_ba_tail[n].each { |c| @sprite_ba[c] = true }
|
|
387
|
+
@sprite_ba_head[n].each { |c| @sprite_ba[c] = true }
|
|
363
388
|
end
|
|
364
389
|
end
|
|
365
390
|
|
|
391
|
+
# Splits the sprite BA windows at the end of the line and marks the hook
|
|
392
|
+
# columns, for a line of the region's length. The marks let an ordinary
|
|
393
|
+
# column cost one array read instead of the dispatch.
|
|
394
|
+
def layout_columns
|
|
395
|
+
last = @last_column
|
|
396
|
+
@sprite_ba_tail = SPRITE_BA_WINDOWS.map { |w| w.select { |c| c <= last } }.freeze
|
|
397
|
+
@sprite_ba_head = SPRITE_BA_WINDOWS.map { |w| w.filter_map { |c| c - last - 1 if c > last } }.freeze
|
|
398
|
+
@hook_columns = Array.new(@columns_per_line) { |c| c == last || HOOKS.include?(c) }.freeze
|
|
399
|
+
end
|
|
400
|
+
|
|
366
401
|
# Runs this column's g-access and draws the one from GRAPHICS_DELAY
|
|
367
402
|
# columns earlier. The access reads its byte now, through the mode,
|
|
368
403
|
# $d018 and bank of this cycle. A column with no g-access, or one made
|
|
@@ -403,15 +438,29 @@ module Badline
|
|
|
403
438
|
@g_color[slot] = @g_kept_color = @color_buffer[vmli] || 1
|
|
404
439
|
end
|
|
405
440
|
|
|
406
|
-
#
|
|
407
|
-
#
|
|
408
|
-
#
|
|
409
|
-
#
|
|
441
|
+
# On the 6569 a bad line that opens display state mid-row (DMA delay)
|
|
442
|
+
# moves the idle g-access of the column that triggers it from $3fff (or
|
|
443
|
+
# $39ff) to DMA_DELAY_IDLE_ADDRESS, unless YSCROLL is 0 (vsp-tester,
|
|
444
|
+
# colorfetchbug/main).
|
|
445
|
+
def dma_delay_idle_access
|
|
446
|
+
slot = @g_tick
|
|
447
|
+
return unless @dma_delay_idle && @g_kind[slot] == G_IDLE
|
|
448
|
+
return if @registers.yscroll.zero?
|
|
449
|
+
|
|
450
|
+
@g_data[slot] = vic_bank.peek(DMA_DELAY_IDLE_ADDRESS)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# The 6569's g-access sees a mode bit that falls a cycle late: it
|
|
454
|
+
# addresses with $d011 as this column has it, OR-ed with the held bits
|
|
455
|
+
# the column before had. When BMM changes and the access moves from RAM
|
|
456
|
+
# onto the character ROM, the low address byte still comes from the old
|
|
457
|
+
# mode. The 8565's addresses with $d011 as the column before had it
|
|
410
458
|
# (VICE x64sc `vicii_fetch_graphics`).
|
|
411
459
|
def fetch_graphics(vmli, counter)
|
|
412
460
|
d011 = @register_bytes[0x11]
|
|
413
461
|
last = @fetch_d011
|
|
414
462
|
return vic_bank.peek(graphics_address(d011, vmli, counter)) if d011 == last
|
|
463
|
+
return vic_bank.peek(graphics_address(last, vmli, counter)) if @delayed_fetch
|
|
415
464
|
|
|
416
465
|
from = graphics_address(last, vmli, counter)
|
|
417
466
|
from_rom = vic_bank.character_rom?(from)
|
data/lib/badline.rb
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "badline/version"
|
|
4
|
+
require "badline/options"
|
|
4
5
|
require "badline/integer_helper"
|
|
6
|
+
require "badline/region"
|
|
5
7
|
require "badline/addressable"
|
|
6
8
|
require "badline/memory"
|
|
7
9
|
require "badline/color_memory"
|
|
8
10
|
require "badline/rom"
|
|
11
|
+
require "badline/ram_expansion"
|
|
9
12
|
require "badline/address_bus"
|
|
10
13
|
require "badline/instruction"
|
|
11
14
|
require "badline/instruction_set"
|
|
@@ -18,6 +21,7 @@ require "badline/cycleable"
|
|
|
18
21
|
require "badline/datasette"
|
|
19
22
|
require "badline/time_of_day"
|
|
20
23
|
require "badline/cia"
|
|
24
|
+
require "badline/via"
|
|
21
25
|
require "badline/sid"
|
|
22
26
|
require "badline/debug_register"
|
|
23
27
|
require "badline/interrupts"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The native badline, built from the `rake native:pack` tarball attached to
|
|
4
|
+
# each release. The release workflow copies this file into the tap with the
|
|
5
|
+
# release's url and sha256 filled in.
|
|
6
|
+
class Badline < Formula
|
|
7
|
+
desc "Cycle-accurate Commodore 64 emulator"
|
|
8
|
+
homepage "https://github.com/elektronaut/badline"
|
|
9
|
+
url "https://github.com/elektronaut/badline/releases/download/v0.3.0/badline-0.3.0-spinel-15f037af.tar.gz"
|
|
10
|
+
sha256 "0000000000000000000000000000000000000000000000000000000000000000"
|
|
11
|
+
license "MIT"
|
|
12
|
+
|
|
13
|
+
depends_on "sdl2"
|
|
14
|
+
|
|
15
|
+
uses_from_macos "libxcrypt"
|
|
16
|
+
|
|
17
|
+
def install
|
|
18
|
+
system "make"
|
|
19
|
+
libexec.install "badline"
|
|
20
|
+
pkgshare.install "roms"
|
|
21
|
+
(bin / "badline").write_env_script libexec / "badline",
|
|
22
|
+
BADLINE_ROM_PATH: "${BADLINE_ROM_PATH:-#{pkgshare}/roms}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
test do
|
|
26
|
+
assert_match "badline #{version}", shell_output("#{bin}/badline --version")
|
|
27
|
+
|
|
28
|
+
ENV["SDL_VIDEODRIVER"] = "dummy"
|
|
29
|
+
ENV["SDL_RENDER_DRIVER"] = "software"
|
|
30
|
+
ENV["SDL_AUDIODRIVER"] = "dummy"
|
|
31
|
+
system bin / "badline", "--unpaced", "--no-sound", "--frames", "10", "--screenshot", testpath / "frame.bmp"
|
|
32
|
+
assert_path_exists testpath / "frame.bmp"
|
|
33
|
+
end
|
|
34
|
+
end
|