vce.rsannotatedvce.rssource173 lines · 5.3 KB · raw

HuC6260 VCE (video color encoder)

3use crate::video::WordByte;
4use crate::video::palette::PcePalette;
5use bincode::{Decode, Encode};
6use jgenesis_common::boxedarray::BoxedWordArray;
7use jgenesis_common::frontend::Color;
8use jgenesis_common::num::{GetBit, U16Ext};
9use std::iter;
11pub const MAX_LINES_PER_FRAME: usize = 263;
12
13pub const CRAM_LEN_WORDS: usize = 512;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
16pub enum DotClockDivider {
17    #[default]
18    Four = 4, // ~5.37 MHz, commonly H256px
19    Three = 3, // ~7.13 MHz, commonly H304px to H352px
20    Two = 2,   // ~10.69 MHz, commonly H512px (not used by commercial releases)
21}
22
23impl DotClockDivider {
24    fn from_bits(bits: u8) -> Self {
25        match bits & 3 {
26            0 => Self::Four,
27            1 => Self::Three,
28            2 | 3 => Self::Two,
29            _ => unreachable!("value & 3 is always <= 3"),
30        }
31    }
32
33    pub fn divide_difference(self, cycles: u64, prev_cycles: u64) -> u64 {
34        debug_assert!(cycles >= prev_cycles);
35
36        // This is faster than dividing by u64::from(self) because it avoids div instructions and
37        // dot clock divider rarely changes
38        match self {
39            Self::Four => (cycles >> 2) - (prev_cycles >> 2),
40            Self::Three => (cycles / 3) - (prev_cycles / 3),
41            Self::Two => (cycles >> 1) - (prev_cycles >> 1),
42        }
43    }
44
45    pub fn divide(self, cycles: u64) -> u64 {
46        // Same as in divide_difference(), avoids div instructions
47        match self {
48            Self::Four => cycles >> 2,
49            Self::Three => cycles / 3,
50            Self::Two => cycles >> 1,
51        }
52    }
53}
54
55impl From<DotClockDivider> for u64 {
56    fn from(value: DotClockDivider) -> Self {
57        match value {
58            DotClockDivider::Four => 4,
59            DotClockDivider::Three => 3,
60            DotClockDivider::Two => 2,
61        }
62    }
63}
64
65#[derive(Debug, Clone, Encode, Decode)]
66pub struct Vce {
67    cram: BoxedWordArray<CRAM_LEN_WORDS>,
68    dot_clock_divider: DotClockDivider,
69    extra_line_per_frame: bool,
70    greyscale: bool,
71    color_table_address: u16,
72}
73
74impl Vce {
75    pub fn new() -> Self {
76        Self {
77            cram: BoxedWordArray::new(),
78            dot_clock_divider: DotClockDivider::default(),
79            extra_line_per_frame: false,
80            greyscale: false,
81            color_table_address: 0,
82        }
83    }
84
85    pub fn overscan_color(&self) -> u16 {
86        // Sprite color 0
87        self.cram[0x100]
88    }
89
90    pub fn read_color(&self, color_idx: u16) -> u16 {
91        self.cram[(color_idx as usize) & (CRAM_LEN_WORDS - 1)]
92    }
93
94    pub fn dot_clock_divider(&self) -> DotClockDivider {
95        self.dot_clock_divider
96    }
97
98    pub fn lines_per_frame(&self) -> u16 {
99        (MAX_LINES_PER_FRAME as u16) - 1 + u16::from(self.extra_line_per_frame)
100    }
101
102    pub fn greyscale(&self) -> bool {
103        self.greyscale
104    }
105
106    // $1FE400: CR (Control register)
107    pub fn write_control(&mut self, value: u8) {
108        self.dot_clock_divider = DotClockDivider::from_bits(value);
109        self.extra_line_per_frame = value.bit(2);
110        self.greyscale = value.bit(7);
111
112        log::trace!("CR write: {value:02X}");
113        log::trace!("  Dot clock divider: {}", u64::from(self.dot_clock_divider));
114        log::trace!("  Lines per frame: {}", if self.extra_line_per_frame { 263 } else { 262 });
115        log::trace!("  Monochrome: {}", self.greyscale);
116    }
117
118    // $1FE402-$1FE403: CTA (Color table address register)
119    pub fn write_color_address(&mut self, value: u8, byte: WordByte) {
120        match byte {
121            WordByte::Low => self.color_table_address.set_lsb(value),
122            WordByte::High => self.color_table_address.set_msb(value & 1),
123        }
124
125        log::trace!("CTA {byte:?} write: {value:02X}");
126        log::trace!("  Color table address: {:03X}", self.color_table_address);
127    }
128
129    // $1FE404-$1FE405: CTR (Color table data read register)
130    pub fn read_color_data(&mut self, byte: WordByte) -> u8 {
131        log::trace!("CTR {byte:?} read (current address {:03X})", self.color_table_address);
132
133        // Highest 7 bits always read 1
134        let color = self.cram[self.color_table_address as usize] | !0x1FF;
135
136        if byte == WordByte::High {
137            self.increment_color_table_address();
138        }
139
140        byte.get(color)
141    }
142
143    // $1FE404-$1FE405: CTW (Color table data write register)
144    pub fn write_color_data(&mut self, value: u8, byte: WordByte) {
145        log::trace!(
146            "CTW {byte:?} write: {value:02X} (current address {:03X})",
147            self.color_table_address
148        );
149
150        let color = &mut self.cram[self.color_table_address as usize];
151        match byte {
152            WordByte::Low => color.set_lsb(value),
153            WordByte::High => color.set_msb(value & 1),
154        }
155
156        if byte == WordByte::High {
157            self.increment_color_table_address();
158        }
159    }
160
161    fn increment_color_table_address(&mut self) {
162        self.color_table_address = (self.color_table_address + 1) & (CRAM_LEN_WORDS - 1) as u16;
163    }
164
165    pub fn dump_palettes(&self, out: &mut [Color], palette: &PcePalette) {
166        for (cram_color, out_color) in
167            iter::zip(self.cram.iter().copied(), &mut out[..CRAM_LEN_WORDS])
168        {
169            let (r, g, b) = palette[(cram_color & 0x1FF) as usize];
170            *out_color = Color::rgb(r, g, b);
171        }
172    }
173}