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,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class TED
6
+ # What the TED reads for the picture, a single clock cycle at a time,
7
+ # and the counters that say where it reads.
8
+ #
9
+ # The two DMA lines of a character row (Beam) fetch a byte a cycle
10
+ # in phi2, in the 40 cycles of the character window, columns 54-56
11
+ # and 0-36: the attributes on the line before the row, from the video
12
+ # matrix base ($FF14 bits 3-7) with A10 clear, and the character
13
+ # pointers on the row's first line, with A10 set (the datasheet's
14
+ # video matrix and attribute addresses). The attributes go to a
15
+ # buffer of their own, and each takes hold as its character's
16
+ # pointer is fetched, so the row before shows its own colours to its
17
+ # last line, as xplus4 shows them.
18
+ #
19
+ # On every line of a row, from the cycle after each pointer's, the
20
+ # TED fetches the character's pattern byte in phi1, columns 55-56
21
+ # and 0-37: from the character base ($FF13 bits 2-7) with the
22
+ # pointer and the row's line counter, or from the bitmap base ($FF12
23
+ # bits 3-5) with the character position and the line counter in
24
+ # bitmap mode. With $FF07 bit 7 clear the pointer's bit 7 asks for
25
+ # reverse and the character base supplies A10; extended colour mode
26
+ # forces A9 and A10 low. The byte stays on the bus, where a read of
27
+ # an address nothing answers finds it (Plus/4 openio's outrun2).
28
+ #
29
+ # The matrix fetches see ROM or RAM as the CPU does at $8000-$FFFF,
30
+ # and the pattern fetches ROM with $FF12 bit 2 set or RAM with it
31
+ # clear (Plus4::Bus#ted_fetch and #ted_pattern_fetch).
32
+ #
33
+ # The counters: the character position reload ($FF1A-$FF1B), the
34
+ # character position the row starts at, which steps by 40 at the end
35
+ # of a row's eighth line, at horizontal count 290; and the vertical
36
+ # sub-address ($FF1F bits 0-2), the row's line, which steps with the
37
+ # line count on the display window's lines once the frame's first
38
+ # row is under way, wrapping from 7 to 0 whatever the DMA lines do.
39
+ # The attribute line reads the row the reload points to after its
40
+ # own latch: 40 on from it on a row's eighth line, the reload itself
41
+ # otherwise. The pointer line is the line after an attribute line. A
42
+ # frame starts with both counters at 0 and no row under way, and the
43
+ # frame's first pointer line puts a row under way to the end of the
44
+ # frame. The display window's lines with a row under way and the
45
+ # display on fetch pattern bytes.
46
+ module Fetches
47
+ # The first and last cycles of the pattern fetches.
48
+ PATTERN_COLUMN = 55
49
+ PATTERN_END_COLUMN = 37
50
+
51
+ # The cycle that latches the next row's character position.
52
+ LATCH_COLUMN = 36
53
+
54
+ # The cycle that counts the blink counter, horizontal count 336.
55
+ BLINK_COLUMN = 42
56
+
57
+ CHARACTERS = 40
58
+
59
+ # The pattern bytes, pointers and attributes of the characters on
60
+ # the line being fetched.
61
+ attr_reader :patterns, :pointers, :attributes
62
+
63
+ # The character position reload, the row's line, and whether a row
64
+ # is under way.
65
+ attr_reader :char_position, :row_line, :row_active
66
+
67
+ # The bus the TED fetches from, the machine's.
68
+ def connect(bus)
69
+ @bus = bus
70
+ end
71
+
72
+ private
73
+
74
+ def reset_fetches
75
+ @patterns = Array.new(CHARACTERS, 0)
76
+ @pointers = Array.new(CHARACTERS, 0)
77
+ @attributes = Array.new(CHARACTERS, 0)
78
+ @fetched_attributes = Array.new(CHARACTERS, 0)
79
+ @char_position = 0
80
+ @line_position = 0
81
+ @row_line = 0
82
+ @row_active = false
83
+ @dma_kind = Beam::NO_DMA
84
+ @pattern_fetches = false
85
+ end
86
+
87
+ # The fetches and counters of the cycle in +column+.
88
+ def fetch(column)
89
+ if column <= Beam::DMA_END_COLUMN || column >= Beam::DMA_COLUMN
90
+ window_fetch(column)
91
+ elsif column == PATTERN_END_COLUMN
92
+ pattern_fetch(column) if @pattern_fetches
93
+ elsif column == Beam::BA_COLUMN
94
+ start_line_fetches
95
+ elsif column == BLINK_COLUMN
96
+ count_blink
97
+ end
98
+ end
99
+
100
+ # A cycle of the character window.
101
+ def window_fetch(column)
102
+ dma_fetch(column) unless @dma_kind == Beam::NO_DMA
103
+ pattern_fetch(column) if @pattern_fetches && column != Beam::DMA_COLUMN
104
+ latch_char_position if column == LATCH_COLUMN
105
+ end
106
+
107
+ # The blink counter counts a frame on one line of the vertical
108
+ # blank, the line xplus4 counts it on.
109
+ def count_blink
110
+ return unless @line == (lines_per_frame == 312 ? 256 : 228)
111
+
112
+ @blink = (@blink + 1) & 0x0f
113
+ blink_carry if @blink.zero?
114
+ end
115
+
116
+ # Column 51: what the line fetches, decided where BA is.
117
+ def start_line_fetches
118
+ @dma_kind = dma_kind
119
+ @row_active = true if @dma_kind == Beam::POINTER_DMA
120
+ @line_position = @char_position
121
+ @pattern_fetches = @row_active && @line <= Beam::LAST_DISPLAY_LINE && display_enabled? && !@bus.nil?
122
+ end
123
+
124
+ # The attributes on a line whose low three bits match $FF06's
125
+ # scroll, below line 203, and the pointers on the line after one
126
+ # that fetched attributes (the datasheet's DMA timing, step 8),
127
+ # whatever the scroll says by then.
128
+ def dma_kind
129
+ return Beam::NO_DMA unless display_enabled?
130
+ return Beam::POINTER_DMA if @dma_kind == Beam::ATTRIBUTE_DMA
131
+ return Beam::NO_DMA unless @line < Beam::LAST_DISPLAY_LINE
132
+
133
+ (@line - @registers[0x06]).nobits?(0x07) ? Beam::ATTRIBUTE_DMA : Beam::NO_DMA
134
+ end
135
+
136
+ def display_enabled? = @registers[0x06].anybits?(0x10)
137
+
138
+ def dma_fetch(column)
139
+ bus = @bus
140
+ return if bus.nil?
141
+
142
+ index = column >= Beam::DMA_COLUMN ? column - Beam::DMA_COLUMN : column + (Beam::CYCLES_PER_LINE - Beam::DMA_COLUMN)
143
+ matrix = (@registers[0x14] & 0xf8) << 8
144
+ if @dma_kind == Beam::ATTRIBUTE_DMA
145
+ position = latching? ? @char_position + CHARACTERS : @char_position
146
+ @fetched_attributes[index] = bus.ted_fetch(matrix | ((position + index) & 0x3ff))
147
+ else
148
+ @pointers[index] = bus.ted_fetch(matrix | 0x400 | ((@char_position + index) & 0x3ff))
149
+ @attributes[index] = @fetched_attributes[index]
150
+ end
151
+ end
152
+
153
+ def pattern_fetch(column)
154
+ index = column >= PATTERN_COLUMN ? column - PATTERN_COLUMN : column + (Beam::CYCLES_PER_LINE - PATTERN_COLUMN)
155
+ @patterns[index] = @bus.ted_pattern_fetch(pattern_address(index), @registers[0x12].anybits?(0x04))
156
+ end
157
+
158
+ def pattern_address(index)
159
+ return bitmap_address(index) if @registers[0x06].anybits?(0x20)
160
+
161
+ pointer = @pointers[index]
162
+ base = @registers[0x13] & 0xf8
163
+ if @registers[0x06].anybits?(0x40)
164
+ pointer &= 0x3f
165
+ elsif !@registers[0x07].anybits?(0x80)
166
+ pointer = (pointer & 0x7f) | ((@registers[0x13] & 0x04) << 5)
167
+ end
168
+ (base << 8) | (pointer << 3) | @row_line
169
+ end
170
+
171
+ def bitmap_address(index)
172
+ ((@registers[0x12] & 0x38) << 10) | (((@line_position + index) & 0x3ff) << 3) | @row_line
173
+ end
174
+
175
+ # The end of a row's eighth line moves the reload on to the next
176
+ # row.
177
+ def latch_char_position
178
+ @char_position = (@char_position + CHARACTERS) & 0x3ff if latching?
179
+ end
180
+
181
+ # Whether the line ends a row, and latches the next row's position.
182
+ def latching? = @row_active && @row_line == 7 && @line <= Beam::LAST_DISPLAY_LINE
183
+
184
+ # The line count steps to +line+: the row's next line, up to the
185
+ # last line of the display window.
186
+ def step_row_line(line)
187
+ @row_line = (@row_line + 1) & 0x07 if @row_active && line <= Beam::LAST_DISPLAY_LINE
188
+ end
189
+
190
+ # The end of the frame.
191
+ def end_fetch_frame
192
+ @char_position = 0
193
+ @row_line = 0
194
+ @row_active = false
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class TED
6
+ class Sound
7
+ # The sound's fields in a keyed snapshot module (Snapshot::Keyed,
8
+ # BL-TEDSOUND): each voice's output and when it next toggles, in
9
+ # cycles from the machine's clock, absent while it never will, the
10
+ # noise's shift register, and how far behind the clock the voices
11
+ # last ran. The frequencies, the volume and the reload bit come from
12
+ # the TED's registers on load (#restored!), and the output's level
13
+ # from those.
14
+ module KeyedState
15
+ def save_fields(out)
16
+ now = @clock.cycles
17
+ run_to(now)
18
+ VOICES.times do |voice|
19
+ due = @next_toggles[voice]
20
+ out.int("voice#{voice + 1}.phase", @phases[voice])
21
+ out.optional_int("voice#{voice + 1}.toggle_in", due >= NEVER ? nil : due - now)
22
+ end
23
+ out.int("noise", @noise).int("ran_ago", now - @time)
24
+ end
25
+
26
+ def load_fields(input)
27
+ now = @clock.cycles
28
+ VOICES.times do |voice|
29
+ @phases[voice] = input.masked("voice#{voice + 1}.phase", @phases[voice], 0x01)
30
+ due = input.optional_int("voice#{voice + 1}.toggle_in")
31
+ @next_toggles[voice] = due.nil? ? NEVER : now + due
32
+ end
33
+ @noise = input.byte("noise", @noise)
34
+ @time = now - input.int("ran_ago", now - @time)
35
+ end
36
+
37
+ # The frequencies, the volume and the reload bit as `ted`'s
38
+ # registers $FF0E-$FF12 hold them, and the level they give.
39
+ def restored!(ted)
40
+ @frequencies[0] = ted.peek(0xff0e) | ((ted.peek(0xff12) & 0x03) << 8)
41
+ @frequencies[1] = ted.peek(0xff0f) | ((ted.peek(0xff10) & 0x03) << 8)
42
+ VOICES.times { |voice| @intervals[voice] = interval_for(@frequencies[voice]) }
43
+ control = ted.peek(0xff11)
44
+ @control = control
45
+ @reloading = control >= 0x80
46
+ @volume = [control & 0x0f, 8].min
47
+ @level = level_now
48
+ end
49
+ end
50
+
51
+ include KeyedState
52
+ end
53
+
54
+ # The DMA a line makes, as a snapshot names it, in Beam's order.
55
+ DMA_KINDS = %w[none attributes pointers].freeze
56
+
57
+ # The shape of the line going out, as the picture paints it. A
58
+ # snapshot whose line.layout differs repaints the line from its
59
+ # start.
60
+ LINE_LAYOUT = 1
61
+
62
+ # The TED's fields in a keyed snapshot module (Snapshot::Keyed,
63
+ # BL-TED): the registers as written, the interrupt flags, the keyboard
64
+ # latch, the blink counter, the timers, the beam and the clock it gave
65
+ # the CPU last, the fetches' buffers and counters, and the picture:
66
+ # where the scanline is, whether the window is open, how far the line
67
+ # going out is painted (line.*), and the picture itself. The interrupt
68
+ # mask and the raster compare come from $FF0A and $FF0B on load.
69
+ module KeyedState
70
+ def save_fields(out)
71
+ out.bytes("registers", @registers).int("interrupt_flags", @interrupt_flags).int("key_latch", @key_latch)
72
+ out.int("blink", @blink).bool("blink_on", @blink_on)
73
+ save_timer_fields(out)
74
+ out.int("column", @column).int("line", @line).bool("ba", @ba_low).int("cpu_slots", @cpu_slots)
75
+ save_fetch_fields(out)
76
+ out.int("scanline", @scanline).bool("window_open", @window_open)
77
+ out.int("line.layout", LINE_LAYOUT).int("line.painted", @painted).bytes("display", @display)
78
+ end
79
+
80
+ def load_fields(input)
81
+ input.bytes_into("registers", @registers)
82
+ @interrupt_flags = input.byte("interrupt_flags", @interrupt_flags)
83
+ @key_latch = input.byte("key_latch", @key_latch)
84
+ @blink = input.masked("blink", @blink, 0x0f)
85
+ @blink_on = input.bool("blink_on", @blink_on)
86
+ load_timer_fields(input)
87
+ @column = input.int("column", @column) % Beam::CYCLES_PER_LINE
88
+ @line = input.masked("line", @line, 0x1ff)
89
+ @ba_low = input.bool("ba", @ba_low)
90
+ @cpu_slots = input.int("cpu_slots", @cpu_slots).clamp(0, 2)
91
+ load_fetch_fields(input)
92
+ @scanline = input.int("scanline", @scanline)
93
+ @window_open = input.bool("window_open", @window_open)
94
+ load_line_fields(input)
95
+ input.bytes_into("display", @display)
96
+ end
97
+
98
+ # The interrupt mask and the raster compare from $FF0A and $FF0B,
99
+ # and every line of the picture changed.
100
+ def restored!
101
+ mask = @registers[0x0a]
102
+ @interrupt_mask = mask & 0x5f
103
+ @raster_compare = ((mask & 0x01) << 8) | @registers[0x0b]
104
+ @dirty_lines.fill(true)
105
+ end
106
+
107
+ private
108
+
109
+ def save_timer_fields(out)
110
+ 3.times do |index|
111
+ out.int("timer#{index + 1}.count", @timer_counts[index])
112
+ out.bool("timer#{index + 1}.running", @timer_running[index])
113
+ end
114
+ out.int("timer1.reload", @timer1_reload)
115
+ end
116
+
117
+ def load_timer_fields(input)
118
+ 3.times do |index|
119
+ @timer_counts[index] = input.word("timer#{index + 1}.count", @timer_counts[index])
120
+ @timer_running[index] = input.bool("timer#{index + 1}.running", @timer_running[index])
121
+ end
122
+ @timer1_reload = input.word("timer1.reload", @timer1_reload)
123
+ end
124
+
125
+ def save_fetch_fields(out)
126
+ out.bytes("fetch.patterns", @patterns).bytes("fetch.pointers", @pointers)
127
+ out.bytes("fetch.attributes", @attributes).bytes("fetch.next_attributes", @fetched_attributes)
128
+ out.int("fetch.char_position", @char_position).int("fetch.line_position", @line_position)
129
+ out.int("fetch.row_line", @row_line).bool("fetch.row_active", @row_active)
130
+ out.text("fetch.dma", DMA_KINDS.fetch(@dma_kind)).bool("fetch.patterns_on", @pattern_fetches)
131
+ end
132
+
133
+ def load_fetch_fields(input)
134
+ input.bytes_into("fetch.patterns", @patterns)
135
+ input.bytes_into("fetch.pointers", @pointers)
136
+ input.bytes_into("fetch.attributes", @attributes)
137
+ input.bytes_into("fetch.next_attributes", @fetched_attributes)
138
+ @char_position = input.masked("fetch.char_position", @char_position, 0x3ff)
139
+ @line_position = input.masked("fetch.line_position", @line_position, 0x3ff)
140
+ @row_line = input.masked("fetch.row_line", @row_line, 0x07)
141
+ @row_active = input.bool("fetch.row_active", @row_active)
142
+ @dma_kind = DMA_KINDS.index(input.choice("fetch.dma", DMA_KINDS, DMA_KINDS.fetch(@dma_kind))) || 0
143
+ @pattern_fetches = input.bool("fetch.patterns_on", @pattern_fetches)
144
+ end
145
+
146
+ def load_line_fields(input)
147
+ line = input.int("line.layout", 0) == LINE_LAYOUT
148
+ input.adjust("line in progress from another renderer, restarted") unless line || input.missing?
149
+ @painted = line ? input.int("line.painted", @painted).clamp(0, Picture::WIDTH) : 0
150
+ end
151
+ end
152
+
153
+ include KeyedState
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class TED
6
+ # The colours a character's pixels take in each mode, by $FF06 bits 5
7
+ # (bitmap) and 6 (extended colour) and $FF07 bit 4 (multicolour), with
8
+ # a pattern byte's set bits as 1 (the datasheet's "Character modes"
9
+ # and "Bit map mode", Härsfalvi's "Talking to TED"). Each was checked
10
+ # against xplus4 screenshots of the same registers and memory.
11
+ #
12
+ # - Text: 1 is the attribute's colour and luminance, bits 0-6, and 0
13
+ # the background, $FF15. With $FF07 bit 7 clear a pointer's bit 7
14
+ # reverses the character. The cursor ($FF0C-$FF0D, a character
15
+ # position) reverses its character, and an attribute's bit 7 makes
16
+ # its character flash to the background, each in turns of 16 frames
17
+ # of the blink counter.
18
+ # - Multicolour text: a character whose attribute has bit 3 set takes
19
+ # its bits in pairs of double-width pixels, 00 the background, 01
20
+ # $FF16, 10 $FF17 and 11 the attribute's bits 0-2 and 4-6. Other
21
+ # characters show as in text. No reverse, cursor or flash.
22
+ # - Extended colour: the pointer's bits 6-7 pick the background,
23
+ # $FF15-$FF18. No reverse, cursor or flash.
24
+ # - Bitmap: 0 takes its colour from the video matrix's low nibble and
25
+ # its luminance from the attribute's bits 4-6, 1 from the matrix's
26
+ # high nibble and the attribute's bits 0-2.
27
+ # - Multicolour bitmap: 00 the background, 01 the bitmap's 1, 10 its
28
+ # 0, 11 $FF16, as xplus4 shows them. The datasheet gives 01 the
29
+ # luminance of bits 4-6 as well.
30
+ # - Extended colour with either of the others: black.
31
+ module Modes
32
+ TEXT = 0x00
33
+ MULTICOLOR_TEXT = 0x10
34
+ BITMAP = 0x20
35
+ MULTICOLOR_BITMAP = 0x30
36
+ EXTENDED_COLOR = 0x40
37
+
38
+ private
39
+
40
+ def reset_modes
41
+ @pixel_bits = 0
42
+ @pixel_colors = Array.new(4, 0)
43
+ @multicolor_pixels = false
44
+ @blink_on = false
45
+ end
46
+
47
+ # The mode, $FF06 bits 5 and 6 with $FF07 bit 4.
48
+ def mode = (@registers[0x06] & 0x60) | (@registers[0x07] & 0x10)
49
+
50
+ # Sets the pixel bits and colours of character +index+ in +mode+.
51
+ def character_colors(index, mode)
52
+ pattern = @patterns[index]
53
+ pointer = @pointers[index]
54
+ attribute = @attributes[index]
55
+ case mode
56
+ when TEXT then text_colors(index, pattern, pointer, attribute)
57
+ when MULTICOLOR_TEXT then multicolor_text_colors(pattern, attribute)
58
+ when BITMAP then bitmap_colors(pattern, pointer, attribute)
59
+ when MULTICOLOR_BITMAP then multicolor_bitmap_colors(pattern, pointer, attribute)
60
+ when EXTENDED_COLOR then hires(pattern, @registers[0x15 + (pointer >> 6)] & 0x7f, attribute & 0x7f)
61
+ else hires(0, 0, 0)
62
+ end
63
+ end
64
+
65
+ # Paints pixels +from+ to +to+ of the line at +base+, of a character
66
+ # whose first pixel is +origin+, in the colours last set.
67
+ def paint_pixels(base, from, to, origin)
68
+ display = @display
69
+ bits = @pixel_bits
70
+ colors = @pixel_colors
71
+ multicolor = @multicolor_pixels
72
+ x = from
73
+ while x < to
74
+ offset = x - origin
75
+ color = multicolor ? colors[(bits >> (6 - (offset & 0x06))) & 0x03] : colors[(bits >> (7 - offset)) & 0x01]
76
+ display[base + x] = color
77
+ x += 1
78
+ end
79
+ end
80
+
81
+ def hires(pattern, background, foreground)
82
+ @pixel_bits = pattern
83
+ @pixel_colors[0] = background
84
+ @pixel_colors[1] = foreground
85
+ @multicolor_pixels = false
86
+ end
87
+
88
+ def multicolor(pattern, color1, color2, color3)
89
+ @pixel_bits = pattern
90
+ colors = @pixel_colors
91
+ colors[0] = @registers[0x15] & 0x7f
92
+ colors[1] = color1
93
+ colors[2] = color2
94
+ colors[3] = color3
95
+ @multicolor_pixels = true
96
+ end
97
+
98
+ def text_colors(index, pattern, pointer, attribute)
99
+ pattern ^= 0xff if pointer >= 0x80 && !@registers[0x07].anybits?(0x80)
100
+ if @blink_on
101
+ pattern ^= 0xff if ((@line_position + index) & 0x3ff) == cursor_position
102
+ elsif attribute >= 0x80
103
+ pattern = 0
104
+ end
105
+ hires(pattern, @registers[0x15] & 0x7f, attribute & 0x7f)
106
+ end
107
+
108
+ def cursor_position = ((@registers[0x0c] & 0x03) << 8) | @registers[0x0d]
109
+
110
+ def multicolor_text_colors(pattern, attribute)
111
+ if attribute.anybits?(0x08)
112
+ multicolor(pattern, @registers[0x16] & 0x7f, @registers[0x17] & 0x7f, attribute & 0x77)
113
+ else
114
+ hires(pattern, @registers[0x15] & 0x7f, attribute & 0x7f)
115
+ end
116
+ end
117
+
118
+ def bitmap_colors(pattern, pointer, attribute)
119
+ hires(pattern, (pointer & 0x0f) | (attribute & 0x70), (pointer >> 4) | ((attribute & 0x07) << 4))
120
+ end
121
+
122
+ def multicolor_bitmap_colors(pattern, pointer, attribute)
123
+ multicolor(pattern, (pointer >> 4) | ((attribute & 0x07) << 4), (pointer & 0x0f) | (attribute & 0x70),
124
+ @registers[0x16] & 0x7f)
125
+ end
126
+
127
+ # The blink counter's carry turns the cursor and flash over.
128
+ def blink_carry
129
+ @blink_on = !@blink_on
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class TED
6
+ # The TED's 128 colours.
7
+ module Palette
8
+ # The colours by index, (luminance << 4) | colour, as RGB, from the
9
+ # Plus/4 hardware manual's TED chapter. Luma comes from each
10
+ # luminance level's voltage (4.4.3): level 0, 2.0 V, is black, the
11
+ # eight luminances are levels 1-8, and 4.8 V is white. Chroma takes
12
+ # the PAL column's hue phase (4.4.4), with U along 0 degrees and V
13
+ # along 90, at an amplitude of half CHROMA OUT's 1 Vp-p over the
14
+ # 2.8 V from black to white (4.4.2). Colour 0 is black at every
15
+ # luminance and colour 1 has no chroma. BT.601 turns them into RGB,
16
+ # B-Y = U / 0.492 and R-Y = V / 0.877, clamped. NTSC machines take
17
+ # the same palette. The datasheet's NTSC column is the PAL one less
18
+ # 33 degrees, but for orange, which it gives 129 on PAL and 90 on
19
+ # NTSC.
20
+ PALETTE = [
21
+ 0x000000, 0x242424, 0x570f10, 0x003a39, 0x4e045c, 0x004400, 0x19197f, 0x303000,
22
+ 0x4d1b00, 0x402600, 0x173d00, 0x580830, 0x00401c, 0x05266e, 0x390a7a, 0x084200,
23
+ 0x000000, 0x323232, 0x651c1d, 0x004847, 0x5c126a, 0x055205, 0x26278c, 0x3e3e00,
24
+ 0x5a2900, 0x4e3300, 0x254a00, 0x66163d, 0x004e2a, 0x13347c, 0x461787, 0x165000,
25
+ 0x000000, 0x404040, 0x722a2b, 0x0d5555, 0x692077, 0x126013, 0x34349a, 0x4b4b00,
26
+ 0x683706, 0x5b4100, 0x325800, 0x73234b, 0x0c5c38, 0x21418a, 0x542595, 0x235d00,
27
+ 0x000000, 0x525252, 0x853c3d, 0x1f6867, 0x7b328a, 0x257225, 0x4646ac, 0x5e5e00,
28
+ 0x7a4918, 0x6d5303, 0x456a00, 0x85365d, 0x1e6e4a, 0x33549c, 0x6637a7, 0x366f04,
29
+ 0x000000, 0x767676, 0xa96162, 0x448c8b, 0xa056ae, 0x49964a, 0x6b6bd1, 0x82821c,
30
+ 0x9f6d3c, 0x927828, 0x698f1d, 0xaa5a82, 0x43926e, 0x5778c0, 0x8b5ccc, 0x5a9429,
31
+ 0x000000, 0x929292, 0xc47c7d, 0x5fa7a7, 0xbb72c9, 0x64b265, 0x8686ec, 0x9d9d38,
32
+ 0xba8857, 0xad9343, 0x84aa38, 0xc5759d, 0x5eae8a, 0x7293dc, 0xa677e7, 0x75af44,
33
+ 0x000000, 0xbfbfbf, 0xf2aaaa, 0x8dd5d4, 0xe99ff7, 0x92df92, 0xb4b4ff, 0xcbcb65,
34
+ 0xe8b685, 0xdbc071, 0xb2d766, 0xf3a3cb, 0x8cdbb7, 0xa0c1ff, 0xd4a4ff, 0xa3dd72,
35
+ 0x000000, 0xffffff, 0xffe9ea, 0xccffff, 0xffdfff, 0xd2ffd2, 0xf3f3ff, 0xffffa5,
36
+ 0xfff6c5, 0xffffb1, 0xf2ffa6, 0xffe3ff, 0xcbfff7, 0xe0ffff, 0xffe4ff, 0xe3ffb1
37
+ ].freeze
38
+
39
+ def palette = PALETTE
40
+ end
41
+ end
42
+ end
43
+ end