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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/hooks/session-start.sh +57 -0
  3. data/.claude/settings.json +14 -0
  4. data/CHANGELOG.md +65 -0
  5. data/CLAUDE.md +45 -16
  6. data/CONTRIBUTING.md +95 -3
  7. data/README.md +111 -61
  8. data/SECURITY.md +9 -0
  9. data/doc/pinned-behaviour.md +275 -33
  10. data/exe/badline-ruby +59 -0
  11. data/lib/badline/address_bus.rb +22 -10
  12. data/lib/badline/audio/aiff.rb +8 -3
  13. data/lib/badline/audio/cli.rb +17 -12
  14. data/lib/badline/audio/console.rb +12 -58
  15. data/lib/badline/audio/playback.rb +3 -0
  16. data/lib/badline/audio/sdl_sink.rb +20 -52
  17. data/lib/badline/audio/terminal.rb +90 -0
  18. data/lib/badline/audio.rb +2 -2
  19. data/lib/badline/cartridge/geo_ram.rb +63 -0
  20. data/lib/badline/cartridge.rb +1 -0
  21. data/lib/badline/cia/interrupt_register.rb +34 -10
  22. data/lib/badline/cia.rb +15 -8
  23. data/lib/badline/computer.rb +28 -13
  24. data/lib/badline/gui/application.rb +33 -21
  25. data/lib/badline/gui/gamepads.rb +22 -22
  26. data/lib/badline/gui/joy_map.rb +1 -1
  27. data/lib/badline/gui/key_map.rb +6 -1
  28. data/lib/badline/gui/pane.rb +7 -6
  29. data/lib/badline/gui/screen_pane.rb +9 -13
  30. data/lib/badline/gui/window.rb +35 -20
  31. data/lib/badline/gui.rb +1 -1
  32. data/lib/badline/input/mouse1351.rb +21 -6
  33. data/lib/badline/input/paddles.rb +11 -6
  34. data/lib/badline/kernal_trap/channel.rb +3 -0
  35. data/lib/badline/kernal_trap/drive/block_commands.rb +33 -4
  36. data/lib/badline/kernal_trap/drive/status.rb +3 -0
  37. data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
  38. data/lib/badline/kernal_trap/drive/writes.rb +92 -0
  39. data/lib/badline/kernal_trap/drive.rb +51 -51
  40. data/lib/badline/kernal_trap/file.rb +6 -9
  41. data/lib/badline/kernal_trap/save.rb +9 -4
  42. data/lib/badline/keyboard.rb +19 -3
  43. data/lib/badline/media.rb +42 -11
  44. data/lib/badline/options.rb +209 -0
  45. data/lib/badline/ram_expansion/banking.rb +30 -0
  46. data/lib/badline/ram_expansion/plus256k.rb +38 -0
  47. data/lib/badline/ram_expansion/plus60k.rb +31 -0
  48. data/lib/badline/ram_expansion/unexpanded.rb +22 -0
  49. data/lib/badline/ram_expansion.rb +21 -0
  50. data/lib/badline/region.rb +51 -0
  51. data/lib/badline/sdl/events.rb +47 -0
  52. data/lib/badline/sdl/functions.rb +73 -0
  53. data/lib/badline/sdl.rb +67 -0
  54. data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
  55. data/lib/badline/sid/waveform.rb +1 -1
  56. data/lib/badline/sid.rb +3 -1
  57. data/lib/badline/storage/d64_image.rb +10 -0
  58. data/lib/badline/storage/d71_image.rb +12 -0
  59. data/lib/badline/storage/d81_image.rb +9 -0
  60. data/lib/badline/storage/disk_image/bam.rb +111 -0
  61. data/lib/badline/storage/disk_image/directory.rb +79 -0
  62. data/lib/badline/storage/disk_image/writing.rb +110 -0
  63. data/lib/badline/storage/disk_image.rb +27 -4
  64. data/lib/badline/storage/host_directory.rb +2 -1
  65. data/lib/badline/storage.rb +15 -0
  66. data/lib/badline/time_of_day.rb +4 -2
  67. data/lib/badline/version.rb +1 -1
  68. data/lib/badline/via/control_lines.rb +104 -0
  69. data/lib/badline/via/interrupt_register.rb +58 -0
  70. data/lib/badline/via/shift_register.rb +135 -0
  71. data/lib/badline/via/timer1.rb +82 -0
  72. data/lib/badline/via/timer2.rb +84 -0
  73. data/lib/badline/via.rb +262 -0
  74. data/lib/badline/vic/bank.rb +4 -3
  75. data/lib/badline/vic/color_patches.rb +6 -3
  76. data/lib/badline/vic/display_state.rb +4 -0
  77. data/lib/badline/vic/graphics_shifter.rb +42 -7
  78. data/lib/badline/vic/registers.rb +3 -0
  79. data/lib/badline/vic/sequencer.rb +18 -18
  80. data/lib/badline/vic/sequencer_output.rb +4 -4
  81. data/lib/badline/vic/sprite/internal_bus.rb +4 -0
  82. data/lib/badline/vic/sprite/shifter.rb +25 -3
  83. data/lib/badline/vic/sprite.rb +5 -4
  84. data/lib/badline/vic/sprites.rb +8 -3
  85. data/lib/badline/vic.rb +93 -44
  86. data/lib/badline.rb +4 -0
  87. data/packaging/homebrew/badline.rb +34 -0
  88. metadata +30 -20
  89. data/exe/badline +0 -78
  90. data/exe/badline-sid +0 -34
  91. 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
@@ -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
@@ -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. Character
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.ram.peek(BANK_STARTS[bits] + offset)
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.ram.peek(BANK_STARTS[bank_switch_register] + offset)
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