video.rsannotatedvideo.rssource393 lines · 12.3 KB · raw
1mod palette;
2mod vce;
3mod vdc;
4
5use crate::api;
6use crate::api::PceEmulatorConfig;
7use crate::video::palette::PcePalette;
8use crate::video::vce::Vce;
9use crate::video::vdc::Vdc;
10use bincode::{Decode, Encode};
11use jgenesis_common::frontend::{
12    Color, CompositeParams, FiniteF64, FrameSize, NtscPerFrameParams, SamplesPerColorCycle,
13};
14use jgenesis_common::num::U16Ext;
15use jgenesis_proc_macros::{FakeDecode, FakeEncode};
16
17pub const FRAME_BUFFER_LEN: usize = vdc::FRAME_BUFFER_WIDTH * vdc::FRAME_BUFFER_HEIGHT;

Number of mclk cycles per scanline is fixed, unaffected by dot clock divider

20pub const MCLK_CYCLES_PER_SCANLINE: u64 = 1365;
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23enum WordByte {
24    Low,
25    High,
26}
27
28impl WordByte {
29    fn get(self, word: u16) -> u8 {
30        match self {
31            Self::Low => word.lsb(),
32            Self::High => word.msb(),
33        }
34    }
35
36    fn set(self, word: &mut u16, byte: u8) {
37        match self {
38            Self::Low => word.set_lsb(byte),
39            Self::High => word.set_msb(byte),
40        }
41    }
42}
43
44#[derive(Debug, Clone, Encode, Decode)]
45struct VideoState {
46    scanline: u16,
47    scanline_mclk: u64,
48    cycles: u64,
49    frame_start_cycles: u64,
50    frame_x_divider: u32,
51}
52
53impl VideoState {
54    fn new() -> Self {
55        Self { scanline: 0, scanline_mclk: 0, cycles: 0, frame_start_cycles: 0, frame_x_divider: 1 }
56    }
57}
58
59#[derive(Debug, Clone, FakeEncode, FakeDecode)]
60struct Rgba8FrameBuffer(Box<[Color; FRAME_BUFFER_LEN]>);
61
62impl Default for Rgba8FrameBuffer {
63    fn default() -> Self {
64        Self(vec![Color::default(); FRAME_BUFFER_LEN].into_boxed_slice().try_into().unwrap())
65    }
66}
67
68struct FrameRenderParams {
69    left_offset: usize,
70    top_offset: usize,
71    width: usize,
72    height: usize,
73}
74
75impl FrameRenderParams {
76    const DEFAULT: Self = Self {
77        left_offset: 0,
78        top_offset: 0,
79        width: vdc::FRAME_BUFFER_WIDTH,
80        height: vdc::FRAME_BUFFER_HEIGHT,
81    };
82
83    const CROP_OVERSCAN: Self = Self {
84        left_offset: 4 * vdc::OVERSCAN_DOTS_DIV_4 as usize,
85        top_offset: 9,
86        width: vdc::FRAME_BUFFER_WIDTH - 2 * 4 * vdc::OVERSCAN_DOTS_DIV_4 as usize,
87        height: vdc::FRAME_BUFFER_HEIGHT - 2 * 9,
88    };
89}
90
91#[derive(Debug, Clone, Encode, Decode)]
92pub struct VideoSubsystem {
93    vdc: Vdc,
94    vce: Vce,
95    state: VideoState,
96    frame_buffer: Rgba8FrameBuffer,
97    // Greyscale bit appears to work by suppressing the NTSC color burst, so it's effectively latched
98    // per-line even though it doesn't really work that way in hardware
99    greyscale_per_line: Box<[bool; vce::MAX_LINES_PER_FRAME]>,
100    palette: Box<PcePalette>,
101    greyscale_palette: Box<PcePalette>,
102    crop_overscan: bool,
103}
104
105impl VideoSubsystem {
106    pub fn new(config: PceEmulatorConfig) -> Self {
107        let palette = palette::for_type(config.palette);
108        let greyscale_palette = palette::make_greyscale(palette);
109
110        Self {
111            vdc: Vdc::new(config),
112            vce: Vce::new(),
113            state: VideoState::new(),
114            frame_buffer: Rgba8FrameBuffer::default(),
115            greyscale_per_line: vec![false; vce::MAX_LINES_PER_FRAME]
116                .into_boxed_slice()
117                .try_into()
118                .unwrap(),
119            palette: Box::new(*palette),
120            greyscale_palette: Box::new(greyscale_palette),
121            crop_overscan: config.crop_overscan,
122        }
123    }
124
125    pub fn step_to(&mut self, cycles: u64, irq1_pending: &mut bool) {
126        let elapsed_cycles = cycles.saturating_sub(self.state.cycles);
127        if elapsed_cycles == 0 {
128            return;
129        }
130        self.state.cycles = cycles;
131
132        let mut prev_scanline_mclk = self.state.scanline_mclk;
133        self.state.scanline_mclk += elapsed_cycles;
134
135        let dot_clock_divider = self.vce.dot_clock_divider();
136
137        if self.state.scanline_mclk >= MCLK_CYCLES_PER_SCANLINE {
138            let elapsed_vdc_dots =
139                dot_clock_divider.divide_difference(MCLK_CYCLES_PER_SCANLINE, prev_scanline_mclk);
140            self.vdc.tick_dots(elapsed_vdc_dots, &self.vce);
141
142            prev_scanline_mclk = 0;
143            self.state.scanline_mclk -= MCLK_CYCLES_PER_SCANLINE;
144
145            let lines_per_frame = self.vce.lines_per_frame();
146
147            self.state.scanline += 1;
148            if self.state.scanline >= lines_per_frame {
149                self.state.scanline = 0;
150                self.vdc.start_new_frame();
151                self.state.frame_start_cycles = self.state.cycles - self.state.scanline_mclk;
152            }
153
154            self.vdc.start_new_line(self.state.scanline, &self.vce);
155
156            self.greyscale_per_line[self.state.scanline as usize] = self.vce.greyscale();
157        }
158
159        let elapsed_vdc_dots = self
160            .vce
161            .dot_clock_divider()
162            .divide_difference(self.state.scanline_mclk, prev_scanline_mclk);
163        self.vdc.tick_dots(elapsed_vdc_dots, &self.vce);
164
165        *irq1_pending = self.vdc.irq();
166    }
167
168    pub fn frame_complete(&self) -> bool {
169        self.vdc.frame_complete()
170    }
171
172    pub fn clear_frame_complete(&mut self) {
173        self.vdc.clear_frame_complete();
174    }
175
176    pub fn render_rgba8_frame_buffer(&mut self) {
177        let vdc_frame_buffer = self.vdc.frame_buffer();
178
179        let mut params = if self.crop_overscan {
180            FrameRenderParams::CROP_OVERSCAN
181        } else {
182            FrameRenderParams::DEFAULT
183        };
184
185        let x_stride = if vdc_frame_buffer
186            .line_dividers
187            .iter()
188            .all(|&divider| divider == vdc_frame_buffer.line_dividers[0])
189        {
190            // If dot clock divider remained unchanged for the entire frame, downsample for more
191            // reasonable behavior with shaders
192            vdc_frame_buffer.line_dividers[0] as usize
193        } else {
194            1
195        };
196        self.state.frame_x_divider = x_stride as u32;
197
198        params.width /= x_stride;
199
200        for row in 0..params.height {
201            let active_display_row = row + params.top_offset;
202            let scanline = active_display_row + (vdc::ACTIVE_DISPLAY_LINES.start as usize);
203            let palette = if self.greyscale_per_line[scanline] {
204                &self.greyscale_palette
205            } else {
206                &self.palette
207            };
208
209            for col in 0..params.width {
210                let active_display_col = col * x_stride + params.left_offset;
211                let vdc_color = vdc_frame_buffer.colors[active_display_row][active_display_col];
212                let (r, g, b) = palette[(vdc_color & 0x1FF) as usize];
213                self.frame_buffer.0[row * params.width + col] = Color::rgb(r, g, b);
214            }
215        }
216    }
217
218    pub fn frame_buffer(&self) -> &[Color] {
219        self.frame_buffer.0.as_slice()
220    }
221
222    pub fn frame_size(&self) -> FrameSize {
223        let mut frame_size = if self.crop_overscan {
224            FrameSize {
225                width: FrameRenderParams::CROP_OVERSCAN.width as u32,
226                height: FrameRenderParams::CROP_OVERSCAN.height as u32,
227            }
228        } else {
229            FrameSize {
230                width: vdc::FRAME_BUFFER_WIDTH as u32,
231                height: vdc::FRAME_BUFFER_HEIGHT as u32,
232            }
233        };
234
235        frame_size.width /= self.state.frame_x_divider;
236
237        frame_size
238    }
239
240    pub fn ntsc_aspect_ratio(&self) -> FiniteF64 {
241        // NTSC aspect ratio should be 8:7 for 5 MHz dot clock (H256px); compute others based on that
242        debug_assert_ne!(self.state.frame_x_divider, 0);
243        let multiplier = f64::from(self.state.frame_x_divider) / 4.0;
244        FiniteF64::try_from(multiplier * 8.0 / 7.0).unwrap()
245    }
246
247    pub fn composite_params(&self) -> CompositeParams {
248        CompositeParams {
249            upscale_factor: 2 * self.state.frame_x_divider,
250            samples_per_color_cycle: SamplesPerColorCycle::Twelve,
251        }
252    }
253
254    pub fn ntsc_per_frame_params(&self) -> NtscPerFrameParams {
255        NtscPerFrameParams {
256            frame_phase_offset: 2 * self.state.frame_start_cycles,
257            per_line_phase_offset: 2 * MCLK_CYCLES_PER_SCANLINE,
258        }
259    }
260
261    pub fn target_fps(&self) -> f64 {
262        // ~60.05 Hz in 262-line mode, ~59.83 Hz in 263-line mode
263        api::MASTER_CLOCK_FREQUENCY
264            / (MCLK_CYCLES_PER_SCANLINE as f64)
265            / f64::from(self.vce.lines_per_frame())
266    }
267
268    // $1FE000-$1FE003: VDC ports
269    pub fn read_vdc(&mut self, address: u32, bus_cycles: &mut u64, irq1_pending: &mut bool) -> u8 {
270        log::trace!(
271            "VDC register read {address:06X}, line {} mclk {}",
272            self.state.scanline,
273            self.state.scanline_mclk
274        );
275
276        let address = address & 3;
277
278        if matches!(address, 2 | 3) {
279            while self.vdc.is_cpu_read_blocked() {
280                log::trace!(
281                    "CPU read {address} stalling! Line {} cycles {}",
282                    self.state.scanline,
283                    self.state.scanline_mclk
284                );
285
286                *bus_cycles += u64::from(self.vce.dot_clock_divider());
287                self.step_to(*bus_cycles, irq1_pending);
288            }
289        }
290
291        match address {
292            0 => self.vdc.read_status(),
293            1 => 0x00, // Unused
294            2 => self.vdc.read_data(WordByte::Low),
295            3 => self.vdc.read_data(WordByte::High),
296            _ => unreachable!("address & 3 is always <= 3"),
297        }
298    }
299
300    // $1FE000-$1FE003: VDC ports
301    pub fn write_vdc(
302        &mut self,
303        address: u32,
304        value: u8,
305        bus_cycles: &mut u64,
306        irq1_pending: &mut bool,
307    ) {
308        log::trace!(
309            "VDC register write {address:06X} {value:02X}, line {} mclk {}",
310            self.state.scanline,
311            self.state.scanline_mclk
312        );
313
314        let address = address & 3;
315
316        if matches!(address, 2 | 3) {
317            while self.vdc.is_cpu_write_blocked() {
318                log::trace!(
319                    "CPU write {address} {value:02X} stalling! Line {} cycles {}",
320                    self.state.scanline,
321                    self.state.scanline_mclk
322                );
323
324                *bus_cycles += u64::from(self.vce.dot_clock_divider());
325                self.step_to(*bus_cycles, irq1_pending);
326            }
327        }
328
329        match address {
330            0 => self.vdc.write_register_select(value),
331            1 => {} // Unused
332            2 => self.vdc.write_data(value, WordByte::Low),
333            3 => self.vdc.write_data(value, WordByte::High),
334            _ => unreachable!("value & 3 is always <= 3"),
335        }
336    }
337
338    // $1FE400-$1FE407: VCE ports
339    #[allow(clippy::match_same_arms)]
340    pub fn read_vce(&mut self, address: u32) -> u8 {
341        log::trace!(
342            "VCE register read {address:06X}, line {} mclk {}",
343            self.state.scanline,
344            self.state.scanline_mclk
345        );
346
347        match address & 7 {
348            0..=3 => 0xFF, // Write-only / unused
349            4 => self.vce.read_color_data(WordByte::Low),
350            5 => self.vce.read_color_data(WordByte::High),
351            6 | 7 => 0xFF, // Unused
352            _ => unreachable!("value & 7 is always <= 7"),
353        }
354    }
355
356    // $1FE400-$1FE407: VCE ports
357    #[allow(clippy::match_same_arms)]
358    pub fn write_vce(&mut self, address: u32, value: u8) {
359        log::trace!(
360            "VCE register write {address:06X} {value:02X}, line {} mclk {}",
361            self.state.scanline,
362            self.state.scanline_mclk
363        );
364
365        match address & 7 {
366            0 => self.vce.write_control(value),
367            1 => {} // Unused
368            2 => self.vce.write_color_address(value, WordByte::Low),
369            3 => self.vce.write_color_address(value, WordByte::High),
370            4 => self.vce.write_color_data(value, WordByte::Low),
371            5 => self.vce.write_color_data(value, WordByte::High),
372            6 | 7 => {} // Unused
373            _ => unreachable!("value & 7 is always <= 7"),
374        }
375    }
376
377    pub fn reload_config(&mut self, config: PceEmulatorConfig) {
378        *self.palette = *palette::for_type(config.palette);
379        *self.greyscale_palette = palette::make_greyscale(&self.palette);
380
381        self.crop_overscan = config.crop_overscan;
382
383        self.vdc.reload_config(config);
384    }
385
386    pub fn dump_vram(&self, palette: u16, out: &mut [[Color; 64]]) {
387        self.vdc.dump_vram(palette, out, &self.vce, &self.palette);
388    }
389
390    pub fn dump_palettes(&self, out: &mut [Color]) {
391        self.vce.dump_palettes(out, &self.palette);
392    }
393}