vdp.rsannotatedvdp.rssource1067 lines · 39.5 KB · raw
1//! 32X VDP (Video Display Processor)
2
3pub(crate) mod debug;
4mod registers;
5
6use crate::GenesisVdp;
7use crate::api::GenesisVdpInfo;
8use crate::registers::SystemRegisters;
9use crate::vdp::registers::{FrameBufferMode, Registers, SelectedFrameBuffer, VerticalResolution};
10use bincode::{Decode, Encode};
11use genesis_components::vdp::BorderSize;
12use genesis_config::{S32XColorTint, S32XVideoOut, S32XVoidColor, Sega32XEmulatorConfig};
13use jgenesis_common::boxedarray::{Boxed2DWordArray, BoxedColorArray, BoxedWordArray};
14use jgenesis_common::frontend::{
15    Color, CompositeParams, FiniteF64, FrameSize, RenderFrameOptions, Renderer,
16    SamplesPerColorCycle, TimingMode,
17};
18use jgenesis_common::num::{GetBit, U16Ext};
19use std::cmp;
20use std::collections::VecDeque;
21use std::ops::Range;
22
23const MCLK_CYCLES_PER_SCANLINE: u64 = genesis_components::vdp::MCLK_CYCLES_PER_SCANLINE;
24
25// 25 pixels after Genesis H40 HBlank start
26const HBLANK_START_MCLK_CYCLES: u64 = 355 * 8;
27const HBLANK_END_MCLK_CYCLES: u64 = HBLANK_START_MCLK_CYCLES - 320 * 8;
28
29// Manual says VBlank begins 27 dots after HBlank begins on the last line, but 27 breaks some tests
30// that depend on timing between H and V interrupts
31const VBLANK_START_MCLK_CYCLES: u64 = HBLANK_START_MCLK_CYCLES + 22 * 8;
32
33// VDP latches registers 76 dots after HBlank begins
34const LATCH_REGISTERS_MCLK_CYCLES: u64 =
35    (HBLANK_START_MCLK_CYCLES + 76 * 8) % MCLK_CYCLES_PER_SCANLINE;
36
37const RENDER_LINE_MCLK_CYCLES: u64 = HBLANK_END_MCLK_CYCLES;
38
39// DRAM refresh takes ~40 SH-2 cycles
40const DRAM_REFRESH_MCLK_CYCLES: Range<u64> =
41    HBLANK_START_MCLK_CYCLES..HBLANK_START_MCLK_CYCLES + 40 * 7 / 3;
42
43const FRAME_BUFFER_LEN_WORDS: usize = 128 * 1024 / 2;
44const CRAM_LEN_WORDS: usize = 512 / 2;
45
46// 32X VDP only supports 320x224 and 320x240 frame sizes
47const FRAME_WIDTH: u32 = 320;
48const V28_FRAME_HEIGHT: u32 = 224;
49const V30_FRAME_HEIGHT: u32 = 240;
50
51// The H32 frame buffer should be large enough to store frames as H1280px resolution (4 * 320)
52const EXPANDED_FRAME_BUFFER_LEN: usize = genesis_components::vdp::FRAME_BUFFER_LEN * 4;
53
54// Offset between the left edge of the Genesis H32 frame and the 32X frame, in H1280px pixels
55//
56// Due to HSYNC/blanking/border timings being slightly different between H32 and H40 mode, and due
57// to the 32X VDP always assuming H40 mode, the Genesis and 32X frames are slightly offset when the
58// Genesis VDP is in H32 mode.
59const H32_H_OFFSET: u32 = 13;
60
61type FrameBufferRam = [u16; FRAME_BUFFER_LEN_WORDS];
62type Cram = [u16; CRAM_LEN_WORDS];
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
65enum WhichFrameBuffer {
66    #[default]
67    Genesis,
68    ExpandedH32,
69    ExpandedH40,
70}
71
72#[derive(Debug, Clone, Encode, Decode)]
73struct State {
74    next_render_buffer: WhichFrameBuffer,
75    fully_blank_frame: bool,
76    scanline: u16,
77    scanline_mclk: u64,
78    scanlines_in_current_frame: u16,
79    h_interrupt_this_line: bool,
80    h_interrupt_counter: u16,
81    h_interrupt_interval_written: bool,
82    vblank_flag: bool,
83    display_frame_buffer: SelectedFrameBuffer,
84    auto_fill_mclk_remaining: u64,
85    fb_write_timing_fifo: VecDeque<u64>,
86    last_fb_write_cycles: u64,
87    cycles_till_next_render: u64,
88}
89
90impl State {
91    fn new() -> Self {
92        Self {
93            next_render_buffer: WhichFrameBuffer::Genesis,
94            fully_blank_frame: true,
95            scanline: 0,
96            scanline_mclk: 0,
97            scanlines_in_current_frame: u16::MAX,
98            h_interrupt_this_line: true,
99            h_interrupt_counter: 0,
100            h_interrupt_interval_written: false,
101            vblank_flag: false,
102            display_frame_buffer: SelectedFrameBuffer::default(),
103            auto_fill_mclk_remaining: 0,
104            fb_write_timing_fifo: VecDeque::with_capacity(4),
105            last_fb_write_cycles: 0,
106            cycles_till_next_render: u64::MAX,
107        }
108    }
109}
110
111#[derive(Debug, Clone, Encode, Decode)]
112struct VdpConfig {
113    video_out: S32XVideoOut,
114    color_tint: S32XColorTint,
115    show_high_priority: bool,
116    show_low_priority: bool,
117    void_color: S32XVoidColor,
118}
119
120impl VdpConfig {
121    fn from_emu_config(config: &Sega32XEmulatorConfig) -> Self {
122        Self {
123            video_out: config.video_out,
124            color_tint: config.color_tint,
125            show_high_priority: config.show_high_priority,
126            show_low_priority: config.show_low_priority,
127            void_color: config.void_color,
128        }
129    }
130}
131
132#[derive(Debug, Clone, Encode, Decode)]
133pub struct Vdp {
134    frame_buffer_0: BoxedWordArray<FRAME_BUFFER_LEN_WORDS>,
135    frame_buffer_1: BoxedWordArray<FRAME_BUFFER_LEN_WORDS>,
136    rendered_frame: Boxed2DWordArray<{ V30_FRAME_HEIGHT as usize }, { FRAME_WIDTH as usize }>,
137    // 1280x224 or 1280x240 (not including borders)
138    // Needed for when a game enables H32 mode on the Genesis side (NFL Quarterback Club does this)
139    expanded_frame_buffer: BoxedColorArray<EXPANDED_FRAME_BUFFER_LEN>,
140    cram: BoxedWordArray<CRAM_LEN_WORDS>,
141    registers: Registers,
142    // Per documentation, the VDP latches registers for rendering once per line beginning shortly
143    // after the start of HBlank
144    latched: Registers,
145    state: State,
146    timing_mode: TimingMode,
147    config: VdpConfig,
148}
149
150macro_rules! front_frame_buffer {
151    ($self:expr) => {
152        match $self.state.display_frame_buffer {
153            SelectedFrameBuffer::Zero => &$self.frame_buffer_0,
154            SelectedFrameBuffer::One => &$self.frame_buffer_1,
155        }
156    };
157}
158
159macro_rules! back_frame_buffer {
160    ($self:expr) => {
161        match $self.state.display_frame_buffer {
162            SelectedFrameBuffer::Zero => &$self.frame_buffer_1,
163            SelectedFrameBuffer::One => &$self.frame_buffer_0,
164        }
165    };
166}
167
168macro_rules! back_frame_buffer_mut {
169    ($self:expr) => {
170        match $self.state.display_frame_buffer {
171            SelectedFrameBuffer::Zero => &mut $self.frame_buffer_1,
172            SelectedFrameBuffer::One => &mut $self.frame_buffer_0,
173        }
174    };
175}
176
177impl Vdp {
178    pub fn new(timing_mode: TimingMode, config: &Sega32XEmulatorConfig) -> Self {
179        Self {
180            frame_buffer_0: BoxedWordArray::new(),
181            frame_buffer_1: BoxedWordArray::new(),
182            rendered_frame: Boxed2DWordArray::new(),
183            expanded_frame_buffer: BoxedColorArray::new(),
184            cram: BoxedWordArray::new(),
185            registers: Registers::default(),
186            latched: Registers::default(),
187            state: State::new(),
188            timing_mode,
189            config: VdpConfig::from_emu_config(config),
190        }
191    }
192
193    pub fn tick(
194        &mut self,
195        mclk_cycles: u64,
196        registers: &mut SystemRegisters,
197        vdp_info: GenesisVdpInfo,
198    ) {
199        self.state.auto_fill_mclk_remaining =
200            self.state.auto_fill_mclk_remaining.saturating_sub(mclk_cycles);
201
202        let prev_scanline_mclk = self.state.scanline_mclk;
203        self.state.scanline_mclk += mclk_cycles;
204
205        if prev_scanline_mclk < LATCH_REGISTERS_MCLK_CYCLES
206            && self.state.scanline_mclk >= LATCH_REGISTERS_MCLK_CYCLES
207        {
208            self.latch_registers();
209        }
210
211        if (self.state.h_interrupt_this_line || self.registers.h_interrupt_in_vblank)
212            && prev_scanline_mclk < HBLANK_START_MCLK_CYCLES
213            && self.state.scanline_mclk >= HBLANK_START_MCLK_CYCLES
214        {
215            self.handle_hblank_start(registers);
216        }
217
218        if prev_scanline_mclk < VBLANK_START_MCLK_CYCLES
219            && self.state.scanline_mclk >= VBLANK_START_MCLK_CYCLES
220        {
221            if self.state.scanline == self.latched.active_scanlines_per_frame - 1 {
222                self.handle_vblank_start(registers, vdp_info);
223            } else if self.state.scanline == self.state.scanlines_in_current_frame - 1 {
224                self.state.vblank_flag = false;
225                registers.notify_vblank_end();
226            }
227        }
228
229        if self.state.scanline_mclk >= MCLK_CYCLES_PER_SCANLINE {
230            self.state.scanline_mclk -= MCLK_CYCLES_PER_SCANLINE;
231            self.state.scanline += 1;
232
233            if self.state.scanline_mclk >= LATCH_REGISTERS_MCLK_CYCLES {
234                self.latch_registers();
235            }
236
237            if self.state.scanline == VerticalResolution::V30.active_scanlines_per_frame()
238                && !self.state.vblank_flag
239            {
240                // Workaround for a timing edge case related to V28/V30 mode switch
241                // TODO is it even allowed to switch between V28 and V30 outside of VBlank?
242                self.state.vblank_flag = true;
243                self.state.scanlines_in_current_frame = vdp_info.scanlines_in_current_frame;
244            }
245
246            if self.state.scanline >= self.state.scanlines_in_current_frame {
247                self.state.scanline = 0;
248            } else if self.state.scanline == self.state.scanlines_in_current_frame - 1 {
249                // First HINT before rendering line 0
250                self.state.h_interrupt_this_line = true;
251            }
252
253            if self.state.scanline_mclk >= RENDER_LINE_MCLK_CYCLES {
254                self.handle_hblank_end();
255                self.state.cycles_till_next_render =
256                    MCLK_CYCLES_PER_SCANLINE + RENDER_LINE_MCLK_CYCLES - self.state.scanline_mclk;
257            }
258        } else if prev_scanline_mclk < RENDER_LINE_MCLK_CYCLES
259            && self.state.scanline_mclk >= RENDER_LINE_MCLK_CYCLES
260        {
261            self.handle_hblank_end();
262            self.state.cycles_till_next_render =
263                MCLK_CYCLES_PER_SCANLINE + RENDER_LINE_MCLK_CYCLES - self.state.scanline_mclk;
264        }
265    }
266
267    fn latch_registers(&mut self) {
268        self.latched = self.registers.clone();
269
270        // In case VDP was switched to blank mode with a pending frame buffer swap
271        if self.latched.frame_buffer_mode == FrameBufferMode::Blank
272            && self.state.display_frame_buffer != self.registers.display_frame_buffer
273        {
274            self.state.display_frame_buffer = self.registers.display_frame_buffer;
275            log::debug!("Front frame buffer set to {:?}", self.state.display_frame_buffer);
276        }
277    }
278
279    fn handle_hblank_start(&mut self, registers: &mut SystemRegisters) {
280        if self.state.h_interrupt_counter == 0 {
281            self.state.h_interrupt_counter = self.registers.h_interrupt_interval;
282            registers.notify_h_interrupt();
283        } else {
284            self.state.h_interrupt_counter -= 1;
285        }
286    }
287
288    fn handle_hblank_end(&mut self) {
289        let active_scanlines_per_frame = self.latched.active_scanlines_per_frame;
290
291        if self.state.h_interrupt_interval_written {
292            // HINT interval changes are only latched after HBlanks that could have triggered HINTs
293            if self.registers.h_interrupt_in_vblank
294                || self.state.scanline <= active_scanlines_per_frame
295            {
296                self.state.h_interrupt_counter = self.registers.h_interrupt_interval;
297                self.state.h_interrupt_interval_written = false;
298            }
299        }
300
301        if self.state.scanline < active_scanlines_per_frame {
302            self.render_line();
303        }
304    }
305
306    fn handle_vblank_start(&mut self, registers: &mut SystemRegisters, vdp_info: GenesisVdpInfo) {
307        if self.state.vblank_flag {
308            return;
309        }
310
311        registers.notify_vblank_start();
312
313        // Beginning of VBlank; frame buffer switches take effect
314        if self.state.display_frame_buffer != self.registers.display_frame_buffer {
315            log::debug!(
316                "VBlank: Changing front frame buffer to {:?}",
317                self.registers.display_frame_buffer
318            );
319        }
320        self.state.display_frame_buffer = self.registers.display_frame_buffer;
321
322        // Grab scanlines in frame at start of VBlank to avoid a dependency on which order
323        // the VDPs execute in, since interlacing state is latched at the start of line 0
324        self.state.scanlines_in_current_frame = vdp_info.scanlines_in_current_frame;
325
326        // No HINTs during VBlank (unless the HEN bit is set)
327        self.state.h_interrupt_this_line = false;
328
329        self.state.vblank_flag = true;
330    }
331
332    pub fn mclk_cycles_until_next_event(&self, h_interrupt_enabled: bool) -> u64 {
333        // Sync at every line render and register latch
334        let mut cycles_till_next = self.state.cycles_till_next_render;
335
336        let cycles_till_register_latch = if self.state.scanline_mclk < LATCH_REGISTERS_MCLK_CYCLES {
337            LATCH_REGISTERS_MCLK_CYCLES - self.state.scanline_mclk
338        } else {
339            MCLK_CYCLES_PER_SCANLINE - self.state.scanline_mclk + LATCH_REGISTERS_MCLK_CYCLES
340        };
341        cycles_till_next = cmp::min(cycles_till_next, cycles_till_register_latch);
342
343        // Sync at HINT if HINT will trigger on this line
344        if h_interrupt_enabled
345            && self.state.h_interrupt_counter == 0
346            && self.state.scanline_mclk < HBLANK_START_MCLK_CYCLES
347        {
348            cycles_till_next =
349                cmp::min(cycles_till_next, HBLANK_START_MCLK_CYCLES - self.state.scanline_mclk);
350        }
351
352        // Sync at VBlank start
353        if self.state.scanline == self.latched.active_scanlines_per_frame - 1
354            && self.state.scanline_mclk < VBLANK_START_MCLK_CYCLES
355        {
356            cycles_till_next =
357                cmp::min(cycles_till_next, VBLANK_START_MCLK_CYCLES - self.state.scanline_mclk);
358        }
359
360        // Sync at auto fill end if an auto fill is in progress
361        if self.state.auto_fill_mclk_remaining != 0 {
362            cycles_till_next = cmp::min(cycles_till_next, self.state.auto_fill_mclk_remaining);
363        }
364
365        cycles_till_next
366    }
367
368    fn render_line(&mut self) {
369        self.state.fully_blank_frame |= self.state.scanline == 0;
370        self.state.fully_blank_frame &= self.latched.frame_buffer_mode == FrameBufferMode::Blank;
371
372        match self.latched.frame_buffer_mode {
373            FrameBufferMode::Blank => {
374                self.rendered_frame[self.state.scanline as usize].fill(0);
375            }
376            FrameBufferMode::PackedPixel => self.render_packed_pixel(),
377            FrameBufferMode::DirectColor => self.render_direct_color(),
378            FrameBufferMode::RunLength => self.render_run_length(),
379        }
380    }
381
382    fn priority_mask_fn(&self) -> impl Fn(u16) -> u16 + 'static {
383        let vdp_priority = u16::from(self.latched.priority) << 15;
384        let show_high_priority = self.config.show_high_priority;
385        let show_low_priority = self.config.show_low_priority;
386        let void_color = match self.config.void_color {
387            S32XVoidColor::PaletteRam { idx } => self.cram[idx as usize],
388            S32XVoidColor::Direct { r, g, b, a } => {
389                u16::from(r & 0x1F)
390                    | (u16::from(g & 0x1F) << 5)
391                    | (u16::from(b & 0x1F) << 10)
392                    | (u16::from(a) << 15)
393            }
394        };
395
396        move |mut pixel| {
397            let pixel_priority = pixel.bit(15);
398            if (pixel_priority && !show_high_priority) || (!pixel_priority && !show_low_priority) {
399                pixel = void_color;
400            }
401            pixel ^ vdp_priority
402        }
403    }
404
405    fn render_packed_pixel(&mut self) {
406        let line = self.state.scanline as usize;
407        let frame_buffer = front_frame_buffer!(self);
408        let line_addr = frame_buffer[line];
409
410        log::trace!(
411            "Rendering line {line} from buffer {:?} in packed pixel mode, addr={line_addr:04X}",
412            self.state.display_frame_buffer
413        );
414
415        let mask_fn = self.priority_mask_fn();
416
417        if self.screen_left_shift(line_addr) {
418            for pixel in 0..FRAME_WIDTH as u16 {
419                let frame_buffer_addr = line_addr.wrapping_add((pixel + 1) >> 1);
420                let color = (frame_buffer[frame_buffer_addr as usize] >> (8 * (pixel & 1))) & 0xFF;
421
422                self.rendered_frame[line][pixel as usize] = mask_fn(self.cram[color as usize]);
423            }
424        } else {
425            for pixel in (0..FRAME_WIDTH as u16).step_by(2) {
426                let frame_buffer_addr = line_addr.wrapping_add(pixel >> 1);
427                let [msb, lsb] = frame_buffer[frame_buffer_addr as usize].to_be_bytes();
428
429                self.rendered_frame[line][pixel as usize] = mask_fn(self.cram[msb as usize]);
430                self.rendered_frame[line][(pixel + 1) as usize] = mask_fn(self.cram[lsb as usize]);
431            }
432        }
433    }
434
435    fn render_direct_color(&mut self) {
436        let line = self.state.scanline as usize;
437        let frame_buffer = front_frame_buffer!(self);
438        let mut line_addr = frame_buffer[line];
439
440        log::trace!(
441            "Rendering line {line} from frame buffer {:?} in direct color mode, addr={line_addr:04X}",
442            self.state.display_frame_buffer
443        );
444
445        let mask_fn = self.priority_mask_fn();
446
447        if self.screen_left_shift(line_addr) {
448            line_addr = line_addr.wrapping_add(1);
449        }
450
451        for pixel in 0..FRAME_WIDTH as u16 {
452            let color = frame_buffer[line_addr as usize];
453            self.rendered_frame[line][pixel as usize] = mask_fn(color);
454
455            line_addr = line_addr.wrapping_add(1);
456        }
457    }
458
459    fn render_run_length(&mut self) {
460        let line = self.state.scanline as usize;
461        let frame_buffer = front_frame_buffer!(self);
462        let mut line_addr = frame_buffer[line];
463
464        log::trace!(
465            "Rendering line {line} from buffer {:?} in run length mode, addr={line_addr:04X}",
466            self.state.display_frame_buffer
467        );
468
469        let mask_fn = self.priority_mask_fn();
470
471        let mut skip_first = self.screen_left_shift(line_addr);
472
473        let mut pixel = 0;
474        while pixel < FRAME_WIDTH {
475            let [run_length_byte, color_idx] = frame_buffer[line_addr as usize].to_be_bytes();
476            line_addr = line_addr.wrapping_add(1);
477
478            let color = self.cram[color_idx as usize];
479            let mut run_length = u16::from(run_length_byte) + 1;
480
481            while pixel < FRAME_WIDTH && run_length != 0 {
482                run_length -= 1;
483                if skip_first {
484                    skip_first = false;
485                    continue;
486                }
487
488                self.rendered_frame[line][pixel as usize] = mask_fn(color);
489                pixel += 1;
490            }
491        }
492    }
493
494    fn screen_left_shift(&self, line_address: u16) -> bool {
495        // Screen shift does not apply when the low byte of the line address is 0xFF
496        // Mentioned in 32X hardware manual supplement and verified on hardware
497        self.latched.screen_left_shift && line_address.lsb() != 0xFF
498    }
499
500    pub fn read_register(&self, address: u32) -> u16 {
501        match address & 0xF {
502            0x0 => self.registers.read_display_mode(self.timing_mode),
503            0x2 => self.registers.read_screen_shift(),
504            0x4 => self.registers.read_auto_fill_length(),
505            0x6 => self.registers.read_auto_fill_start_address(),
506            0xA => self.read_frame_buffer_control(),
507            _ => {
508                log::warn!("Invalid VDP register read {address:08X}");
509                0
510            }
511        }
512    }
513
514    pub fn write_register_byte(&mut self, address: u32, value: u8) {
515        let mut word = self.read_register(address & !1);
516        if !address.bit(0) {
517            word.set_msb(value);
518        } else {
519            word.set_lsb(value);
520        }
521        self.write_register(address & !1, word);
522    }
523
524    pub fn write_register(&mut self, address: u32, value: u16) {
525        log::trace!(
526            "VDP register write on line {}: {address:08X} {value:04X}",
527            self.state.scanline
528        );
529
530        match address & 0xF {
531            0x0 => self.registers.write_display_mode(value),
532            0x2 => self.registers.write_screen_shift(value),
533            0x4 => self.registers.write_auto_fill_length(value),
534            0x6 => self.registers.write_auto_fill_start_address(value),
535            0x8 => {
536                self.registers.write_auto_fill_data(value);
537
538                // Writing auto fill data initiates auto fill
539                self.do_auto_fill();
540            }
541            0xA => {
542                self.registers.write_frame_buffer_control(value);
543                if self.state.vblank_flag
544                    || self.latched.frame_buffer_mode == FrameBufferMode::Blank
545                {
546                    self.state.display_frame_buffer = self.registers.display_frame_buffer;
547                    log::debug!("Front frame buffer set to {:?}", self.state.display_frame_buffer);
548                }
549            }
550            0xC | 0xE => {
551                log::warn!("Invalid VDP register write {address:08X} {value:04X}");
552            }
553            _ => panic!("VDP register write to an unaligned address: {address:08X} {value:04X}"),
554        }
555    }
556
557    pub fn read_frame_buffer(&self, address: u32) -> u16 {
558        let frame_buffer = back_frame_buffer!(self);
559        frame_buffer[((address & 0x1FFFF) >> 1) as usize]
560    }
561
562    pub fn write_frame_buffer_byte(&mut self, address: u32, value: u8) {
563        log::trace!(
564            "Frame buffer byte write {:05X} {value:02X} (word addr {:04X})",
565            address & 0x1FFFF,
566            (address >> 1) & 0xFFFF
567        );
568
569        if value == 0 {
570            return;
571        }
572
573        let frame_buffer = back_frame_buffer_mut!(self);
574        let frame_buffer_addr = ((address & 0x1FFFF) >> 1) as usize;
575
576        if !address.bit(0) {
577            frame_buffer[frame_buffer_addr].set_msb(value);
578        } else {
579            frame_buffer[frame_buffer_addr].set_lsb(value);
580        }
581    }
582
583    pub fn write_frame_buffer_word(&mut self, address: u32, value: u16) {
584        log::trace!(
585            "Frame buffer write {:05X} {value:04X} (word addr {:04X})",
586            address & 0x1FFFF,
587            (address >> 1) & 0xFFFF
588        );
589
590        let frame_buffer = back_frame_buffer_mut!(self);
591        frame_buffer[((address & 0x1FFFF) >> 1) as usize] = value;
592    }
593
594    pub fn frame_buffer_overwrite_word(&mut self, address: u32, value: u16) {
595        log::trace!(
596            "Overwrite image write {:05X} {value:04X} (word addr {:04X})",
597            address & 0x1FFFF,
598            (address >> 1) & 0xFFFF
599        );
600
601        if value == 0 {
602            return;
603        }
604
605        let frame_buffer = back_frame_buffer_mut!(self);
606        let frame_buffer_addr = ((address & 0x1FFFF) >> 1) as usize;
607
608        let [msb, lsb] = value.to_be_bytes();
609
610        if msb != 0 {
611            frame_buffer[frame_buffer_addr].set_msb(msb);
612        }
613
614        if lsb != 0 {
615            frame_buffer[frame_buffer_addr].set_lsb(lsb);
616        }
617    }
618
619    #[must_use]
620    pub fn frame_buffer_write_latency(&mut self, cycles: u64) -> u64 {
621        if cycles <= self.state.last_fb_write_cycles {
622            // Can happen if both SH-2s are writing to the frame buffer simultaneously
623            // Just ignore and return minimum latency
624            return 1;
625        }
626
627        // Progress times in FIFO
628        let cycle_diff = cycles - self.state.last_fb_write_cycles;
629        for _ in 0..cycle_diff {
630            let Some(front) = self.state.fb_write_timing_fifo.front_mut() else { break };
631
632            *front -= 1;
633            if *front == 0 {
634                self.state.fb_write_timing_fifo.pop_front();
635            }
636        }
637
638        // VDP can only accept 1 write every 4 cycles
639        let initial_write_time = if cycle_diff < 3 { 3 - (cycle_diff - 1) } else { 1 };
640
641        // If the 4-entry FIFO is full, must wait for an empty slot
642        let fifo_wait_time = if self.state.fb_write_timing_fifo.len() == 4 {
643            1 + self.state.fb_write_timing_fifo.pop_front().unwrap()
644        } else {
645            0
646        };
647
648        let wait_cycles = cmp::max(initial_write_time, fifo_wait_time);
649        debug_assert!((1..=5).contains(&wait_cycles));
650
651        self.state.fb_write_timing_fifo.push_back(5);
652        self.state.last_fb_write_cycles = cycles + wait_cycles - 1;
653
654        wait_cycles
655    }
656
657    pub fn read_cram(&self, address: u32) -> u16 {
658        // TODO block access to CRAM when in use?
659        self.cram[((address & 0x1FF) >> 1) as usize]
660    }
661
662    pub fn write_cram(&mut self, address: u32, value: u16) {
663        // TODO block access to CRAM while in use?
664        self.cram[((address & 0x1FF) >> 1) as usize] = value;
665    }
666
667    // Interrupt mask bit 7: HEN (H interrupts enabled during VBlank)
668    pub fn hen_bit(&self) -> bool {
669        self.registers.h_interrupt_in_vblank
670    }
671
672    // Interrupt mask bit 7: HEN (H interrupts enabled during VBlank)
673    pub fn write_hen_bit(&mut self, hen: bool) {
674        self.registers.h_interrupt_in_vblank = hen;
675    }
676
677    // SH-2: $4004
678    pub fn h_interrupt_interval(&self) -> u16 {
679        self.registers.h_interrupt_interval
680    }
681
682    // SH-2: $4004
683    pub fn write_h_interrupt_interval(&mut self, value: u16) {
684        self.registers.h_interrupt_interval = value & 0xFF;
685        self.state.h_interrupt_interval_written = true;
686        log::trace!("H interrupt interval write: {value:04X}");
687    }
688
689    // 68000: $A1518A
690    // SH-2: $410A
691    fn read_frame_buffer_control(&self) -> u16 {
692        let in_vblank = self.state.vblank_flag;
693        let in_hblank = self.in_hblank();
694
695        let cram_accessible = in_vblank || in_hblank;
696
697        // Metal Head depends on the FEN bit reading 1 during DRAM refresh (beginning of HBlank every line)
698        // or else it will freeze after the Sega splash screen
699        let frame_buffer_busy = self.state.auto_fill_mclk_remaining != 0
700            || DRAM_REFRESH_MCLK_CYCLES.contains(&self.state.scanline_mclk);
701
702        (u16::from(in_vblank) << 15)
703            | (u16::from(in_hblank) << 14)
704            | (u16::from(cram_accessible) << 13)
705            | (u16::from(frame_buffer_busy) << 1)
706            | (self.state.display_frame_buffer as u16)
707    }
708
709    fn do_auto_fill(&mut self) {
710        let frame_buffer = back_frame_buffer_mut!(self);
711
712        let data = self.registers.auto_fill_data;
713        for _ in 0..self.registers.auto_fill_length {
714            frame_buffer[self.registers.auto_fill_start_address as usize] = data;
715            self.registers.increment_auto_fill_address();
716        }
717
718        // Note: Auto fill finishing too quickly will cause major glitches in Mortal Kombat II.
719        //
720        // At VINT, the master SH-2 immediately starts drawing the next frame without syncing with
721        // the 68000. It first zeroes out the frame buffer using auto fills and then starts drawing
722        // sprites and the HUD.
723        //
724        // If the auto fills finish before the 68000 interrupts the master SH-2 to send updated I/O
725        // data and then the SH-2 sees that user inputs have changed, it will get very confused and
726        // draw a single glitched frame that was partly drawn using the previous inputs and partly
727        // drawn using the new inputs. Depending on emulated SH-2 speed, this can also cause
728        // gameplay glitches as the game may briefly stop responding to user inputs.
729        //
730        // Official documentation appears to say that auto fill takes (7 + 3 * length) Sclk cycles
731        let auto_fill_sclk = 7 + 3 * u64::from(self.registers.auto_fill_length);
732        self.state.auto_fill_mclk_remaining = auto_fill_sclk * 7 / 3;
733    }
734
735    fn in_hblank(&self) -> bool {
736        !(HBLANK_END_MCLK_CYCLES..HBLANK_START_MCLK_CYCLES).contains(&self.state.scanline_mclk)
737    }
738
739    pub fn scanline(&self) -> u16 {
740        self.state.scanline
741    }
742
743    pub fn scanline_mclk(&self) -> u64 {
744        self.state.scanline_mclk
745    }
746
747    pub fn composite_frame(&mut self, genesis_vdp: &mut GenesisVdp) {
748        // Default to rendering from the Genesis VDP frame buffer, switch later if necessary
749        self.state.next_render_buffer = WhichFrameBuffer::Genesis;
750
751        if (self.config.video_out != S32XVideoOut::S32XOnly && self.state.fully_blank_frame)
752            || self.config.video_out == S32XVideoOut::GenesisOnly
753        {
754            // Leave Genesis frame as-is
755            return;
756        }
757
758        let genesis_frame_size = genesis_vdp.frame_size();
759        let border_size = genesis_vdp.border_size();
760
761        let h32 = genesis_frame_size.width - border_size.left - border_size.right == 256;
762        if h32 {
763            // If the Genesis VDP is in H32 mode, render the frame in 1280x224 so that it's possible
764            // to composite the 320x224 32X frame with the 256x224 Genesis frame without needing to
765            // blend or filter any pixels.
766            // NFL Quarterback Club depends on this - it uses H32 mode in menus
767            self.composite_frame_expanded::<true>(genesis_frame_size, border_size, genesis_vdp);
768            return;
769        }
770
771        // Otherwise, composite in H40 into the Genesis VDP frame buffer
772        self.composite_frame_inner::<false, false>(genesis_frame_size, border_size, genesis_vdp);
773    }
774
775    fn composite_frame_expanded<const H32: bool>(
776        &mut self,
777        genesis_frame_size: FrameSize,
778        border_size: BorderSize,
779        genesis_vdp: &mut GenesisVdp,
780    ) {
781        let genesis_frame_width =
782            genesis_frame_size.width.wrapping_sub(border_size.left).wrapping_sub(border_size.right);
783        if H32 {
784            debug_assert_eq!(genesis_frame_width, 256);
785        } else {
786            debug_assert_eq!(genesis_frame_width, 320);
787        }
788
789        self.state.next_render_buffer =
790            if H32 { WhichFrameBuffer::ExpandedH32 } else { WhichFrameBuffer::ExpandedH40 };
791
792        if self.config.video_out != S32XVideoOut::S32XOnly {
793            self.copy_genesis_frame_buffer_to_expanded::<H32>(
794                genesis_frame_size,
795                border_size,
796                genesis_vdp.frame_buffer_mut(),
797            );
798        } else {
799            self.expanded_frame_buffer.fill(Color::rgba(0, 0, 0, 0));
800        }
801
802        self.composite_frame_inner::<H32, true>(genesis_frame_size, border_size, genesis_vdp);
803    }
804
805    fn composite_frame_inner<const H32: bool, const EXPAND_H: bool>(
806        &mut self,
807        frame_size: FrameSize,
808        border_size: BorderSize,
809        genesis_vdp: &mut GenesisVdp,
810    ) {
811        fn should_use_32x_pixel(s32x_only: bool, s32x_pixel: u16, gen_color: Color) -> bool {
812            s32x_only || s32x_pixel.bit(15) || gen_color.a == 0
813        }
814
815        assert!(
816            !H32 || EXPAND_H,
817            "Does not make sense to not expand if Genesis VDP is in H32 mode"
818        );
819
820        let interlaced_frame: u32 = genesis_vdp.is_interlaced_frame().into();
821        let interlaced_odd: u32 = genesis_vdp.is_interlaced_odd().into();
822
823        let frame_buffer = if EXPAND_H {
824            self.expanded_frame_buffer.as_mut_slice()
825        } else {
826            genesis_vdp.frame_buffer_mut()
827        };
828
829        let active_lines_per_frame: u32 = self.latched.active_scanlines_per_frame.into();
830        let s32x_only = self.config.video_out == S32XVideoOut::S32XOnly;
831
832        let frame_width = if EXPAND_H {
833            determine_expanded_buffer_width::<H32>(frame_size, border_size)
834        } else {
835            frame_size.width
836        };
837        let left_offset = match (EXPAND_H, H32) {
838            (false, _) => border_size.left,
839            (true, false) => genesis_expanded_pixel_width::<false>() * border_size.left,
840            (true, true) => {
841                genesis_expanded_pixel_width::<true>() * border_size.left + H32_H_OFFSET
842            }
843        };
844
845        let top_offset = border_size.top << interlaced_frame;
846
847        let color_tables = ColorTables::from_tint(self.config.color_tint);
848
849        for line in 0..active_lines_per_frame {
850            let effective_line = (line << interlaced_frame) + interlaced_odd;
851            let fb_row_addr = ((effective_line + top_offset) * frame_width + left_offset) as usize;
852
853            if EXPAND_H {
854                for pixel in 0..FRAME_WIDTH {
855                    let s32x_pixel = self.rendered_frame[line as usize][pixel as usize];
856                    let fb_addr = fb_row_addr + 4 * pixel as usize;
857
858                    for i in 0..4 {
859                        if should_use_32x_pixel(s32x_only, s32x_pixel, frame_buffer[fb_addr + i]) {
860                            frame_buffer[fb_addr + i] = u16_to_rgb(s32x_pixel, color_tables);
861                        }
862                    }
863                }
864            } else {
865                for pixel in 0..FRAME_WIDTH {
866                    let s32x_pixel = self.rendered_frame[line as usize][pixel as usize];
867                    let fb_addr = fb_row_addr + pixel as usize;
868
869                    if should_use_32x_pixel(s32x_only, s32x_pixel, frame_buffer[fb_addr]) {
870                        frame_buffer[fb_addr] = u16_to_rgb(s32x_pixel, color_tables);
871                    }
872                }
873            }
874        }
875
876        // TODO how do interlaced modes actually work with 32X? Needs testing
877        if interlaced_frame != 0 {
878            for line in 0..active_lines_per_frame {
879                let from_line = 2 * line + interlaced_odd;
880                let to_line = from_line ^ 1;
881
882                let from_line_fb_addr = ((from_line + top_offset) * frame_width) as usize;
883                let to_line_fb_addr = ((to_line + top_offset) * frame_width) as usize;
884
885                if to_line_fb_addr > from_line_fb_addr {
886                    let (a, b) = frame_buffer.split_at_mut(to_line_fb_addr);
887                    b[..frame_width as usize].copy_from_slice(
888                        &a[from_line_fb_addr..from_line_fb_addr + frame_width as usize],
889                    );
890                } else {
891                    let (a, b) = frame_buffer.split_at_mut(from_line_fb_addr);
892                    a[to_line_fb_addr..to_line_fb_addr + frame_width as usize]
893                        .copy_from_slice(&b[..frame_width as usize]);
894                }
895            }
896        }
897    }
898
899    fn copy_genesis_frame_buffer_to_expanded<const H32: bool>(
900        &mut self,
901        frame_size: FrameSize,
902        border_size: BorderSize,
903        frame_buffer: &mut [Color; genesis_components::vdp::FRAME_BUFFER_LEN],
904    ) {
905        let expanded_frame_width = determine_expanded_buffer_width::<H32>(frame_size, border_size);
906        let gen_pixel_width = genesis_expanded_pixel_width::<H32>();
907
908        // Expand the frame buffer from 256x224 to 1280x224 (plus borders)
909        for line in 0..frame_size.height {
910            let h32_fb_line_addr = line * expanded_frame_width;
911            for pixel in 0..frame_size.width {
912                let genesis_fb_addr = (line * frame_size.width + pixel) as usize;
913                let h32_fb_addr = (h32_fb_line_addr + gen_pixel_width * pixel) as usize;
914                self.expanded_frame_buffer[h32_fb_addr..h32_fb_addr + gen_pixel_width as usize]
915                    .fill(frame_buffer[genesis_fb_addr]);
916            }
917
918            if gen_pixel_width * frame_size.width != expanded_frame_width {
919                // Clear right border pixels so 32X pixels will always display over them
920                self.expanded_frame_buffer[(h32_fb_line_addr + gen_pixel_width * frame_size.width)
921                    as usize
922                    ..(h32_fb_line_addr + expanded_frame_width) as usize]
923                    .fill(Color::rgba(0, 0, 0, 0));
924            }
925        }
926    }
927
928    pub fn render_frame<R: Renderer>(
929        &self,
930        genesis_vdp: &GenesisVdp,
931        aspect_ratio: Option<FiniteF64>,
932        renderer: &mut R,
933    ) -> Result<(), R::Err> {
934        if self.state.next_render_buffer == WhichFrameBuffer::Genesis {
935            let target_fps =
936                genesis_components::target_framerate(genesis_vdp, genesis_vdp.timing_mode());
937            return renderer.render_frame(
938                genesis_vdp.frame_buffer(),
939                genesis_vdp.frame_size(),
940                target_fps,
941                RenderFrameOptions {
942                    pixel_aspect_ratio: aspect_ratio,
943                    composite_params: Some(genesis_vdp.composite_params()),
944                    ..RenderFrameOptions::default()
945                },
946            );
947        }
948
949        let h32 = self.state.next_render_buffer == WhichFrameBuffer::ExpandedH32;
950        if h32 {
951            self.render_expanded_frame::<true, _>(genesis_vdp, aspect_ratio, renderer)
952        } else {
953            self.render_expanded_frame::<false, _>(genesis_vdp, aspect_ratio, renderer)
954        }
955    }
956
957    fn render_expanded_frame<const H32: bool, R: Renderer>(
958        &self,
959        genesis_vdp: &GenesisVdp,
960        mut aspect_ratio: Option<FiniteF64>,
961        renderer: &mut R,
962    ) -> Result<(), R::Err> {
963        let mut frame_size = genesis_vdp.frame_size();
964
965        frame_size.width =
966            determine_expanded_buffer_width::<H32>(frame_size, genesis_vdp.border_size());
967
968        let gen_pixel_width = genesis_expanded_pixel_width::<H32>();
969        aspect_ratio = aspect_ratio
970            .map(|par| par * FiniteF64::try_from(1.0 / f64::from(gen_pixel_width)).unwrap());
971
972        let target_fps =
973            genesis_components::target_framerate(genesis_vdp, genesis_vdp.timing_mode());
974        renderer.render_frame(
975            self.expanded_frame_buffer.as_ref(),
976            frame_size,
977            target_fps,
978            RenderFrameOptions {
979                pixel_aspect_ratio: aspect_ratio,
980                composite_params: Some(CompositeParams {
981                    upscale_factor: 2,
982                    samples_per_color_cycle: SamplesPerColorCycle::Fifteen,
983                }),
984                ..RenderFrameOptions::default()
985            },
986        )
987    }
988
989    pub fn reload_config(&mut self, config: &Sega32XEmulatorConfig) {
990        self.config = VdpConfig::from_emu_config(config);
991    }
992}
993
994const RGB_5_TO_8: &[u8; 32] = &[
995    0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132, 140, 148, 156, 165, 173,
996    181, 189, 197, 206, 214, 222, 230, 239, 247, 255,
997];
998
999// Scaled to 0-251 instead of 0-255
1000const RGB_5_TO_8_SLIGHT_DARK: &[u8; 32] = &[
1001    0, 8, 16, 24, 32, 40, 49, 57, 65, 73, 81, 89, 97, 105, 113, 121, 130, 138, 146, 154, 162, 170,
1002    178, 186, 194, 202, 211, 219, 227, 235, 243, 251,
1003];
1004
1005// Scaled to 0-247 instead of 0-255
1006const RGB_5_TO_8_DARK: &[u8; 32] = &[
1007    0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 127, 135, 143, 151, 159, 167,
1008    175, 183, 191, 199, 207, 215, 223, 231, 239, 247,
1009];
1010
1011#[derive(Debug, Clone, Copy)]
1012struct ColorTables {
1013    red: &'static [u8; 32],
1014    green: &'static [u8; 32],
1015    blue: &'static [u8; 32],
1016}
1017
1018impl ColorTables {
1019    fn from_tint(color_tint: S32XColorTint) -> Self {
1020        match color_tint {
1021            S32XColorTint::None => Self { red: RGB_5_TO_8, green: RGB_5_TO_8, blue: RGB_5_TO_8 },
1022            // yellow tint = blue deficiency
1023            S32XColorTint::SlightYellow => {
1024                Self { red: RGB_5_TO_8, green: RGB_5_TO_8, blue: RGB_5_TO_8_SLIGHT_DARK }
1025            }
1026            S32XColorTint::Yellow => {
1027                Self { red: RGB_5_TO_8, green: RGB_5_TO_8, blue: RGB_5_TO_8_DARK }
1028            }
1029            // purple tint = green deficiency
1030            S32XColorTint::SlightPurple => {
1031                Self { red: RGB_5_TO_8, green: RGB_5_TO_8_SLIGHT_DARK, blue: RGB_5_TO_8 }
1032            }
1033            S32XColorTint::Purple => {
1034                Self { red: RGB_5_TO_8, green: RGB_5_TO_8_DARK, blue: RGB_5_TO_8 }
1035            }
1036        }
1037    }
1038}
1039
1040fn u16_to_rgb(s32x_pixel: u16, color_tables: ColorTables) -> Color {
1041    let r = s32x_pixel & 0x1F;
1042    let g = (s32x_pixel >> 5) & 0x1F;
1043    let b = (s32x_pixel >> 10) & 0x1F;
1044
1045    Color::rgb(
1046        color_tables.red[r as usize],
1047        color_tables.green[g as usize],
1048        color_tables.blue[b as usize],
1049    )
1050}
1051
1052const fn genesis_expanded_pixel_width<const H32: bool>() -> u32 {
1053    if H32 { 5 } else { 4 }
1054}
1055
1056fn determine_expanded_buffer_width<const H32: bool>(
1057    frame_size: FrameSize,
1058    border_size: BorderSize,
1059) -> u32 {
1060    let gen_pixel_width = genesis_expanded_pixel_width::<H32>();
1061
1062    if H32 && border_size.right < H32_H_OFFSET.div_ceil(gen_pixel_width) {
1063        gen_pixel_width * frame_size.width + H32_H_OFFSET
1064    } else {
1065        gen_pixel_width * frame_size.width
1066    }
1067}