From 959a5552748f2dcfadc1f37332d4de33c36f71f2 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 22 Feb 2020 00:19:10 +0900 Subject: [PATCH] CDROM: Implement asynchronous disc reading --- src/core/CMakeLists.txt | 2 + src/core/cdrom.cpp | 178 ++++++++++--------- src/core/cdrom.h | 10 +- src/core/cdrom_async_reader.cpp | 162 +++++++++++++++++ src/core/cdrom_async_reader.h | 56 ++++++ src/core/core.vcxproj | 2 + src/core/core.vcxproj.filters | 4 +- src/core/host_interface.cpp | 6 + src/core/settings.cpp | 4 + src/core/settings.h | 2 + src/duckstation-qt/consolesettingswidget.cpp | 1 + src/duckstation-qt/consolesettingswidget.ui | 16 ++ src/duckstation-sdl/sdl_host_interface.cpp | 6 + 13 files changed, 362 insertions(+), 87 deletions(-) create mode 100644 src/core/cdrom_async_reader.cpp create mode 100644 src/core/cdrom_async_reader.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 9546e6c8f..34d92b7c0 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -8,6 +8,8 @@ add_library(core bus.inl cdrom.cpp cdrom.h + cdrom_async_reader.cpp + cdrom_async_reader.h controller.cpp controller.h cpu_code_cache.cpp diff --git a/src/core/cdrom.cpp b/src/core/cdrom.cpp index 647a10a88..9eed0ff76 100644 --- a/src/core/cdrom.cpp +++ b/src/core/cdrom.cpp @@ -5,6 +5,7 @@ #include "dma.h" #include "imgui.h" #include "interrupt_controller.h" +#include "settings.h" #include "spu.h" #include "system.h" Log_SetChannel(CDROM); @@ -26,13 +27,13 @@ void CDROM::Initialize(System* system, DMA* dma, InterruptController* interrupt_ 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); + + if (m_system->GetSettings().cdrom_read_thread) + m_reader.StartThread(); } void CDROM::Reset() { - if (m_media) - m_media->Seek(0); - SoftReset(); } @@ -49,7 +50,9 @@ void CDROM::SoftReset() m_interrupt_flag_register = 0; m_pending_async_interrupt = 0; m_setloc_position = {}; - m_seek_position = {}; + m_last_requested_sector = 0; + if (m_reader.HasMedia()) + m_reader.QueueReadSector(m_last_requested_sector); m_setloc_pending = false; m_read_after_seek = false; m_play_after_seek = false; @@ -96,7 +99,7 @@ bool CDROM::DoState(StateWrapper& sw) sw.Do(&m_interrupt_flag_register); sw.Do(&m_pending_async_interrupt); sw.DoPOD(&m_setloc_position); - sw.DoPOD(&m_seek_position); + sw.DoPOD(&m_last_requested_sector); sw.Do(&m_setloc_pending); sw.Do(&m_read_after_seek); sw.Do(&m_play_after_seek); @@ -121,31 +124,25 @@ bool CDROM::DoState(StateWrapper& sw) sw.Do(&m_data_fifo); sw.Do(&m_sector_buffer); - u32 media_lba = m_media ? m_media->GetPositionOnDisc() : 0; - sw.Do(&media_lba); - if (sw.IsReading()) { + if (m_reader.HasMedia()) + m_reader.QueueReadSector(m_last_requested_sector); UpdateCommandEvent(); m_drive_event->SetState(!IsDriveIdle()); - - // load up media if we had something in there before - if (m_media && !m_media->Seek(media_lba)) - { - Log_ErrorPrint("Failed to seek CD media from save state. Ejecting."); - RemoveMedia(); - } } return !sw.HasError(); } +bool CDROM::HasMedia() const +{ + return m_reader.HasMedia(); +} + std::string CDROM::GetMediaFileName() const { - if (!m_media) - return std::string(); - - return m_media->GetFileName(); + return m_reader.GetMediaFileName(); } void CDROM::InsertMedia(std::unique_ptr media) @@ -153,16 +150,16 @@ void CDROM::InsertMedia(std::unique_ptr media) if (HasMedia()) RemoveMedia(); - m_media = std::move(media); + m_reader.SetMedia(std::move(media)); } void CDROM::RemoveMedia() { - if (!m_media) + if (!m_reader.HasMedia()) return; Log_InfoPrintf("Removing CD..."); - m_media.reset(); + m_reader.RemoveMedia(); m_secondary_status.shell_open = true; @@ -176,6 +173,17 @@ void CDROM::RemoveMedia() } } +void CDROM::SetUseReadThread(bool enabled) +{ + if (enabled == m_reader.IsUsingThread()) + return; + + if (enabled) + m_reader.StartThread(); + else + m_reader.StopThread(); +} + u8 CDROM::ReadRegister(u32 offset) { switch (offset) @@ -502,7 +510,7 @@ TickCount CDROM::GetTicksForRead() const TickCount CDROM::GetTicksForSeek() const { - const CDImage::LBA current_lba = m_secondary_status.motor_on ? m_media->GetPositionOnDisc() : 0; + const CDImage::LBA current_lba = m_secondary_status.motor_on ? m_reader.GetLastReadSector() : 0; const CDImage::LBA new_lba = m_setloc_position.ToLBA(); const u32 lba_diff = static_cast((new_lba > current_lba) ? (new_lba - current_lba) : (current_lba - new_lba)); @@ -558,7 +566,7 @@ void CDROM::ExecuteCommand() SendACKAndStat(); // shell open bit is cleared after sending the status - if (m_media) + if (HasMedia()) m_secondary_status.shell_open = false; EndCommand(); @@ -652,7 +660,7 @@ void CDROM::ExecuteCommand() { const bool logical = (m_command == Command::SeekL); Log_DebugPrintf("CDROM %s command", logical ? "SeekL" : "SeekP"); - if (!m_media) + if (!HasMedia()) { SendErrorResponse(0x80); } @@ -670,7 +678,7 @@ void CDROM::ExecuteCommand() case Command::ReadS: { Log_DebugPrintf("CDROM read command"); - if (!m_media) + if (!HasMedia()) { SendErrorResponse(0x80); } @@ -689,7 +697,7 @@ void CDROM::ExecuteCommand() u8 track = m_param_fifo.IsEmpty() ? 0 : m_param_fifo.Peek(0); Log_DebugPrintf("CDROM play command, track=%u", track); - if (!m_media) + if (!HasMedia()) { SendErrorResponse(0x80); } @@ -825,11 +833,13 @@ void CDROM::ExecuteCommand() case Command::GetTN: { Log_DebugPrintf("CDROM GetTN command"); - if (m_media) + if (HasMedia()) { + m_reader.WaitForReadToComplete(); + m_response_fifo.Push(m_secondary_status.bits); - m_response_fifo.Push(BinaryToBCD(Truncate8(m_media->GetTrackNumber()))); - m_response_fifo.Push(BinaryToBCD(Truncate8(m_media->GetTrackCount()))); + m_response_fifo.Push(BinaryToBCD(Truncate8(m_reader.GetMedia()->GetTrackNumber()))); + m_response_fifo.Push(BinaryToBCD(Truncate8(m_reader.GetMedia()->GetTrackCount()))); SetInterrupt(Interrupt::ACK); } else @@ -847,11 +857,11 @@ void CDROM::ExecuteCommand() Assert(m_param_fifo.GetSize() >= 1); const u8 track = PackedBCDToBinary(m_param_fifo.Peek()); - if (!m_media) + if (!HasMedia()) { SendErrorResponse(0x80); } - else if (track > m_media->GetTrackCount()) + else if (track > m_reader.GetMedia()->GetTrackCount()) { SendErrorResponse(0x10); } @@ -859,9 +869,9 @@ void CDROM::ExecuteCommand() { CDImage::Position pos; if (track == 0) - pos = CDImage::Position::FromLBA(m_media->GetLBACount()); + pos = CDImage::Position::FromLBA(m_reader.GetMedia()->GetLBACount()); else - pos = m_media->GetTrackStartMSFPosition(track); + pos = m_reader.GetMedia()->GetTrackStartMSFPosition(track); m_response_fifo.Push(m_secondary_status.bits); m_response_fifo.Push(BinaryToBCD(Truncate8(pos.minute))); @@ -1005,7 +1015,7 @@ void CDROM::ExecuteDrive(TickCount ticks_late) void CDROM::BeginReading(TickCount ticks_late) { - Log_DebugPrintf("Starting reading"); + Log_DebugPrintf("Starting reading @ LBA %u", m_last_requested_sector); if (m_setloc_pending) { BeginSeeking(true, true, false); @@ -1022,6 +1032,8 @@ void CDROM::BeginReading(TickCount ticks_late) m_drive_state = DriveState::Reading; m_drive_event->SetInterval(ticks); m_drive_event->Schedule(ticks - ticks_late); + + m_reader.QueueReadSector(m_last_requested_sector); } void CDROM::BeginPlaying(u8 track_bcd, TickCount ticks_late) @@ -1034,13 +1046,13 @@ void CDROM::BeginPlaying(u8 track_bcd, TickCount ticks_late) if (track_bcd != 0) { // play specific track? - if (track_bcd > m_media->GetTrackCount()) + if (track_bcd > m_reader.GetMedia()->GetTrackCount()) { // restart current track - track_bcd = BinaryToBCD(Truncate8(m_media->GetTrackNumber())); + track_bcd = BinaryToBCD(Truncate8(m_reader.GetMedia()->GetTrackNumber())); } - m_setloc_position = m_media->GetTrackStartMSFPosition(PackedBCDToBinary(track_bcd)); + m_setloc_position = m_reader.GetMedia()->GetTrackStartMSFPosition(PackedBCDToBinary(track_bcd)); m_setloc_pending = true; } @@ -1060,6 +1072,8 @@ void CDROM::BeginPlaying(u8 track_bcd, TickCount ticks_late) m_drive_state = DriveState::Playing; m_drive_event->SetInterval(ticks); m_drive_event->Schedule(ticks - ticks_late); + + m_reader.QueueReadSector(m_last_requested_sector); } void CDROM::BeginSeeking(bool logical, bool read_after_seek, bool play_after_seek) @@ -1067,13 +1081,12 @@ void CDROM::BeginSeeking(bool logical, bool read_after_seek, bool play_after_see if (!m_setloc_pending) Log_WarningPrintf("Seeking without setloc set"); - m_seek_position = m_setloc_position; m_read_after_seek = read_after_seek; m_play_after_seek = play_after_seek; m_setloc_pending = false; - Log_DebugPrintf("Seeking to [%02u:%02u:%02u] (%s)", m_seek_position.minute, m_seek_position.second, - m_seek_position.frame, logical ? "logical" : "physical"); + Log_DebugPrintf("Seeking to [%02u:%02u:%02u] (LBA %u) (%s)", m_setloc_position.minute, m_setloc_position.second, + m_setloc_position.frame, m_setloc_position.ToLBA(), logical ? "logical" : "physical"); const TickCount seek_time = GetTicksForSeek(); @@ -1084,11 +1097,8 @@ void CDROM::BeginSeeking(bool logical, bool read_after_seek, bool play_after_see m_drive_state = logical ? DriveState::SeekingLogical : DriveState::SeekingPhysical; 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. - CDImage::SubChannelQ subq; - if (m_media->Seek(m_seek_position) && m_media->ReadSubChannelQ(&subq) && subq.IsCRCValid()) - m_last_subq = subq; + m_last_requested_sector = m_setloc_position.ToLBA(); + m_reader.QueueReadSector(m_last_requested_sector); } void CDROM::DoSpinUpComplete() @@ -1111,21 +1121,22 @@ void CDROM::DoSeekComplete(TickCount ticks_late) m_secondary_status.ClearActiveBits(); m_sector_buffer.clear(); - // seek and update sub-q for ReadP command - const auto [seek_mm, seek_ss, seek_ff] = m_seek_position.ToBCD(); - bool seek_okay = (m_last_subq.absolute_minute_bcd == seek_mm && m_last_subq.absolute_second_bcd == seek_ss && - m_last_subq.absolute_frame_bcd == seek_ff); + bool seek_okay = m_reader.WaitForReadToComplete(); if (seek_okay) { - // check for data header for logical seeks - if (logical) + m_last_subq = m_reader.GetSectorSubQ(); + + // seek and update sub-q for ReadP command + DebugAssert(m_last_requested_sector == m_reader.GetLastReadSector()); + const auto [seek_mm, seek_ss, seek_ff] = CDImage::Position::FromLBA(m_last_requested_sector).ToBCD(); + seek_okay = (m_last_subq.IsCRCValid() && m_last_subq.absolute_minute_bcd == seek_mm && + m_last_subq.absolute_second_bcd == seek_ss && m_last_subq.absolute_frame_bcd == seek_ff); + if (seek_okay) { - u8 raw_sector[CDImage::RAW_SECTOR_SIZE]; - seek_okay &= m_media->ReadRawSector(raw_sector); - seek_okay &= m_media->Seek(m_media->GetPositionOnDisc() - 1); - if (seek_okay) + // check for data header for logical seeks + if (logical) { - ProcessDataSectorHeader(raw_sector, false); + ProcessDataSectorHeader(m_reader.GetSectorBuffer().data(), false); // ensure the location matches up (it should) seek_okay = (m_last_sector_header.minute == seek_mm && m_last_sector_header.second == seek_ss && @@ -1154,8 +1165,9 @@ void CDROM::DoSeekComplete(TickCount ticks_late) } else { - Log_WarningPrintf("%s seek to [%02u:%02u:%02u] failed", logical ? "Logical" : "Physical", m_seek_position.minute, - m_seek_position.second, m_seek_position.frame); + CDImage::Position pos(CDImage::Position::FromLBA(m_last_requested_sector)); + Log_WarningPrintf("%s seek to [%02u:%02u:%02u] failed", logical ? "Logical" : "Physical", pos.minute, pos.second, + pos.frame); m_secondary_status.seek_error = true; SendAsyncErrorResponse(0x80); } @@ -1188,7 +1200,7 @@ void CDROM::DoStopComplete() m_secondary_status.motor_on = false; m_sector_buffer.clear(); - m_media->Seek(0); + m_reader.QueueReadSector(0); m_async_response_fifo.Clear(); m_async_response_fifo.Push(m_secondary_status.bits); @@ -1225,12 +1237,12 @@ void CDROM::DoTOCRead() void CDROM::DoSectorRead() { + if (!m_reader.WaitForReadToComplete()) + Panic("Sector read failed"); + // TODO: Error handling // TODO: Check SubQ checksum. - CDImage::SubChannelQ subq; - if (!m_media->ReadSubChannelQ(&subq)) - Panic("SubChannel Q read failed"); - + const CDImage::SubChannelQ& subq = m_reader.GetSectorSubQ(); const bool is_data_sector = subq.control.data; m_secondary_status.playing_cdda = !is_data_sector; if (!is_data_sector) @@ -1245,7 +1257,7 @@ void CDROM::DoSectorRead() { // we don't want to update the position if the track changes, so we check it before reading the actual sector. Log_DevPrintf("Auto pause at the end of track %u (LBA %u)", m_play_track_number_bcd, - m_media->GetPositionOnDisc()); + m_reader.GetLastReadSector()); ClearAsyncInterrupt(); m_async_response_fifo.Push(m_secondary_status.bits); @@ -1258,21 +1270,17 @@ void CDROM::DoSectorRead() } } - u8 raw_sector[CDImage::RAW_SECTOR_SIZE]; - if (!m_media->ReadRawSector(raw_sector)) - Panic("Sector read failed"); - if (subq.IsCRCValid()) { m_last_subq = subq; if (is_data_sector && m_drive_state == DriveState::Reading) { - ProcessDataSector(raw_sector, subq); + ProcessDataSector(m_reader.GetSectorBuffer().data(), subq); } else if (!is_data_sector && m_drive_state == DriveState::Playing) { - ProcessCDDASector(raw_sector, subq); + ProcessCDDASector(m_reader.GetSectorBuffer().data(), subq); } else if (m_drive_state != DriveState::Reading && m_drive_state != DriveState::Playing) { @@ -1280,16 +1288,19 @@ void CDROM::DoSectorRead() } else { - Log_WarningPrintf("Skipping sector %u as it is a %s sector and we're not %s", m_media->GetPositionOnDisc() - 1, + Log_WarningPrintf("Skipping sector %u as it is a %s sector and we're not %s", m_reader.GetLastReadSector(), is_data_sector ? "data" : "audio", is_data_sector ? "reading" : "playing"); } } else { - const CDImage::Position pos(CDImage::Position::FromLBA(m_media->GetPositionOnDisc() - 1)); - Log_DevPrintf("Skipping sector %u [%02u:%02u:%02u] due to invalid subchannel Q", m_media->GetPositionOnDisc() - 1, + const CDImage::Position pos(CDImage::Position::FromLBA(m_reader.GetLastReadSector())); + Log_DevPrintf("Skipping sector %u [%02u:%02u:%02u] due to invalid subchannel Q", m_reader.GetLastReadSector(), pos.minute, pos.second, pos.frame); } + + m_last_requested_sector++; + m_reader.QueueReadSector(m_last_requested_sector); } void CDROM::ProcessDataSectorHeader(const u8* raw_sector, bool set_valid) @@ -1304,7 +1315,7 @@ void CDROM::ProcessDataSector(const u8* raw_sector, const CDImage::SubChannelQ& { ProcessDataSectorHeader(raw_sector, true); - Log_DevPrintf("Read sector %u: mode %u submode 0x%02X", m_media->GetPositionOnDisc() - 1, + Log_DevPrintf("Read sector %u: mode %u submode 0x%02X", m_last_requested_sector, ZeroExtend32(m_last_sector_header.sector_mode), ZeroExtend32(m_last_sector_subheader.submode.bits)); if (m_mode.xa_enable && m_last_sector_header.sector_mode == 2) @@ -1503,7 +1514,7 @@ void CDROM::ProcessXAADPCMSector(const u8* raw_sector, const CDImage::SubChannel void CDROM::ProcessCDDASector(const u8* raw_sector, const CDImage::SubChannelQ& subq) { // For CDDA sectors, the whole sector contains the audio data. - Log_DevPrintf("Read sector %u as CDDA", m_media->GetPositionOnDisc()); + Log_DevPrintf("Read sector %u as CDDA", m_last_requested_sector); if (m_mode.report_audio) { @@ -1594,16 +1605,17 @@ void CDROM::DrawDebugWindow() // draw voice states if (ImGui::CollapsingHeader("Media", ImGuiTreeNodeFlags_DefaultOpen)) { - if (m_media) + if (HasMedia()) { - const auto [disc_minute, disc_second, disc_frame] = m_media->GetMSFPositionOnDisc(); - const auto [track_minute, track_second, track_frame] = m_media->GetMSFPositionInTrack(); + const CDImage* media = m_reader.GetMedia(); + const auto [disc_minute, disc_second, disc_frame] = media->GetMSFPositionOnDisc(); + const auto [track_minute, track_second, track_frame] = media->GetMSFPositionInTrack(); - ImGui::Text("Filename: %s", m_media->GetFileName().c_str()); + ImGui::Text("Filename: %s", media->GetFileName().c_str()); ImGui::Text("Disc Position: MSF[%02u:%02u:%02u] LBA[%u]", disc_minute, disc_second, disc_frame, - m_media->GetPositionOnDisc()); - ImGui::Text("Track Position: Number[%u] MSF[%02u:%02u:%02u] LBA[%u]", m_media->GetTrackNumber(), track_minute, - track_second, track_frame, m_media->GetPositionInTrack()); + media->GetPositionOnDisc()); + ImGui::Text("Track Position: Number[%u] MSF[%02u:%02u:%02u] LBA[%u]", media->GetTrackNumber(), track_minute, + track_second, track_frame, media->GetPositionInTrack()); ImGui::Text("Last Sector: %02X:%02X:%02X (Mode %u)", m_last_sector_header.minute, m_last_sector_header.second, m_last_sector_header.frame, m_last_sector_header.sector_mode); } diff --git a/src/core/cdrom.h b/src/core/cdrom.h index c676c51c6..f22c6ba6a 100644 --- a/src/core/cdrom.h +++ b/src/core/cdrom.h @@ -4,6 +4,7 @@ #include "common/cd_xa.h" #include "common/fifo_queue.h" #include "common/heap_array.h" +#include "cdrom_async_reader.h" #include "types.h" #include #include @@ -27,7 +28,7 @@ public: void Reset(); bool DoState(StateWrapper& sw); - bool HasMedia() const { return static_cast(m_media); } + bool HasMedia() const; std::string GetMediaFileName() const; void InsertMedia(std::unique_ptr media); void RemoveMedia(); @@ -40,6 +41,8 @@ public: // Render statistics debug window. void DrawDebugWindow(); + void SetUseReadThread(bool enabled); + private: enum : u32 { @@ -219,7 +222,6 @@ private: DMA* m_dma = nullptr; 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; @@ -235,7 +237,7 @@ private: u8 m_pending_async_interrupt = 0; CDImage::Position m_setloc_position = {}; - CDImage::Position m_seek_position = {}; + CDImage::LBA m_last_requested_sector{}; bool m_setloc_pending = false; bool m_read_after_seek = false; bool m_play_after_seek = false; @@ -265,4 +267,6 @@ private: InlineFIFOQueue m_async_response_fifo; HeapFIFOQueue m_data_fifo; std::vector m_sector_buffer; + + CDROMAsyncReader m_reader; }; diff --git a/src/core/cdrom_async_reader.cpp b/src/core/cdrom_async_reader.cpp new file mode 100644 index 000000000..2c0870057 --- /dev/null +++ b/src/core/cdrom_async_reader.cpp @@ -0,0 +1,162 @@ +#include "cdrom_async_reader.h" +#include "common/assert.h" +#include "common/log.h" +#include "common/timer.h" +Log_SetChannel(CDROMAsyncReader); + +CDROMAsyncReader::CDROMAsyncReader() = default; + +CDROMAsyncReader::~CDROMAsyncReader() +{ + StopThread(); +} + +void CDROMAsyncReader::StartThread() +{ + if (IsUsingThread()) + return; + + m_shutdown_flag.store(false); + m_read_thread = std::thread(&CDROMAsyncReader::WorkerThreadEntryPoint, this); +} + +void CDROMAsyncReader::StopThread() +{ + if (!IsUsingThread()) + return; + + { + std::unique_lock lock(m_mutex); + if (m_sector_read_pending.load()) + m_notify_read_complete_cv.wait(lock, [this]() { return !m_sector_read_pending.load(); }); + + m_shutdown_flag.store(true); + m_do_read_cv.notify_one(); + } + + m_read_thread.join(); +} + +void CDROMAsyncReader::SetMedia(std::unique_ptr media) +{ + WaitForReadToComplete(); + m_media = std::move(media); +} + +void CDROMAsyncReader::RemoveMedia() +{ + WaitForReadToComplete(); + m_media.reset(); +} + +void CDROMAsyncReader::QueueReadSector(CDImage::LBA lba) +{ + if (!IsUsingThread()) + { + m_sector_read_pending.store(true); + m_next_position_set.store(true); + m_next_position = lba; + DoSectorRead(); + return; + } + + std::unique_lock lock(m_mutex); + if (m_sector_read_pending.load()) + m_notify_read_complete_cv.wait(lock, [this]() { return !m_sector_read_pending.load(); }); + + // don't re-read the same sector if it was the last one we read + // the CDC code does this when seeking->reading + if (m_last_read_sector == lba && m_sector_read_result.load()) + { + Log_DebugPrintf("Skipping re-reading same sector %u", lba); + return; + } + + m_sector_read_pending.store(true); + m_next_position_set.store(true); + m_next_position = lba; + m_do_read_cv.notify_one(); +} + +void CDROMAsyncReader::QueueReadNextSector() +{ + if (!IsUsingThread()) + { + m_sector_read_pending.store(true); + DoSectorRead(); + return; + } + + std::unique_lock lock(m_mutex); + if (m_sector_read_pending.load()) + m_notify_read_complete_cv.wait(lock, [this]() { return !m_sector_read_pending.load(); }); + + m_sector_read_pending.store(true); + m_do_read_cv.notify_one(); +} + +bool CDROMAsyncReader::WaitForReadToComplete() +{ + if (!IsUsingThread()) + return m_sector_read_result.load(); + + std::unique_lock lock(m_mutex); + if (m_sector_read_pending.load()) + { + Log_DebugPrintf("Sector read pending, waiting"); + m_notify_read_complete_cv.wait(lock, [this]() { return !m_sector_read_pending.load(); }); + } + + return m_sector_read_result.load(); +} + +void CDROMAsyncReader::DoSectorRead() +{ +#ifdef _DEBUG + Common::Timer timer; +#endif + + if (m_next_position_set.load()) + { + if (m_media->GetPositionOnDisc() != m_next_position && !m_media->Seek(m_next_position)) + { + Log_WarningPrintf("Seek to LBA %u failed", m_next_position); + m_sector_read_result.store(false); + return; + } + } + + CDImage::LBA pos = m_media->GetPositionOnDisc(); + if (!m_media->ReadSubChannelQ(&m_subq) || !m_media->ReadRawSector(m_sector_buffer.data())) + { + m_sector_read_result.store(false); + Log_WarningPrintf("Read of LBA %u failed", pos); + return; + } + + m_last_read_sector = pos; + m_sector_read_result.store(true); + +#ifdef _DEBUG + if (timer.GetTimeMilliseconds() > 1.0f) + Log_WarningPrintf("Read LBA %u took %.2f msec", pos, timer.GetTimeMilliseconds()); +#endif +} + +void CDROMAsyncReader::WorkerThreadEntryPoint() +{ + std::unique_lock lock(m_mutex); + + while (!m_shutdown_flag.load()) + { + m_do_read_cv.wait(lock, [this]() { return (m_shutdown_flag.load() || m_sector_read_pending.load()); }); + if (m_sector_read_pending.load()) + { + lock.unlock(); + DoSectorRead(); + lock.lock(); + m_sector_read_pending.store(false); + m_notify_read_complete_cv.notify_one(); + } + } +} diff --git a/src/core/cdrom_async_reader.h b/src/core/cdrom_async_reader.h new file mode 100644 index 000000000..0143208e4 --- /dev/null +++ b/src/core/cdrom_async_reader.h @@ -0,0 +1,56 @@ +#pragma once +#include "common/cd_image.h" +#include "types.h" +#include +#include +#include +#include + +class CDROMAsyncReader +{ +public: + using SectorBuffer = std::array; + + CDROMAsyncReader(); + ~CDROMAsyncReader(); + + const CDImage::LBA GetLastReadSector() const { return m_last_read_sector; } + const SectorBuffer& GetSectorBuffer() const { return m_sector_buffer; } + const CDImage::SubChannelQ& GetSectorSubQ() const { return m_subq; } + const bool HasMedia() const { return static_cast(m_media); } + const CDImage* GetMedia() const { return m_media.get(); } + const std::string GetMediaFileName() const { return m_media ? m_media->GetFileName() : std::string(); } + + bool IsUsingThread() const { return m_read_thread.joinable(); } + void StartThread(); + void StopThread(); + + void SetMedia(std::unique_ptr media); + void RemoveMedia(); + + void QueueReadSector(CDImage::LBA lba); + void QueueReadNextSector(); + + bool WaitForReadToComplete(); + +private: + void DoSectorRead(); + void WorkerThreadEntryPoint(); + + std::unique_ptr m_media; + + std::mutex m_mutex; + std::thread m_read_thread; + std::condition_variable m_do_read_cv; + std::condition_variable m_notify_read_complete_cv; + + CDImage::LBA m_next_position{}; + std::atomic_bool m_next_position_set{false}; + std::atomic_bool m_sector_read_pending{false}; + std::atomic_bool m_shutdown_flag{true}; + + CDImage::LBA m_last_read_sector{}; + CDImage::SubChannelQ m_subq{}; + SectorBuffer m_sector_buffer{}; + std::atomic_bool m_sector_read_result{false}; +}; diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 5d5f65e69..eae4750ca 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -39,6 +39,7 @@ + @@ -89,6 +90,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index 215c62e07..a9c285588 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -40,6 +40,7 @@ + @@ -81,10 +82,11 @@ + - + \ No newline at end of file diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp index 603f690ae..84a4709e7 100644 --- a/src/core/host_interface.cpp +++ b/src/core/host_interface.cpp @@ -771,6 +771,8 @@ void HostInterface::SetDefaultSettings() m_settings.display_fullscreen = false; m_settings.video_sync_enabled = true; + m_settings.cdrom_read_thread = true; + m_settings.audio_backend = AudioBackend::Cubeb; m_settings.audio_sync_enabled = true; @@ -800,6 +802,7 @@ void HostInterface::UpdateSettings(const std::function& apply_callback) const bool old_audio_sync_enabled = m_settings.audio_sync_enabled; const bool old_speed_limiter_enabled = m_settings.speed_limiter_enabled; const bool old_display_linear_filtering = m_settings.display_linear_filtering; + const bool old_cdrom_read_thread = m_settings.cdrom_read_thread; std::array old_controller_types = m_settings.controller_types; apply_callback(); @@ -847,6 +850,9 @@ void HostInterface::UpdateSettings(const std::function& apply_callback) { m_system->UpdateGPUSettings(); } + + if (m_settings.cdrom_read_thread != old_cdrom_read_thread) + m_system->GetCDROM()->SetUseReadThread(m_settings.cdrom_read_thread); } for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 6c7244cbc..170638cef 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -30,6 +30,8 @@ void Settings::Load(SettingsInterface& si) display_fullscreen = si.GetBoolValue("Display", "Fullscreen", false); video_sync_enabled = si.GetBoolValue("Display", "VSync", true); + cdrom_read_thread = si.GetBoolValue("CDROM", "ReadThread", true); + audio_backend = ParseAudioBackend(si.GetStringValue("Audio", "Backend", "Cubeb").c_str()).value_or(AudioBackend::Cubeb); audio_sync_enabled = si.GetBoolValue("Audio", "Sync", true); @@ -79,6 +81,8 @@ void Settings::Save(SettingsInterface& si) const si.SetBoolValue("Display", "Fullscreen", display_fullscreen); si.SetBoolValue("Display", "VSync", video_sync_enabled); + si.SetBoolValue("CDROM", "ReadThread", cdrom_read_thread); + si.SetStringValue("Audio", "Backend", GetAudioBackendName(audio_backend)); si.SetBoolValue("Audio", "Sync", audio_sync_enabled); diff --git a/src/core/settings.h b/src/core/settings.h index 4e0ac5147..7546b960f 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -52,6 +52,8 @@ struct Settings bool display_fullscreen = false; bool video_sync_enabled = true; + bool cdrom_read_thread = true; + AudioBackend audio_backend = AudioBackend::Cubeb; bool audio_sync_enabled = true; diff --git a/src/duckstation-qt/consolesettingswidget.cpp b/src/duckstation-qt/consolesettingswidget.cpp index 7ed5fa357..0a2db08d3 100644 --- a/src/duckstation-qt/consolesettingswidget.cpp +++ b/src/duckstation-qt/consolesettingswidget.cpp @@ -30,6 +30,7 @@ ConsoleSettingsWidget::ConsoleSettingsWidget(QtHostInterface* host_interface, QW SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.saveStateOnExit, "General/SaveStateOnExit"); SettingWidgetBinder::BindWidgetToEnumSetting(m_host_interface, m_ui.cpuExecutionMode, "CPU/ExecutionMode", &Settings::ParseCPUExecutionMode, &Settings::GetCPUExecutionModeName); + SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.cdromReadThread, "CDROM/ReadThread"); connect(m_ui.biosPathBrowse, &QPushButton::pressed, this, &ConsoleSettingsWidget::onBrowseBIOSPathButtonClicked); diff --git a/src/duckstation-qt/consolesettingswidget.ui b/src/duckstation-qt/consolesettingswidget.ui index 187f2cb86..d6e528b9c 100644 --- a/src/duckstation-qt/consolesettingswidget.ui +++ b/src/duckstation-qt/consolesettingswidget.ui @@ -182,6 +182,22 @@ + + + + CDROM Emulation + + + + + + Use Read Thread (Asynchronous) + + + + + + diff --git a/src/duckstation-sdl/sdl_host_interface.cpp b/src/duckstation-sdl/sdl_host_interface.cpp index f5b216b6a..390990781 100644 --- a/src/duckstation-sdl/sdl_host_interface.cpp +++ b/src/duckstation-sdl/sdl_host_interface.cpp @@ -971,6 +971,12 @@ void SDLHostInterface::DrawSettingsWindow() settings_changed |= ImGui::Checkbox("Save State On Exit", &m_settings.save_state_on_exit); } + ImGui::NewLine(); + if (DrawSettingsSectionHeader("CDROM Emulation")) + { + settings_changed |= ImGui::Checkbox("Use Read Thread (Asynchronous)", &m_settings.cdrom_read_thread); + } + ImGui::NewLine(); if (DrawSettingsSectionHeader("Audio")) {