1use crate::vdp::registers::{HorizontalDisplaySize, InterlacingMode};
2use crate::vdp::render::{PatternGeneratorRowArgs, RasterLine, read_pattern_generator_row};
3use crate::vdp::{CachedSpriteData, SpriteData, TilePixel, Vdp};
4use bincode::{Decode, Encode};

Sprites with X = $080 display at the left edge of the screen

7pub const SPRITE_H_DISPLAY_START: u16 = 0x080;
9#[derive(Debug, Clone, Default, Encode, Decode)]
10pub struct SpriteState {
11    overflow: bool,
12    collision: bool,
13    dot_overflow_on_prev_line: bool,
14    pixels_disabled_during_hblank: u16,
15    display_enabled: bool,
16    display_enabled_pixel: u16,
17}
18
19impl SpriteState {
20    pub fn overflow_flag(&self) -> bool {
21        self.overflow
22    }
23
24    pub fn collision_flag(&self) -> bool {
25        self.collision
26    }
27
28    pub fn clear_status_flags(&mut self) {
29        self.overflow = false;
30        self.collision = false;
31    }
32
33    pub fn handle_hblank_start(
34        &mut self,
35        h_display_size: HorizontalDisplaySize,
36        display_enabled: bool,
37    ) {
38        self.pixels_disabled_during_hblank = 0;
39
40        self.display_enabled = display_enabled;
41        self.display_enabled_pixel = h_display_size.hblank_begin_h();
42    }
43
44    pub fn handle_display_enabled_write(
45        &mut self,
46        h_display_size: HorizontalDisplaySize,
47        display_enabled: bool,
48        pixel: u16,
49    ) {
50        if (h_display_size.rendering_begin_h()..h_display_size.hblank_begin_h()).contains(&pixel) {
51            // Pre-HBlank write on the next scanline; ignore
52            return;
53        }
54
55        if !self.display_enabled {
56            self.increment_pixels_disabled(pixel, h_display_size);
57        }
58
59        self.display_enabled = display_enabled;
60        self.display_enabled_pixel = pixel;
61    }
62
63    pub fn handle_line_end(&mut self, h_display_size: HorizontalDisplaySize, pixel: u16) {
64        if !self.display_enabled {
65            self.increment_pixels_disabled(pixel, h_display_size);
66        }
67    }
68
69    fn increment_pixels_disabled(&mut self, pixel: u16, h_display_size: HorizontalDisplaySize) {
70        self.pixels_disabled_during_hblank += if pixel >= self.display_enabled_pixel {
71            pixel - self.display_enabled_pixel
72        } else {
73            pixel + h_display_size.pixels_including_hblank() - self.display_enabled_pixel
74        };
75    }
76}
77
78#[derive(Debug, Clone, Encode, Decode)]
79pub struct SpriteBuffers {
80    pub scanned_ids: Vec<u8>,
81    pub sprites: Vec<SpriteData>,
82    pub last_tile_addresses: Box<[u16; 40]>,
83    pub pixels: Box<[TilePixel; 320]>,
84}
85
86impl SpriteBuffers {
87    pub fn new() -> Self {
88        Self {
89            scanned_ids: Vec::with_capacity(20),
90            sprites: Vec::with_capacity(20),
91            last_tile_addresses: vec![0; 40].into_boxed_slice().try_into().unwrap(),
92            pixels: vec![TilePixel::default(); 320].into_boxed_slice().try_into().unwrap(),
93        }
94    }
95}
96
97impl Vdp {
98    // Scan sprites (Phase 1 according to Overdrive 2 documentation).
99    //
100    // This should be called at the end of HBlank 2 scanlines before the line to be rendered, with sprite attributes latched
101    // from around when HINT is generated. Actual hardware does sprite scanning in parallel with sprite pixel fetching for
102    // the next scanline, but here we want to know if there were any pixels where display was disabled during HBlank.
103    pub(super) fn scan_sprites(&mut self, scanline: u16) {
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
112        // In the vertical border, sprite scan only occurs for the scanline immediately following
113        // active display (unless the vertical border was forgotten)
114        if raster_line.in_v_border
115            && !self.state.v_border_forgotten
116            && scanline != self.registers.vertical_display_size.active_scanlines()
117        {
118            return;
119        }
120
121        match self.latched_registers.interlacing_mode {
122            InterlacingMode::Progressive | InterlacingMode::Interlaced => {
123                self.do_sprite_scan(raster_line, false);
124                self.interlaced_sprite_buffers.scanned_ids.clear();
125            }
126            InterlacingMode::InterlacedDouble => {
127                self.do_sprite_scan(raster_line.to_interlaced_even(), false);
128                self.do_sprite_scan(raster_line.to_interlaced_odd(), true);
129            }
130        }
131    }
132
133    fn do_sprite_scan(&mut self, raster_line: RasterLine, use_interlaced_buffers: bool) {
134        let buffers = if use_interlaced_buffers {
135            &mut self.interlaced_sprite_buffers
136        } else {
137            &mut self.sprite_buffers
138        };
139
140        buffers.scanned_ids.clear();
141
142        let h_size = self.latched_registers.horizontal_display_size;
143
144        // If display was disabled during part of HBlank on the scanline before the previous scanline,
145        // the number of sprites scanned for the current scanline is reduced roughly by the number of
146        // pixels that display was disabled for.
147        // Actual hardware doesn't work exactly this way (it depends on exactly which VRAM access slots
148        // display was disabled during), but this approximation works well enough for Mickey Mania's
149        // 3D stages and Titan Overdrive's "your emulator suxx" screen
150        let sprites_skipped: u32 = self.sprite_state.pixels_disabled_during_hblank.into();
151        let max_sprites_to_scan = h_size.sprite_table_len().saturating_sub(sprites_skipped);
152
153        let interlacing_mode = self.latched_registers.interlacing_mode;
154        let sprite_scanline = (interlacing_mode.sprite_display_top() + raster_line.line)
155            & interlacing_mode.sprite_display_mask();
156        let cell_height = interlacing_mode.cell_height();
157        let y_mask = y_position_mask(interlacing_mode);
158
159        let max_sprites_per_line = h_size.max_sprites_per_line() as usize;
160
161        // Sprite 0 is always populated
162        let mut sprite_idx = 0;
163        for _ in 0..max_sprites_to_scan {
164            let CachedSpriteData { v_position, v_size_cells, link_data, .. } =
165                self.latched_sprite_attributes[sprite_idx as usize];
166
167            // Check if sprite falls on this scanline
168            let sprite_top = v_position & y_mask;
169            let sprite_bottom = sprite_top + cell_height * u16::from(v_size_cells);
170            if (sprite_top..sprite_bottom).contains(&sprite_scanline) {
171                // Check if sprite-per-scanline limit has been hit
172                if buffers.scanned_ids.len() == max_sprites_per_line {
173                    self.sprite_state.overflow = true;
174                    if self.config.enforce_sprite_limits {
175                        break;
176                    }
177                }
178
179                buffers.scanned_ids.push(sprite_idx as u8);
180            }
181
182            sprite_idx = link_data.into();
183            if sprite_idx == 0 || sprite_idx >= h_size.sprite_table_len() {
184                break;
185            }
186        }
187    }
188
189    // Fetch sprite attributes from VRAM (Phase 2 in the Overdrive 2 documentation), as well as re-fetch the cached Y
190    // position and sprite size fields. Uses the sprite IDs that were scanned during Phase 1.
191    //
192    // This should be called at the start of HBlank on the scanline before the sprites are to be displayed. On actual
193    // hardware, this occurs in parallel with rendering on the scanline before the sprites are to be displayed.
194    pub(super) fn fetch_sprite_attributes(&mut self) {
195        self.do_sprite_attribute_fetch(false);
196
197        match self.latched_registers.interlacing_mode {
198            InterlacingMode::Progressive | InterlacingMode::Interlaced => {
199                self.interlaced_sprite_buffers.sprites.clear();
200            }
201            InterlacingMode::InterlacedDouble => {
202                self.do_sprite_attribute_fetch(true);
203            }
204        }
205    }
206
207    fn do_sprite_attribute_fetch(&mut self, use_interlaced_buffers: bool) {
208        let buffers = if use_interlaced_buffers {
209            &mut self.interlaced_sprite_buffers
210        } else {
211            &mut self.sprite_buffers
212        };
213
214        buffers.sprites.clear();
215
216        let sprite_table_addr = self.registers.masked_sprite_attribute_table_addr() & 0xFFFF;
217
218        for &sprite_idx in &buffers.scanned_ids {
219            let sprite_addr = sprite_table_addr.wrapping_add(8 * u32::from(sprite_idx)) as usize;
220            let sprite = SpriteData::create(
221                self.cached_sprite_attributes[sprite_idx as usize],
222                &self.vram[sprite_addr + 4..sprite_addr + 8],
223            );
224            buffers.sprites.push(sprite);
225        }
226    }
227
228    // Fetch and render sprite pixels into the line buffer (Phase 3 in the Overdrive 2 documentation). Uses the sprite
229    // attributes that were fetched from VRAM during Phase 2.
230    //
231    // Similar to Phase 1, in actual hardware this occurs throughout HBlank using latched registers. Here, it should be
232    // called at the end of HBlank so that we know how many pixels the display was disabled during HBlank.
233    pub(super) fn render_sprite_pixels(
234        &mut self,
235        raster_line: RasterLine,
236        use_interlaced_buffers: bool,
237    ) {
238        let buffers = if use_interlaced_buffers {
239            &mut self.interlaced_sprite_buffers
240        } else {
241            &mut self.sprite_buffers
242        };
243
244        buffers.pixels.fill(TilePixel::default());
245
246        let h_size = self.latched_registers.horizontal_display_size;
247        let sprite_display_area =
248            SPRITE_H_DISPLAY_START..SPRITE_H_DISPLAY_START + h_size.active_display_pixels();
249
250        let interlacing_mode = self.latched_registers.interlacing_mode;
251        let sprite_scanline = interlacing_mode.sprite_display_top() + raster_line.line;
252        let cell_height_shift = interlacing_mode.cell_height_shift();
253        let y_mask = y_position_mask(interlacing_mode);
254
255        // Apply max sprite tile per scanline limit.
256        //
257        // If display was disabled during HBlank on the previous scanline, the number of sprite tiles
258        // rendered is reduced roughly proportional to the number of pixels during which display was
259        // disabled (1 tile fetched per slot, i.e. 2 pixels).
260        // As above, this is an approximation; in actual hardware it depends on which VRAM access slots
261        // were skipped because display was disabled
262        let tiles_skipped = self.sprite_state.pixels_disabled_during_hblank.div_ceil(2);
263        let max_sprite_tiles_per_line =
264            h_size.max_sprite_tiles_per_line().saturating_sub(tiles_skipped);
265
266        let mut tiles_fetched = 0;
267        let mut dot_overflow = false;
268
269        // Sprites with H position 0 mask all lower priority sprites on the same scanline...with
270        // some quirks. There must be at least one sprite with H != 0 before the H=0 sprite, unless
271        // there was a sprite pixel overflow on the previous scanline.
272        let mut found_non_zero = self.sprite_state.dot_overflow_on_prev_line;
273
274        // Whether H=0 sprite masking is active on this line
275        let mut mask_sprites = false;
276
277        'outer: for sprite in &buffers.sprites {
278            if sprite.h_position == 0 && found_non_zero {
279                // Sprite masking from H=0 sprite; no more sprites will display on this line
280                // Masked sprite tiles are still fetched by the VDP, just not displayed
281                mask_sprites = true;
282            } else if sprite.h_position != 0 {
283                found_non_zero = true;
284            }
285
286            let v_size_cells: u16 = sprite.v_size_cells.into();
287            let h_size_cells: u16 = sprite.h_size_cells.into();
288
289            // The lowest 5 bits of difference between sprite V position and scanline are considered, regardless of whether
290            // the sprite overlaps the current scanline.
291            //
292            // Sprite V position is not necessarily in range of the current line because V position can change between the
293            // sprite scan and tile fetching; Titan Overdrive 2's textured cube depends on handling this correctly
294            let sprite_row = sprite_scanline.wrapping_sub(sprite.v_position & y_mask) & 0x1F;
295            let sprite_row = if sprite.vertical_flip {
296                ((v_size_cells << cell_height_shift) - 1).wrapping_sub(sprite_row) & 0x1F
297            } else {
298                sprite_row
299            };
300
301            for h_cell in 0..h_size_cells {
302                if tiles_fetched == max_sprite_tiles_per_line {
303                    // Exceeded the 40 tile / 320 pixel limit (or 32 tile / 256 pixel in H32 mode)
304                    self.sprite_state.overflow = true;
305                    dot_overflow = true;
306
307                    if self.config.enforce_sprite_limits {
308                        break 'outer;
309                    }
310                }
311
312                let cell_col = if sprite.horizontal_flip {
313                    u16::from(sprite.h_size_cells) - 1 - h_cell
314                } else {
315                    h_cell
316                };
317
318                let pattern_offset = cell_col * v_size_cells + (sprite_row >> cell_height_shift);
319                let pattern_generator = sprite.pattern_generator.wrapping_add(pattern_offset);
320                let cell_addr = (4_u16 << cell_height_shift).wrapping_mul(pattern_generator);
321                let cell_row = sprite_row & ((1 << cell_height_shift) - 1);
322                let row_addr = cell_addr + 4 * cell_row;
323
324                // Record what VRAM addresses were accessed during sprite tile fetching; this is needed for rendering the
325                // borders in Titan Overdrive 2
326                if tiles_fetched < max_sprite_tiles_per_line {
327                    buffers.last_tile_addresses[tiles_fetched as usize] = row_addr;
328                    tiles_fetched += 1;
329                }
330
331                if mask_sprites {
332                    // If H=0 sprite masking applies to this tile, it is fetched but not displayed
333                    // Don't bother reading it from VRAM
334                    continue;
335                }
336
337                let cell_left = sprite.h_position + 8 * h_cell;
338                let cell_right = cell_left + 8;
339                if cell_left >= sprite_display_area.end || cell_right <= sprite_display_area.start {
340                    // Tile is fully offscreen; don't bother fetching the pattern generator
341                    continue;
342                }
343
344                let colors = read_pattern_generator_row(
345                    &self.vram,
346                    PatternGeneratorRowArgs {
347                        vertical_flip: false,
348                        horizontal_flip: sprite.horizontal_flip,
349                        pattern_generator,
350                        row: cell_row,
351                        cell_height_shift,
352                    },
353                );
354
355                let cell_h_position = sprite.h_position + 8 * h_cell;
356                for pixel_offset in 0..8 {
357                    let h_position = cell_h_position + pixel_offset;
358                    if !sprite_display_area.contains(&h_position) {
359                        continue;
360                    }
361
362                    let color = colors[pixel_offset as usize];
363
364                    let pixel = h_position - SPRITE_H_DISPLAY_START;
365                    if buffers.pixels[pixel as usize].color == 0 {
366                        // Transparent pixels are always overwritten, even if the current pixel is also transparent
367                        // Overdrive 2 depends on this for the title screen effect where it masks
368                        // BG pixels using the palettes of transparent sprite pixels
369                        buffers.pixels[pixel as usize] =
370                            TilePixel { color, palette: sprite.palette, priority: sprite.priority };
371                    } else {
372                        // Sprite collision; two non-transparent sprite pixels in the same position
373                        self.sprite_state.collision |= color != 0;
374                    }
375                }
376            }
377        }
378
379        self.sprite_state.dot_overflow_on_prev_line = dot_overflow;
380    }
381}
382
383pub fn y_position_mask(interlacing_mode: InterlacingMode) -> u16 {
384    // V position is 9 bits in progressive mode and interlaced mode 1, and 10 bits in
385    // interlaced mode 2
386    match interlacing_mode {
387        InterlacingMode::Progressive | InterlacingMode::Interlaced => 0x1FF,
388        InterlacingMode::InterlacedDouble => 0x3FF,
389    }
390}