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
data/lib/badline/storage.rb
CHANGED
|
@@ -6,16 +6,35 @@ require "badline/storage/disk_image"
|
|
|
6
6
|
require "badline/storage/d64_image"
|
|
7
7
|
require "badline/storage/d71_image"
|
|
8
8
|
require "badline/storage/d81_image"
|
|
9
|
+
require "badline/storage/t64"
|
|
10
|
+
require "badline/storage/tap"
|
|
9
11
|
require "badline/storage/crt_file"
|
|
12
|
+
require "badline/storage/sid_file"
|
|
13
|
+
require "badline/storage/song_lengths"
|
|
10
14
|
|
|
11
15
|
module Badline
|
|
12
16
|
module Storage
|
|
17
|
+
FILE_TYPES = { "S" => :seq, "P" => :prg, "U" => :usr }.freeze
|
|
18
|
+
|
|
13
19
|
class << self
|
|
14
20
|
# Folds shifted PETSCII letters to their ASCII equivalents.
|
|
15
21
|
def ascii(bytes)
|
|
16
22
|
bytes.map { |b| b.between?(0xc1, 0xda) ? b - 0x80 : b }.pack("C*")
|
|
17
23
|
end
|
|
18
24
|
|
|
25
|
+
# CBM DOS drive prefix: "0:NAME" selects a drive, "@0:NAME" is
|
|
26
|
+
# save-with-replace
|
|
27
|
+
def strip_drive_prefix(name)
|
|
28
|
+
name.sub(/\A@?\d*:/, "")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# CBM DOS names read "NAME,TYPE,MODE". Returns the bare name and the
|
|
32
|
+
# file type it asks for, or nil when it names none.
|
|
33
|
+
def parse_name(name)
|
|
34
|
+
base, type = strip_drive_prefix(name).split(",", 3)
|
|
35
|
+
[base.to_s, FILE_TYPES[type.to_s.strip[0]&.upcase]]
|
|
36
|
+
end
|
|
37
|
+
|
|
19
38
|
# CBM-style filename pattern: "*" and "?" wildcards, case-insensitive.
|
|
20
39
|
def matcher(name)
|
|
21
40
|
escaped = Regexp.escape(name.downcase)
|
data/lib/badline/time_of_day.rb
CHANGED
|
@@ -2,100 +2,153 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
class TimeOfDay
|
|
5
|
-
include IntegerHelper
|
|
6
|
-
|
|
7
5
|
CLOCK_HZ = 985_248 # PAL only for now.
|
|
6
|
+
MAINS_HZ = 50 # The TOD pin is fed from the AC supply.
|
|
8
7
|
|
|
9
|
-
def initialize(clock_hz: CLOCK_HZ)
|
|
10
|
-
# The accumulator advances
|
|
11
|
-
#
|
|
12
|
-
@
|
|
8
|
+
def initialize(clock_hz: CLOCK_HZ, mains_hz: MAINS_HZ)
|
|
9
|
+
# The accumulator advances mains_hz per cycle, so a TOD pin pulse has
|
|
10
|
+
# arrived when it reaches clock_hz. Integer math keeps it exact.
|
|
11
|
+
@cycles_per_pulse = clock_hz
|
|
12
|
+
@mains_hz = mains_hz
|
|
13
13
|
@accumulator = 0
|
|
14
|
-
@
|
|
15
|
-
@
|
|
14
|
+
@pulses = 0
|
|
15
|
+
@clock = { tenths: 0x00, seconds: 0x00, minutes: 0x00, hours: 0x01 }
|
|
16
|
+
@alarm = { tenths: 0x00, seconds: 0x00, minutes: 0x00, hours: 0x00 }
|
|
16
17
|
@latch = nil
|
|
17
|
-
@stopped =
|
|
18
|
+
@stopped = true
|
|
19
|
+
@alarm_pending = false
|
|
20
|
+
self.fifty_hz = false
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
# CRA bit 7 divides the TOD pin by 5 instead of 6. The divider has to
|
|
24
|
+
# match the pin frequency to keep time, so PAL software selects 50 Hz.
|
|
25
|
+
def fifty_hz=(enabled)
|
|
26
|
+
@match = enabled ? 4 : 5
|
|
27
|
+
end
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
def cycle!
|
|
30
|
+
@accumulator += @mains_hz
|
|
31
|
+
pulse! if @accumulator >= @cycles_per_pulse
|
|
32
|
+
return unless @alarm_pending
|
|
25
33
|
|
|
26
|
-
@
|
|
27
|
-
|
|
28
|
-
yield if block_given? && alarm?
|
|
34
|
+
@alarm_pending = false
|
|
35
|
+
yield if block_given?
|
|
29
36
|
end
|
|
30
37
|
|
|
31
38
|
def tenths
|
|
32
39
|
value = (@latch || @clock)[:tenths]
|
|
33
40
|
@latch = nil
|
|
34
|
-
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def seconds
|
|
38
|
-
bcd((@latch || @clock)[:seconds])
|
|
41
|
+
value
|
|
39
42
|
end
|
|
40
43
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
end
|
|
44
|
+
def seconds = (@latch || @clock)[:seconds]
|
|
45
|
+
def minutes = (@latch || @clock)[:minutes]
|
|
44
46
|
|
|
45
47
|
def hours
|
|
46
48
|
@latch ||= @clock.dup
|
|
47
|
-
|
|
49
|
+
@latch[:hours]
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
def write(field, value, alarm:)
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
|
|
53
|
+
# The frequency counter is held clear while the clock is stopped, and
|
|
54
|
+
# starts counting again from the write to tenths that restarts it.
|
|
55
|
+
if field == :tenths && !alarm && @stopped
|
|
56
|
+
@pulses = 0
|
|
57
|
+
@stopped = false
|
|
58
|
+
end
|
|
59
|
+
store(field, value & (field == :tenths ? 0x0f : 0x7f), alarm: alarm)
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
# 12-hour BCD value, bit 7 is the AM/PM flag.
|
|
57
63
|
def write_hours(value, alarm:)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
value &= 0x9f
|
|
65
|
+
# Writing 12 to the hour register flips the AM/PM bit, the same way a
|
|
66
|
+
# carry into 12 does. Writing the alarm hours does not.
|
|
67
|
+
value ^= 0x80 if !alarm && (value & 0x1f) == 0x12
|
|
61
68
|
# Halt the clock when writing hours, so that it doesn't
|
|
62
69
|
# advance mid-update.
|
|
63
70
|
@stopped = true unless alarm
|
|
71
|
+
store(:hours, value, alarm: alarm)
|
|
64
72
|
end
|
|
65
73
|
|
|
66
74
|
private
|
|
67
75
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
def store(field, value, alarm:)
|
|
77
|
+
target = alarm ? @alarm : @clock
|
|
78
|
+
return if target[field] == value
|
|
79
|
+
|
|
80
|
+
target[field] = value
|
|
81
|
+
@alarm_pending = true if alarm?
|
|
71
82
|
end
|
|
72
83
|
|
|
73
|
-
def
|
|
74
|
-
@
|
|
75
|
-
return if @
|
|
84
|
+
def pulse!
|
|
85
|
+
@accumulator -= @cycles_per_pulse
|
|
86
|
+
return if @stopped
|
|
87
|
+
|
|
88
|
+
# A three-bit ring counter with six states, compared against the
|
|
89
|
+
# divider on each pin pulse rather than on its way past it.
|
|
90
|
+
if @pulses == @match
|
|
91
|
+
@pulses = 0
|
|
92
|
+
advance
|
|
93
|
+
else
|
|
94
|
+
@pulses += 1
|
|
95
|
+
@pulses = 0 if @pulses > 5
|
|
96
|
+
end
|
|
97
|
+
end
|
|
76
98
|
|
|
77
|
-
|
|
78
|
-
@clock[:
|
|
79
|
-
|
|
99
|
+
def advance
|
|
100
|
+
tenths = (@clock[:tenths] + 1) & 0x0f
|
|
101
|
+
@clock[:tenths] = tenths == 10 ? 0 : tenths
|
|
102
|
+
carry_seconds if tenths == 10
|
|
103
|
+
@alarm_pending = true if alarm?
|
|
104
|
+
end
|
|
80
105
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return
|
|
106
|
+
def carry_seconds
|
|
107
|
+
return unless advance_pair(:seconds)
|
|
108
|
+
return unless advance_pair(:minutes)
|
|
84
109
|
|
|
85
|
-
|
|
86
|
-
advance_hour
|
|
110
|
+
advance_hours
|
|
87
111
|
end
|
|
88
112
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
113
|
+
# Seconds and minutes hold a four-bit low digit that carries at ten into
|
|
114
|
+
# a three-bit high digit that carries out at six. Both are plain binary
|
|
115
|
+
# counters, so out-of-range BCD counts up from where it was written.
|
|
116
|
+
def advance_pair(field)
|
|
117
|
+
low = ((@clock[field] & 0x0f) + 1) & 0x0f
|
|
118
|
+
high = (@clock[field] >> 4) & 0x07
|
|
119
|
+
carry = false
|
|
120
|
+
|
|
121
|
+
if low == 10
|
|
122
|
+
low = 0
|
|
123
|
+
high = (high + 1) & 0x07
|
|
124
|
+
if high == 6
|
|
125
|
+
high = 0
|
|
126
|
+
carry = true
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
@clock[field] = (high << 4) | low
|
|
131
|
+
carry
|
|
93
132
|
end
|
|
94
133
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
134
|
+
# The hour runs 9 -> 10 and 12 -> 1 by swapping its two digits, and the
|
|
135
|
+
# AM/PM bit flips as it passes through 12.
|
|
136
|
+
def advance_hours
|
|
137
|
+
pm = @clock[:hours] & 0x80
|
|
138
|
+
high = (@clock[:hours] >> 4) & 0x01
|
|
139
|
+
low = @clock[:hours] & 0x0f
|
|
140
|
+
|
|
141
|
+
if (high == 1 && low == 2) || (high.zero? && low == 9)
|
|
142
|
+
low = high
|
|
143
|
+
high ^= 1
|
|
144
|
+
else
|
|
145
|
+
low = (low + 1) & 0x0f
|
|
146
|
+
pm ^= 0x80 if high == 1 && low == 2
|
|
98
147
|
end
|
|
148
|
+
|
|
149
|
+
@clock[:hours] = pm | (high << 4) | low
|
|
99
150
|
end
|
|
151
|
+
|
|
152
|
+
def alarm? = @clock == @alarm
|
|
100
153
|
end
|
|
101
154
|
end
|
data/lib/badline/version.rb
CHANGED
data/lib/badline/vic/bank.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Badline
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def peek(offset)
|
|
21
|
-
return ultimax_peek(offset) if @address_bus.
|
|
21
|
+
return ultimax_peek(offset) if @address_bus.phi1_ultimax
|
|
22
22
|
|
|
23
23
|
bits = bank_switch_register
|
|
24
24
|
if bits.allbits?(0b01) && (offset & 0xf000) == 0x1000
|
|
@@ -28,14 +28,26 @@ module Badline
|
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# A fetch in the second half of the cycle, the c-access, sees the
|
|
32
|
+
# memory configuration the CPU does in that half. No cartridge drives
|
|
33
|
+
# Ultimax mode in the first half alone, so otherwise it reads as #peek.
|
|
34
|
+
def peek_phi2(offset)
|
|
35
|
+
@address_bus.ultimax ? ultimax_peek(offset) : peek(offset)
|
|
36
|
+
end
|
|
37
|
+
|
|
31
38
|
def peek_color(offset)
|
|
32
|
-
address_bus.color_ram.
|
|
39
|
+
address_bus.color_ram.nibble(0xd800 + offset)
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
def poke(_addr, _value)
|
|
36
43
|
raise ReadOnlyMemoryError
|
|
37
44
|
end
|
|
38
45
|
|
|
46
|
+
# True if the VIC reads the character ROM at this offset.
|
|
47
|
+
def character_rom?(offset)
|
|
48
|
+
!@address_bus.phi1_ultimax && bank_switch_register.allbits?(0b01) && (offset & 0xf000) == 0x1000
|
|
49
|
+
end
|
|
50
|
+
|
|
39
51
|
def start
|
|
40
52
|
BANK_STARTS[bank_switch_register]
|
|
41
53
|
end
|
|
@@ -56,10 +68,6 @@ module Badline
|
|
|
56
68
|
def bank_switch_register
|
|
57
69
|
@address_bus.cia2.port_a_lines & 0b11
|
|
58
70
|
end
|
|
59
|
-
|
|
60
|
-
def character_rom?
|
|
61
|
-
bank_switch_register.allbits?(0b01)
|
|
62
|
-
end
|
|
63
71
|
end
|
|
64
72
|
end
|
|
65
73
|
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIC < Cycleable
|
|
5
|
+
# Border coverage per 8-pixel group, tracked as the sequencer outputs
|
|
6
|
+
# a line.
|
|
7
|
+
#
|
|
8
|
+
# The border is repainted over composited sprites from a snapshot of
|
|
9
|
+
# the finished line, which keeps mid-line border color splits intact.
|
|
10
|
+
class BorderMask
|
|
11
|
+
FULL = 0
|
|
12
|
+
NONE = 1
|
|
13
|
+
MIXED = 2
|
|
14
|
+
|
|
15
|
+
# The group table and per-pixel mask are exposed so the sequencer's
|
|
16
|
+
# pixel loops can write them without a method call per column.
|
|
17
|
+
attr_reader :groups, :mask
|
|
18
|
+
|
|
19
|
+
def initialize(width)
|
|
20
|
+
@width = width
|
|
21
|
+
@groups = Array.new(width / 8, FULL)
|
|
22
|
+
@mask = Array.new(width, true)
|
|
23
|
+
@snapshot = Array.new(width, 0)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reset
|
|
27
|
+
@groups.fill(FULL)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def at?(pixel_x)
|
|
31
|
+
case @groups[pixel_x >> 3]
|
|
32
|
+
when FULL then true
|
|
33
|
+
when MIXED then @mask[pixel_x]
|
|
34
|
+
else false
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def snapshot(colors)
|
|
39
|
+
@snapshot[0, @width] = colors
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def restore(colors)
|
|
43
|
+
group = 0
|
|
44
|
+
while group < @groups.length
|
|
45
|
+
case @groups[group]
|
|
46
|
+
when FULL then group = restore_full(colors, group)
|
|
47
|
+
when MIXED then restore_mixed(colors, group * 8)
|
|
48
|
+
end
|
|
49
|
+
group += 1
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Copies a run of full-border groups in one splice and returns its
|
|
56
|
+
# last group.
|
|
57
|
+
def restore_full(colors, first)
|
|
58
|
+
last = first
|
|
59
|
+
last += 1 while @groups[last + 1] == FULL
|
|
60
|
+
x_pos = first * 8
|
|
61
|
+
length = (last - first + 1) * 8
|
|
62
|
+
colors[x_pos, length] = @snapshot[x_pos, length]
|
|
63
|
+
last
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def restore_mixed(colors, x_pos)
|
|
67
|
+
i = 0
|
|
68
|
+
while i < 8
|
|
69
|
+
x = x_pos + i
|
|
70
|
+
colors[x] = @snapshot[x] if @mask[x]
|
|
71
|
+
i += 1
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIC < Cycleable
|
|
5
|
+
# = Sprite collisions
|
|
6
|
+
#
|
|
7
|
+
# The $D01E/$D01F latches and the per-pixel sprite coverage that feeds
|
|
8
|
+
# them. Collisions latch as the beam passes each covered pixel, so a
|
|
9
|
+
# read mid-line sees the pixels drawn before the reading cycle and none
|
|
10
|
+
# of the ones after it.
|
|
11
|
+
class Collisions
|
|
12
|
+
# $D019 latch bits raised by the collision registers.
|
|
13
|
+
SPRITE_COLLISION_IRQ = 0x04 # IMMC, mirrors $D01E
|
|
14
|
+
DATA_COLLISION_IRQ = 0x02 # IMBC, mirrors $D01F
|
|
15
|
+
|
|
16
|
+
# A read holds the register's reset asserted past the cycle that
|
|
17
|
+
# issued it, so the pixels drawn under it never reach the register.
|
|
18
|
+
# The read still reports only the pixels drawn before its own cycle,
|
|
19
|
+
# which is why the two numbers do not cancel out.
|
|
20
|
+
CLEAR_OVERRUN = 12
|
|
21
|
+
|
|
22
|
+
# The pixel the last fold reached, so the sprites know whether a read
|
|
23
|
+
# has anything left to latch.
|
|
24
|
+
attr_reader :folded
|
|
25
|
+
|
|
26
|
+
# One bitmask per pixel of the line, written straight by the sprite
|
|
27
|
+
# merge and read back here and by the composite.
|
|
28
|
+
attr_reader :hits
|
|
29
|
+
|
|
30
|
+
def initialize(registers, width)
|
|
31
|
+
@registers = registers
|
|
32
|
+
@width = width
|
|
33
|
+
@hits = Array.new(width, 0)
|
|
34
|
+
@folded = 0
|
|
35
|
+
@cleared_sprite = 0
|
|
36
|
+
@cleared_data = 0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def start_line
|
|
40
|
+
@folded = 0
|
|
41
|
+
@cleared_sprite = carry(@cleared_sprite)
|
|
42
|
+
@cleared_data = carry(@cleared_data)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Latch every covered pixel between the last fold and the beam. The
|
|
46
|
+
# sprites' touched span bounds the scan; the coverage is zero outside
|
|
47
|
+
# it either way.
|
|
48
|
+
def fold(mask, upto, low, high)
|
|
49
|
+
from = [@folded, low].max
|
|
50
|
+
@folded = upto
|
|
51
|
+
scan(mask, from, [upto, high].min)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A read resets its register for the pixels that follow the reading
|
|
55
|
+
# cycle as well as for everything before it.
|
|
56
|
+
def clear(reg, beam_x)
|
|
57
|
+
if reg == 0x1e
|
|
58
|
+
@cleared_sprite = beam_x + CLEAR_OVERRUN
|
|
59
|
+
else
|
|
60
|
+
@cleared_data = beam_x + CLEAR_OVERRUN
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Two or more sprites on a pixel collide with each other; any sprite
|
|
67
|
+
# over a foreground pixel collides with the graphics. Pixels still
|
|
68
|
+
# under a read's reset are dropped rather than latched.
|
|
69
|
+
def scan(mask, from, upto)
|
|
70
|
+
sprite_clash = 0
|
|
71
|
+
data_clash = 0
|
|
72
|
+
pos = from
|
|
73
|
+
while pos < upto
|
|
74
|
+
bits = @hits[pos]
|
|
75
|
+
unless bits.zero?
|
|
76
|
+
sprite_clash |= bits if pos >= @cleared_sprite && bits.anybits?(bits - 1)
|
|
77
|
+
data_clash |= bits if pos >= @cleared_data && mask[pos]
|
|
78
|
+
end
|
|
79
|
+
pos += 1
|
|
80
|
+
end
|
|
81
|
+
latch(sprite_clash, data_clash)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def latch(sprite_clash, data_clash)
|
|
85
|
+
latch_one(0x1e, sprite_clash, SPRITE_COLLISION_IRQ)
|
|
86
|
+
latch_one(0x1f, data_clash, DATA_COLLISION_IRQ)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The IRQ is raised on the edge out of an empty register, so a
|
|
90
|
+
# collision folded on top of an unread one raises nothing.
|
|
91
|
+
def latch_one(reg, clash, irq)
|
|
92
|
+
return if clash.zero?
|
|
93
|
+
return unless @registers.collide!(reg, clash)
|
|
94
|
+
|
|
95
|
+
@registers.latch_irq!(irq)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# A reset still asserted at the line wrap carries into the new line.
|
|
99
|
+
def carry(cleared)
|
|
100
|
+
cleared > @width ? cleared - @width : 0
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIC < Cycleable
|
|
5
|
+
# A mid-line color register write becomes visible one pixel into the
|
|
6
|
+
# column emitted the cycle after the write. That emit paints the whole
|
|
7
|
+
# column with the new color, so the boundary pixel is patched back to
|
|
8
|
+
# the old one before the line is finished.
|
|
9
|
+
class ColorPatches
|
|
10
|
+
def initialize(sequencer)
|
|
11
|
+
@sequencer = sequencer
|
|
12
|
+
@patches = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def clear = @patches.clear
|
|
16
|
+
|
|
17
|
+
def log(reg, old, value, boundary_x)
|
|
18
|
+
@patches << [boundary_x, reg, old & 0x0f, value & 0x0f]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def apply(colors, fg_mask)
|
|
22
|
+
@patches.each do |(x, reg, old, value)|
|
|
23
|
+
border = @sequencer.border_at?(x)
|
|
24
|
+
if reg == 0x20
|
|
25
|
+
colors[x] = old if border
|
|
26
|
+
elsif !border && !fg_mask[x] && colors[x] == value
|
|
27
|
+
colors[x] = old
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -2,76 +2,139 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
class VIC < Cycleable
|
|
5
|
+
# = VIC-II display state
|
|
6
|
+
#
|
|
7
|
+
# The bad line condition, the video counters it drives, and the DMA it
|
|
8
|
+
# starts. A condition that arrives mid-line opens the row late, which is
|
|
9
|
+
# what makes the screen scroll sideways or lose a column of cells.
|
|
5
10
|
class DisplayState
|
|
6
11
|
FIRST_LINE = 0x30 # 48: bad lines and the DEN-at-$30 latch begin here
|
|
7
12
|
LAST_LINE = 0xf7 # 247
|
|
8
13
|
COLUMNS_PER_ROW = 40
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
# The last column of a rasterline already compares against the line
|
|
16
|
+
# about to start, the same wrap the raster IRQ latch uses.
|
|
17
|
+
WRAP_COLUMN = 62
|
|
18
|
+
|
|
19
|
+
# AEC follows BA three cycles later; the c-accesses before it read
|
|
20
|
+
# the bus the CPU still drives.
|
|
21
|
+
BA_DELAY = 3
|
|
22
|
+
|
|
23
|
+
# Columns run two ahead of Bauer's cycle numbers: column 10 is his
|
|
24
|
+
# cycle 12. BA falls on a match in columns 10-52, the c-accesses run
|
|
25
|
+
# in 13-52 and the g-accesses in 14-53. VC and VMLI reload in column
|
|
26
|
+
# 12 and the row counter steps in column 56.
|
|
27
|
+
DMA_FIRST = 10
|
|
28
|
+
DMA_LAST = 52
|
|
29
|
+
FETCH_FIRST = 13
|
|
30
|
+
GRAPHICS_FIRST = 14
|
|
31
|
+
GRAPHICS_LAST = 53
|
|
32
|
+
LOAD_COLUMN = 12
|
|
33
|
+
RC_COLUMN = 56
|
|
34
|
+
|
|
35
|
+
GRAPHICS_COLUMNS = Array.new(63) { |column| column.between?(GRAPHICS_FIRST, GRAPHICS_LAST) }.freeze
|
|
36
|
+
|
|
37
|
+
attr_reader :vc_base, :vc, :vmli, :rc
|
|
11
38
|
|
|
12
39
|
def initialize(registers)
|
|
13
40
|
@registers = registers
|
|
14
41
|
@vc_base = 0
|
|
42
|
+
@vc = 0
|
|
43
|
+
@vmli = 0
|
|
15
44
|
@rc = 0
|
|
16
45
|
@display = false
|
|
17
46
|
@bad_lines_enabled = false
|
|
18
|
-
@
|
|
47
|
+
@matched = false
|
|
48
|
+
@ba = nil
|
|
49
|
+
new_line(0)
|
|
19
50
|
end
|
|
20
51
|
|
|
21
52
|
def display? = @display
|
|
22
53
|
def idle? = !@display
|
|
23
|
-
def bad_line? =
|
|
54
|
+
def bad_line? = !@ba.nil?
|
|
55
|
+
|
|
56
|
+
# The bad line condition as the last column compared it.
|
|
57
|
+
def bad_line_condition? = @matched
|
|
58
|
+
|
|
59
|
+
# True once AEC has followed BA down and the VIC owns the bus.
|
|
60
|
+
def bus_taken?(column) = @ba ? column >= @ba + BA_DELAY : false
|
|
61
|
+
|
|
62
|
+
def fetching?(column)
|
|
63
|
+
@matched && column >= FETCH_FIRST && column <= DMA_LAST
|
|
64
|
+
end
|
|
24
65
|
|
|
25
66
|
def new_frame
|
|
26
67
|
@vc_base = 0
|
|
27
68
|
@bad_lines_enabled = false
|
|
28
69
|
end
|
|
29
70
|
|
|
30
|
-
def new_line
|
|
31
|
-
@
|
|
71
|
+
def new_line(line)
|
|
72
|
+
@ba = nil
|
|
73
|
+
@line_bits = line & 0b111
|
|
74
|
+
@den_line = line == FIRST_LINE
|
|
75
|
+
@in_window = line.between?(FIRST_LINE, LAST_LINE)
|
|
32
76
|
end
|
|
33
77
|
|
|
34
78
|
def cycle(rasterline, column)
|
|
35
|
-
|
|
79
|
+
match = column == WRAP_COLUMN ? wrap_match(rasterline + 1) : line_match
|
|
80
|
+
@matched = match
|
|
81
|
+
if match
|
|
82
|
+
@display = true
|
|
83
|
+
@ba = column if @ba.nil? && column >= DMA_FIRST && column <= DMA_LAST
|
|
84
|
+
end
|
|
85
|
+
load_counters(match) if column == LOAD_COLUMN
|
|
86
|
+
check_row_counter(match) if column == RC_COLUMN
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A g-access in display state consumes one buffer cell and one video
|
|
90
|
+
# matrix address. A row that opens mid-line makes fewer of them, and
|
|
91
|
+
# the shortfall carries into VCBASE.
|
|
92
|
+
def graphics_column?(column) = GRAPHICS_COLUMNS[column]
|
|
93
|
+
|
|
94
|
+
def graphics_access(column)
|
|
95
|
+
return unless graphics_column?(column)
|
|
36
96
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
check_row_counter if column == 58
|
|
97
|
+
@vc = (@vc + 1) & 0x3ff
|
|
98
|
+
@vmli += 1
|
|
40
99
|
end
|
|
41
100
|
|
|
42
101
|
private
|
|
43
102
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@bad_line = true
|
|
48
|
-
@display = true
|
|
49
|
-
@rc = 0
|
|
103
|
+
def line_match
|
|
104
|
+
latch_den if @den_line
|
|
105
|
+
@bad_lines_enabled && @in_window && @line_bits == @registers.yscroll
|
|
50
106
|
end
|
|
51
107
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
108
|
+
# The wrap column compares against the line about to start. The DEN
|
|
109
|
+
# latch is level-sensitive across the counter's increment, so this one
|
|
110
|
+
# column sees both the line ending and the line starting.
|
|
111
|
+
def wrap_match(line)
|
|
112
|
+
latch_den if @den_line || line == FIRST_LINE
|
|
113
|
+
return false unless line.between?(FIRST_LINE, LAST_LINE)
|
|
55
114
|
|
|
56
|
-
@
|
|
57
|
-
@display = true
|
|
115
|
+
@bad_lines_enabled && (line & 0b111) == @registers.yscroll
|
|
58
116
|
end
|
|
59
117
|
|
|
60
|
-
def
|
|
61
|
-
@bad_lines_enabled
|
|
62
|
-
rasterline.between?(FIRST_LINE, LAST_LINE) &&
|
|
63
|
-
(rasterline & 0b111) == @registers.yscroll
|
|
118
|
+
def latch_den
|
|
119
|
+
@bad_lines_enabled = true if @registers.display_enabled?
|
|
64
120
|
end
|
|
65
121
|
|
|
66
|
-
def
|
|
67
|
-
|
|
122
|
+
def load_counters(match)
|
|
123
|
+
@vc = @vc_base
|
|
124
|
+
@vmli = 0
|
|
125
|
+
@rc = 0 if match
|
|
126
|
+
end
|
|
68
127
|
|
|
128
|
+
# A condition still standing when the row counter wraps puts the logic
|
|
129
|
+
# straight back into display state, so a forced row rolls into the
|
|
130
|
+
# next one instead of leaving the line idle (Bauer 3.7.2 step 5).
|
|
131
|
+
def check_row_counter(match)
|
|
69
132
|
if @rc == 7
|
|
70
133
|
@display = false
|
|
71
|
-
@vc_base =
|
|
72
|
-
else
|
|
73
|
-
@rc = (@rc + 1) & 0b111
|
|
134
|
+
@vc_base = @vc
|
|
74
135
|
end
|
|
136
|
+
@display ||= match
|
|
137
|
+
@rc = (@rc + 1) & 0b111 if @display
|
|
75
138
|
end
|
|
76
139
|
end
|
|
77
140
|
end
|