badline 0.1.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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +40 -0
  4. data/exe/badline +55 -0
  5. data/lib/badline/address_bus.rb +206 -0
  6. data/lib/badline/addressable.rb +61 -0
  7. data/lib/badline/cartridge/magic_desk.rb +23 -0
  8. data/lib/badline/cartridge/ocean.rb +33 -0
  9. data/lib/badline/cartridge/standard.rb +31 -0
  10. data/lib/badline/cartridge.rb +75 -0
  11. data/lib/badline/chrout_trap.rb +39 -0
  12. data/lib/badline/cia/timer.rb +122 -0
  13. data/lib/badline/cia.rb +189 -0
  14. data/lib/badline/color_memory.rb +16 -0
  15. data/lib/badline/computer.rb +100 -0
  16. data/lib/badline/control_ports.rb +24 -0
  17. data/lib/badline/cpu.rb +239 -0
  18. data/lib/badline/cycleable.rb +35 -0
  19. data/lib/badline/gui/application.rb +94 -0
  20. data/lib/badline/gui/joy_map.rb +19 -0
  21. data/lib/badline/gui/key_map.rb +34 -0
  22. data/lib/badline/gui/palette.rb +35 -0
  23. data/lib/badline/gui/pane.rb +35 -0
  24. data/lib/badline/gui/screen_pane.rb +50 -0
  25. data/lib/badline/gui/window.rb +46 -0
  26. data/lib/badline/gui.rb +11 -0
  27. data/lib/badline/instruction.rb +334 -0
  28. data/lib/badline/instruction_set/arithmetic.rb +119 -0
  29. data/lib/badline/instruction_set/bitwise.rb +131 -0
  30. data/lib/badline/instruction_set/branch.rb +78 -0
  31. data/lib/badline/instruction_set/flag.rb +63 -0
  32. data/lib/badline/instruction_set/illegal.rb +278 -0
  33. data/lib/badline/instruction_set/inc_dec.rb +71 -0
  34. data/lib/badline/instruction_set/stack.rb +104 -0
  35. data/lib/badline/instruction_set/transfer.rb +137 -0
  36. data/lib/badline/instruction_set.rb +77 -0
  37. data/lib/badline/integer_helper.rb +39 -0
  38. data/lib/badline/joystick.rb +25 -0
  39. data/lib/badline/kernal_trap/file.rb +54 -0
  40. data/lib/badline/kernal_trap/load.rb +63 -0
  41. data/lib/badline/kernal_trap/save.rb +42 -0
  42. data/lib/badline/kernal_trap.rb +5 -0
  43. data/lib/badline/keyboard.rb +58 -0
  44. data/lib/badline/keyboard_buffer.rb +33 -0
  45. data/lib/badline/media.rb +59 -0
  46. data/lib/badline/memory.rb +43 -0
  47. data/lib/badline/rom.rb +23 -0
  48. data/lib/badline/roms/README +18 -0
  49. data/lib/badline/roms/basic.rom +0 -0
  50. data/lib/badline/roms/character.rom +0 -0
  51. data/lib/badline/roms/kernal.rom +0 -0
  52. data/lib/badline/sid.rb +25 -0
  53. data/lib/badline/status.rb +56 -0
  54. data/lib/badline/storage/crt_file.rb +53 -0
  55. data/lib/badline/storage/d64_image.rb +21 -0
  56. data/lib/badline/storage/d71_image.rb +13 -0
  57. data/lib/badline/storage/d81_image.rb +14 -0
  58. data/lib/badline/storage/disk_image.rb +71 -0
  59. data/lib/badline/storage/host_directory.rb +49 -0
  60. data/lib/badline/storage/p00.rb +24 -0
  61. data/lib/badline/storage.rb +28 -0
  62. data/lib/badline/time_of_day.rb +101 -0
  63. data/lib/badline/traps.rb +15 -0
  64. data/lib/badline/version.rb +5 -0
  65. data/lib/badline/vic/bank.rb +65 -0
  66. data/lib/badline/vic/display_state.rb +78 -0
  67. data/lib/badline/vic/graphics_mode.rb +139 -0
  68. data/lib/badline/vic/registers.rb +170 -0
  69. data/lib/badline/vic/sequencer.rb +237 -0
  70. data/lib/badline/vic/sprite.rb +121 -0
  71. data/lib/badline/vic/sprites.rb +112 -0
  72. data/lib/badline/vic.rb +192 -0
  73. data/lib/badline.rb +29 -0
  74. metadata +131 -0
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class VIC < Cycleable
5
+ # The 16K window the VIC sees into RAM, selected by CIA2 $DD00. Character
6
+ # ROM shadows $1000-$1FFF in banks 0 and 2.
7
+ class Bank
8
+ include Addressable
9
+
10
+ # The CIA2 port A bank bits are inverted: %11 selects bank 0.
11
+ BANK_STARTS = [0xc000, 0x8000, 0x4000, 0x0000].freeze
12
+
13
+ attr_reader :address_bus
14
+
15
+ def initialize(address_bus = nil)
16
+ addressable_at(0x0000, length: 2**14)
17
+ @address_bus = address_bus || AddressBus.new
18
+ end
19
+
20
+ def peek(offset)
21
+ return ultimax_peek(offset) if @address_bus.ultimax
22
+
23
+ bits = bank_switch_register
24
+ if bits.allbits?(0b01) && (offset & 0xf000) == 0x1000
25
+ @address_bus.character_rom.peek(0xc000 + offset)
26
+ else
27
+ @address_bus.ram.peek(BANK_STARTS[bits] + offset)
28
+ end
29
+ end
30
+
31
+ def peek_color(offset)
32
+ address_bus.color_ram.peek(0xd800 + offset) & 0x0f
33
+ end
34
+
35
+ def poke(_addr, _value)
36
+ raise ReadOnlyMemoryError
37
+ end
38
+
39
+ def start
40
+ BANK_STARTS[bank_switch_register]
41
+ end
42
+
43
+ private
44
+
45
+ # In Ultimax mode the cartridge ROMH replaces the character ROM
46
+ # shadow, visible at $3000-$3FFF of the window.
47
+ def ultimax_peek(offset)
48
+ romh = @address_bus.cartridge.romh
49
+ if romh && offset.allbits?(0x3000)
50
+ romh.peek(0xe000 + (offset & 0x1fff))
51
+ else
52
+ @address_bus.ram.peek(BANK_STARTS[bank_switch_register] + offset)
53
+ end
54
+ end
55
+
56
+ def bank_switch_register
57
+ @address_bus.cia2.port_a_lines & 0b11
58
+ end
59
+
60
+ def character_rom?
61
+ bank_switch_register.allbits?(0b01)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class VIC < Cycleable
5
+ class DisplayState
6
+ FIRST_LINE = 0x30 # 48: bad lines and the DEN-at-$30 latch begin here
7
+ LAST_LINE = 0xf7 # 247
8
+ COLUMNS_PER_ROW = 40
9
+
10
+ attr_reader :vc_base, :rc
11
+
12
+ def initialize(registers)
13
+ @registers = registers
14
+ @vc_base = 0
15
+ @rc = 0
16
+ @display = false
17
+ @bad_lines_enabled = false
18
+ @bad_line = false
19
+ end
20
+
21
+ def display? = @display
22
+ def idle? = !@display
23
+ def bad_line? = @bad_line
24
+
25
+ def new_frame
26
+ @vc_base = 0
27
+ @bad_lines_enabled = false
28
+ end
29
+
30
+ def new_line
31
+ @bad_line = false
32
+ end
33
+
34
+ def cycle(rasterline, column)
35
+ @bad_lines_enabled = true if rasterline == FIRST_LINE && @registers.display_enabled?
36
+
37
+ load_vc(rasterline) if column == 14
38
+ trigger_bad_line(rasterline) if column.between?(12, 54)
39
+ check_row_counter if column == 58
40
+ end
41
+
42
+ private
43
+
44
+ def load_vc(rasterline)
45
+ return unless bad_line_condition?(rasterline)
46
+
47
+ @bad_line = true
48
+ @display = true
49
+ @rc = 0
50
+ end
51
+
52
+ def trigger_bad_line(rasterline)
53
+ return if @bad_line
54
+ return unless bad_line_condition?(rasterline)
55
+
56
+ @bad_line = true
57
+ @display = true
58
+ end
59
+
60
+ def bad_line_condition?(rasterline)
61
+ @bad_lines_enabled &&
62
+ rasterline.between?(FIRST_LINE, LAST_LINE) &&
63
+ (rasterline & 0b111) == @registers.yscroll
64
+ end
65
+
66
+ def check_row_counter
67
+ return unless @display
68
+
69
+ if @rc == 7
70
+ @display = false
71
+ @vc_base = (@vc_base + COLUMNS_PER_ROW) & 0x3ff
72
+ else
73
+ @rc = (@rc + 1) & 0b111
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class VIC < Cycleable
5
+ module GraphicsMode
6
+ # Foreground masks depend only on the data byte, so each byte maps to a
7
+ # precomputed frozen pattern shared by reference instead of being
8
+ # written out pixel by pixel.
9
+ HIRES_FG = Array.new(256) do |data|
10
+ Array.new(8) { |i| data.anybits?(1 << (7 - i)) }.freeze
11
+ end.freeze
12
+
13
+ # The high bit of each 2-bit pair (10/11) is foreground.
14
+ PAIR_FG = Array.new(256) do |data|
15
+ Array.new(8) { |i| (data >> (6 - (i & ~1))).allbits?(0b10) }.freeze
16
+ end.freeze
17
+
18
+ NO_FG = HIRES_FG[0]
19
+
20
+ module Hires
21
+ def paint_hires(data, color, background, seq)
22
+ seq.cur_fg = HIRES_FG[data]
23
+ colors = seq.cur_colors
24
+ return colors.fill(background) if data.zero?
25
+
26
+ i = 0
27
+ while i < 8
28
+ colors[i] = data.anybits?(1 << (7 - i)) ? color : background
29
+ i += 1
30
+ end
31
+ end
32
+ end
33
+
34
+ # Decodes 2-bit pixel pairs into double-wide pixels. The colour for each
35
+ # pair is supplied by the block.
36
+ module Multicolor
37
+ def paint_pairs(data, seq)
38
+ seq.cur_fg = PAIR_FG[data]
39
+ colors = seq.cur_colors
40
+ i = 0
41
+ while i < 8
42
+ colors[i] = yield((data >> (6 - (i & ~1))) & 0b11)
43
+ i += 1
44
+ end
45
+ end
46
+ end
47
+
48
+ class Text
49
+ include Hires
50
+
51
+ def decode(screencode, color, _cell, row, seq)
52
+ registers = seq.registers
53
+ data = seq.bank.peek(registers.char_base + (screencode * 8) + row)
54
+ paint_hires(data, color, registers.background, seq)
55
+ end
56
+ end
57
+
58
+ class MulticolorText
59
+ include Hires
60
+ include Multicolor
61
+
62
+ def decode(screencode, color, _cell, row, seq)
63
+ registers = seq.registers
64
+ data = seq.bank.peek(registers.char_base + (screencode * 8) + row)
65
+ if color.anybits?(0x08)
66
+ paint_pairs(data, seq) do |pair|
67
+ multicolor_pixel(pair, color, registers)
68
+ end
69
+ else
70
+ paint_hires(data, color & 0x07, registers.background, seq)
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def multicolor_pixel(pair, color, registers)
77
+ case pair
78
+ when 0b00 then registers.background(0)
79
+ when 0b01 then registers.background(1)
80
+ when 0b10 then registers.background(2)
81
+ else color & 0x07
82
+ end
83
+ end
84
+ end
85
+
86
+ class ExtendedBackgroundText
87
+ include Hires
88
+
89
+ def decode(screencode, color, _cell, row, seq)
90
+ registers = seq.registers
91
+ background = registers.background((screencode >> 6) & 0b11)
92
+ data = seq.bank.peek(registers.char_base + ((screencode & 0x3f) * 8) + row)
93
+ paint_hires(data, color, background, seq)
94
+ end
95
+ end
96
+
97
+ class Bitmap
98
+ include Hires
99
+
100
+ def decode(screencode, _color, cell, row, seq)
101
+ data = seq.bank.peek(seq.registers.bitmap_base + (cell * 8) + row)
102
+ foreground = (screencode >> 4) & 0x0f
103
+ background = screencode & 0x0f
104
+ paint_hires(data, foreground, background, seq)
105
+ end
106
+ end
107
+
108
+ class MulticolorBitmap
109
+ include Multicolor
110
+
111
+ def decode(screencode, color, cell, row, seq)
112
+ registers = seq.registers
113
+ data = seq.bank.peek(registers.bitmap_base + (cell * 8) + row)
114
+ paint_pairs(data, seq) do |pair|
115
+ multicolor_pixel(pair, screencode, color, registers)
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def multicolor_pixel(pair, screencode, color, registers)
122
+ case pair
123
+ when 0b00 then registers.background(0)
124
+ when 0b01 then (screencode >> 4) & 0x0f
125
+ when 0b10 then screencode & 0x0f
126
+ else color & 0x0f
127
+ end
128
+ end
129
+ end
130
+
131
+ class Null
132
+ def decode(_screencode, _color, _cell, _row, seq)
133
+ seq.cur_fg = NO_FG
134
+ seq.cur_colors.fill(0)
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class VIC < Cycleable
5
+ # = VIC-II Registers
6
+ #
7
+ # == Sprite positions
8
+ #
9
+ # $D000: Sprite 0 X
10
+ # $D001: Sprite 0 Y
11
+ # $D002: Sprite 1 X
12
+ # $D003: Sprite 1 Y
13
+ # $D004: Sprite 2 X
14
+ # $D005: Sprite 2 Y
15
+ # $D006: Sprite 3 X
16
+ # $D007: Sprite 3 Y
17
+ # $D008: Sprite 4 X
18
+ # $D009: Sprite 4 Y
19
+ # $D00A: Sprite 5 X
20
+ # $D00B: Sprite 5 Y
21
+ # $D00C: Sprite 6 X
22
+ # $D00D: Sprite 6 Y
23
+ # $D00E: Sprite 7 X
24
+ # $D00F: Sprite 7 Y
25
+ #
26
+ # $D010: Most Significant Bits of Sprites 0-7 Horizontal Position
27
+ # $D011: Vertical Fine Scrolling and Control Register
28
+ # $D012: Rasterline
29
+ # - Read: Current rasterline
30
+ # - Write: Set rasterline interrupt
31
+ #
32
+ # == Light pen
33
+ #
34
+ # $D013: Light Pen Horizontal Position
35
+ # $D014: Light Pen Vertical Position
36
+ #
37
+ # $D015: Sprite Enable Register
38
+ # $D016: Horizontal Fine Scrolling and Control Register
39
+ # $D017: Sprite Vertical Expansion Register
40
+ # $D018: VIC-II Chip Memory Control Register
41
+ # $D019: VIC Interrupt Flag Register
42
+ # $D01A: IRQ Mask Register
43
+ # $D01B: Sprite to Foreground Display Priority Register
44
+ # $D01C: Sprite Multicolor Registers
45
+ # $D01D: Sprite Horizontal Expansion Register
46
+ #
47
+ # == Sprite collision detection
48
+ #
49
+ # $D01E: Sprite to Sprite Collision Register
50
+ # $D01F: Sprite to Foreground Collision Register
51
+ #
52
+ # == Color registers
53
+ #
54
+ # All color registers are 4bit, bits 4-7 always read 1.
55
+ #
56
+ # $D020: Border color - Default: 14, light blue
57
+ # $D021: Background color 0 - Default: 6, blue
58
+ # $D022: Background color 1 - Default: 1, white
59
+ # $D023: Background color 2 - Default: 2, red
60
+ # $D024: Background color 3 - Default: 3, cyan
61
+ # $D025: Sprite multicolor 0 - Default: 4, purple
62
+ # $D026: Sprite multicolor 1 - Default: 0, black
63
+ # $D027: Sprite 0 color - Default: 1, white
64
+ # $D028: Sprite 1 color - Default: 2, red
65
+ # $D029: Sprite 2 color - Default: 3, cyan
66
+ # $D02A: Sprite 3 color - Default: 4, purple
67
+ # $D02B: Sprite 4 color - Default: 5, green
68
+ # $D02C: Sprite 5 color - Default: 6, blue
69
+ # $D02D: Sprite 6 color - Default: 7, yellow
70
+ # $D02E: Sprite 7 color - Default: 12, medium gray
71
+ #
72
+ # $D02F-$D03F: Not in use, always reads 0xff
73
+ # $D040-$D3FF: Repeat $D0000 to $D03F every 64 bytes
74
+ class Registers
75
+ include IntegerHelper
76
+
77
+ def initialize
78
+ @bytes = Array.new(2**6, 0)
79
+ @irq_line = false
80
+ write_defaults
81
+ end
82
+
83
+ # True while any enabled latch bit is set in $D019/$D01A. Cached and
84
+ # re-evaluated on writes, since it's checked every cycle.
85
+ def irq_line? = @irq_line
86
+
87
+ def [](reg) = @bytes[reg]
88
+
89
+ def read(reg)
90
+ case reg
91
+ when 0x1a, 0x20..0x2e then @bytes[reg] | 0xf0
92
+ when 0x1e, 0x1f then read_clear(reg) # collision registers clear on read
93
+ when 0x2f..0x3f then 0xff
94
+ else @bytes[reg]
95
+ end
96
+ end
97
+
98
+ def write(reg, value)
99
+ case reg
100
+ when 0x19 # write 1 to clear
101
+ @bytes[reg] &= ~value
102
+ update_irq_line
103
+ when 0x1a # only the four latch bits
104
+ @bytes[reg] = value & 0x0f
105
+ update_irq_line
106
+ else @bytes[reg] = value
107
+ end
108
+ end
109
+
110
+ # Graphics modes
111
+ def ecm = @bytes[0x11][6]
112
+ def bmm = @bytes[0x11][5]
113
+ def mcm = @bytes[0x16][4]
114
+ # ECM/BMM from $D011 bits 6-5, MCM from $D016 bit 4.
115
+ def mode = ((@bytes[0x11] >> 4) & 0b110) | ((@bytes[0x16] >> 4) & 0b001)
116
+
117
+ def xscroll = @bytes[0x16] & 0b0111
118
+ def yscroll = @bytes[0x11] & 0b0111
119
+
120
+ def rsel? = @bytes[0x11].anybits?(0x08)
121
+ def csel? = @bytes[0x16].anybits?(0x08)
122
+ def display_enabled? = @bytes[0x11].anybits?(0x10)
123
+
124
+ def char_base = (@bytes[0x18] & 0b1110) * 0x400
125
+ def bitmap_base = (@bytes[0x18] & 0b1000) * 0x400
126
+ def screen_base = (@bytes[0x18] >> 4) * 0x400
127
+
128
+ def border = @bytes[0x20] & 0x0f
129
+ def background(index = 0) = @bytes[0x21 + index] & 0x0f
130
+
131
+ def raster_target = uint16(@bytes[0x12], (@bytes[0x11] & 0x80) >> 7)
132
+
133
+ def latch_irq!(bit)
134
+ @bytes[0x19] |= bit
135
+ update_irq_line
136
+ end
137
+
138
+ def latch_raster_irq! = latch_irq!(0x01)
139
+
140
+ def collide!(reg, bits)
141
+ return false if bits.zero?
142
+
143
+ was_zero = @bytes[reg].zero?
144
+ @bytes[reg] |= bits
145
+ was_zero
146
+ end
147
+
148
+ private
149
+
150
+ def update_irq_line
151
+ @irq_line = (@bytes[0x19] & @bytes[0x1a]).anybits?(0x0f)
152
+ end
153
+
154
+ def read_clear(reg)
155
+ @bytes[reg].tap { @bytes[reg] = 0 }
156
+ end
157
+
158
+ def write_defaults
159
+ write_each(0x20, [14, 6, 1, 2, 3, 4, 0, 1, 2, 3, 4, 5, 6, 7, 12])
160
+ write_each(0x11, [0x1b, 0]) # $D011: DEN=1, RSEL=1, YSCROLL=3
161
+ write_each(0x16, [0xc8]) # $D016: Text mode, XSCROLL=0
162
+ write_each(0x19, [0, 0]) # IRQ flags
163
+ end
164
+
165
+ def write_each(addr, values)
166
+ values.each_with_index { |v, i| @bytes[addr + i] = v }
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/vic/graphics_mode"
4
+
5
+ module Badline
6
+ class VIC < Cycleable
7
+ # = VIC-II Sequencer
8
+ #
9
+ # Turns fetched graphics data into output pixels.
10
+ class Sequencer
11
+ DISPLAY_X_BOUNDS = [
12
+ [135, 438].freeze,
13
+ [128, 447].freeze
14
+ ].freeze
15
+
16
+ # The graphics window always spans the full 40 columns, ignoring CSEL.
17
+ GFX_X_START = DISPLAY_X_BOUNDS[1][0]
18
+ GFX_X_END = DISPLAY_X_BOUNDS[1][1] + 1
19
+
20
+ # [left compare, right compare] for the border flip-flop per CSEL state.
21
+ WINDOW_COMPARES = [
22
+ [DISPLAY_X_BOUNDS[0][0], DISPLAY_X_BOUNDS[0][1] + 1].freeze,
23
+ [GFX_X_START, GFX_X_END].freeze
24
+ ].freeze
25
+
26
+ BORDER_Y_BOUNDS = [
27
+ [55, 247].freeze,
28
+ [51, 251].freeze
29
+ ].freeze
30
+
31
+ # Border coverage per 8-pixel group, tracked so apply_border can skip
32
+ # window groups and bulk-fill full border groups.
33
+ BORDER_FULL = 0
34
+ BORDER_NONE = 1
35
+ BORDER_MIXED = 2
36
+
37
+ NULL_MODE = GraphicsMode::Null.new
38
+ MODES = [
39
+ GraphicsMode::Text.new, # 000 standard text
40
+ GraphicsMode::MulticolorText.new, # 001 multicolour text
41
+ GraphicsMode::Bitmap.new, # 010 standard bitmap
42
+ GraphicsMode::MulticolorBitmap.new, # 011 multicolour bitmap
43
+ GraphicsMode::ExtendedBackgroundText.new, # 100 ECM text
44
+ NULL_MODE, # 101 invalid
45
+ NULL_MODE, # 110 invalid
46
+ NULL_MODE # 111 invalid
47
+ ].freeze
48
+
49
+ attr_reader :colors, :fg, :border, :registers, :bank, :cur_colors
50
+ # The fg masks are shared frozen patterns assigned by reference, never
51
+ # mutated in place.
52
+ attr_accessor :cur_fg
53
+
54
+ def initialize(width, registers, bank)
55
+ @width = width
56
+ @registers = registers
57
+ @bank = bank
58
+ @colors = Array.new(width, 0)
59
+ @fg = Array.new(width, false)
60
+ @border = Array.new(width, true)
61
+ @border_groups = Array.new(width / 8, BORDER_FULL)
62
+ @cur_colors = Array.new(8, 0)
63
+ @cur_fg = GraphicsMode::NO_FG
64
+ @prev_colors = Array.new(8, 0)
65
+ @prev_fg = GraphicsMode::NO_FG
66
+ @vertical_border = true
67
+ @main_border = true
68
+ new_line(0)
69
+ end
70
+
71
+ # Reset the line buffers at the start of a rasterline and re-evaluate the
72
+ # vertical border flip-flop.
73
+ def new_line(line)
74
+ update_vertical_border(line)
75
+ @colors.fill(@registers.border)
76
+ @fg.fill(false)
77
+ @border_groups.fill(BORDER_FULL)
78
+ @prev_colors.fill(@registers.background)
79
+ @prev_fg = GraphicsMode::NO_FG
80
+ end
81
+
82
+ # Repaint the border over the composited line, hiding the sprites.
83
+ def apply_border
84
+ color = @registers.border
85
+ group = 0
86
+ while group < @border_groups.length
87
+ case @border_groups[group]
88
+ when BORDER_FULL then @colors.fill(color, group * 8, 8)
89
+ when BORDER_MIXED then paint_mixed_border(color, group * 8)
90
+ end
91
+ group += 1
92
+ end
93
+ end
94
+
95
+ def emit(screencode, color, col, cell, row)
96
+ MODES[@registers.mode].decode(screencode, color, cell, row, self)
97
+ output(col)
98
+ roll
99
+ end
100
+
101
+ def emit_idle(col)
102
+ @cur_colors.fill(@registers.background)
103
+ @cur_fg = GraphicsMode::NO_FG
104
+ output(col)
105
+ roll
106
+ end
107
+
108
+ private
109
+
110
+ # Write the 8-pixel group for a column into the line buffers. The main
111
+ # border flip-flop only changes state in the groups containing the
112
+ # window edge compares, so all other groups take a branch-free bulk
113
+ # path: fully border or fully window.
114
+ def output(col)
115
+ x_pos = (col + 16) * 8
116
+ win_lo, right_compare = WINDOW_COMPARES[@registers.csel? ? 1 : 0]
117
+
118
+ if boundary_group?(x_pos, win_lo, right_compare)
119
+ output_boundary(col, x_pos, win_lo, right_compare)
120
+ elsif @main_border || @vertical_border
121
+ output_border(x_pos)
122
+ else
123
+ output_window(col, x_pos)
124
+ end
125
+ end
126
+
127
+ # True if the group contains a window edge, where the main border
128
+ # flip-flop can change state.
129
+ def boundary_group?(x_pos, win_lo, right_compare)
130
+ lo_delta = win_lo - x_pos
131
+ hi_delta = right_compare - x_pos
132
+ (lo_delta >= 0 && lo_delta < 8) || (hi_delta >= 0 && hi_delta < 8)
133
+ end
134
+
135
+ def output_border(x_pos)
136
+ @colors.fill(@registers.border, x_pos, 8)
137
+ @border_groups[x_pos >> 3] = BORDER_FULL
138
+ @fg.fill(false, x_pos, 8)
139
+ end
140
+
141
+ def output_window(col, x_pos)
142
+ in_gfx = x_pos >= GFX_X_START && x_pos < GFX_X_END
143
+ @border_groups[x_pos >> 3] = BORDER_NONE
144
+
145
+ if @registers.xscroll.zero?
146
+ @colors[x_pos, 8] = @cur_colors
147
+ if in_gfx
148
+ @fg[x_pos, 8] = @cur_fg
149
+ else
150
+ @fg.fill(false, x_pos, 8)
151
+ end
152
+ else
153
+ output_window_shifted(col, x_pos, in_gfx)
154
+ end
155
+ end
156
+
157
+ def output_window_shifted(col, x_pos, in_gfx)
158
+ shift = @registers.xscroll
159
+ keep = 8 - shift
160
+
161
+ @colors[x_pos + shift, keep] = @cur_colors[0, keep]
162
+ if col.positive? # the left column only exists from column 1 on
163
+ @colors[x_pos, shift] = @prev_colors[keep, shift]
164
+ else
165
+ @colors.fill(@registers.background, x_pos, shift)
166
+ end
167
+ output_shifted_fg(col, x_pos, in_gfx, shift, keep)
168
+ end
169
+
170
+ def output_shifted_fg(col, x_pos, in_gfx, shift, keep)
171
+ return @fg.fill(false, x_pos, 8) unless in_gfx
172
+
173
+ @fg[x_pos + shift, keep] = @cur_fg[0, keep]
174
+ if col.positive?
175
+ @fg[x_pos, shift] = @prev_fg[keep, shift]
176
+ else
177
+ @fg.fill(false, x_pos, shift)
178
+ end
179
+ end
180
+
181
+ # Slow path for the groups where the border flip-flop can change state.
182
+ def output_boundary(col, x_pos, win_lo, right_compare)
183
+ shift = @registers.xscroll
184
+ bg = @registers.background
185
+ bleed = col.positive?
186
+ border = @registers.border
187
+ @border_groups[x_pos >> 3] = BORDER_MIXED
188
+
189
+ i = 0
190
+ while i < 8
191
+ src = i - shift
192
+ if src >= 0
193
+ pixel = @cur_colors[src]
194
+ mask = @cur_fg[src]
195
+ elsif bleed
196
+ pixel = @prev_colors[8 + src]
197
+ mask = @prev_fg[8 + src]
198
+ else
199
+ pixel = bg
200
+ mask = false
201
+ end
202
+ x = x_pos + i
203
+ shown = pixel_shown?(x, win_lo, right_compare)
204
+ @colors[x] = shown ? pixel : border
205
+ @border[x] = !shown
206
+ @fg[x] = x >= GFX_X_START && x < GFX_X_END ? mask : false
207
+ i += 1
208
+ end
209
+ end
210
+
211
+ def paint_mixed_border(color, x_pos)
212
+ i = 0
213
+ while i < 8
214
+ @colors[x_pos + i] = color if @border[x_pos + i]
215
+ i += 1
216
+ end
217
+ end
218
+
219
+ def pixel_shown?(pixel_x, left_compare, right_compare)
220
+ @main_border = true if pixel_x == right_compare
221
+ @main_border = false if pixel_x == left_compare && !@vertical_border
222
+ !(@main_border || @vertical_border)
223
+ end
224
+
225
+ def roll
226
+ @prev_colors, @cur_colors = @cur_colors, @prev_colors
227
+ @prev_fg, @cur_fg = @cur_fg, @prev_fg
228
+ end
229
+
230
+ def update_vertical_border(line)
231
+ top, bottom = BORDER_Y_BOUNDS[@registers.rsel? ? 1 : 0]
232
+ @vertical_border = true if line == bottom
233
+ @vertical_border = false if line == top && @registers.display_enabled?
234
+ end
235
+ end
236
+ end
237
+ end