badline 0.3.0 → 0.4.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/.claude/hooks/session-start.sh +57 -0
- data/.claude/settings.json +14 -0
- data/CHANGELOG.md +65 -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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIA
|
|
5
|
+
# The 6522's shift register, with the CB1 clock and CB2 data lines it
|
|
6
|
+
# takes over in every mode but the first.
|
|
7
|
+
#
|
|
8
|
+
# ACR bits 4-2 pick the mode. Modes 1 to 3 shift in, left, with the new
|
|
9
|
+
# bit taken from CB2 into bit 0 on each rising CB1 edge. Modes 4 to 7
|
|
10
|
+
# shift out: each falling CB1 edge puts bit 7 on CB2 and rotates it back
|
|
11
|
+
# round into bit 0, so after eight bits the byte is back where it
|
|
12
|
+
# started. Modes 1, 4 and 5 clock off timer 2, modes 2 and 6 off φ2, and
|
|
13
|
+
# modes 3 and 7 off whatever drives CB1 from outside.
|
|
14
|
+
#
|
|
15
|
+
# Under its own clock the register drives CB1, idling high. Reading or
|
|
16
|
+
# writing the data register starts a byte: CB1 falls on the first clock
|
|
17
|
+
# tick after the access and rises on the second, and so on, so a byte is
|
|
18
|
+
# eight pulses over sixteen ticks. The interrupt flag is set on the tick
|
|
19
|
+
# that raises CB1 over the eighth bit, and CB1 then stays high until the
|
|
20
|
+
# next access. A tick is a φ2 cycle in modes 2 and 6, which makes a byte
|
|
21
|
+
# sixteen cycles, and a timer 2 low byte underflow in modes 1, 4 and 5,
|
|
22
|
+
# which makes it sixteen underflows. Mode 4 never stops and never
|
|
23
|
+
# interrupts: its counter is disabled, and the byte goes round and round.
|
|
24
|
+
#
|
|
25
|
+
# Under an external clock the register shifts on every CB1 edge, access
|
|
26
|
+
# or not. The access only arms the counter: the flag is set on the eighth
|
|
27
|
+
# rising edge after it, and the counter then waits for the next access.
|
|
28
|
+
class ShiftRegister
|
|
29
|
+
DISABLED = 0
|
|
30
|
+
IN_T2 = 1
|
|
31
|
+
IN_PHI2 = 2
|
|
32
|
+
IN_EXTERNAL = 3
|
|
33
|
+
OUT_FREE_RUNNING = 4
|
|
34
|
+
OUT_T2 = 5
|
|
35
|
+
OUT_PHI2 = 6
|
|
36
|
+
OUT_EXTERNAL = 7
|
|
37
|
+
|
|
38
|
+
BITS = 8
|
|
39
|
+
|
|
40
|
+
attr_reader :mode, :data
|
|
41
|
+
attr_accessor :cb2_input
|
|
42
|
+
|
|
43
|
+
def initialize
|
|
44
|
+
@mode = DISABLED
|
|
45
|
+
@data = 0x00
|
|
46
|
+
# Bits still to count before the flag, zero when the counter is idle.
|
|
47
|
+
@bits = 0
|
|
48
|
+
@clock = true
|
|
49
|
+
@cb2 = true
|
|
50
|
+
# Nothing drives CB2 from outside, so it floats high.
|
|
51
|
+
@cb2_input = true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def mode=(value)
|
|
55
|
+
@mode = value & 0x07
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def data=(value)
|
|
59
|
+
@data = value & 0xff
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Every read or write of the data register (re)starts the count of
|
|
63
|
+
# eight bits.
|
|
64
|
+
def access!
|
|
65
|
+
@bits = BITS
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The CB1 level the register drives, or nil when CB1 is an input.
|
|
69
|
+
def cb1_output
|
|
70
|
+
internal_clock? ? @clock : nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The CB2 level the register drives, or nil when it doesn't.
|
|
74
|
+
def cb2_output
|
|
75
|
+
@mode >= OUT_FREE_RUNNING ? @cb2 : nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Clocked once per φ2 cycle, with whether timer 2's low byte underflowed
|
|
79
|
+
# on it. Returns true on the cycle that sets the interrupt flag.
|
|
80
|
+
def cycle!(t2_low_underflow)
|
|
81
|
+
return false if @bits.zero?
|
|
82
|
+
|
|
83
|
+
case @mode
|
|
84
|
+
when IN_PHI2, OUT_PHI2 then tick
|
|
85
|
+
when IN_T2, OUT_T2, OUT_FREE_RUNNING then t2_low_underflow && tick
|
|
86
|
+
else false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A level change on CB1 from outside. Only the external clock modes
|
|
91
|
+
# listen: shifting in samples CB2 as CB1 rises, and shifting out puts
|
|
92
|
+
# the next bit on CB2 as it falls. Returns true when the edge sets the
|
|
93
|
+
# interrupt flag.
|
|
94
|
+
def cb1_edge!(rising)
|
|
95
|
+
return false unless @mode == IN_EXTERNAL || @mode == OUT_EXTERNAL
|
|
96
|
+
|
|
97
|
+
shift_in if rising && @mode == IN_EXTERNAL
|
|
98
|
+
shift_out if !rising && @mode == OUT_EXTERNAL
|
|
99
|
+
rising && eighth_bit?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def internal_clock?
|
|
105
|
+
@mode != DISABLED && @mode != IN_EXTERNAL && @mode != OUT_EXTERNAL
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# One half period of the register's own clock: a falling edge puts a
|
|
109
|
+
# bit out, and a rising one takes a bit in and counts it.
|
|
110
|
+
def tick
|
|
111
|
+
@clock = !@clock
|
|
112
|
+
shift_out if !@clock && @mode >= OUT_FREE_RUNNING
|
|
113
|
+
shift_in if @clock && @mode < IN_EXTERNAL
|
|
114
|
+
@clock && @mode != OUT_FREE_RUNNING && eighth_bit?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Counts a bit, and says whether it was the eighth since the access.
|
|
118
|
+
def eighth_bit?
|
|
119
|
+
return false if @bits.zero?
|
|
120
|
+
|
|
121
|
+
@bits -= 1
|
|
122
|
+
@bits.zero?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def shift_in
|
|
126
|
+
@data = ((@data << 1) | (@cb2_input ? 1 : 0)) & 0xff
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def shift_out
|
|
130
|
+
@cb2 = @data.anybits?(0x80)
|
|
131
|
+
@data = ((@data << 1) | (@cb2 ? 1 : 0)) & 0xff
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIA
|
|
5
|
+
# Timer 1: a 16-bit counter that decrements on every φ2 cycle, with a
|
|
6
|
+
# latch it reloads from on every timeout and the PB7 output.
|
|
7
|
+
#
|
|
8
|
+
# Writing the high counter byte loads the counter on the cycle after,
|
|
9
|
+
# so a timer loaded with N reads N, N-1, ... 0 and then $FFFF, which is
|
|
10
|
+
# the cycle its interrupt flag sets: N + 1.5 cycles after the write in
|
|
11
|
+
# the datasheet's terms. The counter then reloads from the latch on the
|
|
12
|
+
# next cycle, which makes the period N + 2 cycles. It does so in one-shot
|
|
13
|
+
# mode too, unlike timer 2.
|
|
14
|
+
#
|
|
15
|
+
# Only a write of the high counter byte arms the timer. An armed timeout
|
|
16
|
+
# sets the flag and inverts PB7, and in one-shot mode disarms it, so the
|
|
17
|
+
# first timeout after the load alone does. Free-running keeps it armed,
|
|
18
|
+
# but switching to free-running doesn't arm it: an unarmed timer counts
|
|
19
|
+
# and reloads without touching the flag or PB7. The load pulls PB7 low,
|
|
20
|
+
# and ACR bit 7 turning on drives it high, so a timer loaded before its
|
|
21
|
+
# PB7 output is enabled reads high until the timeout pulls it low.
|
|
22
|
+
class Timer1
|
|
23
|
+
attr_accessor :counter, :latch
|
|
24
|
+
|
|
25
|
+
# The level the timer drives on PB7 when ACR bit 7 hands it the pin.
|
|
26
|
+
attr_reader :pb7
|
|
27
|
+
|
|
28
|
+
# ACR bit 7 turning on hands PB7 to the timer high.
|
|
29
|
+
def pb7_enabled!
|
|
30
|
+
@pb7 = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize
|
|
34
|
+
@counter = @latch = 0xffff
|
|
35
|
+
@reload = @hold = false
|
|
36
|
+
@armed = false
|
|
37
|
+
@pb7 = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def write_latch_low(value)
|
|
41
|
+
@latch = (@latch & 0xff00) | value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def write_latch_high(value)
|
|
45
|
+
@latch = (value << 8) | (@latch & 0xff)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The high counter byte write: load from the latch, hold the count
|
|
49
|
+
# for a cycle, arm the interrupt and pull PB7 low.
|
|
50
|
+
def start!
|
|
51
|
+
@counter = @latch
|
|
52
|
+
@hold = true
|
|
53
|
+
@reload = false
|
|
54
|
+
@armed = true
|
|
55
|
+
@pb7 = false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# One φ2 cycle. Returns true on the cycle that sets the interrupt flag.
|
|
59
|
+
def cycle!(free_run)
|
|
60
|
+
if @hold || @reload
|
|
61
|
+
@counter = @latch if @reload
|
|
62
|
+
@hold = @reload = false
|
|
63
|
+
return false
|
|
64
|
+
end
|
|
65
|
+
@counter = (@counter - 1) & 0xffff
|
|
66
|
+
@counter == 0xffff && timeout(free_run)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# The counter reloads either way. Armed, PB7 inverts and the flag
|
|
72
|
+
# sets, and one-shot mode disarms. Returns whether the flag sets.
|
|
73
|
+
def timeout(free_run)
|
|
74
|
+
interrupt = @armed
|
|
75
|
+
@reload = true
|
|
76
|
+
@pb7 = !@pb7 if @armed
|
|
77
|
+
@armed &&= free_run
|
|
78
|
+
interrupt
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIA
|
|
5
|
+
# Timer 2: a one-shot 16-bit counter that decrements on every φ2 cycle,
|
|
6
|
+
# or on every falling edge of PB6 when ACR bit 5 sets it counting pulses.
|
|
7
|
+
# Only its low byte has a latch.
|
|
8
|
+
#
|
|
9
|
+
# Timed, it follows timer 1's one-shot timing: loaded with N it reads N
|
|
10
|
+
# on the cycle after the write, and sets its flag on the cycle it rolls
|
|
11
|
+
# from 0 to $FFFF. Where timer 1 would reload, it rolls on down without
|
|
12
|
+
# further interrupts until the next load. Counting pulses, the flag sets
|
|
13
|
+
# on the pulse that takes it past zero, the N + 1st: the datasheet's
|
|
14
|
+
# "reaches zero" is the same underflow it means in timed mode.
|
|
15
|
+
#
|
|
16
|
+
# While the shift register clocks off it, the low byte also counts as
|
|
17
|
+
# an 8-bit timer of its own: on each low byte underflow it reloads from
|
|
18
|
+
# the latch on the next cycle, which makes the shift clock's half period
|
|
19
|
+
# N + 2 cycles. The borrow still reaches the high byte.
|
|
20
|
+
#
|
|
21
|
+
# ACR bit 5 reaches the count input a cycle after the write, so the
|
|
22
|
+
# counter takes one more step in the mode it leaves.
|
|
23
|
+
class Timer2
|
|
24
|
+
attr_accessor :counter, :latch_low
|
|
25
|
+
|
|
26
|
+
# ACR bit 5, which picks PB6 pulses over φ2.
|
|
27
|
+
attr_writer :count_pulses
|
|
28
|
+
|
|
29
|
+
# Whether the low byte underflowed on the last cycle.
|
|
30
|
+
attr_reader :low_underflowed
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
@counter = 0xffff
|
|
34
|
+
@latch_low = 0xff
|
|
35
|
+
@hold = false
|
|
36
|
+
@low_reload = false
|
|
37
|
+
@armed = false
|
|
38
|
+
@low_underflowed = false
|
|
39
|
+
@count_pulses = @counting_pulses = false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The high counter byte write: load the counter with it and the low
|
|
43
|
+
# latch, arm the interrupt and hold the count for a cycle.
|
|
44
|
+
def load(high)
|
|
45
|
+
@counter = (high << 8) | @latch_low
|
|
46
|
+
@hold = true
|
|
47
|
+
@low_reload = false
|
|
48
|
+
@armed = true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# One φ2 cycle, with whether the shift register clocks off the low
|
|
52
|
+
# byte. Returns true on the cycle that sets the flag.
|
|
53
|
+
def cycle!(sr_service)
|
|
54
|
+
pulses = @counting_pulses
|
|
55
|
+
@counting_pulses = @count_pulses
|
|
56
|
+
return idle! if pulses || @hold
|
|
57
|
+
|
|
58
|
+
@low_underflowed = @counter.nobits?(0xff)
|
|
59
|
+
@hold = @low_reload = sr_service && @low_underflowed
|
|
60
|
+
decrement
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A falling edge on PB6. Returns true when it sets the flag.
|
|
64
|
+
def pulse! = @counting_pulses && decrement
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# A cycle while counting pulses, or held after a load or a low byte
|
|
69
|
+
# underflow: the counter stands still, but the hold still ends, so
|
|
70
|
+
# none carries over into timed mode.
|
|
71
|
+
def idle!
|
|
72
|
+
@counter = (@counter & 0xff00) | @latch_low if @low_reload
|
|
73
|
+
@hold = @low_reload = @low_underflowed = false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def decrement
|
|
77
|
+
@counter = (@counter - 1) & 0xffff
|
|
78
|
+
interrupt = @armed && @counter == 0xffff
|
|
79
|
+
@armed = false if interrupt
|
|
80
|
+
interrupt
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/badline/via.rb
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/via/control_lines"
|
|
4
|
+
require "badline/via/interrupt_register"
|
|
5
|
+
require "badline/via/shift_register"
|
|
6
|
+
require "badline/via/timer1"
|
|
7
|
+
require "badline/via/timer2"
|
|
8
|
+
|
|
9
|
+
module Badline
|
|
10
|
+
# VIA (Versatile Interface Adapter): the MOS 6522, two ports with
|
|
11
|
+
# handshake lines, two timers and a shift register. The 1541 has two.
|
|
12
|
+
#
|
|
13
|
+
# Sixteen registers repeat across a 1 KB window. The control pins take
|
|
14
|
+
# levels from outside through ca1= and friends, and PB6 takes its pulses
|
|
15
|
+
# for timer 2 through pb6= alone: the peripheral's port B input doesn't
|
|
16
|
+
# feed the counter.
|
|
17
|
+
class VIA
|
|
18
|
+
include Addressable
|
|
19
|
+
|
|
20
|
+
attr_reader :start, :peripheral, :shift_register, :acr, :pcr
|
|
21
|
+
|
|
22
|
+
def timer1 = @t1.counter
|
|
23
|
+
|
|
24
|
+
def timer1_latch = @t1.latch
|
|
25
|
+
|
|
26
|
+
def timer2 = @t2.counter
|
|
27
|
+
|
|
28
|
+
def interrupt_flags = @ifr.read
|
|
29
|
+
|
|
30
|
+
def interrupt_enable = @ifr.read_enable
|
|
31
|
+
|
|
32
|
+
def initialize(start: 0, peripheral: nil)
|
|
33
|
+
addressable_at(start, length: 2**10)
|
|
34
|
+
|
|
35
|
+
@peripheral = peripheral
|
|
36
|
+
@t1 = Timer1.new
|
|
37
|
+
@t2 = Timer2.new
|
|
38
|
+
@shift_register = ShiftRegister.new
|
|
39
|
+
@ifr = InterruptRegister.new
|
|
40
|
+
@ca = ControlLines.new(@ifr, c1_flag: InterruptRegister::CA1, c2_flag: InterruptRegister::CA2,
|
|
41
|
+
handshake_on_read: true)
|
|
42
|
+
@cb = ControlLines.new(@ifr, c1_flag: InterruptRegister::CB1, c2_flag: InterruptRegister::CB2,
|
|
43
|
+
handshake_on_read: false)
|
|
44
|
+
@pb6_high = true
|
|
45
|
+
@acr = 0x00
|
|
46
|
+
reset!
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The RES line clears every register but the timers and the shift
|
|
50
|
+
# register, which carry on counting, and so releases IRQ.
|
|
51
|
+
def reset!
|
|
52
|
+
@ora = @orb = @ddra = @ddrb = 0x00
|
|
53
|
+
@latch_a = @latch_b = 0xff
|
|
54
|
+
@ifr.reset!
|
|
55
|
+
@ca.reset!
|
|
56
|
+
@cb.reset!
|
|
57
|
+
write_acr(0x00)
|
|
58
|
+
@pcr = 0x00
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def irq? = @ifr.irq?
|
|
62
|
+
|
|
63
|
+
def cycle!
|
|
64
|
+
@ca.cycle! if @ca.pulsing?
|
|
65
|
+
@cb.cycle! if @cb.pulsing?
|
|
66
|
+
@ifr.set(InterruptRegister::TIMER1) if @t1.cycle!(@acr.anybits?(0x40))
|
|
67
|
+
@ifr.set(InterruptRegister::TIMER2) if @t2.cycle!(@sr_uses_t2)
|
|
68
|
+
@ifr.set(InterruptRegister::SR) if @shift_register.cycle!(@t2.low_underflowed)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Port A as driven by ORA and DDRA, input lines floating high.
|
|
72
|
+
def port_a_output = driven_lines(@ora, @ddra)
|
|
73
|
+
|
|
74
|
+
# Port B as driven by ORB and DDRB, with PB7 handed to timer 1 by ACR
|
|
75
|
+
# bit 7.
|
|
76
|
+
def port_b_output
|
|
77
|
+
lines = driven_lines(@orb, @ddrb)
|
|
78
|
+
return lines unless @acr.anybits?(0x80)
|
|
79
|
+
|
|
80
|
+
@t1.pb7 ? lines | 0x80 : lines & 0x7f
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# An active CA1 edge latches port A while ACR bit 0 is set.
|
|
84
|
+
def ca1=(high)
|
|
85
|
+
return if high == @ca.c1_high
|
|
86
|
+
|
|
87
|
+
@latch_a = pins_a if @acr.anybits?(0x01) && @ca.active_c1_edge?(high)
|
|
88
|
+
@ca.c1 = high
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def ca2=(high)
|
|
92
|
+
@ca.c2 = high
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# An active CB1 edge latches port B while ACR bit 1 is set. CB1 also
|
|
96
|
+
# clocks the shift register in its external clock modes.
|
|
97
|
+
def cb1=(high)
|
|
98
|
+
return if high == @cb.c1_high
|
|
99
|
+
|
|
100
|
+
@ifr.set(InterruptRegister::SR) if @shift_register.cb1_edge!(high)
|
|
101
|
+
@latch_b = pins_b if @acr.anybits?(0x02) && @cb.active_c1_edge?(high)
|
|
102
|
+
@cb.c1 = high
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def cb2=(high)
|
|
106
|
+
@shift_register.cb2_input = high
|
|
107
|
+
@cb.c2 = high
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# A falling edge counts a pulse while timer 2 counts them.
|
|
111
|
+
def pb6=(high)
|
|
112
|
+
fell = @pb6_high && !high
|
|
113
|
+
@pb6_high = high
|
|
114
|
+
@ifr.set(InterruptRegister::TIMER2) if fell && @t2.pulse!
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def ca2_output = @ca.c2_output
|
|
118
|
+
|
|
119
|
+
# The shift register's clock on CB1, or nil when CB1 is an input.
|
|
120
|
+
def cb1_output = @shift_register.cb1_output
|
|
121
|
+
|
|
122
|
+
# The shift register takes CB2 over in its shift-out modes.
|
|
123
|
+
def cb2_output
|
|
124
|
+
level = @shift_register.cb2_output
|
|
125
|
+
level.nil? ? @cb.c2_output : level
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def peek(addr)
|
|
129
|
+
case offset_of(addr) & 0x0f
|
|
130
|
+
when 0x00 then read_port_b
|
|
131
|
+
when 0x01 then read_port_a(handshake: true)
|
|
132
|
+
when 0x02 then @ddrb
|
|
133
|
+
when 0x03 then @ddra
|
|
134
|
+
when 0x04 then read_timer_low(@t1.counter, InterruptRegister::TIMER1)
|
|
135
|
+
when 0x05 then high_byte(@t1.counter)
|
|
136
|
+
when 0x06 then low_byte(@t1.latch)
|
|
137
|
+
when 0x07 then high_byte(@t1.latch)
|
|
138
|
+
when 0x08 then read_timer_low(@t2.counter, InterruptRegister::TIMER2)
|
|
139
|
+
when 0x09 then high_byte(@t2.counter)
|
|
140
|
+
when 0x0a then read_shift_register
|
|
141
|
+
when 0x0b then @acr
|
|
142
|
+
when 0x0c then @pcr
|
|
143
|
+
when 0x0d then @ifr.read
|
|
144
|
+
when 0x0e then @ifr.read_enable
|
|
145
|
+
else read_port_a(handshake: false)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def poke(addr, value)
|
|
150
|
+
case offset_of(addr) & 0x0f
|
|
151
|
+
when 0x00 then write_port_b(value)
|
|
152
|
+
when 0x01 then write_port_a(value, handshake: true)
|
|
153
|
+
when 0x02 then @ddrb = value
|
|
154
|
+
when 0x03 then @ddra = value
|
|
155
|
+
when 0x04, 0x06 then @t1.write_latch_low(value)
|
|
156
|
+
when 0x05 then write_timer1_high(value, start: true)
|
|
157
|
+
when 0x07 then write_timer1_high(value, start: false)
|
|
158
|
+
when 0x08 then @t2.latch_low = value
|
|
159
|
+
when 0x09 then write_timer2_high(value)
|
|
160
|
+
when 0x0a then write_shift_register(value)
|
|
161
|
+
when 0x0b then write_acr(value)
|
|
162
|
+
when 0x0c then write_pcr(value)
|
|
163
|
+
when 0x0d then @ifr.write(value)
|
|
164
|
+
when 0x0e then @ifr.write_enable(value)
|
|
165
|
+
else write_port_a(value, handshake: false)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
# Output bits are driven from the data register; input bits float high.
|
|
172
|
+
# External peripherals can still pull any line low (wired-AND).
|
|
173
|
+
def driven_lines(register, direction)
|
|
174
|
+
(register & direction) | (~direction & 0xff)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def pins_a
|
|
178
|
+
lines = port_a_output
|
|
179
|
+
peripheral ? lines & peripheral.read_a(lines) : lines
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def pins_b
|
|
183
|
+
lines = port_b_output
|
|
184
|
+
peripheral ? lines & peripheral.read_b(lines) : lines
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Port A reads the pins, or what CA1 latched off them.
|
|
188
|
+
def read_port_a(handshake:)
|
|
189
|
+
value = @acr.anybits?(0x01) ? @latch_a : pins_a
|
|
190
|
+
@ca.access!(false) if handshake
|
|
191
|
+
value
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def write_port_a(value, handshake:)
|
|
195
|
+
@ora = value
|
|
196
|
+
@ca.access!(true) if handshake
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Port B reads its output bits from ORB, or from timer 1 for PB7, and
|
|
200
|
+
# its input bits from the pins, or from what CB1 latched off them.
|
|
201
|
+
def read_port_b
|
|
202
|
+
outputs = @acr.anybits?(0x80) ? @ddrb | 0x80 : @ddrb
|
|
203
|
+
inputs = @acr.anybits?(0x02) ? @latch_b : pins_b
|
|
204
|
+
@cb.access!(false)
|
|
205
|
+
(port_b_output & outputs) | (inputs & ~outputs & 0xff)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def write_port_b(value)
|
|
209
|
+
@orb = value
|
|
210
|
+
@cb.access!(true)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# A read of either timer's low counter byte acknowledges its interrupt.
|
|
214
|
+
def read_timer_low(counter, flag)
|
|
215
|
+
@ifr.clear(flag)
|
|
216
|
+
low_byte(counter)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# $5 loads and starts the counter; $7 only writes the latch. Both
|
|
220
|
+
# acknowledge the interrupt.
|
|
221
|
+
def write_timer1_high(value, start:)
|
|
222
|
+
@t1.write_latch_high(value)
|
|
223
|
+
@t1.start! if start
|
|
224
|
+
@ifr.clear(InterruptRegister::TIMER1)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def write_timer2_high(value)
|
|
228
|
+
@t2.load(value)
|
|
229
|
+
@ifr.clear(InterruptRegister::TIMER2)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def read_shift_register
|
|
233
|
+
value = @shift_register.data
|
|
234
|
+
@shift_register.access!
|
|
235
|
+
@ifr.clear(InterruptRegister::SR)
|
|
236
|
+
value
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def write_shift_register(value)
|
|
240
|
+
@shift_register.data = value
|
|
241
|
+
@shift_register.access!
|
|
242
|
+
@ifr.clear(InterruptRegister::SR)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Bits 4-2 pick the shift register's mode. Modes 1, 4 and 5 clock off
|
|
246
|
+
# timer 2's low byte. Bit 7 turning on hands PB7 to timer 1 high.
|
|
247
|
+
def write_acr(value)
|
|
248
|
+
@t1.pb7_enabled! if value.anybits?(0x80) && @acr.nobits?(0x80)
|
|
249
|
+
@acr = value
|
|
250
|
+
@t2.count_pulses = value.anybits?(0x20)
|
|
251
|
+
mode = (value >> 2) & 0x07
|
|
252
|
+
@shift_register.mode = mode
|
|
253
|
+
@sr_uses_t2 = [1, 4, 5].include?(mode)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def write_pcr(value)
|
|
257
|
+
@pcr = value
|
|
258
|
+
@ca.control = value & 0x0f
|
|
259
|
+
@cb.control = value >> 4
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
data/lib/badline/vic/bank.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
class VIC < Cycleable
|
|
5
|
-
# The 16K window the VIC sees into RAM, selected by CIA2 $DD00
|
|
5
|
+
# The 16K window the VIC sees into RAM, selected by CIA2 $DD00, in the
|
|
6
|
+
# bank a +256K picks for it. Character
|
|
6
7
|
# ROM shadows $1000-$1FFF in banks 0 and 2.
|
|
7
8
|
class Bank
|
|
8
9
|
include Addressable
|
|
@@ -24,7 +25,7 @@ module Badline
|
|
|
24
25
|
if bits.allbits?(0b01) && (offset & 0xf000) == 0x1000
|
|
25
26
|
@address_bus.character_rom.peek(0xc000 + offset)
|
|
26
27
|
else
|
|
27
|
-
@address_bus.
|
|
28
|
+
@address_bus.video_ram.peek(BANK_STARTS[bits] + offset)
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
|
|
@@ -61,7 +62,7 @@ module Badline
|
|
|
61
62
|
if romh && offset.allbits?(0x3000)
|
|
62
63
|
romh.peek(0xe000 + (offset & 0x1fff))
|
|
63
64
|
else
|
|
64
|
-
@address_bus.
|
|
65
|
+
@address_bus.video_ram.peek(BANK_STARTS[bank_switch_register] + offset)
|
|
65
66
|
end
|
|
66
67
|
end
|
|
67
68
|
|
|
@@ -5,17 +5,20 @@ module Badline
|
|
|
5
5
|
# A mid-line color register write becomes visible one pixel into the
|
|
6
6
|
# column emitted the cycle after the write. That emit paints the whole
|
|
7
7
|
# column with the new color, so the boundary pixel is patched back to
|
|
8
|
-
# the old one before the line is finished.
|
|
8
|
+
# the old one before the line is finished. On the 8565 the boundary
|
|
9
|
+
# pixel shows light grey instead, the grey dot, whatever the old and new
|
|
10
|
+
# colors are.
|
|
9
11
|
class ColorPatches
|
|
10
|
-
def initialize(sequencer)
|
|
12
|
+
def initialize(sequencer, grey_dots: false)
|
|
11
13
|
@sequencer = sequencer
|
|
14
|
+
@grey_dots = grey_dots
|
|
12
15
|
@patches = []
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
def clear = @patches.clear
|
|
16
19
|
|
|
17
20
|
def log(reg, old, value, boundary_x)
|
|
18
|
-
@patches << [boundary_x, reg, old & 0x0f, value & 0x0f]
|
|
21
|
+
@patches << [boundary_x, reg, @grey_dots ? GREY_DOT : old & 0x0f, value & 0x0f]
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def apply(colors, fg_mask)
|
|
@@ -75,15 +75,19 @@ module Badline
|
|
|
75
75
|
@in_window = line.between?(FIRST_LINE, LAST_LINE)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Returns true when this compare opened display state.
|
|
78
79
|
def cycle(rasterline, column)
|
|
79
80
|
match = column == WRAP_COLUMN ? wrap_match(rasterline + 1) : line_match
|
|
80
81
|
@matched = match
|
|
82
|
+
opened = false
|
|
81
83
|
if match
|
|
84
|
+
opened = !@display
|
|
82
85
|
@display = true
|
|
83
86
|
@ba = column if @ba.nil? && column >= DMA_FIRST && column <= DMA_LAST
|
|
84
87
|
end
|
|
85
88
|
load_counters(match) if column == LOAD_COLUMN
|
|
86
89
|
check_row_counter(match) if column == RC_COLUMN
|
|
90
|
+
opened
|
|
87
91
|
end
|
|
88
92
|
|
|
89
93
|
# A g-access in display state consumes one buffer cell and one video
|