Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test-roms-extra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ jobs:
- name: samesuite
- name: little-things-gb
- name: magen
- name: mealybug-tearoom-tests
# Temporarily disabled while the c-sp v7 Mealybug inventory is validated manually.
# - name: mealybug-tearoom-tests
- name: gbmicrotest
- name: mooneye
- name: wilbertpol
Expand Down
8 changes: 6 additions & 2 deletions crates/gb-core/src/bus/iohram.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,8 +382,12 @@ impl IoHramDomain {
}
}
IoRegisterKind::Hdma5 => {
if let Some(dma) = io.dma {
dma.write_hdma5(value);
let BusIoWriteView { dma, speed, .. } = io;
let speed_mode = speed
.as_deref()
.map_or(CgbSpeedMode::Normal, SpeedController::current_speed);
if let Some(dma) = dma {
dma.write_hdma5_for_speed(value, speed_mode);
}
}
IoRegisterKind::Rp => self.write_rp(value),
Expand Down
164 changes: 139 additions & 25 deletions crates/gb-core/src/dma.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ const OAM_DMA_T_CYCLES_PER_BYTE: u8 = 4;
const OAM_DMA_TOTAL_T_CYCLES: u16 = 648;
const DMG_OAM_DMA_ECHO_ALIAS_OFFSET: u16 = 0x2000;
const VRAM_DMA_BLOCK_BYTES: u16 = 0x10;
const VRAM_DMA_FIRST_BYTE_DELAY_T_CYCLES: u8 = 2;
const VRAM_DMA_NORMAL_SPEED_FIRST_BYTE_DELAY_T_CYCLES: u8 = 2;
const VRAM_DMA_CPU_BUS_RESTRICTION_DELAY_T_CYCLES: u8 = 0;
const VRAM_DMA_T_CYCLES_PER_BYTE: u8 = 2;
const VRAM_DMA_NORMAL_SPEED_T_CYCLES_PER_BYTE: u8 = 2;
const VRAM_DMA_CPU_RELEASE_T_CYCLES: u8 = 5;
const VRAM_DMA_PUBLICATION_T_CYCLES: u8 = 12;
const VRAM_DMA_DESTINATION_END: u16 = 0x9FFF;
const VRAM_DMA_INVALID_SOURCE_READ_VALUE: u8 = 0xFF;
const HDMA5_TRANSFER_LENGTH_MASK: u8 = 0x7F;
Expand Down Expand Up @@ -160,7 +162,7 @@ impl DmaTransfer {
})
}

const fn gdma(transfer: VramDmaTransfer) -> Self {
const fn gdma_for_speed(transfer: VramDmaTransfer, speed_mode: CgbSpeedMode) -> Self {
let total_bytes = transfer.remaining_bytes();
Self::from_spec(DmaTransferSpec {
kind: DmaTransferKind::Gdma,
Expand All @@ -169,24 +171,24 @@ impl DmaTransfer {
total_bytes,
block_size: VRAM_DMA_BLOCK_BYTES,
family: DmaTransferFamily::FullBurst,
timing: vram_dma_timing(total_bytes),
oam_speed_mode: CgbSpeedMode::Normal,
timing: vram_dma_timing(total_bytes, speed_mode),
oam_speed_mode: speed_mode,
cpu_impact_policy: DmaCpuImpactPolicy::CpuFullyStalledUntilDone,
memory_region_impact: DmaMemoryRegionImpact::Vram,
advance_condition: DmaAdvanceCondition::EveryTCycle,
})
}

const fn hdma_block(transfer: VramDmaTransfer) -> Self {
const fn hdma_block_for_speed(transfer: VramDmaTransfer, speed_mode: CgbSpeedMode) -> Self {
Self::from_spec(DmaTransferSpec {
kind: DmaTransferKind::Hdma,
source_start: transfer.source_start(),
destination_start: transfer.destination_start(),
total_bytes: VRAM_DMA_BLOCK_BYTES,
block_size: VRAM_DMA_BLOCK_BYTES,
family: DmaTransferFamily::BlockWindowed,
timing: vram_dma_timing(VRAM_DMA_BLOCK_BYTES),
oam_speed_mode: CgbSpeedMode::Normal,
timing: vram_dma_timing(VRAM_DMA_BLOCK_BYTES, speed_mode),
oam_speed_mode: speed_mode,
cpu_impact_policy: DmaCpuImpactPolicy::CpuStalledPerBlock,
memory_region_impact: DmaMemoryRegionImpact::Vram,
advance_condition: DmaAdvanceCondition::HBlank,
Expand Down Expand Up @@ -250,9 +252,10 @@ impl DmaTransfer {

pub const fn lcd_domain_duration_dots(self) -> u16 {
match (self.kind, self.oam_speed_mode) {
(DmaTransferKind::Oam, CgbSpeedMode::Double) => {
self.timing.total_t_cycles().div_ceil(2)
}
(
DmaTransferKind::Oam | DmaTransferKind::Gdma | DmaTransferKind::Hdma,
CgbSpeedMode::Double,
) => self.timing.total_t_cycles().div_ceil(2),
_ => self.timing.total_t_cycles(),
}
}
Expand All @@ -278,12 +281,45 @@ impl DmaTransfer {
}
}

const fn vram_dma_timing(total_bytes: u16) -> DmaTransferTiming {
const fn vram_dma_first_byte_delay_t_cycles(speed_mode: CgbSpeedMode) -> u8 {
match speed_mode {
CgbSpeedMode::Normal => VRAM_DMA_NORMAL_SPEED_FIRST_BYTE_DELAY_T_CYCLES,
CgbSpeedMode::Double => VRAM_DMA_NORMAL_SPEED_FIRST_BYTE_DELAY_T_CYCLES * 2,
}
}

const fn vram_dma_t_cycles_per_byte(speed_mode: CgbSpeedMode) -> u8 {
match speed_mode {
CgbSpeedMode::Normal => VRAM_DMA_NORMAL_SPEED_T_CYCLES_PER_BYTE,
CgbSpeedMode::Double => VRAM_DMA_NORMAL_SPEED_T_CYCLES_PER_BYTE * 2,
}
}

const fn vram_dma_body_t_cycles(total_bytes: u16, speed_mode: CgbSpeedMode) -> u16 {
let first_byte_delay_t_cycles = vram_dma_first_byte_delay_t_cycles(speed_mode);
let t_cycles_per_byte = vram_dma_t_cycles_per_byte(speed_mode);

if total_bytes == 0 {
0
} else {
first_byte_delay_t_cycles as u16 + (total_bytes - 1) * t_cycles_per_byte as u16
}
}

const fn vram_dma_timing(total_bytes: u16, speed_mode: CgbSpeedMode) -> DmaTransferTiming {
let first_byte_delay_t_cycles = vram_dma_first_byte_delay_t_cycles(speed_mode);
let t_cycles_per_byte = vram_dma_t_cycles_per_byte(speed_mode);
let total_t_cycles = if total_bytes == 0 {
0
} else {
vram_dma_body_t_cycles(total_bytes, speed_mode) + VRAM_DMA_CPU_RELEASE_T_CYCLES as u16
};

DmaTransferTiming {
total_t_cycles: total_bytes * VRAM_DMA_T_CYCLES_PER_BYTE as u16,
first_byte_delay_t_cycles: VRAM_DMA_FIRST_BYTE_DELAY_T_CYCLES,
total_t_cycles,
first_byte_delay_t_cycles,
cpu_bus_restriction_delay_t_cycles: VRAM_DMA_CPU_BUS_RESTRICTION_DELAY_T_CYCLES,
t_cycles_per_byte: VRAM_DMA_T_CYCLES_PER_BYTE,
t_cycles_per_byte,
}
}

Expand Down Expand Up @@ -359,6 +395,29 @@ impl DmaTransferProgress {
self.completed_bytes() / self.transfer.block_size()
}

pub const fn published_completed_blocks(self) -> u16 {
let completed_blocks = self.completed_blocks();
if completed_blocks == 0 {
return 0;
}

let completed_bytes = completed_blocks * self.transfer.block_size();
let last_byte_elapsed_t_cycles = self.transfer.timing().first_byte_delay_t_cycles() as u16
+ (completed_bytes - 1) * self.transfer.timing().t_cycles_per_byte() as u16;
let release_elapsed_t_cycles =
last_byte_elapsed_t_cycles + VRAM_DMA_PUBLICATION_T_CYCLES as u16;

if self.elapsed_t_cycles >= release_elapsed_t_cycles {
completed_blocks
} else {
completed_blocks - 1
}
}

pub const fn has_unpublished_completed_blocks(self) -> bool {
self.completed_blocks() > self.published_completed_blocks()
}

pub const fn remaining_blocks(self) -> u16 {
self.transfer.total_blocks() - self.completed_blocks()
}
Expand Down Expand Up @@ -613,12 +672,18 @@ pub(crate) enum VramDmaHBlankWindow {
}

impl VramDmaHBlankWindow {
const fn from_ppu_bus_state(ppu: PpuBusState, ly: u8) -> Self {
const fn from_ppu_bus_state(
ppu: PpuBusState,
ly: u8,
line_dot: u16,
mode0_start_dot: u16,
) -> Self {
if !ppu.is_lcd_enabled() {
return Self::LcdDisabled;
}

if ly < 144 && matches!(ppu.mode(), PpuAccessMode::HBlank) {
if ly < 144 && matches!(ppu.mode(), PpuAccessMode::HBlank) && line_dot > mode0_start_dot + 2
{
Self::VisibleHBlank { ly }
} else {
Self::None
Expand All @@ -630,25 +695,60 @@ impl VramDmaHBlankWindow {
pub(crate) struct VramDmaRuntimeContext {
ppu_bus_state: PpuBusState,
ppu_ly: u8,
ppu_line_dot: u16,
ppu_mode0_start_dot: u16,
cpu_halted: bool,
speed_mode: CgbSpeedMode,
}

impl VramDmaRuntimeContext {
pub(crate) const fn new(ppu_bus_state: PpuBusState, ppu_ly: u8, cpu_halted: bool) -> Self {
Self::new_for_speed(ppu_bus_state, ppu_ly, cpu_halted, CgbSpeedMode::Normal)
}

pub(crate) const fn new_for_speed(
ppu_bus_state: PpuBusState,
ppu_ly: u8,
cpu_halted: bool,
speed_mode: CgbSpeedMode,
) -> Self {
Self::new_for_speed_at_dot(ppu_bus_state, ppu_ly, 3, 0, cpu_halted, speed_mode)
}

pub(crate) const fn new_for_speed_at_dot(
ppu_bus_state: PpuBusState,
ppu_ly: u8,
ppu_line_dot: u16,
ppu_mode0_start_dot: u16,
cpu_halted: bool,
speed_mode: CgbSpeedMode,
) -> Self {
Self {
ppu_bus_state,
ppu_ly,
ppu_line_dot,
ppu_mode0_start_dot,
cpu_halted,
speed_mode,
}
}

const fn hblank_window(self) -> VramDmaHBlankWindow {
VramDmaHBlankWindow::from_ppu_bus_state(self.ppu_bus_state, self.ppu_ly)
VramDmaHBlankWindow::from_ppu_bus_state(
self.ppu_bus_state,
self.ppu_ly,
self.ppu_line_dot,
self.ppu_mode0_start_dot,
)
}

const fn cpu_halted(self) -> bool {
self.cpu_halted
}

const fn speed_mode(self) -> CgbSpeedMode {
self.speed_mode
}
}

impl Default for VramDmaRuntimeContext {
Expand Down Expand Up @@ -945,7 +1045,7 @@ impl DmaController {
pub(crate) fn requires_t_cycle_tick(&self) -> bool {
self.transfer_state.is_in_flight()
|| self.pending_restart.is_some()
|| matches!(self.vram_dma_state, VramDmaState::HBlankActive(_))
|| self.vram_dma_state.is_active()
}

pub fn transfer_status(&self) -> DmaTransferStatusView {
Expand Down Expand Up @@ -1085,6 +1185,10 @@ impl DmaController {
}

pub fn write_hdma5(&mut self, value: u8) {
self.write_hdma5_for_speed(value, CgbSpeedMode::Normal);
}

pub(crate) fn write_hdma5_for_speed(&mut self, value: u8, speed_mode: CgbSpeedMode) {
if matches!(self.vram_dma_state, VramDmaState::HBlankActive(_)) {
if value & HDMA5_MODE_BIT == 0 {
self.vram_dma_state = VramDmaState::Inactive {
Expand All @@ -1108,8 +1212,9 @@ impl DmaController {
VramDmaTransfer::new(VramDmaMode::GeneralPurpose, self.vram_dma_registers, blocks);
self.vram_dma_state = VramDmaState::GeneralPurposeActive(transfer);
self.vram_dma_last_served_window = VramDmaHBlankWindow::default();
self.transfer_state =
DmaTransferState::Starting(DmaTransferProgress::new(DmaTransfer::gdma(transfer)));
self.transfer_state = DmaTransferState::Starting(DmaTransferProgress::new(
DmaTransfer::gdma_for_speed(transfer, speed_mode),
));
self.pending_restart = None;
} else {
self.vram_dma_state = VramDmaState::HBlankActive(VramDmaTransfer::new(
Expand Down Expand Up @@ -1221,6 +1326,14 @@ impl DmaController {

self.transfer_state = match self.transfer_state {
DmaTransferState::Idle => DmaTransferState::Idle,
DmaTransferState::Completed(progress)
if matches!(
progress.transfer().kind(),
DmaTransferKind::Gdma | DmaTransferKind::Hdma
) && progress.has_unpublished_completed_blocks() =>
{
DmaTransferState::Completed(progress.advance_one_t_cycle())
}
DmaTransferState::Completed(progress) => DmaTransferState::Completed(progress),
DmaTransferState::Starting(progress) => {
let advanced_progress = progress.advance_one_t_cycle();
Expand Down Expand Up @@ -1264,8 +1377,9 @@ impl DmaController {
}

self.vram_dma_last_served_window = window;
self.transfer_state =
DmaTransferState::Starting(DmaTransferProgress::new(DmaTransfer::hdma_block(transfer)));
self.transfer_state = DmaTransferState::Starting(DmaTransferProgress::new(
DmaTransfer::hdma_block_for_speed(transfer, vram_dma_context.speed_mode()),
));
}

fn apply_vram_dma_progress_side_effects(
Expand All @@ -1285,8 +1399,8 @@ impl DmaController {
return;
}

let completed_blocks = current_progress.completed_blocks()
- previous_progress.map_or(0, DmaTransferProgress::completed_blocks);
let completed_blocks = current_progress.published_completed_blocks()
- previous_progress.map_or(0, DmaTransferProgress::published_completed_blocks);
if completed_blocks == 0 {
return;
}
Expand Down
Loading