render.rsannotatedrender.rssource1137 lines · 42.5 KB · raw
1use crate::vdp;
2use crate::vdp::colors::{ColorModifier, ColorTables};
3use crate::vdp::registers::{
4    DebugRegister, HorizontalDisplaySize, InterlacingMode, Plane, RIGHT_BORDER, Registers,
5    ScrollSize, VerticalDisplaySize, VerticalScrollMode,
6};
7use crate::vdp::{TilePixel, TimingModeExt, Vdp, Vram, Vsram, colors};
8use jgenesis_common::frontend::{Color, TimingMode};
9use jgenesis_common::num::GetBit;
10use std::{array, cmp};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13enum BgPlane {
14    A,
15    B,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct RasterLine {
20    pub line: u16,
21    pub in_v_border: bool,
22}
23
24impl RasterLine {
25    pub fn from_scanline(
26        scanline: u16,
27        registers: &Registers,
28        timing_mode: TimingMode,
29        interlaced: bool,
30        interlaced_odd: bool,
31    ) -> Self {
32        let v_display_size = registers.vertical_display_size;
33        let active_scanlines = v_display_size.active_scanlines();
34        let scanlines_per_frame = timing_mode.scanlines_per_frame(interlaced, interlaced_odd);
35        let top_border = v_display_size.top_border(timing_mode);
36
37        if scanline < active_scanlines {
38            // Active display
39            Self { line: scanline, in_v_border: false }
40        } else if scanline >= scanlines_per_frame - top_border {
41            // Top border; bottom line is raster line 511
42            let line = 512 - (scanlines_per_frame - scanline);
43            Self { line, in_v_border: true }
44        } else {
45            // Bottom border and VBlank
46            Self { line: scanline, in_v_border: true }
47        }
48    }
49
50    pub fn to_interlaced_even(self) -> Self {
51        Self { line: (2 * self.line) & 0x1FF, in_v_border: self.in_v_border }
52    }
53
54    pub fn to_interlaced_odd(self) -> Self {
55        Self { line: (2 * self.line + 1) & 0x1FF, in_v_border: self.in_v_border }
56    }
57
58    pub fn to_frame_buffer_row(
59        self,
60        top_border: u16,
61        timing_mode: TimingMode,
62        render_vertical_border: bool,
63    ) -> Option<u32> {
64        if render_vertical_border {
65            if self.line >= 512 - top_border {
66                // Top border
67                Some((self.line - (512 - top_border)).into())
68            } else if self.line < timing_mode.rendered_lines_per_frame() - top_border {
69                // Active display or bottom border
70                Some((self.line + top_border).into())
71            } else {
72                // VBlank
73                None
74            }
75        } else {
76            // If not rendering the vertical border, frame buffer row == raster line
77            (!self.in_v_border).then_some(self.line.into())
78        }
79    }
80
81    pub fn previous_line(self, v_display_size: VerticalDisplaySize) -> Self {
82        if self.line == 0 {
83            Self { line: 511, in_v_border: true }
84        } else {
85            let line = self.line - 1;
86            Self { line, in_v_border: line >= v_display_size.active_scanlines() }
87        }
88    }
89}
90
91impl Vdp {
92    pub(super) fn render_scanline(&mut self, scanline: u16, starting_pixel: u16) {
93        if starting_pixel
94            >= self.latched_registers.horizontal_display_size.active_display_pixels() - 16
95        {
96            // Don't re-render for mid-scanline writes that occur very near the end of active display;
97            // this can cause visual glitches due to some underlying issues in how timing is handled
98            // between the 68000 and VDP
99            return;
100        }
101
102        log::trace!("Rendering line {scanline} from pixel {starting_pixel}");
103
104        let raster_line = RasterLine::from_scanline(
105            scanline,
106            &self.latched_registers,
107            self.timing_mode,
108            self.state.interlaced_frame,
109            self.state.interlaced_odd,
110        );
111        let frame_buffer_row = raster_line.to_frame_buffer_row(
112            self.state.top_border,
113            self.timing_mode,
114            self.config.render_vertical_border,
115        );
116
117        // TODO interlacing mode should be latched at start of VBlank
118        match self.latched_registers.interlacing_mode {
119            InterlacingMode::Progressive => {
120                self.do_render_scanline(
121                    scanline,
122                    raster_line,
123                    starting_pixel,
124                    frame_buffer_row,
125                    false,
126                );
127            }
128            InterlacingMode::Interlaced => {
129                let odd_frame = self.state.interlaced_odd();
130                let frame_buffer_row = frame_buffer_row.map(|row| 2 * row + u32::from(odd_frame));
131
132                self.do_render_scanline(
133                    scanline,
134                    raster_line,
135                    starting_pixel,
136                    frame_buffer_row,
137                    false,
138                );
139
140                if self.config.deinterlace
141                    && let Some(frame_buffer_row) = frame_buffer_row
142                {
143                    self.copy_frame_buffer_row(frame_buffer_row, frame_buffer_row ^ 1);
144                }
145            }
146            InterlacingMode::InterlacedDouble => {
147                let odd_frame = self.state.interlaced_odd();
148
149                if self.config.deinterlace || !odd_frame {
150                    self.do_render_scanline(
151                        scanline,
152                        raster_line.to_interlaced_even(),
153                        starting_pixel,
154                        frame_buffer_row.map(|row| 2 * row),
155                        false,
156                    );
157                }
158                if self.config.deinterlace || odd_frame {
159                    self.do_render_scanline(
160                        scanline,
161                        raster_line.to_interlaced_odd(),
162                        starting_pixel,
163                        frame_buffer_row.map(|row| 2 * row + 1),
164                        true,
165                    );
166                }
167            }
168        }
169
170        if starting_pixel == 0 {
171            self.apply_cram_dots_previous_line(raster_line);
172        }
173    }
174
175    fn do_render_scanline(
176        &mut self,
177        scanline: u16,
178        raster_line: RasterLine,
179        starting_pixel: u16,
180        frame_buffer_row: Option<u32>,
181        interlaced_odd_line: bool,
182    ) {
183        if !self.registers.display_enabled {
184            let Some(frame_buffer_row) = frame_buffer_row else { return };
185
186            let bg_color = self.backdrop_color();
187            self.fill_frame_buffer_row(frame_buffer_row, starting_pixel, bg_color);
188
189            // Clear sprite pixel buffer in case display is enabled during active display
190            if interlaced_odd_line {
191                self.interlaced_sprite_buffers.pixels.fill(TilePixel::default());
192            } else {
193                self.sprite_buffers.pixels.fill(TilePixel::default());
194            }
195
196            return;
197        }
198
199        // Only perform sprite pixel and/or right border rendering if rendering from the start of the line
200        if starting_pixel == 0 {
201            // Sprite pixel rendering + tile fetching is not performed inside the non-forgotten vertical border except
202            // on the line immediately following the end of active display
203            if !raster_line.in_v_border
204                || self.state.v_border_forgotten
205                || raster_line.line
206                    == self.latched_registers.vertical_display_size.active_scanlines()
207            {
208                self.render_sprite_pixels(raster_line, interlaced_odd_line);
209            }
210
211            // Check if the previous line's right border should be rendered
212            // This needs to happen after the previous line is rendered because it depends on which sprite tiles were
213            // fetched for the next/current line
214            if self.config.render_horizontal_border
215                && matches!(self.debug_register.forced_plane, Plane::ScrollA | Plane::ScrollB)
216            {
217                let prev_raster_line =
218                    raster_line.previous_line(self.latched_registers.vertical_display_size);
219                if !prev_raster_line.in_v_border
220                    || self.state.v_border_forgotten
221                    || prev_raster_line.line == 511
222                {
223                    // TODO this does not work correctly in interlaced modes - wrong frame buffer row
224                    if let Some(right_border_row) = prev_raster_line.to_frame_buffer_row(
225                        self.state.top_border,
226                        self.timing_mode,
227                        self.config.render_vertical_border,
228                    ) {
229                        self.render_right_border(right_border_row, self.latched_h_scroll);
230                    }
231                }
232            }
233        }
234
235        let Some(frame_buffer_row) = frame_buffer_row else { return };
236
237        if raster_line.in_v_border && !self.state.v_border_forgotten && raster_line.line != 511 {
238            self.render_vertical_border_line(scanline, frame_buffer_row, starting_pixel);
239            return;
240        }
241
242        let active_display_pixels =
243            self.latched_registers.horizontal_display_size.active_display_pixels();
244        let (fb_start_col, fb_end_col, fb_pixel_offset) = if self.config.render_horizontal_border {
245            let left_border: u32 =
246                self.latched_registers.horizontal_display_size.left_border().into();
247            let start_col =
248                if starting_pixel == 0 { 0 } else { u32::from(starting_pixel) + left_border };
249            let end_col = left_border + u32::from(active_display_pixels) + u32::from(RIGHT_BORDER);
250
251            (start_col as i16, end_col as i16, left_border as i16)
252        } else {
253            (starting_pixel as i16, active_display_pixels as i16, 0)
254        };
255
256        self.render_bgs_to_buffer(raster_line.line, fb_start_col, fb_end_col, fb_pixel_offset);
257        self.render_window_to_buffer(raster_line.line, fb_start_col, fb_end_col, fb_pixel_offset);
258
259        self.merge_layers(
260            raster_line,
261            frame_buffer_row,
262            fb_start_col as u32,
263            fb_end_col as u32,
264            fb_pixel_offset,
265            interlaced_odd_line,
266        );
267
268        // TODO the left border should really be rendered after sprite tile fetching and reading
269        // H scroll values, but before rendering the background planes. Otherwise it uses the
270        // scroll B palettes from the current line instead of the previous line.
271        if self.config.render_horizontal_border {
272            self.render_left_border(frame_buffer_row, self.backdrop_color(), self.latched_h_scroll);
273
274            // If not using debug register to force one of the BG planes, render right border now
275            // in case the backdrop color changes between lines
276            if matches!(self.debug_register.forced_plane, Plane::Background | Plane::Sprite) {
277                self.render_right_border(frame_buffer_row, self.latched_h_scroll);
278            }
279        }
280    }
281
282    fn fill_frame_buffer_row(&mut self, row: u32, starting_pixel: u16, color: u16) {
283        let screen_width = self.screen_width();
284
285        // TODO properly handle BG color changes inside the left border - requires adjusting when
286        // line rendering occurs, or adding some state to track which pixels not to overwrite
287        let left_border = if self.config.render_horizontal_border {
288            self.state.frame_h_resolution.left_border()
289        } else {
290            0
291        };
292        let starting_col =
293            if starting_pixel == 0 { 0 } else { u32::from(starting_pixel + left_border) };
294
295        for pixel in starting_col..screen_width {
296            set_in_frame_buffer(
297                &mut self.frame_buffer,
298                row,
299                pixel,
300                color,
301                ColorModifier::None,
302                screen_width,
303                &self.color_tables,
304            );
305        }
306    }
307
308    fn copy_frame_buffer_row(&mut self, from_row: u32, to_row: u32) {
309        debug_assert_ne!(from_row, to_row);
310
311        let screen_width = self.screen_width();
312        let from_addr = (from_row * screen_width) as usize;
313        let to_addr = (to_row * screen_width) as usize;
314
315        if to_addr > from_addr {
316            let (a, b) = self.frame_buffer.split_at_mut(to_addr);
317            b[..screen_width as usize]
318                .copy_from_slice(&a[from_addr..from_addr + screen_width as usize]);
319        } else {
320            let (a, b) = self.frame_buffer.split_at_mut(from_addr);
321            a[to_addr..to_addr + screen_width as usize]
322                .copy_from_slice(&b[..screen_width as usize]);
323        }
324    }
325
326    #[allow(clippy::identity_op)]
327    fn render_bgs_to_buffer(
328        &mut self,
329        raster_line: u16,
330        fb_start_col: i16,
331        fb_end_col: i16,
332        fb_pixel_offset: i16,
333    ) {
334        let cell_height_shift = self.latched_registers.interlacing_mode.cell_height_shift();
335        let v_scroll_size = self.latched_registers.vertical_scroll_size;
336        let h_scroll_size = self.latched_registers.horizontal_scroll_size;
337
338        let (h_scroll_size_pixels, v_scroll_size_pixels) = match (h_scroll_size, v_scroll_size) {
339            // An invalid H scroll size always produces 32x1 scroll planes
340            (ScrollSize::Invalid, _) => (32 * 8, 1 * 8),
341            // An invalid V scroll size with valid H scroll size functions as a size of 32
342            (_, ScrollSize::Invalid) => (h_scroll_size.to_pixels(), 32 * 8),
343            _ => (h_scroll_size.to_pixels(), v_scroll_size.to_pixels()),
344        };
345        let h_scroll_size_cells = h_scroll_size_pixels / 8;
346
347        let scroll_line_bit_mask = match self.latched_registers.interlacing_mode {
348            InterlacingMode::Progressive | InterlacingMode::Interlaced => v_scroll_size_pixels - 1,
349            InterlacingMode::InterlacedDouble => (v_scroll_size_pixels << 1) - 1,
350        };
351
352        let (h_scroll_a, h_scroll_b) = self.latched_h_scroll;
353
354        let active_display_pixels =
355            self.latched_registers.horizontal_display_size.active_display_pixels();
356        let active_display_cells = active_display_pixels / 8;
357
358        for plane in [BgPlane::A, BgPlane::B] {
359            let (enabled, pixel_buffer, nametable_base_addr, h_scroll) = match plane {
360                BgPlane::A => (
361                    self.config.plane_a_enabled,
362                    &mut self.bg_buffers.plane_a_pixels,
363                    self.latched_registers.scroll_a_base_nt_addr,
364                    h_scroll_a,
365                ),
366                BgPlane::B => (
367                    self.config.plane_b_enabled,
368                    &mut self.bg_buffers.plane_b_pixels,
369                    self.latched_registers.scroll_b_base_nt_addr,
370                    h_scroll_b,
371                ),
372            };
373
374            if !enabled {
375                pixel_buffer.fill(TilePixel::default());
376                continue;
377            }
378
379            // The VDP renders in columns of 16 pixels
380            // If H scroll is not a multiple of 16, consider the remainder "fine H scroll"
381            let coarse_h_scroll = h_scroll & !15;
382            let fine_h_scroll = (h_scroll & 15) as i16;
383
384            // The VDP always renders one column partially or fully to the left of active display,
385            // column -1.
386            // If fine H scrolling is used, the leftmost 1 to 15 pixels in active display come from
387            // this column
388            let start_h_column =
389                if fine_h_scroll != 0 || self.config.render_horizontal_border { -1 } else { 0 };
390            let end_h_column = (active_display_cells / 2) as i16;
391
392            for h_column in start_h_column..end_h_column {
393                let v_scroll = read_v_scroll(
394                    &self.vsram,
395                    plane,
396                    h_column,
397                    &self.latched_registers,
398                    self.latched_full_screen_v_scroll,
399                );
400
401                let scrolled_scanline = raster_line.wrapping_add(v_scroll) & scroll_line_bit_mask;
402                let scrolled_v_cell = scrolled_scanline >> cell_height_shift;
403
404                let column_scrolled_pixel = ((16 * h_column) as u16).wrapping_sub(coarse_h_scroll);
405                let column_scrolled_h_cell = column_scrolled_pixel / 8;
406
407                // Each 16-pixel column consists of two 8-pixel cells
408                for h_cell_offset in 0..2 {
409                    let cell_fb_col =
410                        16 * h_column + 8 * h_cell_offset + fine_h_scroll + fb_pixel_offset;
411                    if cell_fb_col + 8 <= fb_start_col || cell_fb_col >= fb_end_col {
412                        continue;
413                    }
414
415                    let scrolled_h_cell = column_scrolled_h_cell.wrapping_add(h_cell_offset as u16)
416                        & (h_scroll_size_cells - 1);
417
418                    let nametable_word = read_name_table_word(
419                        &self.vram,
420                        nametable_base_addr,
421                        h_scroll_size.into(),
422                        scrolled_v_cell,
423                        scrolled_h_cell,
424                    );
425
426                    if plane == BgPlane::B {
427                        self.state.last_scroll_b_palettes[0] = self.state.last_scroll_b_palettes[1];
428                        self.state.last_scroll_b_palettes[1] = nametable_word.palette;
429                    }
430
431                    let colors = read_pattern_generator_row(
432                        &self.vram,
433                        PatternGeneratorRowArgs {
434                            vertical_flip: nametable_word.vertical_flip,
435                            horizontal_flip: nametable_word.horizontal_flip,
436                            pattern_generator: nametable_word.pattern_generator,
437                            row: scrolled_scanline,
438                            cell_height_shift,
439                        },
440                    );
441
442                    for pixel_offset in 0..8 {
443                        let fb_col = cell_fb_col + pixel_offset;
444                        if !(fb_start_col..fb_end_col).contains(&fb_col) {
445                            continue;
446                        }
447
448                        pixel_buffer[fb_col as usize] = TilePixel {
449                            color: colors[pixel_offset as usize],
450                            palette: nametable_word.palette,
451                            priority: nametable_word.priority,
452                        };
453                    }
454                }
455            }
456        }
457    }
458
459    fn render_window_to_buffer(
460        &mut self,
461        raster_line: u16,
462        fb_start_col: i16,
463        fb_end_col: i16,
464        fb_pixel_offset: i16,
465    ) {
466        if !self.config.window_enabled {
467            return;
468        }
469
470        let active_display_pixels =
471            self.latched_registers.horizontal_display_size.active_display_pixels();
472        let (window_start, window_end) = if self.latched_registers.is_line_in_v_window(raster_line)
473        {
474            (0, active_display_pixels)
475        } else {
476            self.latched_registers.window_h_range(active_display_pixels)
477        };
478
479        if window_start >= window_end {
480            // Window is empty on this line
481            return;
482        }
483
484        let window_start_cell = window_start / 8;
485        let window_end_cell = window_end / 8;
486        for h_cell in window_start_cell..window_end_cell {
487            let pixel = 8 * h_cell;
488            let cell_fb_col = (pixel as i16) + fb_pixel_offset;
489            if cell_fb_col + 8 <= fb_start_col || cell_fb_col >= fb_end_col {
490                continue;
491            }
492
493            let cell_height_shift = self.latched_registers.interlacing_mode.cell_height_shift();
494            let v_cell = raster_line >> cell_height_shift;
495
496            let nametable_word = read_name_table_word(
497                &self.vram,
498                self.latched_registers.masked_window_nametable_addr(),
499                self.latched_registers.horizontal_display_size.window_width_cells(),
500                v_cell,
501                h_cell,
502            );
503
504            let colors = read_pattern_generator_row(
505                &self.vram,
506                PatternGeneratorRowArgs {
507                    vertical_flip: nametable_word.vertical_flip,
508                    horizontal_flip: nametable_word.horizontal_flip,
509                    pattern_generator: nametable_word.pattern_generator,
510                    row: raster_line,
511                    cell_height_shift,
512                },
513            );
514
515            for pixel_offset in 0..8 {
516                let fb_col = cell_fb_col + pixel_offset;
517                if !(fb_start_col..fb_end_col).contains(&fb_col) {
518                    continue;
519                }
520
521                // Window replaces Plane A when enabled
522                self.bg_buffers.plane_a_pixels[fb_col as usize] = TilePixel {
523                    color: colors[pixel_offset as usize],
524                    palette: nametable_word.palette,
525                    priority: nametable_word.priority,
526                };
527            }
528        }
529    }
530
531    fn merge_layers(
532        &mut self,
533        raster_line: RasterLine,
534        frame_buffer_row: u32,
535        fb_start_col: u32,
536        fb_end_col: u32,
537        fb_pixel_offset: i16,
538        interlaced_odd_line: bool,
539    ) {
540        let sprite_buffers = if interlaced_odd_line {
541            &self.interlaced_sprite_buffers
542        } else {
543            &self.sprite_buffers
544        };
545
546        let bg_cram_idx = self.backdrop_cram_idx();
547
548        let screen_width = self.screen_width();
549        let active_display_pixels =
550            self.latched_registers.horizontal_display_size.active_display_pixels();
551
552        for frame_buffer_col in fb_start_col..fb_end_col {
553            let pixel = frame_buffer_col as i16 - fb_pixel_offset;
554
555            let sprite_pixel = if self.config.sprites_enabled {
556                sprite_buffers.pixels.get(pixel as usize).copied().unwrap_or(TilePixel::default())
557            } else {
558                TilePixel::default()
559            };
560
561            let scroll_a_pixel = self.bg_buffers.plane_a_pixels[frame_buffer_col as usize];
562            let scroll_b_pixel = self.bg_buffers.plane_b_pixels[frame_buffer_col as usize];
563
564            let pixel_color_args = PixelColorArgs {
565                sprite_pixel,
566                scroll_a_pixel,
567                scroll_b_pixel,
568                bg_cram_idx,
569                in_h_border: !(0..active_display_pixels as i16).contains(&pixel),
570                in_v_border: raster_line.in_v_border && !self.state.v_border_forgotten,
571            };
572            let (pixel_cram_idx, color_modifier) = if self.latched_registers.shadow_highlight_flag {
573                determine_pixel_color::<true>(self.debug_register, pixel_color_args)
574            } else {
575                determine_pixel_color::<false>(self.debug_register, pixel_color_args)
576            };
577
578            let pixel_color = colors::lookup(&self.cram, pixel_cram_idx);
579
580            set_in_frame_buffer(
581                &mut self.frame_buffer,
582                frame_buffer_row,
583                frame_buffer_col,
584                pixel_color,
585                color_modifier,
586                screen_width,
587                &self.color_tables,
588            );
589        }
590    }
591
592    fn render_vertical_border_line(
593        &mut self,
594        scanline: u16,
595        frame_buffer_row: u32,
596        starting_pixel: u16,
597    ) {
598        match self.debug_register.forced_plane {
599            Plane::Background => {
600                // Fill with the background color
601                let bg_color = self.backdrop_color();
602                self.fill_frame_buffer_row(frame_buffer_row, starting_pixel, bg_color);
603            }
604            Plane::Sprite => {
605                // Fill with color 0
606                self.fill_frame_buffer_row(
607                    frame_buffer_row,
608                    starting_pixel,
609                    colors::lookup(&self.cram, 0),
610                );
611            }
612            Plane::ScrollA | Plane::ScrollB => {
613                // What happens here is quite strange. In actual hardware, the VRAM chip continues cycling through the
614                // 256 bytes in the same VRAM row as the last byte accessed during rendering, which happens to be the
615                // 4th sprite tile fetched for the line immediately after active display. The VDP interprets those
616                // bytes as pixels and displays them using the last palettes that were used during rendering.
617                //
618                // A "row" in VRAM consists of 64 4-byte groups that are each separated by 1KB due to how VRAM addresses
619                // map to physical addresses in the VRAM chip. See:
620                // https://gendev.spritesmind.net/forum/viewtopic.php?p=17583#17583
621                let h_display_size = self.registers.horizontal_display_size;
622                let screen_width = self.screen_width();
623
624                let (start_pixel, end_pixel) = if self.config.render_horizontal_border {
625                    (0, screen_width as u16)
626                } else {
627                    let left_border = h_display_size.left_border();
628                    let active_display_pixels = h_display_size.active_display_pixels();
629                    (left_border, left_border + active_display_pixels)
630                };
631
632                // +2 here is needed to properly align with the horizontal borders in Overdrive 2
633                // The number of 4-byte groups is equal to half the number of pixel clocks per line, 171 in H32 mode
634                // and 210 in H40 mode
635                let group_offset = (scanline + 2
636                    - self.registers.vertical_display_size.active_scanlines())
637                .wrapping_mul(h_display_size.pixels_including_hblank() / 2);
638
639                let base_addr = self.sprite_buffers.last_tile_addresses[3];
640
641                let mut current_addr = base_addr.wrapping_add(group_offset.wrapping_mul(1024));
642                let mut odd_group = false;
643                let mut current_group = [0; 4];
644
645                for pixel in 0..end_pixel {
646                    // +3 here is needed to properly align with the horizontal borders in Overdrive 2
647                    let tile_col = (pixel + 3) % 8;
648                    if pixel == 0 || tile_col == 0 {
649                        current_group.copy_from_slice(
650                            &self.vram[current_addr as usize..(current_addr + 4) as usize],
651                        );
652                        if odd_group {
653                            current_addr = current_addr.wrapping_add(1024);
654                        } else {
655                            current_addr = current_addr.wrapping_add(7 * 1024);
656                        }
657                        odd_group = !odd_group;
658                    }
659
660                    if pixel < start_pixel {
661                        continue;
662                    }
663
664                    let palette = self.state.last_scroll_b_palettes[((pixel / 8) & 1) as usize];
665                    let current_byte = current_group[(tile_col >> 1) as usize];
666                    let color_id = (current_byte >> (4 - ((tile_col & 1) << 2))) & 0x0F;
667                    let color = colors::resolve(&self.cram, palette, color_id);
668
669                    let frame_buffer_col = pixel - start_pixel;
670                    set_in_frame_buffer(
671                        &mut self.frame_buffer,
672                        frame_buffer_row,
673                        frame_buffer_col.into(),
674                        color,
675                        ColorModifier::None,
676                        screen_width,
677                        &self.color_tables,
678                    );
679                }
680            }
681        }
682    }
683
684    fn render_left_border(
685        &mut self,
686        frame_buffer_row: u32,
687        bg_color: u16,
688        (h_scroll_a, h_scroll_b): (u16, u16),
689    ) {
690        let screen_width = self.screen_width();
691        let left_border: u32 = self.latched_registers.horizontal_display_size.left_border().into();
692
693        match self.debug_register.forced_plane {
694            Plane::Background => {
695                // Fill border with background color
696                for col in 0..left_border {
697                    set_in_frame_buffer(
698                        &mut self.frame_buffer,
699                        frame_buffer_row,
700                        col,
701                        bg_color,
702                        ColorModifier::None,
703                        screen_width,
704                        &self.color_tables,
705                    );
706                }
707            }
708            Plane::Sprite => {
709                // Fill border with color 0
710                let color_0 = colors::lookup(&self.cram, 0);
711                for col in 0..left_border {
712                    set_in_frame_buffer(
713                        &mut self.frame_buffer,
714                        frame_buffer_row,
715                        col,
716                        color_0,
717                        ColorModifier::None,
718                        screen_width,
719                        &self.color_tables,
720                    );
721                }
722            }
723            Plane::ScrollA => {
724                // Actual hardware fills the non-rendered pixels with garbage that is somewhat unspecified by Overdrive
725                // 2 docs; just fill them with color 0
726                // Overdrive 2 depends on handling the Scroll A right border correctly but not the left border
727                let border_pixels = h_scroll_a & 15;
728                let border_offset =
729                    16 - self.latched_registers.horizontal_display_size.left_border();
730                let end_col = border_pixels.saturating_sub(border_offset);
731                let color_0 = colors::lookup(&self.cram, 0);
732
733                for col in 0..end_col {
734                    set_in_frame_buffer(
735                        &mut self.frame_buffer,
736                        frame_buffer_row,
737                        col.into(),
738                        color_0,
739                        ColorModifier::None,
740                        screen_width,
741                        &self.color_tables,
742                    );
743                }
744            }
745            Plane::ScrollB => {
746                // Render pixels from sprite tiles 36 and 37 using the palettes from the last 2 tiles of Scroll B in the
747                // previous rendered line
748                let border_pixels = h_scroll_b & 15;
749                let border_offset =
750                    16 - self.latched_registers.horizontal_display_size.left_border();
751                let end_col = border_pixels.saturating_sub(border_offset);
752
753                for col in 0..end_col {
754                    let pixel = 15 - (end_col - 1 - col);
755                    self.render_horizontal_border_sprite_pixel(
756                        frame_buffer_row,
757                        col.into(),
758                        pixel,
759                        36,
760                    );
761                }
762            }
763        }
764    }
765
766    fn render_right_border(&mut self, frame_buffer_row: u32, (h_scroll_a, h_scroll_b): (u16, u16)) {
767        let screen_width = self.screen_width() as u16;
768        let right_border_start = screen_width - RIGHT_BORDER;
769
770        match self.debug_register.forced_plane {
771            Plane::Background => {
772                // Fill border with background color
773                let bg_color = self.backdrop_color();
774                for col in right_border_start..screen_width {
775                    set_in_frame_buffer(
776                        &mut self.frame_buffer,
777                        frame_buffer_row,
778                        col.into(),
779                        bg_color,
780                        ColorModifier::None,
781                        screen_width.into(),
782                        &self.color_tables,
783                    );
784                }
785            }
786            Plane::Sprite => {
787                // Fill border with color 0
788                let color_0 = colors::lookup(&self.cram, 0);
789                for col in right_border_start..screen_width {
790                    set_in_frame_buffer(
791                        &mut self.frame_buffer,
792                        frame_buffer_row,
793                        col.into(),
794                        color_0,
795                        ColorModifier::None,
796                        screen_width.into(),
797                        &self.color_tables,
798                    );
799                }
800            }
801            Plane::ScrollA => {
802                // Render pixels from sprite tiles 0 and 1 using the palettes from the last 2 tiles of Scroll B in the
803                // previous rendered line
804                let last_column_end = right_border_start + cmp::min(h_scroll_a & 15, RIGHT_BORDER);
805
806                for col in last_column_end..screen_width {
807                    let pixel = col - last_column_end;
808                    self.render_horizontal_border_sprite_pixel(
809                        frame_buffer_row,
810                        col.into(),
811                        pixel,
812                        0,
813                    );
814                }
815            }
816            Plane::ScrollB => {
817                // Render pixels from sprite tiles 4 and 5 using the palettes from the last 2 tiles of Scroll B in the
818                // previous rendered line
819                let last_column_end = right_border_start + cmp::min(h_scroll_b & 15, RIGHT_BORDER);
820
821                for col in last_column_end..screen_width {
822                    let pixel = col - last_column_end;
823                    self.render_horizontal_border_sprite_pixel(
824                        frame_buffer_row,
825                        col.into(),
826                        pixel,
827                        4,
828                    );
829                }
830            }
831        }
832    }
833
834    fn render_horizontal_border_sprite_pixel(
835        &mut self,
836        frame_buffer_row: u32,
837        frame_buffer_col: u32,
838        pixel: u16,
839        base_sprite: u16,
840    ) {
841        let sprite_tile = base_sprite + pixel / 8;
842        let sprite_col = pixel % 8;
843        let vram_addr =
844            self.sprite_buffers.last_tile_addresses[sprite_tile as usize] + (sprite_col >> 1);
845        let color_id = (self.vram[vram_addr as usize] >> (4 - ((sprite_col & 1) << 2))) & 0x0F;
846        let palette = self.state.last_scroll_b_palettes[(pixel / 8) as usize];
847        let color = colors::resolve(&self.cram, palette, color_id);
848
849        let screen_width = self.screen_width();
850        set_in_frame_buffer(
851            &mut self.frame_buffer,
852            frame_buffer_row,
853            frame_buffer_col,
854            color,
855            ColorModifier::None,
856            screen_width,
857            &self.color_tables,
858        );
859    }
860
861    fn backdrop_cram_idx(&self) -> u8 {
862        (self.registers.background_palette << 4) | self.registers.background_color_id
863    }
864
865    fn backdrop_color(&self) -> u16 {
866        colors::lookup(&self.cram, self.backdrop_cram_idx())
867    }
868}
869
870pub(super) fn set_in_frame_buffer(
871    frame_buffer: &mut [Color; vdp::FRAME_BUFFER_LEN],
872    row: u32,
873    col: u32,
874    color: u16,
875    modifier: ColorModifier,
876    screen_width: u32,
877    color_tables: &ColorTables,
878) {
879    let r = ((color >> 1) & 0x07) as u8;
880    let g = ((color >> 5) & 0x07) as u8;
881    let b = ((color >> 9) & 0x07) as u8;
882    let a = (color >> 15) as u8;
883    let rgb_color = colors::gen_to_rgba(r, g, b, a, modifier, color_tables);
884
885    frame_buffer[(row * screen_width + col) as usize] = rgb_color;
886}
887
888fn read_v_scroll(
889    vsram: &Vsram,
890    plane: BgPlane,
891    h_column: i16,
892    registers: &Registers,
893    latched_full_screen_v_scroll: (u16, u16),
894) -> u16 {
895    let v_scroll = match registers.vertical_scroll_mode {
896        VerticalScrollMode::FullScreen => match plane {
897            BgPlane::A => latched_full_screen_v_scroll.0,
898            BgPlane::B => latched_full_screen_v_scroll.1,
899        },
900        VerticalScrollMode::TwoCell => {
901            read_two_cell_v_scroll(h_column, plane, vsram, registers.horizontal_display_size)
902        }
903    };
904
905    let v_scroll_mask = registers.interlacing_mode.v_scroll_mask();
906    v_scroll & v_scroll_mask
907}
908
909fn read_two_cell_v_scroll(
910    h_column: i16,
911    plane: BgPlane,
912    vsram: &Vsram,
913    h_display_size: HorizontalDisplaySize,
914) -> u16 {
915    let active_display_columns = (h_display_size.active_display_pixels() / 16) as i16;
916    if h_column < 0 {
917        // Column -1 behaves weirdly.
918        // In H40 mode, it uses a V scroll value of VSRAM[$4C] & VSRAM[$4E] for both backgrounds.
919        // In H32 mode, it always uses a V scroll value of 0.
920        // Source: http://gendev.spritesmind.net/forum/viewtopic.php?t=737&postdays=0&postorder=asc&start=30
921        match h_display_size {
922            HorizontalDisplaySize::ThirtyTwoCell => 0,
923            HorizontalDisplaySize::FortyCell => vsram[0x4C >> 1] & vsram[0x4E >> 1],
924        }
925    } else if h_column < active_display_columns {
926        let offset = match plane {
927            BgPlane::A => 0,
928            BgPlane::B => 1,
929        };
930        let addr = 2 * (h_column as usize) + offset;
931        vsram[addr]
932    } else {
933        0
934    }
935}
936
937#[derive(Debug, Clone, Copy, Default)]
938struct NameTableWord {
939    priority: bool,
940    palette: u8,
941    vertical_flip: bool,
942    horizontal_flip: bool,
943    pattern_generator: u16,
944}
945
946fn read_name_table_word(
947    vram: &Vram,
948    base_addr: u16,
949    name_table_width: u16,
950    row: u16,
951    col: u16,
952) -> NameTableWord {
953    // Nametable size is limited to 8KB
954    // If dimensions are 64x128, 128x64, or 128x128 then addresses will wrap at the 8KB boundary
955    let relative_addr = (2 * (row * name_table_width + col)) & 0x1FFF;
956    let addr = base_addr.wrapping_add(relative_addr);
957    let word = u16::from_be_bytes([vram[addr as usize], vram[addr.wrapping_add(1) as usize]]);
958
959    NameTableWord {
960        priority: word.bit(15),
961        palette: ((word >> 13) & 0x03) as u8,
962        vertical_flip: word.bit(12),
963        horizontal_flip: word.bit(11),
964        pattern_generator: word & 0x07FF,
965    }
966}
967
968#[derive(Debug, Clone)]
969pub struct PatternGeneratorRowArgs {
970    pub vertical_flip: bool,
971    pub horizontal_flip: bool,
972    pub pattern_generator: u16,
973    pub row: u16,
974    pub cell_height_shift: u16,
975}
976
977#[inline]
978pub fn read_pattern_generator_row(
979    vram: &Vram,
980    PatternGeneratorRowArgs {
981        vertical_flip,
982        horizontal_flip,
983        pattern_generator,
984        row,
985        cell_height_shift,
986    }: PatternGeneratorRowArgs,
987) -> [u8; 8] {
988    let cell_height = 1 << cell_height_shift;
989    let cell_row = if vertical_flip {
990        cell_height - 1 - (row & (cell_height - 1))
991    } else {
992        row & (cell_height - 1)
993    };
994
995    let cell_addr = (4 * cell_height).wrapping_mul(pattern_generator);
996    let row_addr = (cell_addr + 4 * cell_row) as usize;
997    let mut colors: [u8; 8] = array::from_fn(|i| {
998        let addr = row_addr + i / 2;
999        let byte = vram[addr];
1000        (byte >> (((i & 1) ^ 1) << 2)) & 0x0F
1001    });
1002
1003    if horizontal_flip {
1004        colors.reverse();
1005    }
1006
1007    colors
1008}
1009
1010#[derive(Debug, Clone, Copy)]
1011struct UnresolvedColor {
1012    palette: u8,
1013    color_id: u8,
1014    is_sprite: bool,
1015}
1016
1017struct PixelColorArgs {
1018    sprite_pixel: TilePixel,
1019    scroll_a_pixel: TilePixel,
1020    scroll_b_pixel: TilePixel,
1021    bg_cram_idx: u8,
1022    in_h_border: bool,
1023    in_v_border: bool,
1024}
1025
1026#[inline]
1027#[allow(clippy::unnested_or_patterns)]
1028fn determine_pixel_color<const SHADOW_HIGHLIGHT: bool>(
1029    debug_register: DebugRegister,
1030    PixelColorArgs {
1031        sprite_pixel,
1032        scroll_a_pixel,
1033        scroll_b_pixel,
1034        bg_cram_idx,
1035        in_h_border,
1036        in_v_border,
1037    }: PixelColorArgs,
1038) -> (u8, ColorModifier) {
1039    let sprite_cram_idx = (sprite_pixel.palette << 4) | sprite_pixel.color;
1040    let scroll_a_cram_idx = (scroll_a_pixel.palette << 4) | scroll_a_pixel.color;
1041    let scroll_b_cram_idx = (scroll_b_pixel.palette << 4) | scroll_b_pixel.color;
1042
1043    if in_h_border || in_v_border || debug_register.display_disabled {
1044        let cram_idx = match debug_register.forced_plane {
1045            Plane::Background => bg_cram_idx,
1046            Plane::Sprite => {
1047                if in_h_border {
1048                    0
1049                } else {
1050                    sprite_cram_idx
1051                }
1052            }
1053            Plane::ScrollA => scroll_a_cram_idx,
1054            Plane::ScrollB => scroll_b_cram_idx,
1055        };
1056        return (cram_idx, ColorModifier::None);
1057    }
1058
1059    let mut modifier = if SHADOW_HIGHLIGHT && !scroll_a_pixel.priority && !scroll_b_pixel.priority {
1060        // If shadow/highlight bit is set and all priority flags are 0, default modifier to shadow
1061        ColorModifier::Shadow
1062    } else {
1063        ColorModifier::None
1064    };
1065
1066    let sprite = UnresolvedColor {
1067        palette: sprite_pixel.palette,
1068        color_id: sprite_pixel.color,
1069        is_sprite: true,
1070    };
1071    let scroll_a = UnresolvedColor {
1072        palette: scroll_a_pixel.palette,
1073        color_id: scroll_a_pixel.color,
1074        is_sprite: false,
1075    };
1076    let scroll_b = UnresolvedColor {
1077        palette: scroll_b_pixel.palette,
1078        color_id: scroll_b_pixel.color,
1079        is_sprite: false,
1080    };
1081    let colors = match (sprite_pixel.priority, scroll_a_pixel.priority, scroll_b_pixel.priority) {
1082        (false, false, false) | (true, false, false) | (true, true, false) | (true, true, true) => {
1083            [sprite, scroll_a, scroll_b]
1084        }
1085        (false, true, false) => [scroll_a, sprite, scroll_b],
1086        (false, false, true) => [scroll_b, sprite, scroll_a],
1087        (true, false, true) => [sprite, scroll_b, scroll_a],
1088        (false, true, true) => [scroll_a, scroll_b, sprite],
1089    };
1090
1091    for UnresolvedColor { palette, color_id, is_sprite } in colors {
1092        if color_id == 0 {
1093            // Pixel is transparent
1094            continue;
1095        }
1096
1097        if SHADOW_HIGHLIGHT && is_sprite && palette == 3 {
1098            if color_id == 14 {
1099                // Palette 3 + color 14 = highlight; sprite is transparent, underlying pixel is highlighted
1100                modifier += ColorModifier::Highlight;
1101                continue;
1102            } else if color_id == 15 {
1103                // Palette 3 + color 15 = shadow; sprite is transparent, underlying pixel is shadowed
1104                modifier = ColorModifier::Shadow;
1105                continue;
1106            }
1107        }
1108
1109        // If debug register is used to force a plane, the 6-bit color value from that plane masks
1110        // the 6-bit color value of the frontmost pixel.
1111        // Titan Overdrive 2 uses this extensively
1112        let cram_idx_mask = match debug_register.forced_plane {
1113            Plane::Background => 0x3F,
1114            Plane::Sprite => sprite_cram_idx,
1115            Plane::ScrollA => scroll_a_cram_idx,
1116            Plane::ScrollB => scroll_b_cram_idx,
1117        };
1118        let cram_idx = ((palette << 4) | color_id) & cram_idx_mask;
1119
1120        // Sprite color id 14 is never shadowed/highlighted, and neither is a sprite with the priority
1121        // bit set
1122        if SHADOW_HIGHLIGHT && is_sprite && (color_id == 14 || sprite_pixel.priority) {
1123            modifier = ColorModifier::None;
1124        }
1125
1126        return (cram_idx, modifier);
1127    }
1128
1129    let fallback_color = match debug_register.forced_plane {
1130        Plane::Background => bg_cram_idx,
1131        Plane::Sprite => sprite_cram_idx,
1132        Plane::ScrollA => scroll_a_cram_idx,
1133        Plane::ScrollB => scroll_b_cram_idx,
1134    };
1135
1136    (fallback_color, modifier)
1137}