6use crate::api::PceEmulatorConfig; 7use crate::video::MCLK_CYCLES_PER_SCANLINE; 8use crate::video::vce::{DotClockDivider, Vce}; 9use crate::video::vdc::registers::{SpriteAccessWidth, VramAccessWidth}; 10use bincode::{Decode, Encode}; 11use jgenesis_common::boxedarray::{Boxed2DWordArray, BoxedWordArray}; 12use jgenesis_common::define_bit_enum; 13use jgenesis_common::num::GetBit; 14use registers::VdcRegisters; 15use std::ops::Range; 16use std::{array, cmp, hint, mem}; 17 18pub const VRAM_LEN_WORDS: usize = 64 * 1024 / 2; 19pub const SPRITE_TABLE_LEN: usize = 64; 20 21pub const DOTS_PER_LINE_DIV_4: u64 = MCLK_CYCLES_PER_SCANLINE / 4; 22pub const DOTS_PER_LINE_DIV_3: u64 = MCLK_CYCLES_PER_SCANLINE / 3; 23pub const DOTS_PER_LINE_DIV_2: u64 = MCLK_CYCLES_PER_SCANLINE / 2;
Guesses, probably not accurate
Numbers derived from Mednafen's frame X offsets Dot clock divider 3 and 2 seem to have more left padding than just 11*ratio
36pub const STANDARD_WIDTH_DIV_4: u16 = 256; 37pub const STANDARD_WIDTH_DIV_3: u16 = 256 * 4 / 3; // ~341 38pub const STANDARD_WIDTH_DIV_2: u16 = 256 * 4 / 2; // 512 39 40pub const MAX_WIDTH_DIV_4: u16 = STANDARD_WIDTH_DIV_4 + 2 * OVERSCAN_DOTS_DIV_4; 41pub const MAX_WIDTH_DIV_3: u16 = STANDARD_WIDTH_DIV_3 + 2 * OVERSCAN_DOTS_DIV_3; 42pub const MAX_WIDTH_DIV_2: u16 = STANDARD_WIDTH_DIV_2 + 2 * OVERSCAN_DOTS_DIV_2; 43 44pub const LINE_BUFFER_LEN: usize = MAX_WIDTH_DIV_2 as usize; 45 46pub const ACTIVE_DISPLAY_DOTS_DIV_4: Range<u16> = 47 LEFT_BORDER_DIV_4..LEFT_BORDER_DIV_4 + STANDARD_WIDTH_DIV_4 + 2 * OVERSCAN_DOTS_DIV_4; 48pub const ACTIVE_DISPLAY_DOTS_DIV_3: Range<u16> = 49 LEFT_BORDER_DIV_3..LEFT_BORDER_DIV_3 + STANDARD_WIDTH_DIV_3 + 2 * OVERSCAN_DOTS_DIV_3; 50pub const ACTIVE_DISPLAY_DOTS_DIV_2: Range<u16> = 51 LEFT_BORDER_DIV_2..LEFT_BORDER_DIV_2 + STANDARD_WIDTH_DIV_2 + 2 * OVERSCAN_DOTS_DIV_2;
Raster compare counter always resets to 64 (0x40) at the beginning of HBlank before the first line of active display
54pub const RASTER_COMPARE_DISPLAY_START: u16 = 64;
Raster compare IRQ seems to trigger a bit before the end of active display
57pub const RASTER_COMPARE_INCREMENT_OFFSET: u16 = 8;
14 lines of top blanking before active display, 4 lines of bottom blanking + 3 lines of VSYNC after
60pub const ACTIVE_DISPLAY_LINES: Range<u16> = 14..256;
Large enough to fit video output at H1365px, after removing overscan
63pub const FRAME_BUFFER_WIDTH: usize = (2 * MAX_WIDTH_DIV_2) as usize;
There are always 242 lines of active display, regardless of vertical display settings Some of these lines are usually overscan, where the VDC constantly outputs sprite color 0
66pub const FRAME_BUFFER_HEIGHT: usize = 242;
68pub const DMA_DOTS_PER_WORD: u8 = 4; 69 70pub const MAX_SPRITES_PER_LINE: usize = 16; 71 72impl DotClockDivider { 73 pub fn dots_per_line(self) -> u64 { 74 match self { 75 Self::Four => DOTS_PER_LINE_DIV_4, 76 Self::Three => DOTS_PER_LINE_DIV_3, 77 Self::Two => DOTS_PER_LINE_DIV_2, 78 } 79 } 80 81 pub fn overscan_dots(self) -> u16 { 82 match self { 83 Self::Four => OVERSCAN_DOTS_DIV_4, 84 Self::Three => OVERSCAN_DOTS_DIV_3, 85 Self::Two => OVERSCAN_DOTS_DIV_2, 86 } 87 } 88 89 pub fn standard_width_dots(self) -> u16 { 90 match self { 91 Self::Four => STANDARD_WIDTH_DIV_4, 92 Self::Three => STANDARD_WIDTH_DIV_3, 93 Self::Two => STANDARD_WIDTH_DIV_2, 94 } 95 } 96 97 pub fn max_width_dots(self) -> u16 { 98 match self { 99 Self::Four => MAX_WIDTH_DIV_4, 100 Self::Three => MAX_WIDTH_DIV_3, 101 Self::Two => MAX_WIDTH_DIV_2, 102 } 103 } 104 105 pub fn active_display_dots(self) -> Range<u16> { 106 match self { 107 Self::Four => ACTIVE_DISPLAY_DOTS_DIV_4, 108 Self::Three => ACTIVE_DISPLAY_DOTS_DIV_3, 109 Self::Two => ACTIVE_DISPLAY_DOTS_DIV_2, 110 } 111 } 112} 113 114#[derive(Debug, Clone, Encode, Decode)] 115pub struct VdcFrameBuffer { 116 // Contains GRB333 VCE colors 117 pub colors: Boxed2DWordArray<FRAME_BUFFER_HEIGHT, FRAME_BUFFER_WIDTH>, 118 pub line_dividers: Box<[DotClockDivider; FRAME_BUFFER_HEIGHT]>, 119} 120 121impl VdcFrameBuffer { 122 fn new() -> Self { 123 Self { 124 colors: Boxed2DWordArray::new(), 125 line_dividers: Box::new(array::from_fn(|_| DotClockDivider::default())), 126 } 127 } 128} 129 130define_bit_enum!(CgMode, [ZeroOne, TwoThree]);
Single = 16px, Double = 32px
133define_bit_enum!(SpriteWidth, [Single, Double]);
135impl SpriteWidth { 136 // 16px tiles 137 pub fn to_sprite_tiles(self) -> u16 { 138 match self { 139 Self::Single => 1, 140 Self::Double => 2, 141 } 142 } 143 144 pub fn tile_number_mask(self) -> u16 { 145 match self { 146 Self::Single => !0, 147 Self::Double => !1, 148 } 149 } 150} 151 152#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 153pub enum SpriteHeight { 154 #[default] 155 Single, // 16px 156 Double, // 32px 157 Quad, // 64px 158} 159 160impl SpriteHeight { 161 fn from_bits(bits: u16) -> Self { 162 match bits & 3 { 163 0 => Self::Single, 164 1 => Self::Double, 165 2 | 3 => Self::Quad, 166 _ => unreachable!("value & 3 is always <= 3"), 167 } 168 } 169 170 pub fn to_pixels(self) -> u16 { 171 match self { 172 Self::Single => 16, 173 Self::Double => 32, 174 Self::Quad => 64, 175 } 176 } 177 178 pub fn tile_number_mask(self) -> u16 { 179 match self { 180 Self::Single => !0, 181 Self::Double => !0b010, 182 Self::Quad => !0b110, 183 } 184 } 185} 186 187#[derive(Debug, Clone, Copy, Default, Encode, Decode)] 188pub struct SpriteTableEntry { 189 pub x: u16, 190 pub y: u16, 191 pub tile_number: u16, 192 pub h_flip: bool, 193 pub v_flip: bool, 194 pub width: SpriteWidth, 195 pub height: SpriteHeight, 196 pub palette: u16, 197 pub priority: bool, 198 pub cg_mode: CgMode, 199} 200 201impl SpriteTableEntry { 202 pub fn write_word(&mut self, i: u16, word: u16) { 203 match i & 3 { 204 0 => self.write_first_word(word), 205 1 => self.write_second_word(word), 206 2 => self.write_third_word(word), 207 3 => self.write_fourth_word(word), 208 _ => unreachable!("value & 3 is always <= 3"), 209 } 210 } 211 212 pub fn write_first_word(&mut self, word: u16) { 213 self.y = word & 0x3FF; 214 } 215 216 pub fn write_second_word(&mut self, word: u16) { 217 self.x = word & 0x3FF; 218 } 219 220 pub fn write_third_word(&mut self, word: u16) { 221 self.cg_mode = CgMode::from_bit(word.bit(0)); 222 self.tile_number = (word >> 1) & 0x3FF; 223 } 224 225 pub fn write_fourth_word(&mut self, word: u16) { 226 self.palette = word & 0xF; 227 self.priority = word.bit(7); 228 self.width = SpriteWidth::from_bit(word.bit(8)); 229 self.h_flip = word.bit(11); 230 self.height = SpriteHeight::from_bits(word >> 12); 231 self.v_flip = word.bit(15); 232 } 233} 234 235#[derive(Debug, Clone, Encode, Decode)] 236pub struct EvaluatedSpriteEntry { 237 pub sprite_idx: u8, 238 pub x: u16, 239 pub tile_number: u16, 240 pub tile_row: u16, 241 pub h_flip: bool, 242 pub priority: bool, 243 pub palette: u16, 244 pub cg_mode: CgMode, 245} 246 247#[derive(Debug, Clone, Copy)] 248pub struct BgTileRow { 249 pub cg0: u16, 250 pub cg1: u16, 251 pub palette: u16, 252} 253 254#[derive(Debug, Clone, Copy, Encode, Decode)] 255pub struct SpritePixel { 256 pub sprite_idx: u8, 257 pub priority: bool, 258 pub palette: u16, 259 pub color_idx: u16, 260} 261 262impl SpritePixel { 263 pub const TRANSPARENT: Self = Self { sprite_idx: 0, priority: false, palette: 0, color_idx: 0 }; 264 265 pub fn transparent(self) -> bool { 266 self.color_idx == 0 267 } 268} 269 270define_bit_enum!(DmaStep, [Increment, Decrement]); 271 272impl DmaStep { 273 fn apply(self, address: &mut u16) { 274 match self { 275 Self::Increment => { 276 *address = address.wrapping_add(1); 277 } 278 Self::Decrement => { 279 *address = address.wrapping_sub(1); 280 } 281 } 282 } 283} 284 285#[derive(Debug, Clone, Copy, Encode, Decode)] 286pub struct LatchedVerticalState { 287 pub v_sync_width: u16, 288 pub v_display_start: u16, 289 pub v_display_width: u16, 290 pub v_display_end: u16, 291 // The VDC enters "burst mode" when both BG and sprites are disabled at start of frame 292 // Burst mode enables DMA and unlimited VRAM access throughout the entire frame, not only during VBlank 293 pub burst_mode: bool, 294} 295 296impl LatchedVerticalState { 297 fn latch(registers: &VdcRegisters) -> Self { 298 Self { 299 v_sync_width: registers.v_sync_width, 300 v_display_start: registers.v_display_start, 301 v_display_width: registers.v_display_width, 302 v_display_end: registers.v_display_end, 303 burst_mode: !registers.bg_enabled && !registers.sprites_enabled, 304 } 305 } 306} 307 308#[derive(Debug, Clone, Copy, Encode, Decode)] 309pub struct LatchedHorizontalState { 310 pub h_sync_width: u16, 311 pub h_display_start: u16, 312 pub h_display_width: u16, 313 pub h_display_end: u16, 314 pub bg_x_scroll: u16, 315 pub vram_access_width: VramAccessWidth, 316 pub sprite_access_width: SpriteAccessWidth, 317} 318 319impl LatchedHorizontalState { 320 fn latch(registers: &VdcRegisters) -> Self { 321 Self { 322 h_sync_width: registers.h_sync_width, 323 h_display_start: registers.h_display_start, 324 h_display_width: registers.h_display_width, 325 h_display_end: registers.h_display_end, 326 bg_x_scroll: registers.bg_x_scroll, 327 vram_access_width: registers.vram_access_width, 328 sprite_access_width: registers.sprite_access_width, 329 } 330 } 331} 332 333#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 334pub enum VerticalMode { 335 TopBorder, 336 ActiveDisplay, 337 BottomBorder, 338 VSync, 339} 340 341impl VerticalMode { 342 fn length(self, latch: LatchedVerticalState) -> u16 { 343 match self { 344 Self::TopBorder => latch.v_display_start, 345 Self::ActiveDisplay => latch.v_display_width, 346 Self::BottomBorder => latch.v_display_end, 347 Self::VSync => latch.v_sync_width, 348 } 349 } 350 351 fn next(self) -> Self { 352 match self { 353 Self::TopBorder => Self::ActiveDisplay, 354 Self::ActiveDisplay => Self::BottomBorder, 355 Self::BottomBorder => Self::VSync, 356 Self::VSync => Self::TopBorder, 357 } 358 } 359} 360 361#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 362pub enum HorizontalMode { 363 LeftBorder, 364 ActiveDisplay, 365 RightBorder, 366 HSync, 367} 368 369impl HorizontalMode { 370 fn length(self, latch: LatchedHorizontalState) -> u16 { 371 match self { 372 Self::LeftBorder => latch.h_display_start, 373 Self::ActiveDisplay => latch.h_display_width, 374 Self::RightBorder => latch.h_display_end, 375 Self::HSync => latch.h_sync_width, 376 } 377 } 378 379 fn next(self) -> Self { 380 match self { 381 Self::LeftBorder => Self::ActiveDisplay, 382 Self::ActiveDisplay => Self::RightBorder, 383 Self::RightBorder => Self::HSync, 384 Self::HSync => Self::LeftBorder, 385 } 386 } 387} 388 389#[derive(Debug, Clone, Encode, Decode)] 390pub struct DmaState { 391 pub vram_triggered: bool, 392 pub vram_active: bool, 393 pub sat_triggered: bool, 394 pub sat_active: bool, 395 pub sat_address: u16, 396 pub dots_till_next_word: u8, 397} 398 399impl DmaState { 400 fn new() -> Self { 401 Self { 402 vram_triggered: false, 403 vram_active: false, 404 sat_triggered: false, 405 sat_active: false, 406 sat_address: 0, 407 dots_till_next_word: DMA_DOTS_PER_WORD, 408 } 409 } 410 411 fn start_vram(&mut self) { 412 self.vram_active = true; 413 414 // Don't interrupt an in-progress VRAM-to-SAT DMA read 415 if !self.sat_active { 416 self.dots_till_next_word = DMA_DOTS_PER_WORD; 417 } 418 } 419 420 fn start_sat(&mut self) { 421 self.sat_active = true; 422 self.sat_address = 0; 423 self.dots_till_next_word = DMA_DOTS_PER_WORD; 424 } 425 426 fn halt(&mut self) { 427 self.vram_active = false; 428 self.sat_active = false; 429 } 430} 431 432#[derive(Debug, Clone, Copy, PartialEq, Eq)] 433pub enum VdcIrq { 434 VBlank, 435 RasterCompare, 436 SpriteOverflow, 437 SpriteCollision, 438 VramDma, 439 SatDma, 440} 441 442#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 443pub enum PendingCpuAccess { 444 Read { address: u16 }, 445 Write { address: u16, value: u16 }, 446} 447 448#[derive(Debug, Clone, Encode, Decode)] 449pub struct VdcState { 450 pub scanline: u16, 451 pub scanline_dot: u16, 452 pub h_latch: LatchedHorizontalState, 453 pub v_latch: LatchedVerticalState, 454 pub h_mode: HorizontalMode, 455 pub v_mode: VerticalMode, 456 pub h_counter: u16, 457 pub h_mode_start_dot: u16, 458 pub v_counter: u16, 459 pub v_mode_start_line: u16, 460 pub bg_y_counter: u16, 461 pub bg_y_scroll_written: bool, 462 pub dma: DmaState, 463 // Dot clock divider is not _really_ latched per line, but pretending that it is 464 // simplifies a lot of things 465 pub line_divider: DotClockDivider, 466 pub vblank_irq_pending: bool, 467 pub raster_compare_irq_pending: bool, 468 pub sprite_overflow_irq_pending: bool, 469 pub sprite_collision_irq_pending: bool, 470 pub vram_dma_irq_pending: bool, 471 pub sat_dma_irq_pending: bool, 472 pub any_irq_pending: bool, 473 pub vblank_irq_this_frame: bool, 474 // If true, generate sprite overflow IRQ the next time active display begins 475 pub sprite_overflow_irq_at_display: bool, 476 // If Some, generate sprite collision IRQ at this dot 477 pub sprite_collision_irq_dot: Option<u16>, 478 pub raster_compare_counter: u16, 479 pub frame_complete: bool, 480 pub pending_cpu_access: Option<PendingCpuAccess>, 481 pub sprite_fetch_dots_this_line: u64, 482} 483 484impl VdcState { 485 fn new(registers: &VdcRegisters) -> Self { 486 Self { 487 scanline: 0, 488 scanline_dot: 0, 489 h_latch: LatchedHorizontalState::latch(registers), 490 v_latch: LatchedVerticalState::latch(registers), 491 h_mode: HorizontalMode::LeftBorder, 492 v_mode: VerticalMode::TopBorder, 493 h_counter: 0, 494 h_mode_start_dot: 0, 495 v_counter: 0, 496 v_mode_start_line: 0, 497 bg_y_counter: 0, 498 bg_y_scroll_written: false, 499 dma: DmaState::new(), 500 line_divider: DotClockDivider::default(), 501 vblank_irq_pending: false, 502 raster_compare_irq_pending: false, 503 sprite_overflow_irq_pending: false, 504 sprite_collision_irq_pending: false, 505 vram_dma_irq_pending: false, 506 sat_dma_irq_pending: false, 507 any_irq_pending: false, 508 vblank_irq_this_frame: false, 509 sprite_overflow_irq_at_display: false, 510 sprite_collision_irq_dot: None, 511 raster_compare_counter: RASTER_COMPARE_DISPLAY_START, 512 frame_complete: false, 513 pending_cpu_access: None, 514 sprite_fetch_dots_this_line: 0, 515 } 516 } 517 518 fn can_start_vram_dma(&self) -> bool { 519 if self.v_latch.burst_mode { 520 // Can always run during burst mode 521 return true; 522 } 523 524 if self.v_mode == VerticalMode::ActiveDisplay { 525 // Can never run during active display 526 return false; 527 } 528 529 if self.v_mode == VerticalMode::TopBorder 530 && self.v_counter == self.v_latch.v_display_start - 1 531 && matches!(self.h_mode, HorizontalMode::RightBorder | HorizontalMode::HSync) 532 { 533 // Can't run during HBlank on the last line before active display 534 return false; 535 } 536 537 true 538 } 539} 540 541#[derive(Debug, Clone, Encode, Decode)] 542pub struct Vdc { 543 vram: BoxedWordArray<VRAM_LEN_WORDS>, 544 sprite_table: Box<[SpriteTableEntry; SPRITE_TABLE_LEN]>, 545 registers: VdcRegisters, 546 state: VdcState, 547 sprite_evaluation_buffer: Vec<EvaluatedSpriteEntry>, 548 sprite_line_buffer: Box<[SpritePixel; LINE_BUFFER_LEN]>, 549 enforce_sprite_limits: bool, 550 frame_buffer: VdcFrameBuffer, 551 // Contains 9-bit color indices (0-255 for BG colors, 256-511 for sprite colors) 552 line_buffer: Box<[u16; LINE_BUFFER_LEN]>, 553 selected_register: u8, 554} 555 556impl Vdc { 557 pub fn new(config: PceEmulatorConfig) -> Self { 558 let registers = VdcRegisters::new(); 559 let state = VdcState::new(®isters); 560 561 Self { 562 vram: BoxedWordArray::new_random(), 563 sprite_table: vec![SpriteTableEntry::default(); SPRITE_TABLE_LEN] 564 .into_boxed_slice() 565 .try_into() 566 .unwrap(), 567 registers, 568 state, 569 sprite_evaluation_buffer: Vec::with_capacity(MAX_SPRITES_PER_LINE), 570 sprite_line_buffer: vec![SpritePixel::TRANSPARENT; LINE_BUFFER_LEN] 571 .into_boxed_slice() 572 .try_into() 573 .unwrap(), 574 enforce_sprite_limits: !config.remove_sprite_limits, 575 frame_buffer: VdcFrameBuffer::new(), 576 line_buffer: Box::new(array::from_fn(|_| 0)), 577 selected_register: 0x1F, 578 } 579 } 580 581 pub fn tick_dots(&mut self, dots: u64, vce: &Vce) { 582 let active_display_dots = self.state.line_divider.active_display_dots(); 583 584 let line_divider = self.state.line_divider as u16; 585 586 let active_line = ACTIVE_DISPLAY_LINES.contains(&self.state.scanline); 587 let frame_buffer_row = self.state.scanline.wrapping_sub(ACTIVE_DISPLAY_LINES.start); 588 debug_assert!(!active_line || (frame_buffer_row as usize) < self.frame_buffer.colors.len()); 589 590 let mut h_mode_length = self.state.h_mode.length(self.state.h_latch); 591 592 let burst_mode = self.state.v_latch.burst_mode; 593 594 // TODO this is very inefficient 595 for _ in 0..dots { 596 // DMA always takes priority over CPU VRAM access 597 // SAT DMA probably takes priority over VRAM copy DMA? 598 if self.state.dma.sat_active { 599 self.progress_sat_dma(); 600 } else if self.state.dma.vram_active { 601 self.progress_vram_dma(); 602 } else if self.state.pending_cpu_access.is_some() { 603 self.progress_cpu_access(); 604 } 605 606 // Render color to frame buffer if inside VCE active display 607 if active_line && active_display_dots.contains(&self.state.scanline_dot) { 608 let color = if !burst_mode 609 && self.state.v_mode == VerticalMode::ActiveDisplay 610 && self.state.h_mode == HorizontalMode::ActiveDisplay 611 { 612 let line_buffer_idx = self.state.scanline_dot - self.state.h_mode_start_dot; 613 vce.read_color(self.line_buffer[line_buffer_idx as usize]) 614 } else { 615 // Always render overscan color in burst mode and outside of VDC active display 616 vce.overscan_color() 617 }; 618 619 let frame_buffer_col = 620 line_divider * (self.state.scanline_dot - active_display_dots.start); 621 for i in 0..line_divider { 622 self.frame_buffer.colors[frame_buffer_row as usize] 623 [(frame_buffer_col + i) as usize] = color; 624 } 625 } 626 627 // Increment raster compare counter shortly before the end of horizontal display 628 // (Timing is probably not accurate) 629 if self.state.h_mode == HorizontalMode::ActiveDisplay 630 && self.state.h_counter 631 == self 632 .state 633 .h_latch 634 .h_display_width 635 .wrapping_sub(RASTER_COMPARE_INCREMENT_OFFSET) 636 { 637 self.increment_raster_compare_counter(); 638 639 if self.state.sprite_overflow_irq_at_display { 640 self.set_irq(VdcIrq::SpriteOverflow); 641 self.state.sprite_overflow_irq_at_display = false; 642 } 643 } 644 645 self.state.scanline_dot += 1; 646 if self.state.sprite_collision_irq_dot.is_some_and(|dot| dot == self.state.scanline_dot) 647 { 648 self.set_irq(VdcIrq::SpriteCollision); 649 self.state.sprite_collision_irq_dot = None; 650 } 651 652 self.state.h_counter += 1; 653 if self.state.h_counter >= h_mode_length { 654 self.state.h_counter = 0; 655 self.state.h_mode = self.state.h_mode.next(); 656 self.state.h_mode_start_dot = self.state.scanline_dot; 657 658 h_mode_length = self.state.h_mode.length(self.state.h_latch); 659 660 let is_sprite_line = self.state.v_mode == VerticalMode::ActiveDisplay 661 || (self.state.v_mode == VerticalMode::TopBorder 662 && self.state.v_counter == self.state.v_latch.v_display_start - 1); 663 664 match self.state.h_mode { 665 HorizontalMode::ActiveDisplay => { 666 if self.state.v_mode == VerticalMode::ActiveDisplay { 667 self.render_line(); 668 } 669 670 if is_sprite_line && !burst_mode { 671 self.run_sprite_evaluation(); 672 } 673 } 674 HorizontalMode::RightBorder => { 675 if self.state.v_mode == VerticalMode::TopBorder 676 && self.state.v_counter == self.state.v_latch.v_display_start - 1 677 { 678 // DMAs cannot run once sprite tile fetching for the first line of active display begins 679 if !burst_mode { 680 self.state.dma.halt(); 681 } 682 } 683 684 if is_sprite_line && !burst_mode { 685 self.fetch_sprite_tiles(); 686 } 687 } 688 _ => {} 689 } 690 } 691 } 692 } 693 694 fn increment_raster_compare_counter(&mut self) { 695 if self.state.v_mode == VerticalMode::TopBorder 696 && self.state.v_counter == self.state.v_latch.v_display_start - 1 697 { 698 self.state.raster_compare_counter = RASTER_COMPARE_DISPLAY_START; 699 } else { 700 self.state.raster_compare_counter += 1; 701 } 702 703 if self.state.raster_compare_counter == self.registers.raster_compare { 704 self.set_irq(VdcIrq::RasterCompare); 705 } 706 } 707 708 pub fn start_new_line(&mut self, scanline: u16, vce: &Vce) { 709 if self.state.h_mode == HorizontalMode::ActiveDisplay { 710 if self.state.h_counter 711 < self.state.h_latch.h_display_width.saturating_sub(RASTER_COMPARE_INCREMENT_OFFSET) 712 { 713 // If active display began but the raster compare increment didn't happen, do it at the 714 // line change 715 // 716 // D&D: Order of the Griffon depends on this else there will be a glitchy line under 717 // the character portraits; it depends on the increment happening twice in one line 718 // when it changes the dot clock divider from 4 to 3 719 self.increment_raster_compare_counter(); 720 } 721 722 if ACTIVE_DISPLAY_LINES.contains(&self.state.scanline) { 723 let active_display_dots = self.state.line_divider.active_display_dots(); 724 if active_display_dots.contains(&self.state.scanline_dot) { 725 // Fill remainder of frame buffer row with overscan color 726 // Prevents some visual glitches in D&D: Order of the Griffon due to mid-frame 727 // dot clock divider changes 728 let frame_buffer_row = 729 (self.state.scanline - ACTIVE_DISPLAY_LINES.start) as usize; 730 let frame_buffer_col = (self.state.scanline_dot - active_display_dots.start) 731 as usize 732 * self.state.line_divider as usize; 733 self.frame_buffer.colors[frame_buffer_row][frame_buffer_col..] 734 .fill(vce.overscan_color()); 735 } 736 } 737 } 738 739 let dot_clock_divider = vce.dot_clock_divider(); 740 let lines_per_frame = vce.lines_per_frame(); 741 742 // TODO latch timing is probably not accurate for everything in here 743 self.state.h_latch = LatchedHorizontalState::latch(&self.registers); 744 self.state.h_mode = HorizontalMode::LeftBorder; 745 self.state.h_counter = 0; 746 self.state.h_mode_start_dot = 0; 747 748 self.state.line_divider = dot_clock_divider; 749 750 self.state.scanline = scanline; 751 self.state.scanline_dot = 0; 752 753 if self.state.bg_y_scroll_written { 754 self.state.bg_y_counter = self.registers.bg_y_scroll; 755 self.state.bg_y_scroll_written = false; 756 } 757 self.state.bg_y_counter = self.state.bg_y_counter.wrapping_add(1); 758 759 if ACTIVE_DISPLAY_LINES.contains(&scanline) { 760 let frame_buffer_row = scanline - ACTIVE_DISPLAY_LINES.start; 761 self.frame_buffer.line_dividers[frame_buffer_row as usize] = dot_clock_divider; 762 } 763 764 self.state.frame_complete |= scanline == ACTIVE_DISPLAY_LINES.end; 765 766 if scanline != 0 { 767 self.state.v_counter += 1; 768 if self.state.v_counter >= self.state.v_mode.length(self.state.v_latch) { 769 self.state.v_counter = 0; 770 self.state.v_mode = self.state.v_mode.next(); 771 self.state.v_mode_start_line = self.state.scanline; 772 773 match self.state.v_mode { 774 VerticalMode::ActiveDisplay => { 775 self.state.bg_y_counter = self.registers.bg_y_scroll; 776 777 if !self.state.v_latch.burst_mode { 778 // DMAs cannot run during active display when not in burst mode 779 self.state.dma.halt(); 780 } 781 } 782 VerticalMode::BottomBorder => { 783 if self.state.dma.sat_triggered || self.registers.sat_dma_repeat { 784 self.state.dma.start_sat(); 785 self.state.dma.sat_triggered = false; 786 787 log::trace!("Starting VRAM-to-SAT DMA on line {scanline}"); 788 } 789 790 if self.state.dma.vram_triggered { 791 self.state.dma.start_vram(); 792 793 log::trace!("Starting VRAM-to-VRAM DMA on line {scanline}"); 794 } 795 796 self.set_irq(VdcIrq::VBlank); 797 798 self.state.vblank_irq_this_frame = true; 799 800 self.state.sprite_collision_irq_dot = None; 801 } 802 _ => {} 803 } 804 } 805 } 806 807 if !self.state.vblank_irq_this_frame && scanline == lines_per_frame - 2 { 808 // VDC supposedly always generates a VBlank IRQ when the VCE asserts VSYNC if it didn't 809 // already generate one earlier in the frame 810 self.set_irq(VdcIrq::VBlank); 811 } 812 } 813 814 pub fn start_new_frame(&mut self) { 815 self.state.v_latch = LatchedVerticalState::latch(&self.registers); 816 self.state.v_mode = VerticalMode::TopBorder; 817 self.state.v_counter = 0; 818 self.state.v_mode_start_line = 0; 819 self.state.vblank_irq_this_frame = false; 820 } 821 822 pub fn frame_complete(&self) -> bool { 823 self.state.frame_complete 824 } 825 826 pub fn clear_frame_complete(&mut self) { 827 self.state.frame_complete = false; 828 } 829 830 pub fn frame_buffer(&self) -> &VdcFrameBuffer { 831 &self.frame_buffer 832 } 833 834 pub fn irq(&self) -> bool { 835 self.state.any_irq_pending 836 } 837 838 pub fn is_cpu_read_blocked(&self) -> bool { 839 // VRR (VRAM read register) 840 // Block if any CPU access is in progress 841 self.state.pending_cpu_access.is_some() 842 } 843 844 #[allow(clippy::match_same_arms)] 845 pub fn is_cpu_write_blocked(&self) -> bool { 846 match self.selected_register { 847 // MAWR (Memory address write register) 848 // Block if a CPU write is in progress 849 0x00 => matches!(self.state.pending_cpu_access, Some(PendingCpuAccess::Write { .. })), 850 // MARR (Memory address read register) 851 // Block if any CPU access is in progress (since MSB write can initiate a read) 852 0x01 => self.state.pending_cpu_access.is_some(), 853 // VWR (VRAM write register) 854 // Block if any CPU access is in progress 855 0x02 => self.state.pending_cpu_access.is_some(), 856 _ => false, 857 } 858 } 859 860 pub fn reload_config(&mut self, config: PceEmulatorConfig) { 861 self.enforce_sprite_limits = !config.remove_sprite_limits; 862 } 863 864 fn render_line(&mut self) { 865 const BACKDROP_COLOR: u16 = 0x000; 866 867 self.line_buffer.fill(BACKDROP_COLOR); 868 869 if self.state.v_latch.burst_mode 870 || (!self.registers.bg_enabled && !self.registers.sprites_enabled) 871 { 872 return; 873 } 874 875 let line_width_dots = 876 cmp::min(self.state.line_divider.max_width_dots(), self.state.h_latch.h_display_width); 877 878 let screen_width_tiles = self.registers.virtual_screen_width.to_tiles(); 879 let screen_height_tiles = self.registers.virtual_screen_height.to_tiles(); 880 881 let bg_x_scroll = self.state.h_latch.bg_x_scroll; 882 let bg_y_counter = self.state.bg_y_counter; 883 884 let mut bg_tile_x = (bg_x_scroll / 8) & (screen_width_tiles - 1); 885 let bg_tile_y = (bg_y_counter / 8) & (screen_height_tiles - 1); 886 887 let tile_row = (bg_y_counter & 7) as usize; 888 889 let start_x = -i32::from(bg_x_scroll & 7); 890 let end_x = i32::from(line_width_dots); 891 for x in (start_x..end_x).step_by(8) { 892 // BAT (BG attribute table) always starts at $0000 in VRAM and has 1 word per tile 893 let bat_addr = bg_tile_y * screen_width_tiles + bg_tile_x; 894 895 let BgTileRow { cg0, cg1, palette: bg_palette } = if self.registers.bg_enabled { 896 self.read_bg_tile_row(bat_addr, tile_row) 897 } else { 898 BgTileRow { cg0: 0, cg1: 0, palette: 0 } 899 }; 900 901 for tile_col in 0..8 { 902 let pixel = x + tile_col; 903 if !(0..end_x).contains(&pixel) { 904 continue; 905 } 906 907 let bitplane_shift = 7 - tile_col; 908 let bg_color_idx = ((cg0 >> bitplane_shift) & 1) 909 | (((cg0 >> (8 + bitplane_shift)) & 1) << 1) 910 | (((cg1 >> bitplane_shift) & 1) << 2) 911 | (((cg1 >> (8 + bitplane_shift)) & 1) << 3); 912 913 let sprite_pixel = if self.registers.sprites_enabled { 914 self.sprite_line_buffer[pixel as usize] 915 } else { 916 SpritePixel::TRANSPARENT 917 }; 918 919 let rendered_color = if !sprite_pixel.transparent() 920 && (sprite_pixel.priority || bg_color_idx == 0) 921 { 922 0x100 | (sprite_pixel.palette << 4) | sprite_pixel.color_idx 923 } else if bg_color_idx != 0 { 924 (bg_palette << 4) | bg_color_idx 925 } else { 926 BACKDROP_COLOR 927 }; 928 929 self.line_buffer[pixel as usize] = rendered_color; 930 } 931 932 bg_tile_x = (bg_tile_x + 1) & (screen_width_tiles - 1); 933 } 934 } 935 936 fn read_bg_tile_row(&self, bat_addr: u16, tile_row: usize) -> BgTileRow { 937 let bg_attributes = self.vram[bat_addr as usize]; 938 let tile_number = bg_attributes & 0xFFF; 939 let palette = bg_attributes >> 12; 940 941 let tile_addr = (16 * tile_number) as usize; 942 943 let (mut cg0, mut cg1) = if tile_addr < VRAM_LEN_WORDS { 944 (self.vram[tile_addr + tile_row], self.vram[tile_addr + tile_row + 8]) 945 } else { 946 // Tiles 2048-4095 are supposedly filled with "garbage" 947 (0xFFFF, 0xFFFF) 948 }; 949 950 if self.state.h_latch.vram_access_width == VramAccessWidth::Four { 951 hint::cold_path(); 952 953 // VDC only fetches half of the bitplanes in width 4 954 match self.registers.bg_cg_mode { 955 CgMode::ZeroOne => { 956 cg1 = 0; 957 } 958 CgMode::TwoThree => { 959 cg0 = mem::take(&mut cg1); 960 } 961 } 962 } 963 964 BgTileRow { cg0, cg1, palette } 965 } 966 967 fn progress_sat_dma(&mut self) { 968 self.state.dma.dots_till_next_word -= 1; 969 if self.state.dma.dots_till_next_word != 0 { 970 return; 971 } 972 973 self.state.dma.dots_till_next_word = DMA_DOTS_PER_WORD; 974 975 let sat_address = self.state.dma.sat_address; 976 let word = self.read_vram(self.registers.sat_dma_source_address.wrapping_add(sat_address)); 977 978 let sprite_idx = sat_address / 4; 979 self.sprite_table[sprite_idx as usize].write_word(sat_address % 4, word); 980 981 self.state.dma.sat_address = sat_address.wrapping_add(1); 982 if self.state.dma.sat_address == (4 * SPRITE_TABLE_LEN) as u16 { 983 self.state.dma.sat_active = false; 984 self.set_irq(VdcIrq::SatDma); 985 986 log::trace!("Finished SAT DMA on line {}", self.state.scanline); 987 } 988 } 989 990 fn progress_vram_dma(&mut self) { 991 self.state.dma.dots_till_next_word -= 1; 992 if self.state.dma.dots_till_next_word != 0 { 993 return; 994 } 995 996 self.state.dma.dots_till_next_word = DMA_DOTS_PER_WORD; 997 998 let word = self.read_vram(self.registers.vram_dma_source_address); 999 self.registers.vram_dma_source_step.apply(&mut self.registers.vram_dma_source_address); 1000 1001 self.write_vram(self.registers.vram_dma_destination_address, word); 1002 self.registers 1003 .vram_dma_destination_step 1004 .apply(&mut self.registers.vram_dma_destination_address); 1005 1006 let overflowed; 1007 (self.registers.vram_dma_length, overflowed) = 1008 self.registers.vram_dma_length.overflowing_sub(1); 1009 1010 if overflowed { 1011 self.state.dma.vram_triggered = false; 1012 self.state.dma.vram_active = false; 1013 self.set_irq(VdcIrq::VramDma); 1014 1015 log::trace!("Finished VRAM DMA on line {}", self.state.scanline); 1016 } 1017 } 1018 1019 fn progress_cpu_access(&mut self) { 1020 if !self.can_perform_cpu_access() { 1021 return; 1022 } 1023 1024 match self.state.pending_cpu_access.take() { 1025 Some(PendingCpuAccess::Read { address }) => { 1026 self.registers.vram_read_buffer = self.read_vram(address); 1027 } 1028 Some(PendingCpuAccess::Write { address, value }) => { 1029 self.write_vram(address, value); 1030 } 1031 None => {} 1032 } 1033 } 1034 1035 fn can_perform_cpu_access(&self) -> bool { 1036 if self.state.dma.vram_active || self.state.dma.sat_active { 1037 // CPU cannot access VRAM during DMA 1038 return false; 1039 } 1040 1041 if self.state.v_latch.burst_mode { 1042 // CPU can always access during burst mode (when DMA is not running) 1043 return true; 1044 } 1045 1046 if self.state.v_mode == VerticalMode::ActiveDisplay 1047 && self.state.h_mode == HorizontalMode::ActiveDisplay 1048 { 1049 // BG tile fetching + sprite evaluation + rendering 1050 // CPU access slots depend on VRAM access width: 1051 // Width 1: CPU BAT CPU ??? CPU CG0 CPU CG1 (8 slots, 4 accesses) 1052 // Width 2: BAT CPU CG0 CG1 (4 slots, 1 access) 1053 // Width 4: BAT CG0/CG1 (2 slots, 0 accesses) 1054 1055 // TODO what happens if BG is disabled? 1056 if !self.registers.bg_enabled { 1057 return true; 1058 } 1059 1060 return match self.state.h_latch.vram_access_width { 1061 VramAccessWidth::One => self.state.h_counter & 1 == 0, 1062 VramAccessWidth::Two => self.state.h_counter & 7 == 2, 1063 VramAccessWidth::Four => false, 1064 }; 1065 } 1066 1067 let is_fetching_sprite_tiles = match self.state.v_mode { 1068 VerticalMode::ActiveDisplay => self.state.h_mode != HorizontalMode::ActiveDisplay, 1069 VerticalMode::TopBorder => { 1070 matches!(self.state.h_mode, HorizontalMode::RightBorder | HorizontalMode::HSync) 1071 && self.state.v_counter == self.state.v_latch.v_display_start - 1 1072 } 1073 VerticalMode::BottomBorder | VerticalMode::VSync => false, 1074 }; 1075 1076 if self.registers.sprites_enabled && is_fetching_sprite_tiles { 1077 // CPU cannot access VRAM during sprite tile fetching, regardless of sprite access width 1078 // Check if tile fetching is done for this line 1079 // TODO this doesn't handle horizontal timings or dot clock divider changing between lines 1080 let sprite_dot: u64 = match self.state.h_mode { 1081 HorizontalMode::RightBorder => self.state.h_counter.into(), 1082 HorizontalMode::HSync => { 1083 (self.state.h_latch.h_display_end + self.state.h_counter).into() 1084 } 1085 HorizontalMode::LeftBorder => { 1086 let hds: u64 = self.state.h_latch.h_display_start.into(); 1087 let hdw: u64 = self.state.h_latch.h_display_width.into(); 1088 let sprite_dots_per_line = (MCLK_CYCLES_PER_SCANLINE 1089 / u64::from(self.state.line_divider)) 1090 .saturating_sub(hdw); 1091 sprite_dots_per_line.saturating_sub(hds) + u64::from(self.state.h_counter) 1092 } 1093 HorizontalMode::ActiveDisplay => unreachable!( 1094 "is_fetching_sprite_tiles is never true when h_mode is ActiveDisplay" 1095 ), 1096 }; 1097 1098 return sprite_dot >= self.state.sprite_fetch_dots_this_line; 1099 } 1100 1101 true 1102 } 1103 1104 fn run_sprite_evaluation(&mut self) { 1105 // In sprite coordinates, Y=64 is the first line of active display 1106 const SCREEN_TOP: u16 = 64; 1107 1108 // The official HuC6270 manual suggests that sprite evaluation runs for 1 tile longer than 1109 // active display width, and that eval takes 4 dots per sprite. 1110 // It also says that VRAM access width can affect sprite eval, but I don't think this makes 1111 // sense? Sprite eval doesn't need to access VRAM 1112 let sprites_evaluated_this_line = if self.enforce_sprite_limits { 1113 let sprite_eval_cycles = self.state.h_latch.h_display_width + 8; 1114 1115 cmp::min((sprite_eval_cycles / 4) as usize, self.sprite_table.len()) 1116 } else { 1117 self.sprite_table.len() 1118 }; 1119 1120 self.sprite_evaluation_buffer.clear(); 1121 1122 let sprite_line = match self.state.v_mode { 1123 VerticalMode::ActiveDisplay => SCREEN_TOP + self.state.v_counter + 1, 1124 _ => SCREEN_TOP, // Last line of top border; evaluate for first line of active display 1125 }; 1126 1127 for (sprite_idx, sprite) in 1128 self.sprite_table[..sprites_evaluated_this_line].iter().enumerate() 1129 { 1130 let sprite_height_pixels = sprite.height.to_pixels(); 1131 let sprite_y_range = sprite.y..sprite.y + sprite_height_pixels; 1132 if !sprite_y_range.contains(&sprite_line) { 1133 continue; 1134 } 1135 1136 let mut sprite_row = sprite_line - sprite.y; 1137 if sprite.v_flip { 1138 sprite_row = sprite_height_pixels - 1 - sprite_row; 1139 } 1140 1141 let mut base_tile_number = sprite.tile_number 1142 & sprite.width.tile_number_mask() 1143 & sprite.height.tile_number_mask(); 1144 base_tile_number += 2 * (sprite_row / 16); 1145 sprite_row %= 16; 1146 1147 let width_tiles = sprite.width.to_sprite_tiles(); 1148 for i in 0..width_tiles { 1149 let x_tile = match sprite.width { 1150 SpriteWidth::Single => 0, 1151 SpriteWidth::Double => i ^ u16::from(sprite.h_flip), 1152 }; 1153 1154 let x = sprite.x + 16 * i; 1155 let tile_number = base_tile_number + x_tile; 1156 1157 if self.sprite_evaluation_buffer.len() == MAX_SPRITES_PER_LINE { 1158 self.state.sprite_overflow_irq_at_display = true; 1159 if self.enforce_sprite_limits { 1160 return; 1161 } 1162 } 1163 1164 self.sprite_evaluation_buffer.push(EvaluatedSpriteEntry { 1165 sprite_idx: sprite_idx as u8, 1166 x, 1167 tile_number, 1168 tile_row: sprite_row, 1169 h_flip: sprite.h_flip, 1170 priority: sprite.priority, 1171 palette: sprite.palette, 1172 cg_mode: sprite.cg_mode, 1173 }); 1174 } 1175 } 1176 } 1177 1178 fn fetch_sprite_tiles(&mut self) { 1179 // In sprite coordinates, X=32 is the leftmost column of active display 1180 const SCREEN_LEFT: u16 = 32; 1181 1182 self.sprite_line_buffer.fill(SpritePixel::TRANSPARENT); 1183 self.state.sprite_fetch_dots_this_line = 0; 1184 1185 if !self.registers.sprites_enabled { 1186 return; 1187 } 1188 1189 // TODO this is not quite right if the game changes HDS or the dot clock divider during the right border or HSync 1190 let sprite_fetch_cycles = { 1191 let dots_per_line = MCLK_CYCLES_PER_SCANLINE / u64::from(self.state.line_divider); 1192 1193 // Based on https://pcengine.proboards.com/thread/84/why-pce-games-horizontal-rare?page=2 1194 // Games that use sprite access width other than 1 (e.g. R-Type) have too many sprites 1195 // per line without the extra 16 1196 let hdw = self.state.h_latch.h_display_width; 1197 dots_per_line.saturating_sub((hdw + 16).into()) 1198 }; 1199 1200 let cycles_per_sprite = match self.state.h_latch.sprite_access_width { 1201 SpriteAccessWidth::One | SpriteAccessWidth::TwoHalfBpp => 4, 1202 SpriteAccessWidth::TwoFullBpp | SpriteAccessWidth::Four => 8, 1203 }; 1204 1205 let num_sprite_tiles = cmp::min( 1206 (sprite_fetch_cycles / cycles_per_sprite) as usize, 1207 cmp::min(MAX_SPRITES_PER_LINE, self.sprite_evaluation_buffer.len()), 1208 ); 1209 1210 self.state.sprite_fetch_dots_this_line = (num_sprite_tiles as u64) * cycles_per_sprite; 1211 1212 let half_bpp = self.state.h_latch.sprite_access_width.is_half_bpp(); 1213 1214 for (i, sprite) in self.sprite_evaluation_buffer.iter().enumerate() { 1215 let within_sprite_limits = i < num_sprite_tiles; 1216 if self.enforce_sprite_limits && !within_sprite_limits { 1217 break; 1218 } 1219 1220 let tile_addr = (64 * sprite.tile_number) as usize; 1221 let tile_data = if tile_addr < VRAM_LEN_WORDS { 1222 &self.vram[tile_addr..tile_addr + 64] 1223 } else { 1224 // Tiles 512-1023 supposedly contain "garbage" 1225 &[0xFFFF; 64] 1226 }; 1227 1228 let tile_row = sprite.tile_row as usize; 1229 let mut cg0 = tile_data[tile_row]; 1230 let mut cg1 = tile_data[tile_row + 16]; 1231 let mut cg2 = tile_data[tile_row + 32]; 1232 let mut cg3 = tile_data[tile_row + 48]; 1233 1234 if half_bpp { 1235 hint::cold_path(); 1236 1237 match sprite.cg_mode { 1238 CgMode::ZeroOne => { 1239 cg2 = 0; 1240 cg3 = 0; 1241 } 1242 CgMode::TwoThree => { 1243 cg0 = mem::take(&mut cg2); 1244 cg1 = mem::take(&mut cg3); 1245 } 1246 } 1247 } 1248 1249 let mut color_indices: [u16; 16] = array::from_fn(|i| { 1250 ((cg0 >> (15 - i)) & 1) 1251 | (((cg1 >> (15 - i)) & 1) << 1) 1252 | (((cg2 >> (15 - i)) & 1) << 2) 1253 | (((cg3 >> (15 - i)) & 1) << 3) 1254 }); 1255 if sprite.h_flip { 1256 color_indices.reverse(); 1257 } 1258 1259 for (i, color_idx) in color_indices.into_iter().enumerate() { 1260 if color_idx == 0 { 1261 // Transparent 1262 continue; 1263 } 1264 1265 let x = sprite.x + i as u16; 1266 if x.wrapping_sub(SCREEN_LEFT) >= LINE_BUFFER_LEN as u16 { 1267 // Horizontally out of bounds 1268 continue; 1269 } 1270 1271 let line_buffer_idx = (x - SCREEN_LEFT) as usize; 1272 if !self.sprite_line_buffer[line_buffer_idx].transparent() { 1273 // Already an opaque sprite pixel in this position 1274 1275 // Check for sprite 0 collision, only if sprite is within hardware sprite limits 1276 if within_sprite_limits 1277 && self.sprite_line_buffer[line_buffer_idx].sprite_idx == 0 1278 { 1279 // TODO timing probably not accurate 1280 let collision_dot = 1281 self.state.h_latch.h_display_start + line_buffer_idx as u16; 1282 self.state.sprite_collision_irq_dot = Some(cmp::min( 1283 collision_dot, 1284 self.state.sprite_collision_irq_dot.unwrap_or(u16::MAX), 1285 )); 1286 } 1287 continue; 1288 } 1289 1290 self.sprite_line_buffer[line_buffer_idx] = SpritePixel { 1291 sprite_idx: sprite.sprite_idx, 1292 priority: sprite.priority, 1293 palette: sprite.palette, 1294 color_idx, 1295 }; 1296 } 1297 } 1298 } 1299 1300 fn read_vram(&self, address: u16) -> u16 { 1301 // Actual hardware usually returns "corrupted" data for out-of-bounds VRAM addresses 1302 self.vram.get(address as usize).copied().unwrap_or(0xFFFF) 1303 } 1304 1305 fn write_vram(&mut self, address: u16, value: u16) { 1306 let address = address as usize; 1307 if address < VRAM_LEN_WORDS { 1308 self.vram[address] = value; 1309 log::trace!(" VRAM WRITE: {address:04X} = {value:04X}"); 1310 } 1311 } 1312 1313 fn increment_vram_read_address(&mut self) { 1314 self.registers.vram_read_address = 1315 self.registers.vram_read_address.wrapping_add(self.registers.vram_address_increment); 1316 } 1317 1318 fn increment_vram_write_address(&mut self) { 1319 self.registers.vram_write_address = 1320 self.registers.vram_write_address.wrapping_add(self.registers.vram_address_increment); 1321 } 1322 1323 fn set_irq(&mut self, irq: VdcIrq) { 1324 log::trace!( 1325 "Triggering IRQ {irq:?} (if enabled), line {} dot {}", 1326 self.state.scanline, 1327 self.state.scanline_dot 1328 ); 1329 1330 match irq { 1331 VdcIrq::VBlank => { 1332 self.state.vblank_irq_pending |= self.registers.vblank_irq_enabled; 1333 } 1334 VdcIrq::RasterCompare => { 1335 self.state.raster_compare_irq_pending |= self.registers.raster_compare_irq_enabled; 1336 } 1337 VdcIrq::SpriteOverflow => { 1338 self.state.sprite_overflow_irq_pending |= 1339 self.registers.sprite_overflow_irq_enabled; 1340 } 1341 VdcIrq::SpriteCollision => { 1342 self.state.sprite_collision_irq_pending |= 1343 self.registers.sprite_collision_irq_enabled; 1344 } 1345 VdcIrq::VramDma => { 1346 self.state.vram_dma_irq_pending |= self.registers.vram_dma_irq_enabled; 1347 } 1348 VdcIrq::SatDma => { 1349 self.state.sat_dma_irq_pending |= self.registers.sat_dma_irq_enabled; 1350 } 1351 } 1352 1353 self.state.any_irq_pending = self.state.vblank_irq_pending 1354 || self.state.raster_compare_irq_pending 1355 || self.state.sprite_collision_irq_pending 1356 || self.state.sprite_overflow_irq_pending 1357 || self.state.vram_dma_irq_pending 1358 || self.state.sat_dma_irq_pending; 1359 } 1360}