1//! Sega CD's graphics ASIC, which can perform hardware-accelerated image scaling and rotation 2 3mod fixedpoint; 4 5use crate::graphics::fixedpoint::FixedPointDecimal; 6use crate::wordram; 7use crate::wordram::{Nibble, WordRam}; 8use bincode::{Decode, Encode}; 9use jgenesis_common::num::GetBit; 10use std::array; 11 12const SUB_CPU_DIVIDER: u32 = crate::api::DEFAULT_SUB_CPU_DIVIDER as u32; 13 14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 15enum StampSizeDots { 16 #[default] 17 Sixteen, 18 ThirtyTwo, 19} 20 21impl StampSizeDots { 22 fn to_bit(self) -> bool { 23 self == Self::ThirtyTwo 24 } 25 26 fn from_bit(bit: bool) -> Self { 27 if bit { Self::ThirtyTwo } else { Self::Sixteen } 28 } 29 30 fn one_dimension_in_pixels(self) -> u32 { 31 match self { 32 Self::Sixteen => 16, 33 Self::ThirtyTwo => 32, 34 } 35 } 36} 37 38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 39enum StampMapSizeScreens { 40 #[default] 41 One, 42 Sixteen, 43} 44 45impl StampMapSizeScreens { 46 fn to_bit(self) -> bool { 47 self == Self::Sixteen 48 } 49 50 fn from_bit(bit: bool) -> Self { 51 if bit { Self::Sixteen } else { Self::One } 52 } 53 54 fn one_dimension_in_pixels(self) -> u32 { 55 // One "screen" is 256x256 pixels 56 match self { 57 Self::One => 256, 58 Self::Sixteen => 4096, 59 } 60 } 61} 62 63#[derive(Debug, Clone, Copy, PartialEq, Eq)] 64enum StampRotation { 65 Zero, 66 Ninety, 67 OneEighty, 68 TwoSeventy, 69} 70 71#[derive(Debug, Clone, Copy)] 72struct StampData { 73 stamp_number: u16, 74 rotation: StampRotation, 75 horizontal_flip: bool, 76} 77 78impl StampData { 79 fn from_word(word: u16) -> Self { 80 let horizontal_flip = word.bit(15); 81 let rotation = match word & 0x6000 { 82 0x0000 => StampRotation::Zero, 83 0x2000 => StampRotation::Ninety, 84 0x4000 => StampRotation::OneEighty, 85 0x6000 => StampRotation::TwoSeventy, 86 _ => unreachable!("value & 0x6000 is always 0x0000/0x2000/0x4000/0x6000"), 87 }; 88 let stamp_number = word & 0x07FF; 89 90 Self { stamp_number, rotation, horizontal_flip } 91 } 92} 93 94#[derive(Debug, Clone)] 95struct TraceVectorData { 96 start_x: FixedPointDecimal, 97 start_y: FixedPointDecimal, 98 delta_x: FixedPointDecimal, 99 delta_y: FixedPointDecimal, 100} 101 102impl TraceVectorData { 103 fn from_bytes(bytes: [u8; 8]) -> Self { 104 let start_x_word = u16::from_be_bytes([bytes[0], bytes[1]]); 105 let start_y_word = u16::from_be_bytes([bytes[2], bytes[3]]); 106 let delta_x_word = u16::from_be_bytes([bytes[4], bytes[5]]); 107 let delta_y_word = u16::from_be_bytes([bytes[6], bytes[7]]); 108 109 Self { 110 start_x: FixedPointDecimal::from_position(start_x_word), 111 start_y: FixedPointDecimal::from_position(start_y_word), 112 delta_x: FixedPointDecimal::from_delta(delta_x_word), 113 delta_y: FixedPointDecimal::from_delta(delta_y_word), 114 } 115 } 116} 117 118#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 119enum State { 120 Idle, 121 Processing { mclk_cycles_remaining: u64, operation_performed: bool }, 122} 123 124#[derive(Debug, Clone, Encode, Decode)] 125pub struct GraphicsCoprocessor { 126 stamp_size: StampSizeDots, 127 stamp_map_size: StampMapSizeScreens, 128 stamp_map_repeats: bool, 129 stamp_map_base_address: u32, 130 image_buffer_v_cell_size: u32, 131 image_buffer_start_address: u32, 132 image_buffer_v_offset: u32, 133 image_buffer_h_offset: u32, 134 image_buffer_v_dot_size: u32, 135 image_buffer_h_dot_size: u32, 136 trace_vector_base_address: u32, 137 state: State, 138 interrupt_pending: bool, 139} 140 141impl GraphicsCoprocessor { 142 pub fn new() -> Self { 143 Self { 144 stamp_size: StampSizeDots::default(), 145 stamp_map_size: StampMapSizeScreens::default(), 146 stamp_map_repeats: false, 147 stamp_map_base_address: 0, 148 image_buffer_v_cell_size: 1, 149 image_buffer_start_address: 0, 150 image_buffer_v_offset: 0, 151 image_buffer_h_offset: 0, 152 image_buffer_v_dot_size: 0, 153 image_buffer_h_dot_size: 0, 154 trace_vector_base_address: 0, 155 state: State::Idle, 156 interrupt_pending: false, 157 } 158 } 159 160 pub fn read_register(&self, address: u32) -> u16 { 161 match address & 0x1FF { 162 0x058 | 0x059 => { 163 // Stamp data size 164 let in_progress = matches!(self.state, State::Processing { .. }); 165 166 (u16::from(in_progress) << 15) 167 | (u16::from(self.stamp_map_size.to_bit()) << 2) 168 | (u16::from(self.stamp_size.to_bit()) << 1) 169 | u16::from(self.stamp_map_repeats) 170 } 171 0x05A | 0x05B => { 172 // Stamp map base address 173 (self.stamp_map_base_address >> 2) as u16 174 } 175 0x05C | 0x05D => { 176 // Image buffer V cell size (minus one) 177 (self.image_buffer_v_cell_size - 1) as u16 178 } 179 0x05E | 0x05F => { 180 // Image buffer start address 181 (self.image_buffer_start_address >> 2) as u16 182 } 183 0x060 | 0x061 => { 184 // Image buffer offset 185 (((self.image_buffer_v_offset) << 3) | self.image_buffer_h_offset) as u16 186 } 187 0x062 | 0x063 => { 188 // Image buffer H dot size 189 self.image_buffer_h_dot_size as u16 190 } 191 0x064 | 0x065 => { 192 // Image buffer V dot size 193 self.image_buffer_v_dot_size as u16 194 } 195 _ => 0, 196 } 197 } 198 199 #[allow(clippy::match_same_arms)] 200 pub fn write_register_byte(&mut self, address: u32, value: u8) { 201 match address & 0x1FF { 202 0x059 => { 203 // Stamp data size 204 self.stamp_map_size = StampMapSizeScreens::from_bit(value.bit(2)); 205 self.stamp_size = StampSizeDots::from_bit(value.bit(1)); 206 self.stamp_map_repeats = value.bit(0); 207 } 208 0x05A..=0x05B => { 209 // Stamp map base address (word access only) 210 self.write_register_word(address & !1, u16::from_le_bytes([value, value])); 211 } 212 0x05D => { 213 // Image buffer V cell size (minus one) 214 self.image_buffer_v_cell_size = ((value & 0x1F) + 1).into(); 215 } 216 0x05E..=0x05F => { 217 // Image buffer start address (word access only) 218 self.write_register_word(address & !1, u16::from_le_bytes([value, value])); 219 } 220 0x061 => { 221 // Image buffer offset 222 self.image_buffer_v_offset = u32::from(value >> 3) & 0x07; 223 self.image_buffer_h_offset = (value & 0x07).into(); 224 } 225 0x062..=0x063 => { 226 // Image buffer H dot size (word access only) 227 self.write_register_word(address & !1, u16::from_le_bytes([value, value])); 228 } 229 0x064..=0x065 => { 230 // Image buffer V dot size (word access only) 231 self.write_register_word(address & !1, u16::from_le_bytes([value, value])); 232 } 233 0x066..=0x067 => { 234 // Trace vector base address (word access only) 235 self.write_register_word(address & !1, u16::from_le_bytes([value, value])); 236 } 237 _ => {} 238 } 239 } 240 241 #[allow(clippy::match_same_arms)] 242 pub fn write_register_word(&mut self, address: u32, value: u16) { 243 match address & 0x1FF { 244 0x058 => { 245 // Stamp data size (only low byte is writable) 246 self.write_register_byte(address | 1, value as u8); 247 } 248 0x05A => { 249 // Stamp map base address (bits 17-7) 250 self.stamp_map_base_address = u32::from(value & 0xFFE0) << 2; 251 } 252 0x05C => { 253 // Image buffer V cell size (only low byte is writable) 254 self.write_register_byte(address | 1, value as u8); 255 } 256 0x05E => { 257 // Image buffer start address (bits 17-5) 258 self.image_buffer_start_address = u32::from(value & 0xFFF8) << 2; 259 } 260 0x060 => { 261 // Image buffer offset (only low byte is writable) 262 self.write_register_byte(address | 1, value as u8); 263 } 264 0x062 => { 265 // Image buffer H dot size 266 self.image_buffer_h_dot_size = (value & 0x01FF).into(); 267 } 268 0x064 => { 269 // Image buffer V dot size 270 self.image_buffer_v_dot_size = (value & 0x00FF).into(); 271 } 272 0x066 => { 273 // Trace vector base address / begin graphics operation 274 self.trace_vector_base_address = u32::from(value & 0xFFFE) << 2; 275 276 // Mostly a guess at timing, based on this: 277 // https://gendev.spritesmind.net/forum/viewtopic.php?t=908 278 // 279 // Each word RAM access takes 3 sub CPU cycles / 12 MCLK cycles, and the ASIC has 280 // to perform the following accesses per image buffer line: 281 // - Read trace vector (4 words) 282 // - Read stamp map entry per pixel (1 word * H size) 283 // - Read stamp generator per pixel (1 word * H size) 284 // - Write to the image buffer (1 word * H size / 4) 285 // - Divide by 4 because there are 4 pixels per image buffer word 286 let h_dot_size = self.image_buffer_h_dot_size; 287 let v_dot_size = self.image_buffer_v_dot_size; 288 let estimated_words_per_line = 4 + 2 * h_dot_size + h_dot_size / 4; 289 let estimated_mclk_cycles = 290 SUB_CPU_DIVIDER * 3 * v_dot_size * estimated_words_per_line; 291 self.state = State::Processing { 292 mclk_cycles_remaining: estimated_mclk_cycles.into(), 293 operation_performed: false, 294 }; 295 } 296 _ => {} 297 } 298 } 299 300 pub fn tick( 301 &mut self, 302 mclk_cycles: u64, 303 word_ram: &mut WordRam, 304 graphics_interrupt_enabled: bool, 305 ) { 306 let State::Processing { mclk_cycles_remaining, operation_performed } = self.state else { 307 return; 308 }; 309 310 if !operation_performed { 311 self.perform_graphics_operation(word_ram); 312 } 313 314 if mclk_cycles >= mclk_cycles_remaining { 315 log::trace!("Graphics operation completed"); 316 317 self.state = State::Idle; 318 // In actual hardware V dot size is decremented as the operation goes; here, we're just 319 // clearing at the end 320 self.image_buffer_v_dot_size = 0; 321 322 if graphics_interrupt_enabled { 323 self.interrupt_pending = true; 324 } 325 } else { 326 self.state = State::Processing { 327 mclk_cycles_remaining: mclk_cycles_remaining - mclk_cycles, 328 operation_performed: true, 329 }; 330 } 331 } 332 333 pub fn interrupt_pending(&self) -> bool { 334 self.interrupt_pending 335 } 336 337 pub fn acknowledge_interrupt(&mut self) { 338 self.interrupt_pending = false; 339 } 340 341 fn perform_graphics_operation(&self, word_ram: &mut WordRam) { 342 log::trace!("Beginning graphics operation with current state:\n{self:#X?}"); 343 344 let stamp_map_size = self.stamp_map_size; 345 let stamp_map_dimension_pixels = stamp_map_size.one_dimension_in_pixels(); 346 let stamp_map_repeats = self.stamp_map_repeats; 347 let stamp_size = self.stamp_size; 348 349 let stamp_map_base_address = self.stamp_map_base_address_masked(); 350 let trace_vector_base_address = self.trace_vector_base_address; 351 352 // 8 lines per cell 353 let image_buffer_v_cell_size = self.image_buffer_v_cell_size; 354 let image_buffer_line_size = 8 * image_buffer_v_cell_size; 355 let image_buffer_v_dot_size = self.image_buffer_v_dot_size; 356 let image_buffer_h_dot_size = self.image_buffer_h_dot_size; 357 let image_buffer_h_offset = self.image_buffer_h_offset; 358 359 let mut image_buffer_start_address = self.image_buffer_start_address; 360 let mut image_buffer_line = self.image_buffer_v_offset; 361 for line in 0..image_buffer_v_dot_size { 362 // One trace vector per line 363 let trace_vector_address = 364 (trace_vector_base_address + 8 * line) & wordram::ADDRESS_MASK; 365 let trace_vector = TraceVectorData::from_bytes(array::from_fn(|i| { 366 read_word_ram(word_ram, trace_vector_address + i as u32) 367 })); 368 369 let mut trace_x_position = trace_vector.start_x; 370 let mut trace_y_position = trace_vector.start_y; 371 for dot in 0..image_buffer_h_dot_size { 372 let x = trace_x_position.integer_part(); 373 let y = trace_y_position.integer_part(); 374 let position_out_of_bounds = 375 x >= stamp_map_dimension_pixels || y >= stamp_map_dimension_pixels; 376 377 let sample = if !stamp_map_repeats && position_out_of_bounds { 378 // Sampling outside of a non-repeating stamp map is always 0 379 0 380 } else { 381 let stamp_map_addr = compute_stamp_map_address( 382 stamp_map_base_address, 383 stamp_size, 384 stamp_map_size, 385 x, 386 y, 387 ); 388 let stamp = StampData::from_word(u16::from_be_bytes([ 389 read_word_ram(word_ram, stamp_map_addr), 390 read_word_ram(word_ram, stamp_map_addr + 1), 391 ])); 392 393 sample_stamp(word_ram, stamp, stamp_size, x, y) 394 }; 395 396 let image_buffer_dot = image_buffer_h_offset + dot; 397 let image_buffer_addr = image_buffer_start_address 398 + compute_relative_addr_v_then_h( 399 image_buffer_line_size, 400 image_buffer_dot, 401 image_buffer_line, 402 ); 403 404 let nibble = if image_buffer_dot.bit(0) { Nibble::Low } else { Nibble::High }; 405 write_word_ram(word_ram, image_buffer_addr, nibble, sample); 406 407 trace_x_position += trace_vector.delta_x; 408 trace_y_position += trace_vector.delta_y; 409 } 410 411 image_buffer_line += 1; 412 if image_buffer_line == image_buffer_line_size { 413 image_buffer_line = 0; 414 415 // "Wrap" by shifting the image buffer start address right 1 cell 416 let image_buffer_size_pixels = image_buffer_line_size * 8; 417 image_buffer_start_address = (image_buffer_start_address 418 + image_buffer_size_pixels / 2) 419 & wordram::ADDRESS_MASK; 420 } 421 } 422 } 423 424 fn stamp_map_base_address_masked(&self) -> u32 { 425 use StampMapSizeScreens as Screens; 426 use StampSizeDots as Dots; 427 428 let stamp_map_base_address_mask = match (self.stamp_map_size, self.stamp_size) { 429 (Screens::One, Dots::Sixteen) => { 430 // Bits 17-9 431 0x03FE00 432 } 433 (Screens::One, Dots::ThirtyTwo) => { 434 // Bits 17-7 435 0x03FF80 436 } 437 (Screens::Sixteen, Dots::Sixteen) => { 438 // Bit 17 only 439 0x020000 440 } 441 (Screens::Sixteen, Dots::ThirtyTwo) => { 442 // Bits 17-15 443 0x038000 444 } 445 }; 446 447 self.stamp_map_base_address & stamp_map_base_address_mask 448 } 449} 450 451fn read_word_ram(word_ram: &mut WordRam, address: u32) -> u8 { 452 word_ram.sub_cpu_read_ram(wordram::SUB_BASE_ADDRESS | address) 453} 454 455fn write_word_ram(word_ram: &mut WordRam, address: u32, nibble: Nibble, value: u8) { 456 word_ram.graphics_write_ram(wordram::SUB_BASE_ADDRESS | address, nibble, value); 457} 458 459fn compute_stamp_map_address( 460 stamp_map_base_address: u32, 461 stamp_size: StampSizeDots, 462 stamp_map_size: StampMapSizeScreens, 463 x: u32, 464 y: u32, 465) -> u32 { 466 let stamp_dimension_pixels = stamp_size.one_dimension_in_pixels(); 467 let stamp_map_dimension_pixels = stamp_map_size.one_dimension_in_pixels(); 468 469 let stamp_map_x = (x & (stamp_map_dimension_pixels - 1)) / stamp_dimension_pixels; 470 let stamp_map_y = (y & (stamp_map_dimension_pixels - 1)) / stamp_dimension_pixels; 471 472 // 2 bytes per stamp 473 let stamp_map_relative_addr = 474 2 * (stamp_map_y * stamp_map_dimension_pixels / stamp_dimension_pixels + stamp_map_x); 475 stamp_map_base_address + stamp_map_relative_addr 476} 477 478fn sample_stamp( 479 word_ram: &mut WordRam, 480 stamp: StampData, 481 stamp_size: StampSizeDots, 482 x: u32, 483 y: u32, 484) -> u8 { 485 let stamp_number = match stamp_size { 486 StampSizeDots::Sixteen => stamp.stamp_number, 487 StampSizeDots::ThirtyTwo => { 488 // Lowest 2 bits are ignored in 32x32 stamp mode; treat the remaining bits as a stamp 489 // number for 32x32 tiles (4x the byte size of 16x16 tiles) 490 stamp.stamp_number >> 2 491 } 492 }; 493 let stamp_number: u32 = stamp_number.into(); 494 495 if stamp_number == 0 { 496 // Sampling stamp 0 always results in 0 regardless of what is in word RAM 497 return 0; 498 } 499 500 let stamp_size_dimension_pixels = stamp_size.one_dimension_in_pixels(); 501 let stamp_addr = stamp_number * (stamp_size_dimension_pixels * stamp_size_dimension_pixels / 2); 502 503 let x = x & (stamp_size_dimension_pixels - 1); 504 let y = y & (stamp_size_dimension_pixels - 1); 505 506 let x = if stamp.horizontal_flip { 507 flip_stamp_coordinate(x, stamp_size_dimension_pixels) 508 } else { 509 x 510 }; 511 let (x, y) = match stamp.rotation { 512 StampRotation::Zero => (x, y), 513 StampRotation::Ninety => (y, flip_stamp_coordinate(x, stamp_size_dimension_pixels)), 514 StampRotation::OneEighty => ( 515 flip_stamp_coordinate(x, stamp_size_dimension_pixels), 516 flip_stamp_coordinate(y, stamp_size_dimension_pixels), 517 ), 518 StampRotation::TwoSeventy => (flip_stamp_coordinate(y, stamp_size_dimension_pixels), x), 519 }; 520 521 let sample_addr = stamp_addr 522 + compute_relative_addr_v_then_h( 523 stamp_size_dimension_pixels, 524 x & (stamp_size_dimension_pixels - 1), 525 y & (stamp_size_dimension_pixels - 1), 526 ); 527 let byte = read_word_ram(word_ram, sample_addr); 528 if x.bit(0) { byte & 0x0F } else { byte >> 4 } 529} 530 531fn flip_stamp_coordinate(coordinate: u32, stamp_size_dimension_pixels: u32) -> u32 { 532 stamp_size_dimension_pixels - 1 - (coordinate & (stamp_size_dimension_pixels - 1)) 533} 534 535fn compute_relative_addr_v_then_h(v_size_pixels: u32, x: u32, y: u32) -> u32 { 536 assert!(y < v_size_pixels); 537 538 let v_size_cells = v_size_pixels / 8; 539 540 let cell_x = x / 8; 541 let cell_y = y / 8; 542 let cell_number = cell_x * v_size_cells + cell_y; 543 544 // 32 bytes per cell 545 let cell_addr = 32 * cell_number; 546 547 // 4 bytes per row 548 let addr_in_cell = 4 * (y & 0x07) + ((x & 0x07) >> 1); 549 cell_addr + addr_in_cell 550} 551 552#[cfg(test)] 553mod tests { 554 use super::*; 555 556 #[test] 557 fn stamp_map_address() { 558 let stamp_size = StampSizeDots::Sixteen; 559 let stamp_map_size = StampMapSizeScreens::Sixteen; 560 561 assert_eq!(0, compute_stamp_map_address(0, stamp_size, stamp_map_size, 0, 0)); 562 assert_eq!(0x20000, compute_stamp_map_address(0x20000, stamp_size, stamp_map_size, 0, 0)); 563 564 assert_eq!(0x20000, compute_stamp_map_address(0x20000, stamp_size, stamp_map_size, 15, 15)); 565 assert_eq!(0x20002, compute_stamp_map_address(0x20000, stamp_size, stamp_map_size, 16, 15)); 566 assert_eq!(0x20200, compute_stamp_map_address(0x20000, stamp_size, stamp_map_size, 15, 16)); 567 assert_eq!( 568 0x3FFFE, 569 compute_stamp_map_address(0x20000, stamp_size, stamp_map_size, 4095, 4095) 570 ); 571 } 572}