From 1b9609ef619934a3e2691383ef12c1b71820e2a8 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Fri, 24 Jan 2020 14:53:40 +1000 Subject: [PATCH] Implement event-based scheduler instead of lock-step components --- src/common/audio_stream.cpp | 26 ++-- src/common/audio_stream.h | 6 +- src/core/CMakeLists.txt | 2 + src/core/cdrom.cpp | 202 +++++++++++++------------ src/core/cdrom.h | 15 +- src/core/core.vcxproj | 2 + src/core/core.vcxproj.filters | 4 +- src/core/cpu_core.h | 7 +- src/core/dma.cpp | 113 ++++++-------- src/core/dma.h | 15 +- src/core/gpu.cpp | 216 +++++++++++++++++---------- src/core/gpu.h | 12 +- src/core/host_interface.cpp | 5 +- src/core/host_interface.h | 6 +- src/core/mdec.cpp | 66 +++----- src/core/mdec.h | 8 +- src/core/pad.cpp | 45 +++--- src/core/pad.h | 8 +- src/core/spu.cpp | 225 +++++++++++++++++----------- src/core/spu.h | 12 +- src/core/system.cpp | 273 +++++++++++++++++++++++++++++----- src/core/system.h | 51 ++++++- src/core/timers.cpp | 68 +++++++-- src/core/timers.h | 16 +- src/core/timing_event.cpp | 130 ++++++++++++++++ src/core/timing_event.h | 76 ++++++++++ 26 files changed, 1089 insertions(+), 520 deletions(-) create mode 100644 src/core/timing_event.cpp create mode 100644 src/core/timing_event.h diff --git a/src/common/audio_stream.cpp b/src/common/audio_stream.cpp index 9bf936f0b..0c0e816a3 100644 --- a/src/common/audio_stream.cpp +++ b/src/common/audio_stream.cpp @@ -59,34 +59,34 @@ void AudioStream::Shutdown() m_output_paused = true; } -void AudioStream::BeginWrite(SampleType** buffer_ptr, u32* num_samples) +void AudioStream::BeginWrite(SampleType** buffer_ptr, u32* num_frames) { m_buffer_mutex.lock(); EnsureBuffer(); Buffer& buffer = m_buffers[m_first_free_buffer]; - *buffer_ptr = buffer.data.data() + buffer.write_position; - *num_samples = m_buffer_size - buffer.write_position; + *buffer_ptr = buffer.data.data() + (buffer.write_position * m_channels); + *num_frames = m_buffer_size - buffer.write_position; } -void AudioStream::WriteSamples(const SampleType* samples, u32 num_samples) +void AudioStream::WriteFrames(const SampleType* frames, u32 num_frames) { - u32 remaining_samples = num_samples; + u32 remaining_frames = num_frames; std::unique_lock lock(m_buffer_mutex); - while (remaining_samples > 0) + while (remaining_frames > 0) { EnsureBuffer(); Buffer& buffer = m_buffers[m_first_free_buffer]; - const u32 to_this_buffer = std::min(m_buffer_size - buffer.write_position, remaining_samples); + const u32 to_this_buffer = std::min(m_buffer_size - buffer.write_position, remaining_frames); const u32 copy_count = to_this_buffer * m_channels; - std::memcpy(&buffer.data[buffer.write_position * m_channels], samples, copy_count * sizeof(SampleType)); - samples += copy_count; + std::memcpy(&buffer.data[buffer.write_position * m_channels], frames, copy_count * sizeof(SampleType)); + frames += copy_count; - remaining_samples -= to_this_buffer; + remaining_frames -= to_this_buffer; buffer.write_position += to_this_buffer; // End of the buffer? @@ -102,11 +102,11 @@ void AudioStream::WriteSamples(const SampleType* samples, u32 num_samples) } } -void AudioStream::EndWrite(u32 num_samples) +void AudioStream::EndWrite(u32 num_frames) { Buffer& buffer = m_buffers[m_first_free_buffer]; - DebugAssert((buffer.write_position + num_samples) <= m_buffer_size); - buffer.write_position += num_samples; + DebugAssert((buffer.write_position + num_frames) <= m_buffer_size); + buffer.write_position += num_frames; // End of the buffer? if (buffer.write_position == m_buffer_size) diff --git a/src/common/audio_stream.h b/src/common/audio_stream.h index ea8e73e0c..030bc28d2 100644 --- a/src/common/audio_stream.h +++ b/src/common/audio_stream.h @@ -37,9 +37,9 @@ public: void Shutdown(); - void BeginWrite(SampleType** buffer_ptr, u32* num_samples); - void WriteSamples(const SampleType* samples, u32 num_samples); - void EndWrite(u32 num_samples); + void BeginWrite(SampleType** buffer_ptr, u32* num_frames); + void WriteFrames(const SampleType* frames, u32 num_frames); + void EndWrite(u32 num_frames); static std::unique_ptr CreateNullAudioStream(); diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 53d8d1967..8953056ae 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -65,6 +65,8 @@ add_library(core system.h timers.cpp timers.h + timing_event.cpp + timing_event.h types.h ) diff --git a/src/core/cdrom.cpp b/src/core/cdrom.cpp index 58b5a3106..647a10a88 100644 --- a/src/core/cdrom.cpp +++ b/src/core/cdrom.cpp @@ -1,6 +1,6 @@ #include "cdrom.h" -#include "common/log.h" #include "common/cd_image.h" +#include "common/log.h" #include "common/state_wrapper.h" #include "dma.h" #include "imgui.h" @@ -22,6 +22,10 @@ void CDROM::Initialize(System* system, DMA* dma, InterruptController* interrupt_ m_dma = dma; m_interrupt_controller = interrupt_controller; m_spu = spu; + m_command_event = + m_system->CreateTimingEvent("CDROM Command Event", 1, 1, std::bind(&CDROM::ExecuteCommand, this), false); + m_drive_event = m_system->CreateTimingEvent("CDROM Drive Event", 1, 1, + std::bind(&CDROM::ExecuteDrive, this, std::placeholders::_2), false); } void CDROM::Reset() @@ -35,9 +39,9 @@ void CDROM::Reset() void CDROM::SoftReset() { m_command = Command::None; + m_command_event->Deactivate(); m_drive_state = DriveState::Idle; - m_command_remaining_ticks = 0; - m_drive_remaining_ticks = 0; + m_drive_event->Deactivate(); m_status.bits = 0; m_secondary_status.bits = 0; m_mode.bits = 0; @@ -85,8 +89,6 @@ bool CDROM::DoState(StateWrapper& sw) { sw.Do(&m_command); sw.Do(&m_drive_state); - sw.Do(&m_command_remaining_ticks); - sw.Do(&m_drive_remaining_ticks); sw.Do(&m_status.bits); sw.Do(&m_secondary_status.bits); sw.Do(&m_mode.bits); @@ -124,10 +126,8 @@ bool CDROM::DoState(StateWrapper& sw) if (sw.IsReading()) { - if (HasPendingCommand()) - m_system->SetDowncount(m_command_remaining_ticks); - if (!IsDriveIdle()) - m_system->SetDowncount(m_drive_remaining_ticks); + UpdateCommandEvent(); + m_drive_event->SetState(!IsDriveIdle()); // load up media if we had something in there before if (m_media && !m_media->Seek(media_lba)) @@ -361,8 +361,8 @@ void CDROM::WriteRegister(u32 offset, u8 value) { if (HasPendingAsyncInterrupt()) DeliverAsyncInterrupt(); - else if (HasPendingCommand()) - m_system->SetDowncount(m_command_remaining_ticks); + else + UpdateCommandEvent(); } // Bit 6 clears the parameter FIFO. @@ -443,6 +443,7 @@ void CDROM::DeliverAsyncInterrupt() m_pending_async_interrupt = 0; UpdateInterruptRequest(); UpdateStatusRegister(); + UpdateCommandEvent(); } void CDROM::SendACKAndStat() @@ -520,73 +521,11 @@ TickCount CDROM::GetTicksForSeek() const return ticks; } -void CDROM::Execute(TickCount ticks) -{ - if (HasPendingCommand() && !HasPendingInterrupt()) - { - m_command_remaining_ticks -= ticks; - if (m_command_remaining_ticks <= 0) - ExecuteCommand(); - else - m_system->SetDowncount(m_command_remaining_ticks); - } - - if (m_drive_state != DriveState::Idle) - { - m_drive_remaining_ticks -= ticks; - if (m_drive_remaining_ticks <= 0) - { - switch (m_drive_state) - { - case DriveState::SpinningUp: - DoSpinUpComplete(); - break; - - case DriveState::SeekingPhysical: - case DriveState::SeekingLogical: - DoSeekComplete(); - break; - - case DriveState::Pausing: - DoPauseComplete(); - break; - - case DriveState::Stopping: - DoStopComplete(); - break; - - case DriveState::ReadingID: - DoIDRead(); - break; - - case DriveState::ReadingTOC: - DoTOCRead(); - break; - - case DriveState::Reading: - case DriveState::Playing: - DoSectorRead(); - break; - - case DriveState::Idle: - default: - break; - } - } - else - { - m_system->SetDowncount(m_drive_remaining_ticks); - } - } -} - void CDROM::BeginCommand(Command command) { - m_system->Synchronize(); - m_command = command; - m_command_remaining_ticks = GetAckDelayForCommand(); - m_system->SetDowncount(m_command_remaining_ticks); + m_command_event->SetDowncount(GetAckDelayForCommand()); + UpdateCommandEvent(); UpdateStatusRegister(); } @@ -595,7 +534,7 @@ void CDROM::EndCommand() m_param_fifo.Clear(); m_command = Command::None; - m_command_remaining_ticks = 0; + m_command_event->Deactivate(); UpdateStatusRegister(); } @@ -645,7 +584,7 @@ void CDROM::ExecuteCommand() SendACKAndStat(); m_drive_state = DriveState::ReadingID; - m_drive_remaining_ticks = 18000; + m_drive_event->Schedule(18000); } EndCommand(); @@ -664,7 +603,7 @@ void CDROM::ExecuteCommand() SendACKAndStat(); m_drive_state = DriveState::ReadingTOC; - m_drive_remaining_ticks = MASTER_CLOCK / 2; // half a second + m_drive_event->Schedule(MASTER_CLOCK / 2); // half a second } EndCommand(); @@ -767,12 +706,12 @@ void CDROM::ExecuteCommand() case Command::Pause: { const bool was_reading = (m_drive_state == DriveState::Reading || m_drive_state == DriveState::Playing); + const TickCount pause_time = was_reading ? (m_mode.double_speed ? 2000000 : 1000000) : 7000; Log_DebugPrintf("CDROM pause command"); SendACKAndStat(); m_drive_state = DriveState::Pausing; - m_drive_remaining_ticks = was_reading ? (m_mode.double_speed ? 2000000 : 1000000) : 7000; - m_system->SetDowncount(m_drive_remaining_ticks); + m_drive_event->Schedule(pause_time); EndCommand(); return; @@ -781,12 +720,12 @@ void CDROM::ExecuteCommand() case Command::Stop: { const bool was_motor_on = m_secondary_status.motor_on; + const TickCount stop_time = was_motor_on ? (m_mode.double_speed ? 25000000 : 13000000) : 7000; Log_DebugPrintf("CDROM stop command"); SendACKAndStat(); m_drive_state = DriveState::Stopping; - m_drive_remaining_ticks = was_motor_on ? (m_mode.double_speed ? 25000000 : 13000000) : 7000; - m_system->SetDowncount(m_drive_remaining_ticks); + m_drive_event->Schedule(stop_time); EndCommand(); return; @@ -801,8 +740,7 @@ void CDROM::ExecuteCommand() m_mode.bits = 0; m_drive_state = DriveState::SpinningUp; - m_drive_remaining_ticks = 80000; - m_system->SetDowncount(m_drive_remaining_ticks); + m_drive_event->Schedule(80000); EndCommand(); return; @@ -821,8 +759,7 @@ void CDROM::ExecuteCommand() SendACKAndStat(); m_drive_state = DriveState::SpinningUp; - m_drive_remaining_ticks = 80000; - m_system->SetDowncount(m_drive_remaining_ticks); + m_drive_event->Schedule(80000); } EndCommand(); @@ -1011,7 +948,62 @@ void CDROM::ExecuteTestCommand(u8 subcommand) } } -void CDROM::BeginReading() +void CDROM::UpdateCommandEvent() +{ + // if there's a pending interrupt, we can't execute the command yet + // so deactivate it until the interrupt is acknowledged + if (!HasPendingCommand() || HasPendingInterrupt()) + { + m_command_event->Deactivate(); + return; + } + else if (HasPendingCommand()) + { + m_command_event->Activate(); + } +} + +void CDROM::ExecuteDrive(TickCount ticks_late) +{ + switch (m_drive_state) + { + case DriveState::SpinningUp: + DoSpinUpComplete(); + break; + + case DriveState::SeekingPhysical: + case DriveState::SeekingLogical: + DoSeekComplete(ticks_late); + break; + + case DriveState::Pausing: + DoPauseComplete(); + break; + + case DriveState::Stopping: + DoStopComplete(); + break; + + case DriveState::ReadingID: + DoIDRead(); + break; + + case DriveState::ReadingTOC: + DoTOCRead(); + break; + + case DriveState::Reading: + case DriveState::Playing: + DoSectorRead(); + break; + + case DriveState::Idle: + default: + break; + } +} + +void CDROM::BeginReading(TickCount ticks_late) { Log_DebugPrintf("Starting reading"); if (m_setloc_pending) @@ -1026,12 +1018,13 @@ void CDROM::BeginReading() // TODO: Should the sector buffer be cleared here? m_sector_buffer.clear(); + const TickCount ticks = GetTicksForRead(); m_drive_state = DriveState::Reading; - m_drive_remaining_ticks = GetTicksForRead(); - m_system->SetDowncount(m_drive_remaining_ticks); + m_drive_event->SetInterval(ticks); + m_drive_event->Schedule(ticks - ticks_late); } -void CDROM::BeginPlaying(u8 track_bcd) +void CDROM::BeginPlaying(u8 track_bcd, TickCount ticks_late) { Log_DebugPrintf("Starting playing CDDA track %x", track_bcd); m_last_cdda_report_frame_nibble = 0xFF; @@ -1063,9 +1056,10 @@ void CDROM::BeginPlaying(u8 track_bcd) // TODO: Should the sector buffer be cleared here? m_sector_buffer.clear(); + const TickCount ticks = GetTicksForRead(); m_drive_state = DriveState::Playing; - m_drive_remaining_ticks = GetTicksForRead(); - m_system->SetDowncount(m_drive_remaining_ticks); + m_drive_event->SetInterval(ticks); + m_drive_event->Schedule(ticks - ticks_late); } void CDROM::BeginSeeking(bool logical, bool read_after_seek, bool play_after_seek) @@ -1088,8 +1082,7 @@ void CDROM::BeginSeeking(bool logical, bool read_after_seek, bool play_after_see m_sector_buffer.clear(); m_drive_state = logical ? DriveState::SeekingLogical : DriveState::SeekingPhysical; - m_drive_remaining_ticks = seek_time; - m_system->SetDowncount(m_drive_remaining_ticks); + m_drive_event->SetIntervalAndSchedule(seek_time); // Read sub-q early.. this is because we're not reading sectors while seeking. // Fixes music looping in Spyro. @@ -1101,6 +1094,7 @@ void CDROM::BeginSeeking(bool logical, bool read_after_seek, bool play_after_see void CDROM::DoSpinUpComplete() { m_drive_state = DriveState::Idle; + m_drive_event->Deactivate(); m_secondary_status.motor_on = true; @@ -1109,10 +1103,11 @@ void CDROM::DoSpinUpComplete() SetAsyncInterrupt(Interrupt::INT2); } -void CDROM::DoSeekComplete() +void CDROM::DoSeekComplete(TickCount ticks_late) { const bool logical = (m_drive_state == DriveState::SeekingLogical); m_drive_state = DriveState::Idle; + m_drive_event->Deactivate(); m_secondary_status.ClearActiveBits(); m_sector_buffer.clear(); @@ -1145,11 +1140,11 @@ void CDROM::DoSeekComplete() // INT2 is not sent on play/read if (m_read_after_seek) { - BeginReading(); + BeginReading(ticks_late); } else if (m_play_after_seek) { - BeginPlaying(m_play_track_number_bcd); + BeginPlaying(m_play_track_number_bcd, ticks_late); } else { @@ -1175,6 +1170,7 @@ void CDROM::DoPauseComplete() { Log_DebugPrintf("Pause complete"); m_drive_state = DriveState::Idle; + m_drive_event->Deactivate(); m_secondary_status.ClearActiveBits(); m_sector_buffer.clear(); @@ -1187,6 +1183,7 @@ void CDROM::DoStopComplete() { Log_DebugPrintf("Stop complete"); m_drive_state = DriveState::Idle; + m_drive_event->Deactivate(); m_secondary_status.ClearActiveBits(); m_secondary_status.motor_on = false; m_sector_buffer.clear(); @@ -1204,6 +1201,7 @@ void CDROM::DoIDRead() Log_DebugPrintf("ID read complete"); m_drive_state = DriveState::Idle; + m_drive_event->Deactivate(); m_secondary_status.ClearActiveBits(); m_secondary_status.motor_on = true; m_sector_buffer.clear(); @@ -1219,6 +1217,7 @@ void CDROM::DoTOCRead() { Log_DebugPrintf("TOC read complete"); m_drive_state = DriveState::Idle; + m_drive_event->Deactivate(); m_async_response_fifo.Clear(); m_async_response_fifo.Push(m_secondary_status.bits); SetAsyncInterrupt(Interrupt::INT2); @@ -1254,6 +1253,7 @@ void CDROM::DoSectorRead() m_secondary_status.ClearActiveBits(); m_drive_state = DriveState::Idle; + m_drive_event->Deactivate(); return; } } @@ -1290,9 +1290,6 @@ void CDROM::DoSectorRead() Log_DevPrintf("Skipping sector %u [%02u:%02u:%02u] due to invalid subchannel Q", m_media->GetPositionOnDisc() - 1, pos.minute, pos.second, pos.frame); } - - m_drive_remaining_ticks += GetTicksForRead(); - m_system->SetDowncount(m_drive_remaining_ticks); } void CDROM::ProcessDataSectorHeader(const u8* raw_sector, bool set_valid) @@ -1711,7 +1708,7 @@ void CDROM::DrawDebugWindow() if (HasPendingCommand()) { ImGui::TextColored(active_color, "Command: 0x%02X (%d ticks remaining)", static_cast(m_command), - m_command_remaining_ticks); + m_command_event->IsActive() ? m_command_event->GetTicksUntilNextExecution() : 0); } else { @@ -1725,7 +1722,8 @@ void CDROM::DrawDebugWindow() else { ImGui::TextColored(active_color, "Drive: %s (%d ticks remaining)", - drive_state_names[static_cast(m_drive_state)], m_drive_remaining_ticks); + drive_state_names[static_cast(m_drive_state)], + m_drive_event->IsActive() ? m_drive_event->GetTicksUntilNextExecution() : 0); } ImGui::Text("Interrupt Enable Register: 0x%02X", m_interrupt_enable_register); diff --git a/src/core/cdrom.h b/src/core/cdrom.h index 02a95cca7..c676c51c6 100644 --- a/src/core/cdrom.h +++ b/src/core/cdrom.h @@ -12,6 +12,7 @@ class StateWrapper; class System; +class TimingEvent; class DMA; class InterruptController; class SPU; @@ -36,8 +37,6 @@ public: void WriteRegister(u32 offset, u8 value); void DMARead(u32* words, u32 word_count); - void Execute(TickCount ticks); - // Render statistics debug window. void DrawDebugWindow(); @@ -198,10 +197,12 @@ private: void EndCommand(); // also updates status register void ExecuteCommand(); void ExecuteTestCommand(u8 subcommand); - void BeginReading(); - void BeginPlaying(u8 track_bcd); + void UpdateCommandEvent(); + void ExecuteDrive(TickCount ticks_late); + void BeginReading(TickCount ticks_late = 0); + void BeginPlaying(u8 track_bcd, TickCount ticks_late = 0); void DoSpinUpComplete(); - void DoSeekComplete(); + void DoSeekComplete(TickCount ticks_late); void DoPauseComplete(); void DoStopComplete(); void DoIDRead(); @@ -219,11 +220,11 @@ private: InterruptController* m_interrupt_controller = nullptr; SPU* m_spu = nullptr; std::unique_ptr m_media; + std::unique_ptr m_command_event; + std::unique_ptr m_drive_event; Command m_command = Command::None; DriveState m_drive_state = DriveState::Idle; - TickCount m_command_remaining_ticks = 0; - TickCount m_drive_remaining_ticks = 0; StatusRegister m_status = {}; SecondaryStatusRegister m_secondary_status = {}; diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 8f077ff9b..3a2f52a74 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -82,6 +82,7 @@ + @@ -121,6 +122,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index 651b6c191..215c62e07 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -39,6 +39,7 @@ + @@ -79,10 +80,11 @@ + - \ No newline at end of file + diff --git a/src/core/cpu_core.h b/src/core/cpu_core.h index 13f1f5964..d91cf0dfd 100644 --- a/src/core/cpu_core.h +++ b/src/core/cpu_core.h @@ -51,11 +51,8 @@ public: ALWAYS_INLINE void ResetPendingTicks() { m_pending_ticks = 0; } ALWAYS_INLINE void AddPendingTicks(TickCount ticks) { m_pending_ticks += ticks; } - ALWAYS_INLINE void SetDowncount(TickCount downcount) - { - m_downcount = (downcount < m_downcount) ? downcount : m_downcount; - } - ALWAYS_INLINE void ResetDowncount() { m_downcount = MAX_SLICE_SIZE; } + ALWAYS_INLINE TickCount GetDowncount() const { return m_downcount; } + ALWAYS_INLINE void SetDowncount(TickCount downcount) { m_downcount = downcount; } // Sets the PC and flushes the pipeline. void SetPC(u32 new_pc); diff --git a/src/core/dma.cpp b/src/core/dma.cpp index 646b019fa..7007b53fd 100644 --- a/src/core/dma.cpp +++ b/src/core/dma.cpp @@ -1,8 +1,9 @@ #include "dma.h" -#include "common/log.h" #include "bus.h" #include "cdrom.h" +#include "common/log.h" #include "common/state_wrapper.h" +#include "common/string_util.h" #include "gpu.h" #include "interrupt_controller.h" #include "mdec.h" @@ -25,14 +26,28 @@ void DMA::Initialize(System* system, Bus* bus, InterruptController* interrupt_co m_spu = spu; m_mdec = mdec; m_transfer_buffer.resize(32); + + for (u32 i = 0; i < NUM_CHANNELS; i++) + { + m_state[i].transfer_event = system->CreateTimingEvent( + StringUtil::StdStringFromFormat("DMA%u Transfer", i), 1, 1, + std::bind(&DMA::TransferChannel, this, static_cast(i), std::placeholders::_2), false); + } } void DMA::Reset() { - m_transfer_in_progress = false; - std::memset(&m_state, 0, sizeof(m_state)); m_DPCR.bits = 0x07654321; m_DICR.bits = 0; + for (u32 i = 0; i < NUM_CHANNELS; i++) + { + ChannelState& cs = m_state[i]; + cs.base_address = 0; + cs.block_control.bits = 0; + cs.channel_control.bits = 0; + cs.request = false; + cs.transfer_event->Deactivate(); + } } bool DMA::DoState(StateWrapper& sw) @@ -43,7 +58,6 @@ bool DMA::DoState(StateWrapper& sw) sw.Do(&cs.base_address); sw.Do(&cs.block_control.bits); sw.Do(&cs.channel_control.bits); - sw.Do(&cs.transfer_ticks); sw.Do(&cs.request); } @@ -52,13 +66,11 @@ bool DMA::DoState(StateWrapper& sw) if (sw.IsReading()) { - m_transfer_min_ticks = std::numeric_limits::max(); - for (const ChannelState& cs : m_state) + for (u32 i = 0; i < NUM_CHANNELS; i++) { - if (cs.transfer_ticks > 0) - m_transfer_min_ticks = std::min(m_transfer_min_ticks, cs.transfer_ticks); + m_state[i].transfer_event->Deactivate(); + UpdateChannelTransferEvent(static_cast(i)); } - m_system->SetDowncount(m_transfer_min_ticks); } return !sw.HasError(); @@ -126,7 +138,7 @@ void DMA::WriteRegister(u32 offset, u32 value) { Log_TracePrintf("DMA channel %u block control <- 0x%08X", channel_index, value); state.block_control.bits = value; - QueueTransferChannel(static_cast(channel_index)); + UpdateChannelTransferEvent(static_cast(channel_index)); return; } @@ -135,7 +147,7 @@ void DMA::WriteRegister(u32 offset, u32 value) state.channel_control.bits = (state.channel_control.bits & ~ChannelState::ChannelControl::WRITE_MASK) | (value & ChannelState::ChannelControl::WRITE_MASK); Log_TracePrintf("DMA channel %u channel control <- 0x%08X", channel_index, state.channel_control.bits); - QueueTransferChannel(static_cast(channel_index)); + UpdateChannelTransferEvent(static_cast(channel_index)); return; } @@ -151,7 +163,8 @@ void DMA::WriteRegister(u32 offset, u32 value) { Log_TracePrintf("DPCR <- 0x%08X", value); m_DPCR.bits = value; - QueueTransfer(); + for (u32 i = 0; i < NUM_CHANNELS; i++) + UpdateChannelTransferEvent(static_cast(i)); return; } @@ -180,7 +193,7 @@ void DMA::SetRequest(Channel channel, bool request) cs.request = request; if (request) - QueueTransfer(); + UpdateChannelTransferEvent(channel); } TickCount DMA::GetTransferDelay(Channel channel) const @@ -188,6 +201,10 @@ TickCount DMA::GetTransferDelay(Channel channel) const const ChannelState& cs = m_state[static_cast(channel)]; switch (channel) { + case Channel::MDECin: + case Channel::MDECout: + return 1; + case Channel::SPU: { if (cs.channel_control.sync_mode == SyncMode::Request) @@ -198,7 +215,7 @@ TickCount DMA::GetTransferDelay(Channel channel) const break; default: - return 1; + return 0; } } @@ -217,9 +234,6 @@ bool DMA::CanTransferChannel(Channel channel) const if (cs.channel_control.sync_mode == SyncMode::Manual && !cs.channel_control.start_trigger) return false; - if (cs.transfer_ticks > 0) - return false; - return true; } @@ -244,72 +258,34 @@ void DMA::UpdateIRQ() } } -void DMA::QueueTransferChannel(Channel channel) +void DMA::UpdateChannelTransferEvent(Channel channel) { ChannelState& cs = m_state[static_cast(channel)]; - if (cs.transfer_ticks > 0 || !CanTransferChannel(channel)) + if (!CanTransferChannel(channel)) + { + cs.transfer_event->Deactivate(); + return; + } + + if (cs.transfer_event->IsActive()) return; const TickCount ticks = GetTransferDelay(channel); if (ticks == 0) { // immediate transfer - TransferChannel(channel); + TransferChannel(channel, 0); return; } - if (!m_transfer_in_progress) - m_system->Synchronize(); - - cs.transfer_ticks = ticks; - m_transfer_min_ticks = std::min(m_transfer_min_ticks, ticks); - m_system->SetDowncount(ticks); + cs.transfer_event->SetPeriodAndSchedule(ticks); } -void DMA::QueueTransfer() -{ - for (u32 i = 0; i < NUM_CHANNELS; i++) - QueueTransferChannel(static_cast(i)); -} - -void DMA::Execute(TickCount ticks) -{ - m_transfer_min_ticks -= ticks; - if (m_transfer_min_ticks > 0) - { - m_system->SetDowncount(m_transfer_min_ticks); - return; - } - - DebugAssert(!m_transfer_in_progress); - m_transfer_in_progress = true; - - // keep going until all transfers are done. one channel can start others (e.g. MDEC) - m_transfer_min_ticks = std::numeric_limits::max(); - for (u32 i = 0; i < NUM_CHANNELS; i++) - { - const Channel channel = static_cast(i); - if (m_state[i].transfer_ticks <= 0) - continue; - - m_state[i].transfer_ticks -= ticks; - if (CanTransferChannel(channel)) - { - TransferChannel(channel); - } - else - { - m_transfer_min_ticks = std::min(m_transfer_min_ticks, ticks); - } - } - - m_system->SetDowncount(m_transfer_min_ticks); - m_transfer_in_progress = false; -} - -void DMA::TransferChannel(Channel channel) +void DMA::TransferChannel(Channel channel, TickCount ticks_late) { ChannelState& cs = m_state[static_cast(channel)]; + cs.transfer_event->Deactivate(); + const bool copy_to_device = cs.channel_control.copy_to_device; // start/trigger bit is cleared on beginning of transfer @@ -416,7 +392,6 @@ void DMA::TransferChannel(Channel channel) } // start/busy bit is cleared on end of transfer - cs.transfer_ticks = 0; cs.channel_control.enable_busy = false; if (m_DICR.IsIRQEnabled(channel)) { diff --git a/src/core/dma.h b/src/core/dma.h index dc66b77ca..80c0c5863 100644 --- a/src/core/dma.h +++ b/src/core/dma.h @@ -2,11 +2,13 @@ #include "common/bitfield.h" #include "types.h" #include +#include #include class StateWrapper; class System; +class TimingEvent; class Bus; class InterruptController; class GPU; @@ -49,8 +51,6 @@ public: // changing interfaces void SetGPU(GPU* gpu) { m_gpu = gpu; } - void Execute(TickCount ticks); - private: static constexpr PhysicalMemoryAddress BASE_ADDRESS_MASK = UINT32_C(0x00FFFFFF); static constexpr PhysicalMemoryAddress ADDRESS_MASK = UINT32_C(0x001FFFFC); @@ -72,10 +72,8 @@ private: bool CanRunAnyChannels() const; void UpdateIRQ(); - void QueueTransferChannel(Channel channel); - void QueueTransfer(); - - void TransferChannel(Channel channel); + void UpdateChannelTransferEvent(Channel channel); + void TransferChannel(Channel channel, TickCount ticks_late); // from device -> memory void TransferDeviceToMemory(Channel channel, u32 address, u32 increment, u32 word_count); @@ -95,6 +93,7 @@ private: struct ChannelState { + std::unique_ptr transfer_event; u32 base_address; union BlockControl @@ -131,7 +130,6 @@ private: static constexpr u32 WRITE_MASK = 0b01110001'01110111'00000111'00000011; } channel_control; - TickCount transfer_ticks = 0; bool request = false; }; @@ -207,7 +205,4 @@ private: master_flag = master_enable && ((((bits >> 16) & u32(0b1111111)) & ((bits >> 24) & u32(0b1111111))) != 0); } } m_DICR = {}; - - TickCount m_transfer_min_ticks = 0; - bool m_transfer_in_progress = false; }; diff --git a/src/core/gpu.cpp b/src/core/gpu.cpp index a0d2d85bf..3878efbf5 100644 --- a/src/core/gpu.cpp +++ b/src/core/gpu.cpp @@ -1,6 +1,6 @@ #include "gpu.h" -#include "common/log.h" #include "common/heap_array.h" +#include "common/log.h" #include "common/state_wrapper.h" #include "dma.h" #include "host_interface.h" @@ -27,6 +27,8 @@ bool GPU::Initialize(HostDisplay* host_display, System* system, DMA* dma, Interr m_interrupt_controller = interrupt_controller; m_timers = timers; m_force_progressive_scan = m_system->GetSettings().gpu_force_progressive_scan; + m_tick_event = + m_system->CreateTimingEvent("GPU Tick", 1, 1, std::bind(&GPU::Execute, this, std::placeholders::_1), true); return true; } @@ -63,6 +65,9 @@ void GPU::SoftReset() m_draw_mode.SetTextureWindow(0); UpdateGPUSTAT(); UpdateCRTCConfig(); + + m_tick_event->Deactivate(); + UpdateSliceTicks(); } bool GPU::DoState(StateWrapper& sw) @@ -273,6 +278,11 @@ void GPU::DMAWrite(const u32* words, u32 word_count) } } +void GPU::Synchronize() +{ + m_tick_event->InvokeEarly(); +} + void GPU::UpdateCRTCConfig() { static constexpr std::array dot_clock_dividers = {{10, 8, 5, 4, 7, 7, 7, 7}}; @@ -357,31 +367,31 @@ void GPU::UpdateCRTCConfig() // Ensure the numbers are sane, and not due to a misconfigured active display range. cs.display_aspect_ratio = (std::isnormal(display_ratio) && display_ratio != 0.0f) ? display_ratio : (4.0f / 3.0f); + m_tick_event->SetInterval(cs.horizontal_total); +} - UpdateSliceTicks(); +static TickCount GPUTicksToSystemTicks(u32 gpu_ticks) +{ + // convert to master clock, rounding up as we want to overshoot not undershoot + return (gpu_ticks * 7 + 10) / 11; } void GPU::UpdateSliceTicks() { - // the next event is at the end of the next scanline -#if 1 - TickCount ticks_until_next_event; - if (m_crtc_state.current_tick_in_scanline < m_crtc_state.horizontal_display_start) - ticks_until_next_event = m_crtc_state.horizontal_display_start - m_crtc_state.current_tick_in_scanline; - else if (m_crtc_state.current_tick_in_scanline < m_crtc_state.horizontal_display_end) - ticks_until_next_event = m_crtc_state.horizontal_display_end - m_crtc_state.current_tick_in_scanline; - else - ticks_until_next_event = m_crtc_state.horizontal_total - m_crtc_state.current_tick_in_scanline; -#else - // or at vblank. this will depend on the timer config.. - const TickCount ticks_until_next_event = - ((m_crtc_state.vertical_total - m_crtc_state.current_scanline) * m_crtc_state.horizontal_total) - - m_crtc_state.current_tick_in_scanline; -#endif + // figure out how many GPU ticks until the next vblank + const u32 lines_until_vblank = + (m_crtc_state.current_scanline >= m_crtc_state.vertical_display_end ? + (m_crtc_state.vertical_total - m_crtc_state.current_scanline + m_crtc_state.vertical_display_end) : + (m_crtc_state.vertical_display_end - m_crtc_state.current_scanline)); + const u32 ticks_until_vblank = + lines_until_vblank * m_crtc_state.horizontal_total - m_crtc_state.current_tick_in_scanline; + const u32 ticks_until_hblank = + (m_crtc_state.current_tick_in_scanline >= m_crtc_state.horizontal_display_end) ? + (m_crtc_state.horizontal_total - m_crtc_state.current_tick_in_scanline + m_crtc_state.horizontal_display_end) : + (m_crtc_state.horizontal_display_end - m_crtc_state.current_tick_in_scanline); - // convert to master clock, rounding up as we want to overshoot not undershoot - const TickCount system_ticks = (ticks_until_next_event * 7 + 10) / 11; - m_system->SetDowncount(system_ticks); + m_tick_event->Schedule(GPUTicksToSystemTicks(ticks_until_vblank)); + m_tick_event->SetPeriod(GPUTicksToSystemTicks(ticks_until_hblank)); } void GPU::Execute(TickCount ticks) @@ -393,13 +403,76 @@ void GPU::Execute(TickCount ticks) m_crtc_state.fractional_ticks = temp % 7; } - while (m_crtc_state.current_tick_in_scanline >= m_crtc_state.horizontal_total) + if (m_crtc_state.current_tick_in_scanline < m_crtc_state.horizontal_total) { - m_crtc_state.current_tick_in_scanline -= m_crtc_state.horizontal_total; - m_crtc_state.current_scanline++; + // short path when we execute <1 line.. this shouldn't occur often. + const bool old_hblank = m_crtc_state.in_hblank; + const bool new_hblank = m_crtc_state.current_tick_in_scanline < m_crtc_state.horizontal_display_start || + m_crtc_state.current_tick_in_scanline >= m_crtc_state.horizontal_display_end; + if (!old_hblank && new_hblank && m_timers->IsUsingExternalClock(HBLANK_TIMER_INDEX)) + m_timers->AddTicks(HBLANK_TIMER_INDEX, 1); + + UpdateSliceTicks(); + return; + } + + u32 lines_to_draw = m_crtc_state.current_tick_in_scanline / m_crtc_state.horizontal_total; + m_crtc_state.current_tick_in_scanline %= m_crtc_state.horizontal_total; +#if 0 + Log_WarningPrintf("Old line: %u, new line: %u, drawing %u", m_crtc_state.current_scanline, + m_crtc_state.current_scanline + lines_to_draw, lines_to_draw); +#endif + + const bool old_hblank = m_crtc_state.in_hblank; + const bool new_hblank = m_crtc_state.current_tick_in_scanline < m_crtc_state.horizontal_display_start || + m_crtc_state.current_tick_in_scanline >= m_crtc_state.horizontal_display_end; + m_crtc_state.in_hblank = new_hblank; + if (m_timers->IsUsingExternalClock(HBLANK_TIMER_INDEX)) + { + const u32 hblank_timer_ticks = BoolToUInt32(!old_hblank) + BoolToUInt32(new_hblank) + (lines_to_draw - 1); + m_timers->AddTicks(HBLANK_TIMER_INDEX, static_cast(hblank_timer_ticks)); + } + + while (lines_to_draw > 0) + { + const u32 lines_to_draw_this_loop = + std::min(lines_to_draw, m_crtc_state.vertical_total - m_crtc_state.current_scanline); + const u32 prev_scanline = m_crtc_state.current_scanline; + m_crtc_state.current_scanline += lines_to_draw_this_loop; + DebugAssert(m_crtc_state.current_scanline <= m_crtc_state.vertical_total); + lines_to_draw -= lines_to_draw_this_loop; + + // clear the vblank flag if the beam would pass through the display area + if (prev_scanline < m_crtc_state.vertical_display_start && + m_crtc_state.current_scanline >= m_crtc_state.vertical_display_end) + { + m_crtc_state.in_vblank = false; + } + + const bool new_vblank = m_crtc_state.current_scanline < m_crtc_state.vertical_display_start || + m_crtc_state.current_scanline >= m_crtc_state.vertical_display_end; + if (m_crtc_state.in_vblank != new_vblank) + { + m_crtc_state.in_vblank = new_vblank; + + if (new_vblank) + { + static u32 x = 0; + Log_DebugPrintf("Now in v-blank %u ticks %u hblanks", m_system->GetGlobalTickCounter() - x); + x = m_system->GetGlobalTickCounter(); + m_interrupt_controller->InterruptRequest(InterruptController::IRQ::VBLANK); + + // flush any pending draws and "scan out" the image + FlushRender(); + UpdateDisplay(); + m_system->IncrementFrameNumber(); + } + + m_timers->SetGate(HBLANK_TIMER_INDEX, new_vblank); + } // past the end of vblank? - if (m_crtc_state.current_scanline >= m_crtc_state.vertical_total) + if (m_crtc_state.current_scanline == m_crtc_state.vertical_total) { // start the new frame m_crtc_state.current_scanline = 0; @@ -415,47 +488,18 @@ void GPU::Execute(TickCount ticks) m_GPUSTAT.interlaced_field = false; } } - - const bool new_vblank = m_crtc_state.current_scanline < m_crtc_state.vertical_display_start || - m_crtc_state.current_scanline >= m_crtc_state.vertical_display_end; - if (m_crtc_state.in_vblank != new_vblank) - { - m_crtc_state.in_vblank = new_vblank; - - if (new_vblank) - { - Log_DebugPrintf("Now in v-blank"); - m_interrupt_controller->InterruptRequest(InterruptController::IRQ::VBLANK); - - // flush any pending draws and "scan out" the image - FlushRender(); - UpdateDisplay(); - m_system->IncrementFrameNumber(); - } - - m_timers->SetGate(HBLANK_TIMER_INDEX, new_vblank); - } - - // alternating even line bit in 240-line mode - if (m_GPUSTAT.In480iMode()) - { - m_GPUSTAT.drawing_even_line = - ConvertToBoolUnchecked((m_crtc_state.regs.Y + BoolToUInt32(!m_GPUSTAT.interlaced_field)) & u32(1)); - } - else - { - m_GPUSTAT.drawing_even_line = - ConvertToBoolUnchecked((m_crtc_state.regs.Y + m_crtc_state.current_scanline) & u32(1)); - } } - const bool new_hblank = m_crtc_state.current_tick_in_scanline < m_crtc_state.horizontal_display_start || - m_crtc_state.current_tick_in_scanline >= m_crtc_state.horizontal_display_end; - if (m_crtc_state.in_hblank != new_hblank) + // alternating even line bit in 240-line mode + if (m_GPUSTAT.In480iMode()) { - m_crtc_state.in_hblank = new_hblank; - if (new_hblank && m_timers->IsUsingExternalClock(HBLANK_TIMER_INDEX)) - m_timers->AddTicks(HBLANK_TIMER_INDEX, 1); + m_GPUSTAT.drawing_even_line = + ConvertToBoolUnchecked((m_crtc_state.regs.Y + BoolToUInt32(!m_GPUSTAT.interlaced_field)) & u32(1)); + } + else + { + m_GPUSTAT.drawing_even_line = + ConvertToBoolUnchecked((m_crtc_state.regs.Y + m_crtc_state.current_scanline) & u32(1)); } UpdateSliceTicks(); @@ -560,17 +604,29 @@ void GPU::WriteGP1(u32 value) case 0x06: // Set horizontal display range { - m_crtc_state.regs.horizontal_display_range = param & CRTCState::Regs::HORIZONTAL_DISPLAY_RANGE_MASK; - Log_DebugPrintf("Horizontal display range <- 0x%08X", m_crtc_state.regs.horizontal_display_range); - UpdateCRTCConfig(); + const u32 new_value = param & CRTCState::Regs::HORIZONTAL_DISPLAY_RANGE_MASK; + Log_DebugPrintf("Horizontal display range <- 0x%08X", new_value); + + if (m_crtc_state.regs.horizontal_display_range != new_value) + { + m_tick_event->InvokeEarly(true); + m_crtc_state.regs.horizontal_display_range = new_value; + UpdateCRTCConfig(); + } } break; case 0x07: // Set display start address { - m_crtc_state.regs.vertical_display_range = param & CRTCState::Regs::VERTICAL_DISPLAY_RANGE_MASK; - Log_DebugPrintf("Vertical display range <- 0x%08X", m_crtc_state.regs.vertical_display_range); - UpdateCRTCConfig(); + const u32 new_value = param & CRTCState::Regs::VERTICAL_DISPLAY_RANGE_MASK; + Log_DebugPrintf("Vertical display range <- 0x%08X", new_value); + + if (m_crtc_state.regs.vertical_display_range != new_value) + { + m_tick_event->InvokeEarly(true); + m_crtc_state.regs.vertical_display_range = new_value; + UpdateCRTCConfig(); + } } break; @@ -590,16 +646,22 @@ void GPU::WriteGP1(u32 value) }; const GP1_08h dm{param}; - m_GPUSTAT.horizontal_resolution_1 = dm.horizontal_resolution_1; - m_GPUSTAT.vertical_resolution = dm.vertical_resolution; - m_GPUSTAT.pal_mode = dm.pal_mode; - m_GPUSTAT.display_area_color_depth_24 = dm.display_area_color_depth; - m_GPUSTAT.vertical_interlace = dm.vertical_interlace; - m_GPUSTAT.horizontal_resolution_2 = dm.horizontal_resolution_2; - m_GPUSTAT.reverse_flag = dm.reverse_flag; - + GPUSTAT new_GPUSTAT{m_GPUSTAT.bits}; + new_GPUSTAT.horizontal_resolution_1 = dm.horizontal_resolution_1; + new_GPUSTAT.vertical_resolution = dm.vertical_resolution; + new_GPUSTAT.pal_mode = dm.pal_mode; + new_GPUSTAT.display_area_color_depth_24 = dm.display_area_color_depth; + new_GPUSTAT.vertical_interlace = dm.vertical_interlace; + new_GPUSTAT.horizontal_resolution_2 = dm.horizontal_resolution_2; + new_GPUSTAT.reverse_flag = dm.reverse_flag; Log_DebugPrintf("Set display mode <- 0x%08X", dm.bits); - UpdateCRTCConfig(); + + if (m_GPUSTAT.bits != new_GPUSTAT.bits) + { + m_tick_event->InvokeEarly(true); + m_GPUSTAT.bits = new_GPUSTAT.bits; + UpdateCRTCConfig(); + } } break; diff --git a/src/core/gpu.h b/src/core/gpu.h index da98dea36..23d4d1ade 100644 --- a/src/core/gpu.h +++ b/src/core/gpu.h @@ -15,6 +15,7 @@ class StateWrapper; class HostDisplay; class System; +class TimingEvent; class DMA; class InterruptController; class Timers; @@ -127,12 +128,12 @@ public: void DMARead(u32* words, u32 word_count); void DMAWrite(const u32* words, u32 word_count); + // Synchronizes the CRTC, updating the hblank timer. + void Synchronize(); + // Recompile shaders/recreate framebuffers when needed. virtual void UpdateSettings(); - // Ticks for hblank/vblank. - void Execute(TickCount ticks); - // gpu_hw_d3d11.cpp static std::unique_ptr CreateHardwareD3D11Renderer(); @@ -299,6 +300,9 @@ protected: // Updates dynamic bits in GPUSTAT (ready to send VRAM/ready to receive DMA) void UpdateGPUSTAT(); + // Ticks for hblank/vblank. + void Execute(TickCount ticks); + /// Returns true if scanout should be interlaced. bool IsDisplayInterlaced() const { return !m_force_progressive_scan && m_GPUSTAT.In480iMode(); } @@ -331,6 +335,8 @@ protected: InterruptController* m_interrupt_controller = nullptr; Timers* m_timers = nullptr; + std::unique_ptr m_tick_event; + // Pointer to VRAM, used for reads/writes. In the hardware backends, this is the shadow buffer. u16* m_vram_ptr = nullptr; diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp index 6ec6a33e4..e63d93ca9 100644 --- a/src/core/host_interface.cpp +++ b/src/core/host_interface.cpp @@ -360,7 +360,7 @@ void HostInterface::Throttle() const s64 sleep_time = static_cast(m_last_throttle_time - time); if (std::abs(sleep_time) >= MAX_VARIANCE_TIME) { -#ifdef Y_BUILD_CONFIG_RELEASE +#ifndef _DEBUG // Don't display the slow messages in debug, it'll always be slow... // Limit how often the messages are displayed. if (m_speed_lost_time_timestamp.GetTimeSeconds() >= 1.0f) @@ -437,6 +437,9 @@ void HostInterface::UpdateSpeedLimiterState() (audio_sync_enabled && video_sync_enabled) ? " and video" : (video_sync_enabled ? "video" : "")); m_audio_stream->SetSync(audio_sync_enabled); + if (audio_sync_enabled) + m_audio_stream->EmptyBuffers(); + m_display->SetVSync(video_sync_enabled); m_throttle_timer.Reset(); m_last_throttle_time = 0; diff --git a/src/core/host_interface.h b/src/core/host_interface.h index a155e8e5c..d15346ba5 100644 --- a/src/core/host_interface.h +++ b/src/core/host_interface.h @@ -68,6 +68,9 @@ public: /// Returns a path relative to the user directory. std::string GetUserDirectoryRelativePath(const char* format, ...) const; + /// Throttles the system, i.e. sleeps until it's time to execute the next frame. + void Throttle(); + protected: using ThrottleClock = std::chrono::steady_clock; @@ -131,9 +134,6 @@ protected: void RunFrame(); - /// Throttles the system, i.e. sleeps until it's time to execute the next frame. - void Throttle(); - void UpdateSpeedLimiterState(); void DrawFPSWindow(); diff --git a/src/core/mdec.cpp b/src/core/mdec.cpp index 3dc2b3d79..ad96dd5c4 100644 --- a/src/core/mdec.cpp +++ b/src/core/mdec.cpp @@ -15,6 +15,8 @@ void MDEC::Initialize(System* system, DMA* dma) { m_system = system; m_dma = dma; + m_block_copy_out_event = system->CreateTimingEvent("MDEC Block Copy Out", TICKS_PER_BLOCK, TICKS_PER_BLOCK, + std::bind(&MDEC::CopyOutBlock, this), false); } void MDEC::Reset() @@ -39,8 +41,11 @@ bool MDEC::DoState(StateWrapper& sw) sw.Do(&m_current_coefficient); sw.Do(&m_current_q_scale); sw.Do(&m_block_rgb); - sw.Do(&m_block_copy_out_ticks); - sw.Do(&m_block_copy_out_pending); + + bool block_copy_out_pending = HasPendingBlockCopyOut(); + sw.Do(&block_copy_out_pending); + if (sw.IsReading()) + m_block_copy_out_event->SetState(block_copy_out_pending); return !sw.HasError(); } @@ -131,6 +136,11 @@ void MDEC::DMAWrite(const u32* words, u32 word_count) } while (word_count > 0); } +bool MDEC::HasPendingBlockCopyOut() const +{ + return m_block_copy_out_event->IsActive(); +} + void MDEC::SoftReset() { m_status.bits = 0; @@ -143,8 +153,7 @@ void MDEC::SoftReset() m_current_block = 0; m_current_coefficient = 64; m_current_q_scale = 0; - m_block_copy_out_ticks = TICKS_PER_BLOCK; - m_block_copy_out_pending = false; + m_block_copy_out_event->Deactivate(); UpdateStatus(); } @@ -173,11 +182,10 @@ u32 MDEC::ReadDataRegister() if (m_data_out_fifo.IsEmpty()) { // Stall the CPU until we're done processing. - if (m_block_copy_out_pending) + if (HasPendingBlockCopyOut()) { Log_DevPrint("MDEC data out FIFO empty on read - stalling CPU"); - m_system->StallCPU(m_block_copy_out_ticks); - Execute(m_block_copy_out_ticks); + m_system->StallCPU(m_block_copy_out_event->GetTicksUntilNextExecution()); } else { @@ -205,27 +213,9 @@ void MDEC::WriteCommandRegister(u32 value) ExecutePendingCommand(); } -void MDEC::Execute(TickCount ticks) -{ - if (!m_block_copy_out_pending) - return; - - m_block_copy_out_ticks -= ticks; - if (m_block_copy_out_ticks <= 0) - { - DebugAssert(m_command == Command::DecodeMacroblock); - CopyOutBlock(); - - if (m_remaining_halfwords == 0) - EndCommand(); - else - ExecutePendingCommand(); - } -} - void MDEC::ExecutePendingCommand() { - if (m_block_copy_out_pending) + if (HasPendingBlockCopyOut()) { // can't do anything while waiting UpdateStatus(); @@ -234,7 +224,7 @@ void MDEC::ExecutePendingCommand() while (!m_data_in_fifo.IsEmpty()) { - DebugAssert(!m_block_copy_out_pending); + DebugAssert(!HasPendingBlockCopyOut()); if (m_command == Command::None) { @@ -417,20 +407,16 @@ bool MDEC::DecodeColoredMacroblock() void MDEC::ScheduleBlockCopyOut(TickCount ticks) { - DebugAssert(!m_block_copy_out_pending); + DebugAssert(!HasPendingBlockCopyOut()); Log_DebugPrintf("Scheduling block copy out in %d ticks", ticks); - m_system->Synchronize(); - m_block_copy_out_pending = true; - m_block_copy_out_ticks = ticks; - m_system->SetDowncount(ticks); + m_block_copy_out_event->Schedule(TICKS_PER_BLOCK); } void MDEC::CopyOutBlock() { - DebugAssert(m_block_copy_out_pending); - m_block_copy_out_pending = false; - m_block_copy_out_ticks = 0; + DebugAssert(m_command == Command::DecodeMacroblock); + m_block_copy_out_event->Deactivate(); Log_DebugPrintf("Copying out block"); @@ -535,13 +521,9 @@ void MDEC::CopyOutBlock() // if we've copied out all blocks, command is complete if (m_remaining_halfwords == 0) - { - DebugAssert(m_command == Command::DecodeMacroblock); - m_command = Command::None; - m_current_block = 0; - m_current_coefficient = 64; - m_current_q_scale = 0; - } + EndCommand(); + else + ExecutePendingCommand(); } static constexpr std::array zigzag = {{0, 1, 5, 6, 14, 15, 27, 28, 2, 4, 7, 13, 16, 26, 29, 42, diff --git a/src/core/mdec.h b/src/core/mdec.h index c6cb73e68..d1e4b4a9a 100644 --- a/src/core/mdec.h +++ b/src/core/mdec.h @@ -3,10 +3,12 @@ #include "common/fifo_queue.h" #include "types.h" #include +#include class StateWrapper; class System; +class TimingEvent; class DMA; class MDEC @@ -26,8 +28,6 @@ public: void DMARead(u32* words, u32 word_count); void DMAWrite(const u32* words, u32 word_count); - void Execute(TickCount ticks); - void DrawDebugStateWindow(); private: @@ -88,6 +88,7 @@ private: }; bool HasPendingCommand() const { return m_command != Command::None; } + bool HasPendingBlockCopyOut() const; void SoftReset(); void UpdateStatus(); @@ -138,8 +139,7 @@ private: u16 m_current_q_scale = 0; std::array m_block_rgb{}; - TickCount m_block_copy_out_ticks = TICKS_PER_BLOCK; - bool m_block_copy_out_pending = false; + std::unique_ptr m_block_copy_out_event; u32 m_total_blocks_decoded = 0; }; diff --git a/src/core/pad.cpp b/src/core/pad.cpp index 995f1754c..19fe48904 100644 --- a/src/core/pad.cpp +++ b/src/core/pad.cpp @@ -16,6 +16,8 @@ void Pad::Initialize(System* system, InterruptController* interrupt_controller) { m_system = system; m_interrupt_controller = interrupt_controller; + m_transfer_event = system->CreateTimingEvent("Pad Serial Transfer", 1, 1, + std::bind(&Pad::TransferEvent, this, std::placeholders::_2), false); } void Pad::Reset() @@ -81,7 +83,6 @@ bool Pad::DoState(StateWrapper& sw) } sw.Do(&m_state); - sw.Do(&m_ticks_remaining); sw.Do(&m_JOY_CTRL.bits); sw.Do(&m_JOY_STAT.bits); sw.Do(&m_JOY_MODE.bits); @@ -91,6 +92,9 @@ bool Pad::DoState(StateWrapper& sw) sw.Do(&m_receive_buffer_full); sw.Do(&m_transmit_buffer_full); + if (sw.IsReading() && IsTransmitting()) + m_transfer_event->Activate(); + return !sw.HasError(); } @@ -213,24 +217,6 @@ void Pad::WriteRegister(u32 offset, u32 value) } } -void Pad::Execute(TickCount ticks) -{ - if (m_state == State::Idle) - return; - - m_ticks_remaining -= ticks; - if (m_ticks_remaining > 0) - { - m_system->SetDowncount(m_ticks_remaining); - return; - } - - if (m_state == State::Transmitting) - DoTransfer(); - else - DoACK(); -} - void Pad::SoftReset() { if (IsTransmitting()) @@ -254,6 +240,14 @@ void Pad::UpdateJoyStat() m_JOY_STAT.TXRDY = !m_transmit_buffer_full; } +void Pad::TransferEvent(TickCount ticks_late) +{ + if (m_state == State::Transmitting) + DoTransfer(ticks_late); + else + DoACK(); +} + void Pad::BeginTransfer() { DebugAssert(m_state == State::Idle && CanTransfer()); @@ -278,13 +272,11 @@ void Pad::BeginTransfer() // test in (7) will fail, and it won't send any more data. So, the transfer/interrupt must be delayed // until after (4) and (5) have been completed. - m_system->Synchronize(); m_state = State::Transmitting; - m_ticks_remaining = GetTransferTicks(); - m_system->SetDowncount(m_ticks_remaining); + m_transfer_event->SetPeriodAndSchedule(GetTransferTicks()); } -void Pad::DoTransfer() +void Pad::DoTransfer(TickCount ticks_late) { Log_DebugPrintf("Transferring slot %d", m_JOY_CTRL.SLOT.GetValue()); @@ -361,11 +353,10 @@ void Pad::DoTransfer() const TickCount ack_timer = GetACKTicks(); Log_DebugPrintf("Delaying ACK for %d ticks", ack_timer); m_state = State::WaitingForACK; - m_ticks_remaining += ack_timer; - if (m_ticks_remaining <= 0) + if (ticks_late >= ack_timer) DoACK(); else - m_system->SetDowncount(m_ticks_remaining); + m_transfer_event->SetPeriodAndSchedule(ack_timer - ticks_late); } UpdateJoyStat(); @@ -395,7 +386,7 @@ void Pad::EndTransfer() Log_DebugPrintf("Ending transfer"); m_state = State::Idle; - m_ticks_remaining = 0; + m_transfer_event->Deactivate(); } void Pad::ResetDeviceTransferState() diff --git a/src/core/pad.h b/src/core/pad.h index 69ef2bde6..4e4492599 100644 --- a/src/core/pad.h +++ b/src/core/pad.h @@ -8,6 +8,7 @@ class StateWrapper; class System; +class TimingEvent; class InterruptController; class Controller; class MemoryCard; @@ -31,8 +32,6 @@ public: u32 ReadRegister(u32 offset); void WriteRegister(u32 offset, u32 value); - void Execute(TickCount ticks); - private: static constexpr u32 NUM_SLOTS = 2; @@ -97,8 +96,9 @@ private: void SoftReset(); void UpdateJoyStat(); + void TransferEvent(TickCount ticks_late); void BeginTransfer(); - void DoTransfer(); + void DoTransfer(TickCount ticks_late); void DoACK(); void EndTransfer(); void ResetDeviceTransferState(); @@ -109,8 +109,8 @@ private: std::array, NUM_SLOTS> m_controllers; std::array, NUM_SLOTS> m_memory_cards; + std::unique_ptr m_transfer_event; State m_state = State::Idle; - TickCount m_ticks_remaining = 0; JOY_CTRL m_JOY_CTRL = {}; JOY_STAT m_JOY_STAT = {}; diff --git a/src/core/spu.cpp b/src/core/spu.cpp index a60d1441d..dc8e45619 100644 --- a/src/core/spu.cpp +++ b/src/core/spu.cpp @@ -1,6 +1,6 @@ #include "spu.h" -#include "common/log.h" #include "common/audio_stream.h" +#include "common/log.h" #include "common/state_wrapper.h" #include "dma.h" #include "host_interface.h" @@ -24,6 +24,8 @@ void SPU::Initialize(System* system, DMA* dma, InterruptController* interrupt_co m_system = system; m_dma = dma; m_interrupt_controller = interrupt_controller; + m_sample_event = m_system->CreateTimingEvent("SPU Sample", SYSCLK_TICKS_PER_SPU_TICK, SYSCLK_TICKS_PER_SPU_TICK, + std::bind(&SPU::Execute, this, std::placeholders::_1), false); } void SPU::Reset() @@ -65,6 +67,7 @@ void SPU::Reset() } m_ram.fill(0); + UpdateEventInterval(); } bool SPU::DoState(StateWrapper& sw) @@ -108,7 +111,10 @@ bool SPU::DoState(StateWrapper& sw) sw.DoBytes(m_ram.data(), RAM_SIZE); if (sw.IsReading()) + { m_system->GetHostInterface()->GetAudioStream()->EmptyBuffers(); + UpdateEventInterval(); + } return !sw.HasError(); } @@ -178,6 +184,7 @@ u16 SPU::ReadRegister(u32 offset) case 0x1F801DAE - SPU_BASE: // Log_DebugPrintf("SPU status register -> 0x%04X", ZeroExtend32(m_SPUCNT.bits)); + m_sample_event->InvokeEarly(); return m_SPUSTAT.bits; case 0x1F801DB0 - SPU_BASE: @@ -205,7 +212,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801D80 - SPU_BASE: { Log_DebugPrintf("SPU main volume left <- 0x%04X", ZeroExtend32(value)); - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_main_volume_left.bits = value; return; } @@ -213,7 +220,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801D82 - SPU_BASE: { Log_DebugPrintf("SPU main volume right <- 0x%04X", ZeroExtend32(value)); - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_main_volume_right.bits = value; return; } @@ -221,7 +228,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801D88 - SPU_BASE: { Log_DebugPrintf("SPU key on low <- 0x%04X", ZeroExtend32(value)); - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_key_on_register = (m_key_on_register & 0xFFFF0000) | ZeroExtend32(value); u16 bits = value; @@ -240,7 +247,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801D8A - SPU_BASE: { Log_DebugPrintf("SPU key on high <- 0x%04X", ZeroExtend32(value)); - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_key_on_register = (m_key_on_register & 0x0000FFFF) | (ZeroExtend32(value) << 16); u16 bits = value; @@ -259,7 +266,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801D8C - SPU_BASE: { Log_DebugPrintf("SPU key off low <- 0x%04X", ZeroExtend32(value)); - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_key_on_register = (m_key_on_register & 0xFFFF0000) | ZeroExtend32(value); u16 bits = value; @@ -278,7 +285,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801D8E - SPU_BASE: { Log_DebugPrintf("SPU key off high <- 0x%04X", ZeroExtend32(value)); - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_key_on_register = (m_key_on_register & 0x0000FFFF) | (ZeroExtend32(value) << 16); u16 bits = value; @@ -296,7 +303,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801D90 - SPU_BASE: { - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_pitch_modulation_enable_register = (m_pitch_modulation_enable_register & 0xFFFF0000) | ZeroExtend32(value); Log_DebugPrintf("SPU pitch modulation enable register <- 0x%08X", m_pitch_modulation_enable_register); } @@ -304,7 +311,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801D92 - SPU_BASE: { - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_pitch_modulation_enable_register = (m_pitch_modulation_enable_register & 0x0000FFFF) | (ZeroExtend32(value) << 16); Log_DebugPrintf("SPU pitch modulation enable register <- 0x%08X", m_pitch_modulation_enable_register); @@ -314,7 +321,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801D94 - SPU_BASE: { Log_DebugPrintf("SPU noise mode register <- 0x%04X", ZeroExtend32(value)); - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_noise_mode_register = (m_noise_mode_register & 0xFFFF0000) | ZeroExtend32(value); } break; @@ -322,7 +329,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801D96 - SPU_BASE: { Log_DebugPrintf("SPU noise mode register <- 0x%04X", ZeroExtend32(value)); - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_noise_mode_register = (m_noise_mode_register & 0x0000FFFF) | (ZeroExtend32(value) << 16); } break; @@ -330,7 +337,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801D98 - SPU_BASE: { Log_DebugPrintf("SPU reverb on register <- 0x%04X", ZeroExtend32(value)); - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_reverb_on_register = (m_reverb_on_register & 0xFFFF0000) | ZeroExtend32(value); } break; @@ -338,7 +345,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801D9A - SPU_BASE: { Log_DebugPrintf("SPU reverb off register <- 0x%04X", ZeroExtend32(value)); - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_reverb_on_register = (m_reverb_on_register & 0x0000FFFF) | (ZeroExtend32(value) << 16); } break; @@ -346,6 +353,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801DA4 - SPU_BASE: { Log_DebugPrintf("SPU IRQ address register <- 0x%04X", ZeroExtend32(value)); + m_sample_event->InvokeEarly(); m_irq_address = value; return; } @@ -369,6 +377,8 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801DAA - SPU_BASE: { Log_DebugPrintf("SPU control register <- 0x%04X", ZeroExtend32(value)); + m_sample_event->InvokeEarly(true); + m_SPUCNT.bits = value; m_SPUSTAT.mode = m_SPUCNT.mode.GetValue(); m_SPUSTAT.dma_read_write_request = m_SPUCNT.ram_transfer_mode >= RAMTransferMode::DMAWrite; @@ -377,6 +387,7 @@ void SPU::WriteRegister(u32 offset, u16 value) m_SPUSTAT.irq9_flag = false; UpdateDMARequest(); + UpdateEventInterval(); return; } @@ -390,7 +401,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801DB0 - SPU_BASE: { Log_DebugPrintf("SPU left cd audio register <- 0x%04X", ZeroExtend32(value)); - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_cd_audio_volume_left = value; } break; @@ -398,7 +409,7 @@ void SPU::WriteRegister(u32 offset, u16 value) case 0x1F801DB2 - SPU_BASE: { Log_DebugPrintf("SPU right cd audio register <- 0x%04X", ZeroExtend32(value)); - m_system->Synchronize(); + m_sample_event->InvokeEarly(); m_cd_audio_volume_right = value; } break; @@ -424,6 +435,12 @@ u16 SPU::ReadVoiceRegister(u32 offset) const u32 voice_index = (offset / 0x10); //((offset >> 4) & 0x1F); Assert(voice_index < 24); + if (reg_index >= 6) + { + // adsr volume needs to be updated when reading + m_sample_event->InvokeEarly(); + } + return m_voices[voice_index].regs.index[reg_index]; } @@ -436,7 +453,7 @@ void SPU::WriteVoiceRegister(u32 offset, u16 value) Voice& voice = m_voices[voice_index]; if (voice.IsOn()) - m_system->Synchronize(); + m_sample_event->InvokeEarly(); switch (reg_index) { @@ -606,13 +623,103 @@ void SPU::IncrementCaptureBufferPosition() void SPU::Execute(TickCount ticks) { - TickCount num_samples = (ticks + m_ticks_carry) / SYSCLK_TICKS_PER_SPU_TICK; + DebugAssert(m_SPUCNT.enable || m_SPUCNT.cd_audio_enable); + + u32 remaining_frames = static_cast((ticks + m_ticks_carry) / SYSCLK_TICKS_PER_SPU_TICK); m_ticks_carry = (ticks + m_ticks_carry) % SYSCLK_TICKS_PER_SPU_TICK; - if (num_samples == 0 || (!m_SPUCNT.enable && !m_SPUCNT.cd_audio_enable)) + + while (remaining_frames > 0) + { + AudioStream* const output_stream = m_system->GetHostInterface()->GetAudioStream(); + s16* output_frame; + u32 output_frame_space; + output_stream->BeginWrite(&output_frame, &output_frame_space); + + const u32 frames_in_this_batch = std::min(remaining_frames, output_frame_space); + for (u32 i = 0; i < frames_in_this_batch; i++) + { + s32 left_sum = 0; + s32 right_sum = 0; + if (m_SPUCNT.enable) + { + for (u32 voice = 0; voice < NUM_VOICES; voice++) + { + const auto [left, right] = SampleVoice(voice); + left_sum += left; + right_sum += right; + } + + if (!m_SPUCNT.mute_n) + { + left_sum = 0; + right_sum = 0; + } + } + + // Mix in CD audio. + s16 cd_audio_left; + s16 cd_audio_right; + if (!m_cd_audio_buffer.IsEmpty()) + { + cd_audio_left = m_cd_audio_buffer.Pop(); + cd_audio_right = m_cd_audio_buffer.Pop(); + if (m_SPUCNT.cd_audio_enable) + { + left_sum += ApplyVolume(s32(cd_audio_left), m_cd_audio_volume_left); + right_sum += ApplyVolume(s32(cd_audio_right), m_cd_audio_volume_right); + } + } + else + { + cd_audio_left = 0; + cd_audio_right = 0; + } + + // Apply main volume before clamping. + *(output_frame++) = Clamp16(ApplyVolume(left_sum, m_main_volume_left.GetVolume())); + *(output_frame++) = Clamp16(ApplyVolume(right_sum, m_main_volume_right.GetVolume())); + + // Write to capture buffers. + WriteToCaptureBuffer(0, cd_audio_left); + WriteToCaptureBuffer(1, cd_audio_right); + WriteToCaptureBuffer(2, Clamp16(m_voices[1].last_amplitude)); + WriteToCaptureBuffer(3, Clamp16(m_voices[3].last_amplitude)); + IncrementCaptureBufferPosition(); + } + + output_stream->EndWrite(frames_in_this_batch); + remaining_frames -= frames_in_this_batch; + } +} + +void SPU::UpdateEventInterval() +{ + if (!m_SPUCNT.enable && !m_SPUCNT.cd_audio_enable) + { + m_sample_event->Deactivate(); + return; + } + + // Don't generate more than the audio buffer since in a single slice, otherwise we'll both overflow the buffers when + // we do write it, and the audio thread will underflow since it won't have enough data it the game isn't messing with + // the SPU state. + const u32 max_slice_frames = m_system->GetHostInterface()->GetAudioStream()->GetBufferSize(); + + // TODO: Make this predict how long until the interrupt will be hit instead... + const u32 interval = m_SPUCNT.irq9_enable ? 1 : max_slice_frames; + const TickCount interval_ticks = static_cast(interval) * SYSCLK_TICKS_PER_SPU_TICK; + if (m_sample_event->IsActive() && m_sample_event->GetInterval() == interval_ticks) return; - for (TickCount i = 0; i < num_samples; i++) - GenerateSample(); + // Ensure all pending ticks have been executed, since we won't get them back after rescheduling. + m_sample_event->InvokeEarly(true); + m_sample_event->SetInterval(interval_ticks); + m_sample_event->Schedule(interval_ticks - m_ticks_carry); +} + +void SPU::GeneratePendingSamples() +{ + m_sample_event->InvokeEarly(); } void SPU::Voice::KeyOn() @@ -952,80 +1059,22 @@ std::tuple SPU::SampleVoice(u32 voice_index) return std::make_tuple(left, right); } -void SPU::EnsureCDAudioSpace(u32 num_samples) +void SPU::EnsureCDAudioSpace(u32 remaining_frames) { - if (m_cd_audio_buffer.GetSpace() < (num_samples * 2)) + if (m_cd_audio_buffer.IsEmpty()) { - Log_WarningPrintf("SPU CD Audio buffer overflow - writing %u samples with %u samples space", num_samples, + // we want the audio to start playing at the right point, not a few cycles early, otherwise this'll cause sync issues. + m_sample_event->InvokeEarly(); + } + + if (m_cd_audio_buffer.GetSpace() < (remaining_frames * 2)) + { + Log_WarningPrintf("SPU CD Audio buffer overflow - writing %u samples with %u samples space", remaining_frames, m_cd_audio_buffer.GetSpace() / 2); - m_cd_audio_buffer.Remove((num_samples * 2) - m_cd_audio_buffer.GetSpace()); + m_cd_audio_buffer.Remove((remaining_frames * 2) - m_cd_audio_buffer.GetSpace()); } } -void SPU::GenerateSample() -{ - s32 left_sum = 0; - s32 right_sum = 0; - if (m_SPUCNT.enable) - { - for (u32 i = 0; i < NUM_VOICES; i++) - { - const auto [left, right] = SampleVoice(i); - left_sum += left; - right_sum += right; - } - - if (!m_SPUCNT.mute_n) - { - left_sum = 0; - right_sum = 0; - } - } - - // Mix in CD audio. - s16 cd_audio_left; - s16 cd_audio_right; - if (!m_cd_audio_buffer.IsEmpty()) - { - cd_audio_left = m_cd_audio_buffer.Pop(); - cd_audio_right = m_cd_audio_buffer.Pop(); - if (m_SPUCNT.cd_audio_enable) - { - left_sum += ApplyVolume(s32(cd_audio_left), m_cd_audio_volume_left); - right_sum += ApplyVolume(s32(cd_audio_right), m_cd_audio_volume_right); - } - } - else - { - cd_audio_left = 0; - cd_audio_right = 0; - } - - // Apply main volume before clamping. - std::array out_samples; - out_samples[0] = Clamp16(ApplyVolume(left_sum, m_main_volume_left.GetVolume())); - out_samples[1] = Clamp16(ApplyVolume(right_sum, m_main_volume_right.GetVolume())); - m_system->GetHostInterface()->GetAudioStream()->WriteSamples(out_samples.data(), 1); - - // Write to capture buffers. - WriteToCaptureBuffer(0, cd_audio_left); - WriteToCaptureBuffer(1, cd_audio_right); - WriteToCaptureBuffer(2, Clamp16(m_voices[1].last_amplitude)); - WriteToCaptureBuffer(3, Clamp16(m_voices[3].last_amplitude)); - IncrementCaptureBufferPosition(); - -#if 0 - static FILE* fp = nullptr; - if (!fp) - fp = std::fopen("D:\\spu.raw", "wb"); - if (fp) - { - std::fwrite(out_samples.data(), sizeof(AudioStream::SampleType), 2, fp); - std::fflush(fp); - } -#endif -} - void SPU::DrawDebugStateWindow() { static const ImVec4 active_color{1.0f, 1.0f, 1.0f, 1.0f}; diff --git a/src/core/spu.h b/src/core/spu.h index c40a99a24..4eb5130f5 100644 --- a/src/core/spu.h +++ b/src/core/spu.h @@ -3,11 +3,12 @@ #include "common/fifo_queue.h" #include "types.h" #include +#include -class AudioStream; class StateWrapper; class System; +class TimingEvent; class DMA; class InterruptController; @@ -27,8 +28,6 @@ public: void DMARead(u32* words, u32 word_count); void DMAWrite(const u32* words, u32 word_count); - void Execute(TickCount ticks); - // Render statistics debug window. void DrawDebugStateWindow(); @@ -40,6 +39,9 @@ public: } void EnsureCDAudioSpace(u32 num_samples); + // Executes the SPU, generating any pending samples. + void GeneratePendingSamples(); + private: static constexpr u32 RAM_SIZE = 512 * 1024; static constexpr u32 RAM_MASK = RAM_SIZE - 1; @@ -282,11 +284,13 @@ private: void ReadADPCMBlock(u16 address, ADPCMBlock* block); std::tuple SampleVoice(u32 voice_index); - void GenerateSample(); + void Execute(TickCount ticks); + void UpdateEventInterval(); System* m_system = nullptr; DMA* m_dma = nullptr; InterruptController* m_interrupt_controller = nullptr; + std::unique_ptr m_sample_event = nullptr; SPUCNT m_SPUCNT = {}; SPUSTAT m_SPUSTAT = {}; diff --git a/src/core/system.cpp b/src/core/system.cpp index eb769c25d..6abe99017 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -40,7 +40,11 @@ System::System(HostInterface* host_interface) : m_host_interface(host_interface) m_cpu_execution_mode = host_interface->m_settings.cpu_execution_mode; } -System::~System() = default; +System::~System() +{ + // we have to explicitly destroy components because they can deregister events + DestroyComponents(); +} std::unique_ptr System::Create(HostInterface* host_interface) { @@ -56,7 +60,7 @@ bool System::RecreateGPU(GPURenderer renderer) // save current state std::unique_ptr state_stream = ByteStream_CreateGrowableMemoryStream(); StateWrapper sw(state_stream.get(), StateWrapper::Mode::Write); - const bool state_valid = m_gpu->DoState(sw); + const bool state_valid = m_gpu->DoState(sw) && DoEventsState(sw); if (!state_valid) Log_ErrorPrintf("Failed to save old GPU state when switching renderers"); @@ -73,6 +77,7 @@ bool System::RecreateGPU(GPURenderer renderer) state_stream->SeekAbsolute(0); sw.SetMode(StateWrapper::Mode::Read); m_gpu->DoState(sw); + DoEventsState(sw); } return true; @@ -188,11 +193,26 @@ void System::InitializeComponents() m_cdrom->Initialize(this, m_dma.get(), m_interrupt_controller.get(), m_spu.get()); m_pad->Initialize(this, m_interrupt_controller.get()); - m_timers->Initialize(this, m_interrupt_controller.get()); + m_timers->Initialize(this, m_interrupt_controller.get(), m_gpu.get()); m_spu->Initialize(this, m_dma.get(), m_interrupt_controller.get()); m_mdec->Initialize(this, m_dma.get()); } +void System::DestroyComponents() +{ + m_mdec.reset(); + m_spu.reset(); + m_timers.reset(); + m_pad.reset(); + m_cdrom.reset(); + m_gpu.reset(); + m_interrupt_controller.reset(); + m_dma.reset(); + m_bus.reset(); + m_cpu_code_cache.reset(); + m_cpu.reset(); +} + bool System::CreateGPU(GPURenderer renderer) { switch (renderer) @@ -230,6 +250,7 @@ bool System::CreateGPU(GPURenderer renderer) m_bus->SetGPU(m_gpu.get()); m_dma->SetGPU(m_gpu.get()); + m_timers->SetGPU(m_gpu.get()); return true; } @@ -298,6 +319,9 @@ bool System::DoState(StateWrapper& sw) if (!sw.DoMarker("SIO") || !m_sio->DoState(sw)) return false; + if (!sw.DoMarker("Events") || !DoEventsState(sw)) + return false; + return !sw.HasError(); } @@ -318,6 +342,7 @@ void System::Reset() m_frame_number = 1; m_internal_frame_number = 0; m_global_tick_counter = 0; + m_last_event_run_time = 0; } bool System::LoadState(ByteStream* state) @@ -335,23 +360,28 @@ bool System::SaveState(ByteStream* state) void System::RunFrame() { // Duplicated to avoid branch in the while loop, as the downcount can be quite low at times. - u32 current_frame_number = m_frame_number; + m_frame_done = false; if (m_cpu_execution_mode == CPUExecutionMode::Interpreter) { - while (current_frame_number == m_frame_number) + do { + UpdateCPUDowncount(); m_cpu->Execute(); - Synchronize(); - } + RunEvents(); + } while (!m_frame_done); } else { - while (current_frame_number == m_frame_number) + do { + UpdateCPUDowncount(); m_cpu_code_cache->Execute(); - Synchronize(); - } + RunEvents(); + } while (!m_frame_done); } + + // Generate any pending samples from the SPU before sleeping, this way we reduce the chances of underruns. + m_spu->GeneratePendingSamples(); } bool System::LoadEXE(const char* filename, std::vector& bios_image) @@ -438,34 +468,11 @@ bool System::SetExpansionROM(const char* filename) return true; } -void System::Synchronize() -{ - const TickCount pending_ticks = m_cpu->GetPendingTicks(); - if (pending_ticks == 0) - return; - - m_cpu->ResetPendingTicks(); - m_cpu->ResetDowncount(); - - m_global_tick_counter += static_cast(pending_ticks); - - m_gpu->Execute(pending_ticks); - m_timers->Execute(pending_ticks); - m_cdrom->Execute(pending_ticks); - m_pad->Execute(pending_ticks); - m_spu->Execute(pending_ticks); - m_mdec->Execute(pending_ticks); - m_dma->Execute(pending_ticks); -} - -void System::SetDowncount(TickCount downcount) -{ - m_cpu->SetDowncount(downcount); -} - void System::StallCPU(TickCount ticks) { m_cpu->AddPendingTicks(ticks); + if (m_cpu->GetPendingTicks() >= m_cpu->GetDowncount() && !m_running_events) + RunEvents(); } Controller* System::GetController(u32 slot) const @@ -530,6 +537,202 @@ void System::RemoveMedia() m_cdrom->RemoveMedia(); } +std::unique_ptr System::CreateTimingEvent(std::string name, TickCount period, TickCount interval, + TimingEventCallback callback, bool activate) +{ + std::unique_ptr event = + std::make_unique(this, std::move(name), period, interval, std::move(callback)); + if (activate) + event->Activate(); + + return event; +} + +static bool CompareEvents(const TimingEvent* lhs, const TimingEvent* rhs) +{ + return lhs->GetDowncount() > rhs->GetDowncount(); +} + +void System::AddActiveEvent(TimingEvent* event) +{ + m_events.push_back(event); + if (!m_running_events) + { + std::push_heap(m_events.begin(), m_events.end(), CompareEvents); + if (!m_frame_done) + UpdateCPUDowncount(); + } + else + { + m_events_need_sorting = true; + } +} + +void System::RemoveActiveEvent(TimingEvent* event) +{ + auto iter = std::find_if(m_events.begin(), m_events.end(), [event](const auto& it) { return event == it; }); + if (iter == m_events.end()) + { + Panic("Attempt to remove inactive event"); + return; + } + + m_events.erase(iter); + if (!m_running_events) + { + std::make_heap(m_events.begin(), m_events.end(), CompareEvents); + if (!m_events.empty() && !m_frame_done) + UpdateCPUDowncount(); + } + else + { + m_events_need_sorting = true; + } +} + +void System::SortEvents() +{ + if (!m_running_events) + { + std::make_heap(m_events.begin(), m_events.end(), CompareEvents); + if (!m_frame_done) + UpdateCPUDowncount(); + } + else + { + m_events_need_sorting = true; + } +} + +void System::RunEvents() +{ + DebugAssert(!m_running_events && !m_events.empty()); + + const TickCount pending_ticks = m_cpu->GetPendingTicks(); + m_global_tick_counter += static_cast(pending_ticks); + m_cpu->ResetPendingTicks(); + + TickCount time = static_cast(m_global_tick_counter - m_last_event_run_time); + m_running_events = true; + m_last_event_run_time = m_global_tick_counter; + + // Apply downcount to all events. + // This will result in a negative downcount for those events which are late. + for (TimingEvent* evt : m_events) + { + evt->m_downcount -= time; + evt->m_time_since_last_run += time; + } + + // Now we can actually run the callbacks. + while (m_events.front()->GetDowncount() <= 0) + { + TimingEvent* evt = m_events.front(); + const TickCount ticks_late = -evt->m_downcount; + std::pop_heap(m_events.begin(), m_events.end(), CompareEvents); + + // Factor late time into the time for the next invocation. + const TickCount ticks_to_execute = evt->m_time_since_last_run; + evt->m_downcount += evt->m_interval; + evt->m_time_since_last_run = 0; + + // The cycles_late is only an indicator, it doesn't modify the cycles to execute. + evt->m_callback(ticks_to_execute, ticks_late); + + // Place it in the appropriate position in the queue. + if (m_events_need_sorting) + { + // Another event may have been changed by this event, or the interval/downcount changed. + std::make_heap(m_events.begin(), m_events.end(), CompareEvents); + m_events_need_sorting = false; + } + else + { + // Keep the event list in a heap. The event we just serviced will be in the last place, + // so we can use push_here instead of make_heap, which should be faster. + std::push_heap(m_events.begin(), m_events.end(), CompareEvents); + } + } + + m_running_events = false; + m_cpu->SetDowncount(m_events.front()->GetDowncount()); +} + +void System::UpdateCPUDowncount() +{ + m_cpu->SetDowncount(m_events[0]->GetDowncount()); +} + +bool System::DoEventsState(StateWrapper& sw) +{ + if (sw.IsReading()) + { + // Load timestamps for the clock events. + // Any oneshot events should be recreated by the load state method, so we can fix up their times here. + u32 event_count = 0; + sw.Do(&event_count); + + for (u32 i = 0; i < event_count; i++) + { + std::string event_name; + TickCount downcount, time_since_last_run, period, interval; + sw.Do(&event_name); + sw.Do(&downcount); + sw.Do(&time_since_last_run); + sw.Do(&period); + sw.Do(&interval); + if (sw.HasError()) + return false; + + TimingEvent* event = FindActiveEvent(event_name.c_str()); + if (!event) + { + Log_WarningPrintf("Save state has event '%s', but couldn't find this event when loading.", event_name.c_str()); + continue; + } + + // Using reschedule is safe here since we call sort afterwards. + event->m_downcount = downcount; + event->m_time_since_last_run = time_since_last_run; + event->m_period = period; + event->m_interval = interval; + } + + sw.Do(&m_last_event_run_time); + + Log_DevPrintf("Loaded %u events from save state.", event_count); + SortEvents(); + } + else + { + u32 event_count = static_cast(m_events.size()); + sw.Do(&event_count); + + for (TimingEvent* evt : m_events) + { + sw.Do(&evt->m_name); + sw.Do(&evt->m_downcount); + sw.Do(&evt->m_time_since_last_run); + sw.Do(&evt->m_period); + sw.Do(&evt->m_interval); + } + + sw.Do(&m_last_event_run_time); + + Log_DevPrintf("Wrote %u events to save state.", event_count); + } + + return !sw.HasError(); +} + +TimingEvent* System::FindActiveEvent(const char* name) +{ + auto iter = + std::find_if(m_events.begin(), m_events.end(), [&name](auto& ev) { return ev->GetName().compare(name) == 0; }); + + return (iter != m_events.end()) ? *iter : nullptr; +} + void System::UpdateRunningGame(const char* path, CDImage* image) { m_running_game_path.clear(); diff --git a/src/core/system.h b/src/core/system.h index 7809f049c..7679d2c8c 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -1,5 +1,6 @@ #pragma once #include "host_interface.h" +#include "timing_event.h" #include "types.h" #include #include @@ -28,6 +29,8 @@ class SIO; class System { + friend TimingEvent; + public: ~System(); @@ -52,7 +55,11 @@ public: u32 GetFrameNumber() const { return m_frame_number; } u32 GetInternalFrameNumber() const { return m_internal_frame_number; } u32 GetGlobalTickCounter() const { return m_global_tick_counter; } - void IncrementFrameNumber() { m_frame_number++; } + void IncrementFrameNumber() + { + m_frame_number++; + m_frame_done = true; + } void IncrementInternalFrameNumber() { m_internal_frame_number++; } const Settings& GetSettings() { return m_host_interface->GetSettings(); } @@ -78,9 +85,6 @@ public: bool LoadEXE(const char* filename, std::vector& bios_image); bool SetExpansionROM(const char* filename); - void SetDowncount(TickCount downcount); - void Synchronize(); - // Adds ticks to the global tick counter, simulating the CPU being stalled. void StallCPU(TickCount ticks); @@ -94,6 +98,12 @@ public: bool InsertMedia(const char* path); void RemoveMedia(); + /// Creates a new event. + std::unique_ptr CreateTimingEvent(std::string name, TickCount period, TickCount interval, + TimingEventCallback callback, bool activate); + + bool RUNNING_EVENTS() const { return m_running_events; } + private: System(HostInterface* host_interface); @@ -101,6 +111,33 @@ private: bool CreateGPU(GPURenderer renderer); void InitializeComponents(); + void DestroyComponents(); + + // Active event management + void AddActiveEvent(TimingEvent* event); + void RemoveActiveEvent(TimingEvent* event); + void SortEvents(); + + // Runs any pending events. Call when CPU downcount is zero. + void RunEvents(); + + // Updates the downcount of the CPU (event scheduling). + void UpdateCPUDowncount(); + + bool DoEventsState(StateWrapper& sw); + + // Event lookup, use with care. + // If you modify an event, call SortEvents afterwards. + TimingEvent* FindActiveEvent(const char* name); + + // Event enumeration, use with care. + // Don't remove an event while enumerating the list, as it will invalidate the iterator. + template + void EnumerateActiveEvents(T callback) const + { + for (const TimingEvent* ev : m_events) + callback(ev); + } void UpdateRunningGame(const char* path, CDImage* image); @@ -123,6 +160,12 @@ private: u32 m_internal_frame_number = 1; u32 m_global_tick_counter = 0; + std::vector m_events; + u32 m_last_event_run_time = 0; + bool m_running_events = false; + bool m_events_need_sorting = false; + bool m_frame_done = false; + std::string m_running_game_path; std::string m_running_game_code; std::string m_running_game_title; diff --git a/src/core/timers.cpp b/src/core/timers.cpp index 00c9f493f..63733ee88 100644 --- a/src/core/timers.cpp +++ b/src/core/timers.cpp @@ -1,6 +1,7 @@ #include "timers.h" #include "common/log.h" #include "common/state_wrapper.h" +#include "gpu.h" #include "interrupt_controller.h" #include "system.h" #include @@ -10,10 +11,13 @@ Timers::Timers() = default; Timers::~Timers() = default; -void Timers::Initialize(System* system, InterruptController* interrupt_controller) +void Timers::Initialize(System* system, InterruptController* interrupt_controller, GPU* gpu) { m_system = system; m_interrupt_controller = interrupt_controller; + m_gpu = gpu; + m_sysclk_event = system->CreateTimingEvent("Timer SysClk Interrupt", 1, 1, + std::bind(&Timers::AddSysClkTicks, this, std::placeholders::_1), false); } void Timers::Reset() @@ -30,6 +34,7 @@ void Timers::Reset() } m_sysclk_div_8_carry = 0; + UpdateSysClkEvent(); } bool Timers::DoState(StateWrapper& sw) @@ -47,6 +52,10 @@ bool Timers::DoState(StateWrapper& sw) } sw.Do(&m_sysclk_div_8_carry); + + if (sw.IsReading()) + UpdateSysClkEvent(); + return !sw.HasError(); } @@ -88,12 +97,12 @@ void Timers::AddTicks(u32 timer, TickCount count) bool interrupt_request = false; if (cs.counter >= cs.target && old_counter < cs.target) { - interrupt_request = true; + interrupt_request |= cs.mode.irq_at_target; cs.mode.reached_target = true; } if (cs.counter >= 0xFFFF) { - interrupt_request = true; + interrupt_request |= cs.mode.irq_on_overflow; cs.mode.reached_overflow = true; } @@ -126,7 +135,7 @@ void Timers::AddTicks(u32 timer, TickCount count) } } -void Timers::Execute(TickCount sysclk_ticks) +void Timers::AddSysClkTicks(TickCount sysclk_ticks) { if (!m_states[0].external_counting_enabled && m_states[0].counting_enabled) AddTicks(0, sysclk_ticks); @@ -143,7 +152,7 @@ void Timers::Execute(TickCount sysclk_ticks) AddTicks(2, sysclk_ticks); } - UpdateDowncount(); + UpdateSysClkEvent(); } u32 Timers::ReadRegister(u32 offset) @@ -157,13 +166,28 @@ u32 Timers::ReadRegister(u32 offset) { case 0x00: { - m_system->Synchronize(); + if (timer_index < 2) + { + // timers 0/1 depend on the GPU + if (cs.external_counting_enabled) + m_gpu->Synchronize(); + } + + m_sysclk_event->InvokeEarly(); + return cs.counter; } case 0x04: { - m_system->Synchronize(); + if (timer_index < 2) + { + // timers 0/1 depend on the GPU + if (cs.external_counting_enabled) + m_gpu->Synchronize(); + } + + m_sysclk_event->InvokeEarly(); const u32 bits = cs.mode.bits; cs.mode.reached_overflow = false; @@ -192,7 +216,7 @@ void Timers::WriteRegister(u32 offset, u32 value) case 0x00: { Log_DebugPrintf("Timer %u write counter %u", timer_index, value); - m_system->Synchronize(); + m_sysclk_event->InvokeEarly(); cs.counter = value & u32(0xFFFF); } break; @@ -200,7 +224,7 @@ void Timers::WriteRegister(u32 offset, u32 value) case 0x04: { Log_DebugPrintf("Timer %u write mode register 0x%04X", timer_index, value); - m_system->Synchronize(); + m_sysclk_event->InvokeEarly(); cs.mode.bits = value & u32(0x1FFF); cs.use_external_clock = (cs.mode.clock_source & (timer_index == 2 ? 2 : 1)) != 0; cs.counter = 0; @@ -210,13 +234,14 @@ void Timers::WriteRegister(u32 offset, u32 value) UpdateCountingEnabled(cs); UpdateIRQ(timer_index); + UpdateSysClkEvent(); } break; case 0x08: { Log_DebugPrintf("Timer %u write target 0x%04X", timer_index, ZeroExtend32(Truncate16(value))); - m_system->Synchronize(); + m_sysclk_event->InvokeEarly(); cs.target = value & u32(0xFFFF); } break; @@ -267,16 +292,19 @@ void Timers::UpdateIRQ(u32 index) static_cast(static_cast(InterruptController::IRQ::TMR0) + index)); } -void Timers::UpdateDowncount() +TickCount Timers::GetTicksUntilNextInterrupt() const { TickCount min_ticks = std::numeric_limits::max(); for (u32 i = 0; i < NUM_TIMERS; i++) { - CounterState& cs = m_states[i]; - if (!cs.counting_enabled || (i < 2 && cs.external_counting_enabled)) + const CounterState& cs = m_states[i]; + if (!cs.counting_enabled || (i < 2 && cs.external_counting_enabled) || + (!cs.mode.irq_at_target && !cs.mode.irq_on_overflow)) + { continue; + } - TickCount min_ticks_for_this_timer = min_ticks; + TickCount min_ticks_for_this_timer = std::numeric_limits::max(); if (cs.mode.irq_at_target && cs.counter < cs.target) min_ticks_for_this_timer = static_cast(cs.target - cs.counter); if (cs.mode.irq_on_overflow && cs.counter < cs.target) @@ -288,7 +316,17 @@ void Timers::UpdateDowncount() min_ticks = std::min(min_ticks, min_ticks_for_this_timer); } - m_system->SetDowncount(min_ticks); + return min_ticks; +} + +void Timers::UpdateSysClkEvent() +{ + // Still update once every 100ms. If we get polled we'll execute sooner. + const TickCount ticks = GetTicksUntilNextInterrupt(); + if (ticks == std::numeric_limits::max()) + m_sysclk_event->Schedule(MAX_SLICE_SIZE); + else + m_sysclk_event->Schedule(ticks); } void Timers::DrawDebugStateWindow() diff --git a/src/core/timers.h b/src/core/timers.h index 1d55c6778..ace159a91 100644 --- a/src/core/timers.h +++ b/src/core/timers.h @@ -2,11 +2,14 @@ #include "common/bitfield.h" #include "types.h" #include +#include class StateWrapper; class System; +class TimingEvent; class InterruptController; +class GPU; class Timers { @@ -14,7 +17,7 @@ public: Timers(); ~Timers(); - void Initialize(System* system, InterruptController* interrupt_controller); + void Initialize(System* system, InterruptController* interrupt_controller, GPU* gpu); void Reset(); bool DoState(StateWrapper& sw); @@ -25,11 +28,13 @@ public: // dot clock/hblank/sysclk div 8 bool IsUsingExternalClock(u32 timer) const { return m_states[timer].external_counting_enabled; } void AddTicks(u32 timer, TickCount ticks); - void Execute(TickCount sysclk_ticks); u32 ReadRegister(u32 offset); void WriteRegister(u32 offset, u32 value); + // changing interfaces + void SetGPU(GPU* gpu) { m_gpu = gpu; } + private: static constexpr u32 NUM_TIMERS = 3; @@ -73,10 +78,15 @@ private: void UpdateCountingEnabled(CounterState& cs); void UpdateIRQ(u32 index); - void UpdateDowncount(); + void AddSysClkTicks(TickCount sysclk_ticks); + + TickCount GetTicksUntilNextInterrupt() const; + void UpdateSysClkEvent(); System* m_system = nullptr; InterruptController* m_interrupt_controller = nullptr; + GPU* m_gpu = nullptr; + std::unique_ptr m_sysclk_event = nullptr; std::array m_states{}; u32 m_sysclk_div_8_carry = 0; // partial ticks for timer 3 with sysclk/8 diff --git a/src/core/timing_event.cpp b/src/core/timing_event.cpp new file mode 100644 index 000000000..a45c443c4 --- /dev/null +++ b/src/core/timing_event.cpp @@ -0,0 +1,130 @@ +#include "timing_event.h" +#include "common/assert.h" +#include "cpu_core.h" +#include "system.h" + +TimingEvent::TimingEvent(System* system, std::string name, TickCount period, TickCount interval, + TimingEventCallback callback) + : m_downcount(interval), m_time_since_last_run(0), m_period(period), m_interval(interval), + m_callback(std::move(callback)), m_system(system), m_name(std::move(name)), m_active(false) +{ +} + +TimingEvent::~TimingEvent() +{ + if (m_active) + m_system->RemoveActiveEvent(this); +} + +TickCount TimingEvent::GetTicksSinceLastExecution() const +{ + return m_system->m_cpu->GetPendingTicks() + m_time_since_last_run; +} + +TickCount TimingEvent::GetTicksUntilNextExecution() const +{ + return std::max(m_downcount - m_system->m_cpu->GetPendingTicks(), static_cast(0)); +} + +void TimingEvent::Schedule(TickCount ticks) +{ + m_downcount = ticks; + m_time_since_last_run = 0; + + // Factor in partial time if this was rescheduled outside of an event handler. Say, an MMIO write. + if (!m_system->m_running_events) + { + const TickCount pending_ticks = m_system->m_cpu->GetPendingTicks(); + m_downcount += pending_ticks; + m_time_since_last_run -= pending_ticks; + } + + if (m_active) + { + // If this is a call from an IO handler for example, re-sort the event queue. + m_system->SortEvents(); + } + else + { + m_active = true; + m_system->AddActiveEvent(this); + } +} + +void TimingEvent::SetIntervalAndSchedule(TickCount ticks) +{ + SetInterval(ticks); + Schedule(ticks); +} + +void TimingEvent::SetPeriodAndSchedule(TickCount ticks) +{ + SetPeriod(ticks); + SetInterval(ticks); + Schedule(ticks); +} + +void TimingEvent::Reset() +{ + if (!m_active) + return; + + m_downcount = m_interval; + m_time_since_last_run = 0; + m_system->SortEvents(); +} + +void TimingEvent::InvokeEarly(bool force /* = false */) +{ + if (!m_active) + return; + + const TickCount pending_ticks = m_system->m_running_events ? 0 : m_system->m_cpu->GetPendingTicks(); + const TickCount ticks_to_execute = m_time_since_last_run + pending_ticks; + if (!force && ticks_to_execute < m_period) + return; + + m_downcount = pending_ticks + m_interval; + m_time_since_last_run -= ticks_to_execute; + m_callback(ticks_to_execute, 0); + + // Since we've changed the downcount, we need to re-sort the events. + m_system->SortEvents(); +} + +void TimingEvent::Activate() +{ + if (m_active) + return; + + // leave the downcount intact + const TickCount pending_ticks = m_system->m_running_events ? 0 : m_system->m_cpu->GetPendingTicks(); + m_downcount += pending_ticks; + m_time_since_last_run -= pending_ticks; + + m_active = true; + m_system->AddActiveEvent(this); +} + +void TimingEvent::Deactivate() +{ + if (!m_active) + return; + + const TickCount pending_ticks = m_system->m_running_events ? 0 : m_system->m_cpu->GetPendingTicks(); + m_downcount -= pending_ticks; + m_time_since_last_run += pending_ticks; + + m_active = false; + m_system->RemoveActiveEvent(this); +} + +void TimingEvent::SetDowncount(TickCount downcount) +{ + const TickCount pending_ticks = m_system->m_running_events ? 0 : m_system->m_cpu->GetPendingTicks(); + m_downcount = downcount + pending_ticks; + m_time_since_last_run = -pending_ticks; + + if (m_active) + m_system->SortEvents(); +} diff --git a/src/core/timing_event.h b/src/core/timing_event.h new file mode 100644 index 000000000..3a9111fd3 --- /dev/null +++ b/src/core/timing_event.h @@ -0,0 +1,76 @@ +#pragma once +#include +#include +#include + +#include "types.h" + +class System; +class TimingEvent; + +// Event callback type. Second parameter is the number of cycles the event was executed "late". +using TimingEventCallback = std::function; + +class TimingEvent +{ + friend System; + +public: + TimingEvent(System* system, std::string name, TickCount period, TickCount interval, TimingEventCallback callback); + ~TimingEvent(); + + System* GetSystem() const { return m_system; } + const std::string& GetName() const { return m_name; } + bool IsActive() const { return m_active; } + + // Returns the number of ticks between each event. + TickCount GetPeriod() const { return m_period; } + TickCount GetInterval() const { return m_interval; } + + TickCount GetDowncount() const { return m_downcount; } + + // Includes pending time. + TickCount GetTicksSinceLastExecution() const; + TickCount GetTicksUntilNextExecution() const; + + void Schedule(TickCount ticks); + void SetIntervalAndSchedule(TickCount ticks); + void SetPeriodAndSchedule(TickCount ticks); + + void Reset(); + + // Services the event with the current accmulated time. If force is set, when not enough time is pending to + // simulate a single cycle, the callback will still be invoked, otherwise it won't be. + void InvokeEarly(bool force = false); + + // Deactivates the event, preventing it from firing again. + // Do not call within a callback, return Deactivate instead. + void Activate(); + void Deactivate(); + + ALWAYS_INLINE void SetState(bool active) + { + if (active) + Activate(); + else + Deactivate(); + } + + // Directly alters the downcount of the event. + void SetDowncount(TickCount downcount); + + // Directly alters the interval of the event. + void SetInterval(TickCount interval) { m_interval = interval; } + void SetPeriod(TickCount period) { m_period = period; } + +private: + TickCount m_downcount; + TickCount m_time_since_last_run; + TickCount m_period; + TickCount m_interval; + + TimingEventCallback m_callback; + System* m_system; + std::string m_name; + bool m_active; +};