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}