badline 0.14.0 → 0.15.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/CLAUDE.md +15 -5
  4. data/README.md +68 -8
  5. data/doc/pinned-behaviour.md +145 -0
  6. data/doc/snapshot-format.md +188 -38
  7. data/doc/snapshots.md +18 -1
  8. data/lib/badline/audio/bare_player.rb +4 -8
  9. data/lib/badline/audio/machine_player.rb +2 -12
  10. data/lib/badline/c128/banks.rb +1 -3
  11. data/lib/badline/c128/bus.rb +19 -1
  12. data/lib/badline/c128/mmu_pages.rb +7 -0
  13. data/lib/badline/c64/bus/extra_sids.rb +80 -0
  14. data/lib/badline/c64/bus.rb +1 -0
  15. data/lib/badline/c64/cartridge/action_replay.rb +6 -2
  16. data/lib/badline/c64/cartridge/comal80.rb +1 -0
  17. data/lib/badline/c64/cartridge/dinamic.rb +3 -1
  18. data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
  19. data/lib/badline/c64/cartridge/fun_play.rb +2 -1
  20. data/lib/badline/c64/cartridge/game_system.rb +2 -1
  21. data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
  22. data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
  23. data/lib/badline/c64/cartridge/ocean.rb +1 -0
  24. data/lib/badline/c64/cartridge/pagefox.rb +1 -0
  25. data/lib/badline/c64/cartridge/rgcd.rb +2 -1
  26. data/lib/badline/c64/cartridge/super_games.rb +3 -2
  27. data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
  28. data/lib/badline/c64/cartridge.rb +3 -3
  29. data/lib/badline/c64/keyed_modules.rb +6 -2
  30. data/lib/badline/chips/ted/beam.rb +13 -14
  31. data/lib/badline/chips/ted/fetches.rb +199 -0
  32. data/lib/badline/chips/ted/keyed_state.rb +156 -0
  33. data/lib/badline/chips/ted/modes.rb +134 -0
  34. data/lib/badline/chips/ted/palette.rb +43 -0
  35. data/lib/badline/chips/ted/picture.rb +182 -0
  36. data/lib/badline/chips/ted/sound/output.rb +86 -0
  37. data/lib/badline/chips/ted/sound.rb +284 -0
  38. data/lib/badline/chips/ted.rb +53 -6
  39. data/lib/badline/chips/tia.rb +12 -0
  40. data/lib/badline/drives/datasette/keyed_state.rb +7 -2
  41. data/lib/badline/drives/datasette.rb +22 -5
  42. data/lib/badline/frontend/boot.rb +3 -1
  43. data/lib/badline/frontend/controls.rb +7 -3
  44. data/lib/badline/frontend/keys.rb +14 -2
  45. data/lib/badline/frontend/media_slots.rb +11 -1
  46. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  47. data/lib/badline/frontend/menu/menu_media.rb +13 -3
  48. data/lib/badline/frontend/menu/menu_pages.rb +8 -8
  49. data/lib/badline/frontend/menu/menu_ports.rb +13 -1
  50. data/lib/badline/frontend/screen.rb +33 -12
  51. data/lib/badline/frontend/screens.rb +2 -1
  52. data/lib/badline/frontend/sound.rb +4 -4
  53. data/lib/badline/frontend/timeline.rb +19 -6
  54. data/lib/badline/machine/settling.rb +1 -1
  55. data/lib/badline/machine.rb +26 -6
  56. data/lib/badline/media/plus4_media.rb +68 -0
  57. data/lib/badline/media/program.rb +20 -6
  58. data/lib/badline/media.rb +33 -12
  59. data/lib/badline/options/family.rb +1 -0
  60. data/lib/badline/options/help.rb +30 -6
  61. data/lib/badline/options/table.rb +15 -2
  62. data/lib/badline/options/validation.rb +26 -4
  63. data/lib/badline/options.rb +37 -9
  64. data/lib/badline/plus4/bus.rb +77 -14
  65. data/lib/badline/plus4/cassette_sense.rb +21 -0
  66. data/lib/badline/plus4/keyboard_port.rb +35 -10
  67. data/lib/badline/plus4/keyed_modules.rb +99 -0
  68. data/lib/badline/plus4/keyed_parts.rb +143 -0
  69. data/lib/badline/plus4/keyed_state.rb +41 -0
  70. data/lib/badline/plus4.rb +92 -16
  71. data/lib/badline/snapshot/container.rb +2 -0
  72. data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
  73. data/lib/badline/snapshot/field_doc.rb +46 -7
  74. data/lib/badline/snapshot/image.rb +12 -3
  75. data/lib/badline/snapshot/plus4_setup.rb +39 -0
  76. data/lib/badline/snapshot.rb +9 -4
  77. data/lib/badline/storage/g64_image.rb +1 -1
  78. data/lib/badline/storage/tap.rb +21 -12
  79. data/lib/badline/support/checkpoint.rb +3 -2
  80. data/lib/badline/support/plus4_checkpoint.rb +42 -0
  81. data/lib/badline/version.rb +1 -1
  82. metadata +15 -1
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class TED
6
+ # The picture: a palette index a pixel (Palette), 384 pixels a line
7
+ # and every line of the frame. A line's pixels start at horizontal
8
+ # count 419, so the 40-column window, counts 451-315, runs from pixel
9
+ # 32 to 351, as in xplus4's screenshots.
10
+ #
11
+ # The picture's lines are the scanlines of the frame as they go out,
12
+ # which follow the line count until a write to $FF1C-$FF1D moves it:
13
+ # the frame's scanlines start over where the line count wraps to 0 at
14
+ # the end of the frame, and step with each line otherwise, as in
15
+ # xplus4's screenshots of TED/write-scanline-coarse.
16
+ #
17
+ # A line is painted at column 44, once its last pixel is out, from
18
+ # what the line fetched (Fetches) in the colours of its mode (Modes).
19
+ # A write to a register the picture reads paints the line up to the
20
+ # pixel the write lands on first, so the old value holds to there and
21
+ # the new one from there.
22
+ #
23
+ # Outside the window the border colour ($FF19) shows: above and below
24
+ # it, from the line count reaching line 4 to its reaching line 204 with
25
+ # $FF06 bit 3 set (25 rows), or 8 and 200 (24 rows), so a line count
26
+ # moved past line 4 leaves the frame's window shut;
27
+ # beside it, the 38-column window ($FF07 bit 3 clear) is pixels 39 to
28
+ # 342, as in xplus4's screenshots, where the datasheet's counts would
29
+ # make it 40 to 343. $FF06 bit 4 clear blanks the whole picture to the
30
+ # border colour. A line of the window with no row under way shows the
31
+ # background colour, $FF15.
32
+ #
33
+ # $FF07 bits 0-2 move the characters right by as many pixels, the
34
+ # window's pixels before the first character showing the background.
35
+ module Picture
36
+ WIDTH = 384
37
+
38
+ # The horizontal count of a line's first pixel.
39
+ FIRST_COUNT = 419
40
+
41
+ # The column whose cycle paints the line, and the first whose writes
42
+ # land on the next line.
43
+ PAINT_COLUMN = 44
44
+ NEXT_LINE_COLUMN = 52
45
+
46
+ WINDOW_LEFT = 32
47
+ WINDOW_RIGHT = 352
48
+ NARROW_LEFT = 39
49
+ NARROW_RIGHT = 343
50
+
51
+ # How many dots after its cycle's first a register write takes hold:
52
+ # where xplus4 shows a raster interrupt's colour change, a cell
53
+ # boundary with no horizontal scroll.
54
+ WRITE_DELAY = 19
55
+
56
+ # The picture, a palette index a pixel.
57
+ attr_reader :display
58
+
59
+ # Whether each line has been painted since the front end last
60
+ # cleared them.
61
+ attr_reader :dirty_lines
62
+
63
+ # Whether the TED paints its picture. It fetches either way.
64
+ attr_reader :render
65
+
66
+ def width = WIDTH
67
+
68
+ def height = lines_per_frame
69
+
70
+ def clear_dirty_lines!
71
+ @dirty_lines.fill(false)
72
+ end
73
+
74
+ def render=(value)
75
+ @render = value
76
+ end
77
+
78
+ private
79
+
80
+ def reset_picture
81
+ @display = Array.new(WIDTH * 312, 0)
82
+ @dirty_lines = Array.new(312, true)
83
+ @render = true
84
+ @painted = 0
85
+ @scanline = 0
86
+ @window_open = false
87
+ reset_modes
88
+ end
89
+
90
+ # The line count steps to +line+: the next scanline, and the
91
+ # window opens or shuts on its first and last lines.
92
+ def step_scanline(line)
93
+ @scanline = (@scanline + 1) % 312
94
+ rows25 = @registers[0x06].anybits?(0x08)
95
+ if line == (rows25 ? 4 : 8)
96
+ @window_open = true
97
+ elsif line == (rows25 ? 204 : 200)
98
+ @window_open = false
99
+ end
100
+ end
101
+
102
+ # The line count wraps to 0: the frame's first scanline.
103
+ def end_picture_frame
104
+ @scanline = -1
105
+ end
106
+
107
+ # The picture's part of a cycle in +column+.
108
+ def picture_cycle(column)
109
+ return unless column == PAINT_COLUMN
110
+
111
+ paint(WIDTH) if @render
112
+ @painted = 0
113
+ end
114
+
115
+ # Paints the line up to the pixel a register write lands on.
116
+ def paint_to_write
117
+ return unless @render
118
+
119
+ column = @column
120
+ return if column >= PAINT_COLUMN && column < NEXT_LINE_COLUMN
121
+
122
+ x = ((column * 8) + WRITE_DELAY - FIRST_COUNT) % 456
123
+ x = WIDTH if x > WIDTH
124
+ paint(x)
125
+ end
126
+
127
+ # Paints the line's pixels from where it was left up to +stop+.
128
+ def paint(stop)
129
+ start = @painted
130
+ return if stop <= start
131
+
132
+ @painted = stop
133
+ scanline = @scanline
134
+ @dirty_lines[scanline] = true
135
+ base = scanline * WIDTH
136
+ border = @registers[0x19] & 0x7f
137
+ return fill_span(base, start, stop, border) unless @window_open && @registers[0x06].anybits?(0x10)
138
+
139
+ narrow = !@registers[0x07].anybits?(0x08)
140
+ left = narrow ? NARROW_LEFT : WINDOW_LEFT
141
+ right = narrow ? NARROW_RIGHT : WINDOW_RIGHT
142
+ fill_span(base, start, stop.clamp(..left), border)
143
+ paint_window(base, start.clamp(left..), stop.clamp(..right))
144
+ fill_span(base, start.clamp(right..), stop, border)
145
+ end
146
+
147
+ def fill_span(base, start, stop, color)
148
+ @display.fill(color, base + start, stop - start) if stop > start
149
+ end
150
+
151
+ def paint_window(base, start, stop)
152
+ return if stop <= start
153
+
154
+ background = @registers[0x15] & 0x7f
155
+ return fill_span(base, start, stop, background) unless @pattern_fetches
156
+
157
+ first = WINDOW_LEFT + (@registers[0x07] & 0x07)
158
+ if start < first
159
+ fill_span(base, start, stop.clamp(..first), background)
160
+ start = first
161
+ end
162
+ paint_characters(base, start, stop, first)
163
+ end
164
+
165
+ # Paints the characters' pixels +start+ to +stop+, the first
166
+ # character's first pixel at +first+.
167
+ def paint_characters(base, start, stop, first)
168
+ mode = self.mode
169
+ x = start
170
+ while x < stop
171
+ index = (x - first) >> 3
172
+ origin = first + (index << 3)
173
+ to = (origin + 8).clamp(..stop)
174
+ character_colors(index, mode)
175
+ paint_pixels(base, x, to, origin)
176
+ x = to
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class TED
6
+ class Sound
7
+ # The sound's level averaged over each sample of the audio rate,
8
+ # which integrates the SND pin as the board's RC network does, then
9
+ # through the network's coupling capacitor, a one-pole high-pass at
10
+ # 2.7 Hz run at the audio rate.
11
+ #
12
+ # The corner comes from the Plus/4 schematic (310164, sheet 2): SND
13
+ # reaches the video connector through R10 (1K) and C18 (10 uF) into
14
+ # R11 (12K) and R12 (10K) beside Q3's emitter follower. The
15
+ # network's low-pass, C19 (2,700 pF) at about 70 kHz, is far above
16
+ # any audio rate.
17
+ class Output
18
+ HIGH_PASS_HZ = 2.7
19
+
20
+ # Scales the loudest level, both voices high at volume 8, to the
21
+ # top of a signed 16-bit sample.
22
+ SCALE = 32_767.0 / 19_976
23
+
24
+ attr_reader :samples
25
+
26
+ # `level` is the sound's level as the recording starts, which the
27
+ # stage takes as settled.
28
+ def initialize(clock_hz:, rate:, level:)
29
+ @clock_hz = clock_hz
30
+ @rate = rate
31
+ @remainder = 0
32
+ @window = next_window
33
+ @left = @window
34
+ @sum = 0
35
+ @drift = level.to_f
36
+ @high_pass = 1.0 - Math.exp(-2.0 * Math::PI * HIGH_PASS_HZ / rate)
37
+ @samples = []
38
+ end
39
+
40
+ # Adds `cycles` cycles at `level`, closing every sample they finish.
41
+ def hold_level(level, cycles)
42
+ left = @left
43
+ while cycles >= left
44
+ @sum += level * left
45
+ cycles -= left
46
+ close
47
+ left = @left
48
+ end
49
+ @sum += level * cycles
50
+ @left = left - cycles
51
+ end
52
+
53
+ # The samples closed since the last drain.
54
+ def drain
55
+ samples = @samples
56
+ @samples = []
57
+ samples
58
+ end
59
+
60
+ private
61
+
62
+ def close
63
+ average = @sum.fdiv(@window)
64
+ @drift += @high_pass * (average - @drift)
65
+ sample = ((average - @drift) * SCALE).round
66
+ sample = 32_767 if sample > 32_767
67
+ sample = -32_768 if sample < -32_768
68
+ @samples << sample
69
+ @sum = 0
70
+ @window = next_window
71
+ @left = @window
72
+ end
73
+
74
+ # Each window is a whole number of cycles, and they add up to the
75
+ # clock's cycles over a second.
76
+ def next_window
77
+ @remainder += @clock_hz
78
+ cycles = @remainder / @rate
79
+ @remainder -= cycles * @rate
80
+ cycles
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/chips/ted/sound/output"
4
+
5
+ module Badline
6
+ module Chips
7
+ class TED
8
+ # The TED's sound: two voices, the second a square wave or noise.
9
+ # Voice 1's 10-bit frequency is $FF0E with $FF12 bits 0-1 above it,
10
+ # voice 2's $FF0F with $FF10 bits 0-1. $FF11 holds the volume in bits
11
+ # 0-3 (0 silent, 8 and above the loudest), voice 1 on in bit 4, voice
12
+ # 2's square wave in bit 5 and its noise in bit 6 (the square wins if
13
+ # both are set), and the reload bit in bit 7.
14
+ #
15
+ # Each voice counts once every 4 cycles, and its output toggles every
16
+ # 1023 - x counts for a frequency x, so a square wave of the clock
17
+ # over 8 * (1023 - x). $3FF counts 1,024 between toggles, and $3FE
18
+ # stops the voice after its next toggle, with its output high. A new
19
+ # frequency takes hold at the next toggle. The voices count whether
20
+ # they are on or not.
21
+ #
22
+ # The noise is an 8-bit shift register that shifts at each of voice
23
+ # 2's toggles: bits 1, 4, 5 and 7 XORed go in at bit 0, and bit 1 is
24
+ # the noise's output.
25
+ #
26
+ # While the reload bit is set, the counters hold at their frequencies,
27
+ # the outputs stay high and the noise's register holds all ones, with
28
+ # the noise silent. Writing it clear starts each voice on a full high
29
+ # half-wave. Samples play this way, with the voices held high and the
30
+ # volume as a 9-level D/A converter.
31
+ #
32
+ # The output takes a level from the volume and how many voices are on
33
+ # and high. The levels are measured from xplus4 recordings, whose two
34
+ # voices together come out above twice one.
35
+ #
36
+ # The voices catch up to the current cycle whenever a write lands, the
37
+ # samples are drained or #catch_up! asks, whether or not #record
38
+ # collects the output, so their state doesn't depend on whether the
39
+ # host plays it.
40
+ class Sound
41
+ VOICES = 2
42
+ CYCLES_PER_COUNT = 4
43
+
44
+ # The frequency that stops a voice with its output high.
45
+ HELD = 0x3fe
46
+
47
+ NOISE_TAPS = 0xb2
48
+ NOISE_PERIOD = 255
49
+
50
+ # The output's level for each volume 0-8, with no voice, one voice
51
+ # and both voices high.
52
+ LEVELS = [
53
+ 0, 0, 0, 0, 0, 0, 0, 0, 0,
54
+ 0, 586, 1610, 2634, 3658, 4682, 5706, 6730, 7754,
55
+ 0, 1172, 3284, 5526, 7984, 10_658, 13_548, 16_654, 19_976
56
+ ].freeze
57
+
58
+ # Far enough ahead that a voice stopped at $3FE, or held by the
59
+ # reload bit, never toggles.
60
+ NEVER = 1 << 60
61
+
62
+ attr_reader :volume, :control
63
+
64
+ # `clock` answers the machine's cycles, `clock_hz` the rate they run at.
65
+ def initialize(clock, clock_hz)
66
+ @clock = clock
67
+ @clock_hz = clock_hz
68
+ @output = nil
69
+ @time = 0
70
+ @frequencies = [0, 0]
71
+ @intervals = [0, 0]
72
+ @phases = [1, 1]
73
+ @next_toggles = [0, 0]
74
+ power_on!
75
+ end
76
+
77
+ def model = :mos7360
78
+
79
+ # The power-on state: every register clear, both voices starting on
80
+ # a high half-wave and the noise's register full of ones.
81
+ def power_on!
82
+ now = @clock.cycles
83
+ run_to(now)
84
+ @volume = 0
85
+ @control = 0
86
+ @reloading = false
87
+ @noise = 0xff
88
+ VOICES.times do |voice|
89
+ @frequencies[voice] = 0
90
+ @intervals[voice] = interval_for(0)
91
+ @phases[voice] = 1
92
+ @next_toggles[voice] = now + @intervals[voice]
93
+ end
94
+ @level = level_now
95
+ end
96
+
97
+ # Starts collecting the output, averaged down to `rate` samples a
98
+ # second, for #drain_samples. `clock_hz` is the rate the cycles are
99
+ # taken to run at.
100
+ def record(rate:, clock_hz: @clock_hz)
101
+ run_to(@clock.cycles)
102
+ @output = Output.new(clock_hz:, rate:, level: @level)
103
+ end
104
+
105
+ def recording? = !@output.nil?
106
+
107
+ # Runs the voices up to the current cycle.
108
+ def catch_up! = run_to(@clock.cycles)
109
+
110
+ # The samples recorded since the last drain.
111
+ def drain_samples
112
+ output = @output
113
+ return [] if output.nil?
114
+
115
+ run_to(@clock.cycles)
116
+ output.drain
117
+ end
118
+
119
+ # A write to $FF0E-$FF12, which takes hold from the next cycle.
120
+ def write(register, value)
121
+ run_to(@clock.cycles + 1)
122
+ case register
123
+ when 0x0e then tune(0, (@frequencies[0] & 0x300) | value)
124
+ when 0x0f then tune(1, (@frequencies[1] & 0x300) | value)
125
+ when 0x10 then tune(1, (@frequencies[1] & 0xff) | ((value & 0x03) << 8))
126
+ when 0x11 then write_control(value)
127
+ when 0x12 then tune(0, (@frequencies[0] & 0xff) | ((value & 0x03) << 8))
128
+ end
129
+ @level = level_now
130
+ end
131
+
132
+ def frequency(voice) = @frequencies[voice]
133
+
134
+ def reloading? = @reloading
135
+
136
+ # A voice's square-wave output as of the current cycle, 1 high.
137
+ def phase(voice)
138
+ run_to(@clock.cycles)
139
+ @phases[voice]
140
+ end
141
+
142
+ # The noise's shift register as of the current cycle.
143
+ def noise
144
+ run_to(@clock.cycles)
145
+ @noise
146
+ end
147
+
148
+ # The output's level as of the current cycle.
149
+ def level
150
+ run_to(@clock.cycles)
151
+ @level
152
+ end
153
+
154
+ private
155
+
156
+ # Runs the voices up to the start of cycle `target`, the output holding
157
+ # each level for the cycles it lasts.
158
+ def run_to(target)
159
+ time = @time
160
+ return if target <= time
161
+
162
+ output = @output
163
+ return skip_to(target) if output.nil?
164
+
165
+ following = next_toggle
166
+ while following < target
167
+ output.hold_level(@level, following - time)
168
+ time = following
169
+ toggle_due(time)
170
+ @level = level_now
171
+ following = next_toggle
172
+ end
173
+ output.hold_level(@level, target - time)
174
+ @time = target
175
+ end
176
+
177
+ # Moves each voice through its toggles before `target` at once,
178
+ # the noise by as many shifts as voice 2 toggles, less whole turns
179
+ # of its sequence.
180
+ def skip_to(target)
181
+ VOICES.times do |voice|
182
+ due = @next_toggles[voice]
183
+ next if due >= target
184
+
185
+ interval = @intervals[voice]
186
+ toggles = ((target - 1 - due) / interval) + 1
187
+ @next_toggles[voice] = due + (toggles * interval)
188
+ @phases[voice] ^= toggles & 1
189
+ next unless voice == 1
190
+
191
+ (toggles % NOISE_PERIOD).times { shift_noise }
192
+ end
193
+ @level = level_now
194
+ @time = target
195
+ end
196
+
197
+ def next_toggle
198
+ toggles = @next_toggles
199
+ following = toggles[0]
200
+ following = toggles[1] if toggles[1] < following
201
+ following
202
+ end
203
+
204
+ def toggle_due(time)
205
+ toggles = @next_toggles
206
+ VOICES.times do |voice|
207
+ next unless toggles[voice] == time
208
+
209
+ @phases[voice] ^= 1
210
+ toggles[voice] = time + @intervals[voice]
211
+ shift_noise if voice == 1
212
+ end
213
+ end
214
+
215
+ def shift_noise
216
+ noise = @noise
217
+ taps = noise & NOISE_TAPS
218
+ taps ^= taps >> 4
219
+ taps ^= taps >> 2
220
+ taps ^= taps >> 1
221
+ @noise = ((noise << 1) | (taps & 1)) & 0xff
222
+ end
223
+
224
+ # The cycles between a voice's toggles at frequency `value`, and
225
+ # never again after the next one at $3FE.
226
+ def interval_for(value)
227
+ return NEVER if value == HELD
228
+
229
+ (((HELD - value) & 0x3ff) + 1) * CYCLES_PER_COUNT
230
+ end
231
+
232
+ # A new frequency for `voice`, which takes hold at its next toggle. A
233
+ # voice stopped at $3FE starts counting now.
234
+ def tune(voice, value)
235
+ @frequencies[voice] = value
236
+ @intervals[voice] = interval_for(value)
237
+ return if @reloading || @next_toggles[voice] < NEVER
238
+
239
+ @next_toggles[voice] = @time + @intervals[voice]
240
+ end
241
+
242
+ def write_control(value)
243
+ reload = value >= 0x80
244
+ if reload && !@reloading
245
+ hold_voices
246
+ elsif !reload && @reloading
247
+ release_voices
248
+ end
249
+ @reloading = reload
250
+ @control = value
251
+ volume = value & 0x0f
252
+ volume = 8 if volume > 8
253
+ @volume = volume
254
+ end
255
+
256
+ def hold_voices
257
+ @phases[0] = 1
258
+ @phases[1] = 1
259
+ @noise = 0xff
260
+ @next_toggles[0] = NEVER
261
+ @next_toggles[1] = NEVER
262
+ end
263
+
264
+ def release_voices
265
+ VOICES.times { |voice| @next_toggles[voice] = @time + @intervals[voice] }
266
+ end
267
+
268
+ def level_now
269
+ control = @control
270
+ high = 0
271
+ high += 1 if control.anybits?(0x10) && high?(0)
272
+ if control.anybits?(0x20)
273
+ high += 1 if high?(1)
274
+ elsif control.anybits?(0x40) && !@reloading
275
+ high += 1 if @noise.anybits?(0x02) || @frequencies[1] == HELD
276
+ end
277
+ LEVELS[(high * 9) + @volume]
278
+ end
279
+
280
+ def high?(voice) = @phases[voice] == 1 || @frequencies[voice] == HELD
281
+ end
282
+ end
283
+ end
284
+ end
@@ -2,14 +2,21 @@
2
2
 
3
3
  require "badline/chips/ted/timers"
4
4
  require "badline/chips/ted/beam"
5
+ require "badline/chips/ted/fetches"
6
+ require "badline/chips/ted/palette"
7
+ require "badline/chips/ted/modes"
8
+ require "badline/chips/ted/picture"
9
+ require "badline/chips/ted/sound"
10
+ require "badline/chips/ted/keyed_state"
5
11
 
6
12
  module Badline
7
13
  module Chips
8
14
  # The TED of the Commodore 264 series, the 7360 and 8360: its registers
9
15
  # at $FF00-$FF1F, the raster, the three timers, the interrupt, the
10
- # keyboard latch and the clock it gives the CPU (Beam). It paints no
11
- # picture and plays no sound yet: those registers keep what is written
12
- # to them.
16
+ # keyboard latch, the clock it gives the CPU (Beam), what it fetches
17
+ # for the picture (Fetches) and the picture (Picture). Writes to the
18
+ # sound's registers, $FF0E-$FF12, go on to the Sound attached as
19
+ # #sound.
13
20
  #
14
21
  # The machine clocks it once a single clock cycle, 57 to a line, and
15
22
  # asks it then how many cycles the CPU runs in that one (#cpu_slots)
@@ -23,10 +30,16 @@ module Badline
23
30
  # and $FF0B the compare's low byte. $FF13 bit 0 reads whether the CPU
24
31
  # sees ROM at $8000-$FFFF (#rom_selected=). $FF1C and $FF1D read and
25
32
  # set the raster line, and $FF1E reads the horizontal position's top 8
26
- # bits.
33
+ # bits. $FF1A and $FF1B read and set the character position reload,
34
+ # and $FF1F the row's line in bits 0-2 and in bits 3-6 the blink
35
+ # counter, which counts frames.
27
36
  class TED
28
37
  include Timers
29
38
  include Beam
39
+ include Fetches
40
+ include Palette
41
+ include Modes
42
+ include Picture
30
43
 
31
44
  # The clock a TED is fed, its single clock: the crystal divided by 20
32
45
  # on PAL machines and by 16 on NTSC ones (the Plus/4 hardware manual's
@@ -42,6 +55,10 @@ module Badline
42
55
  # pen and the three timers.
43
56
  SOURCES = 0x5e
44
57
 
58
+ # The registers the picture reads as it paints: $FF06, $FF07, the
59
+ # cursor and the five colours.
60
+ PICTURE_REGISTERS = 0x03e030c0
61
+
45
62
  attr_reader :profile, :interrupt_flags, :interrupt_mask
46
63
 
47
64
  # What answers the keyboard latch: an object with #latch(value).
@@ -50,10 +67,15 @@ module Badline
50
67
  # Whether the CPU sees ROM at $8000-$FFFF, which $FF13 bit 0 reads.
51
68
  attr_writer :rom_selected
52
69
 
70
+ # The Sound the sound's registers drive.
71
+ attr_accessor :sound
72
+
53
73
  def initialize(profile = PAL)
54
74
  @profile = profile
55
75
  @keyboard_port = nil
56
76
  @rom_selected = true
77
+ @bus = nil
78
+ @sound = nil
57
79
  power_on!
58
80
  end
59
81
 
@@ -63,16 +85,22 @@ module Badline
63
85
  @interrupt_mask = 0
64
86
  @raster_compare = 0
65
87
  @key_latch = 0xff
88
+ @blink = 0
66
89
  reset_timers
90
+ reset_fetches
91
+ reset_picture
67
92
  reset_beam
68
93
  end
69
94
 
70
- # Counts the timers and moves the beam on a cycle, unless $FF07's
71
- # freeze bit holds them, then decides the CPU's clock for the cycle.
95
+ # Counts the timers, moves the beam on a cycle and makes the cycle's
96
+ # fetches, unless $FF07's freeze bit holds them, then decides the
97
+ # CPU's clock for the cycle.
72
98
  def cycle!
73
99
  unless frozen?
74
100
  count_timers
75
101
  move_beam
102
+ fetch(@column)
103
+ picture_cycle(@column)
76
104
  end
77
105
  decide_clock
78
106
  end
@@ -92,9 +120,12 @@ module Badline
92
120
  when 0x0a then @interrupt_mask | 0xa0
93
121
  when 0x0b then @raster_compare & 0xff
94
122
  when 0x13 then (@registers[0x13] & 0xfe) | (@rom_selected ? 1 : 0)
123
+ when 0x1a then 0xfc | (@char_position >> 8)
124
+ when 0x1b then @char_position & 0xff
95
125
  when 0x1c then 0xfe | (@line >> 8)
96
126
  when 0x1d then @line & 0xff
97
127
  when 0x1e then @column << 2
128
+ when 0x1f then 0x80 | (@blink << 3) | @row_line
98
129
  else @registers[register]
99
130
  end
100
131
  end
@@ -103,6 +134,7 @@ module Badline
103
134
  register = addr & 0x3f
104
135
  return if register >= 0x20
105
136
 
137
+ paint_to_write if (PICTURE_REGISTERS >> register).anybits?(1)
106
138
  @registers[register] = value
107
139
  case register
108
140
  when 0x00..0x05 then write_timer(register, value)
@@ -110,8 +142,12 @@ module Badline
110
142
  when 0x09 then @interrupt_flags &= ~value
111
143
  when 0x0a then write_interrupt_mask(value)
112
144
  when 0x0b then @raster_compare = (@raster_compare & 0x100) | value
145
+ when 0x0e..0x12 then @sound&.write(register, value)
146
+ when 0x1a then @char_position = (@char_position & 0xff) | ((value & 0x03) << 8)
147
+ when 0x1b then @char_position = (@char_position & 0x300) | value
113
148
  when 0x1c then self.line = (@line & 0xff) | ((value & 0x01) << 8)
114
149
  when 0x1d then self.line = (@line & 0x100) | value
150
+ when 0x1f then write_row_line(value)
115
151
  end
116
152
  end
117
153
 
@@ -125,6 +161,17 @@ module Badline
125
161
  irq? ? value | 0x80 : value
126
162
  end
127
163
 
164
+ def write_row_line(value)
165
+ @row_line = value & 0x07
166
+ @blink = (value >> 3) & 0x0f
167
+ end
168
+
169
+ # The end of a frame: the fetches' counters start over.
170
+ def end_frame
171
+ end_fetch_frame
172
+ end_picture_frame
173
+ end
174
+
128
175
  def write_interrupt_mask(value)
129
176
  @interrupt_mask = value & 0x5f
130
177
  @raster_compare = (@raster_compare & 0xff) | ((value & 0x01) << 8)