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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +40 -0
- data/exe/badline +55 -0
- data/lib/badline/address_bus.rb +206 -0
- data/lib/badline/addressable.rb +61 -0
- data/lib/badline/cartridge/magic_desk.rb +23 -0
- data/lib/badline/cartridge/ocean.rb +33 -0
- data/lib/badline/cartridge/standard.rb +31 -0
- data/lib/badline/cartridge.rb +75 -0
- data/lib/badline/chrout_trap.rb +39 -0
- data/lib/badline/cia/timer.rb +122 -0
- data/lib/badline/cia.rb +189 -0
- data/lib/badline/color_memory.rb +16 -0
- data/lib/badline/computer.rb +100 -0
- data/lib/badline/control_ports.rb +24 -0
- data/lib/badline/cpu.rb +239 -0
- data/lib/badline/cycleable.rb +35 -0
- data/lib/badline/gui/application.rb +94 -0
- data/lib/badline/gui/joy_map.rb +19 -0
- data/lib/badline/gui/key_map.rb +34 -0
- data/lib/badline/gui/palette.rb +35 -0
- data/lib/badline/gui/pane.rb +35 -0
- data/lib/badline/gui/screen_pane.rb +50 -0
- data/lib/badline/gui/window.rb +46 -0
- data/lib/badline/gui.rb +11 -0
- data/lib/badline/instruction.rb +334 -0
- data/lib/badline/instruction_set/arithmetic.rb +119 -0
- data/lib/badline/instruction_set/bitwise.rb +131 -0
- data/lib/badline/instruction_set/branch.rb +78 -0
- data/lib/badline/instruction_set/flag.rb +63 -0
- data/lib/badline/instruction_set/illegal.rb +278 -0
- data/lib/badline/instruction_set/inc_dec.rb +71 -0
- data/lib/badline/instruction_set/stack.rb +104 -0
- data/lib/badline/instruction_set/transfer.rb +137 -0
- data/lib/badline/instruction_set.rb +77 -0
- data/lib/badline/integer_helper.rb +39 -0
- data/lib/badline/joystick.rb +25 -0
- data/lib/badline/kernal_trap/file.rb +54 -0
- data/lib/badline/kernal_trap/load.rb +63 -0
- data/lib/badline/kernal_trap/save.rb +42 -0
- data/lib/badline/kernal_trap.rb +5 -0
- data/lib/badline/keyboard.rb +58 -0
- data/lib/badline/keyboard_buffer.rb +33 -0
- data/lib/badline/media.rb +59 -0
- data/lib/badline/memory.rb +43 -0
- data/lib/badline/rom.rb +23 -0
- data/lib/badline/roms/README +18 -0
- data/lib/badline/roms/basic.rom +0 -0
- data/lib/badline/roms/character.rom +0 -0
- data/lib/badline/roms/kernal.rom +0 -0
- data/lib/badline/sid.rb +25 -0
- data/lib/badline/status.rb +56 -0
- data/lib/badline/storage/crt_file.rb +53 -0
- data/lib/badline/storage/d64_image.rb +21 -0
- data/lib/badline/storage/d71_image.rb +13 -0
- data/lib/badline/storage/d81_image.rb +14 -0
- data/lib/badline/storage/disk_image.rb +71 -0
- data/lib/badline/storage/host_directory.rb +49 -0
- data/lib/badline/storage/p00.rb +24 -0
- data/lib/badline/storage.rb +28 -0
- data/lib/badline/time_of_day.rb +101 -0
- data/lib/badline/traps.rb +15 -0
- data/lib/badline/version.rb +5 -0
- data/lib/badline/vic/bank.rb +65 -0
- data/lib/badline/vic/display_state.rb +78 -0
- data/lib/badline/vic/graphics_mode.rb +139 -0
- data/lib/badline/vic/registers.rb +170 -0
- data/lib/badline/vic/sequencer.rb +237 -0
- data/lib/badline/vic/sprite.rb +121 -0
- data/lib/badline/vic/sprites.rb +112 -0
- data/lib/badline/vic.rb +192 -0
- data/lib/badline.rb +29 -0
- 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
|