diff --git a/common/ProgressCallback.cpp b/common/ProgressCallback.cpp index 45d3ae47f0..05c5777dc1 100644 --- a/common/ProgressCallback.cpp +++ b/common/ProgressCallback.cpp @@ -101,40 +101,48 @@ void ProgressCallback::DisplayFormattedModalInformation(const char* format, ...) ModalInformation(str.c_str()); } -class NullProgressCallbacks final : public ProgressCallback +namespace { -public: - void PushState() override {} - void PopState() override {} - - bool IsCancelled() const override { return false; } - bool IsCancellable() const override { return false; } - - void SetCancellable(bool cancellable) override {} - void SetTitle(const char* title) override {} - void SetStatusText(const char* statusText) override {} - void SetProgressRange(u32 range) override {} - void SetProgressValue(u32 value) override {} - void IncrementProgressValue() override {} - void SetProgressState(ProgressState state) override {} - - void DisplayError(const char* message) override { Console.Error("%s", message); } - void DisplayWarning(const char* message) override { Console.Warning("%s", message); } - void DisplayInformation(const char* message) override { Console.WriteLn("%s", message); } - void DisplayDebugMessage(const char* message) override { DevCon.WriteLn("%s", message); } - - void ModalError(const char* message) override { Console.Error(message); } - bool ModalConfirmation(const char* message) override + class NullProgressCallbacks final : public ProgressCallback { - Console.WriteLn("%s", message); - return false; - } - void ModalInformation(const char* message) override { Console.WriteLn("%s", message); } -}; + public: + void PushState() override {} + void PopState() override {} + + bool IsCancelled() const override { return false; } + bool IsCancellable() const override { return false; } + + void SetCancellable(bool cancellable) override {} + void SetTitle(const char* title) override {} + void SetStatusText(const char* statusText) override {} + void SetProgressRange(u32 range) override {} + void SetProgressValue(u32 value) override {} + void IncrementProgressValue() override {} + void SetProgressState(ProgressState state) override {} + + void DisplayError(const char* message) override { Console.Error("%s", message); } + void DisplayWarning(const char* message) override { Console.Warning("%s", message); } + void DisplayInformation(const char* message) override { Console.WriteLn("%s", message); } + void DisplayDebugMessage(const char* message) override { DevCon.WriteLn("%s", message); } + + void ModalError(const char* message) override { Console.Error(message); } + bool ModalConfirmation(const char* message) override + { + Console.WriteLn("%s", message); + return false; + } + void ModalInformation(const char* message) override { Console.WriteLn("%s", message); } + }; +} // namespace static NullProgressCallbacks s_nullProgressCallbacks; ProgressCallback* ProgressCallback::NullProgressCallback = &s_nullProgressCallbacks; +std::unique_ptr ProgressCallback::CreateNullProgressCallback() +{ + return std::make_unique(); +} + BaseProgressCallback::BaseProgressCallback() { } @@ -171,8 +179,8 @@ void BaseProgressCallback::PopState() // impose the current position into the previous range const u32 new_progress_value = (m_progress_range != 0) ? - static_cast(((float)m_progress_value / (float)m_progress_range) * (float)state->progress_range) : - state->progress_value; + static_cast(((float)m_progress_value / (float)m_progress_range) * (float)state->progress_range) : + state->progress_value; m_cancellable = state->cancellable; m_status_text = std::move(state->status_text); diff --git a/common/ProgressCallback.h b/common/ProgressCallback.h index 4fade10a40..ffac670b1b 100644 --- a/common/ProgressCallback.h +++ b/common/ProgressCallback.h @@ -3,6 +3,8 @@ #pragma once #include "Pcsx2Defs.h" + +#include #include /** @@ -59,6 +61,8 @@ public: public: static ProgressCallback* NullProgressCallback; + + static std::unique_ptr CreateNullProgressCallback(); }; class BaseProgressCallback : public ProgressCallback diff --git a/pcsx2-gsrunner/Main.cpp b/pcsx2-gsrunner/Main.cpp index 8b64ae49e2..2f7c22a88f 100644 --- a/pcsx2-gsrunner/Main.cpp +++ b/pcsx2-gsrunner/Main.cpp @@ -20,6 +20,7 @@ #include "common/FileSystem.h" #include "common/MemorySettingsInterface.h" #include "common/Path.h" +#include "common/ProgressCallback.h" #include "common/SettingsWrapper.h" #include "common/StringUtil.h" @@ -165,6 +166,11 @@ void Host::SetDefaultUISettings(SettingsInterface& si) // nothing } +std::unique_ptr Host::CreateHostProgressCallback() +{ + return ProgressCallback::CreateNullProgressCallback(); +} + void Host::ReportErrorAsync(const std::string_view title, const std::string_view message) { if (!title.empty() && !message.empty()) diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index 7eb0d05069..f3eb569e91 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -1704,6 +1704,247 @@ void Host::SetMouseMode(bool relative_mode, bool hide_cursor) emit g_emu_thread->onMouseModeRequested(relative_mode, hide_cursor); } +namespace { +class QtHostProgressCallback final : public BaseProgressCallback +{ +public: + QtHostProgressCallback(); + ~QtHostProgressCallback() override; + + __fi const std::string& GetName() const { return m_name; } + + void PushState() override; + void PopState() override; + + bool IsCancelled() const override; + + void SetCancellable(bool cancellable) override; + void SetTitle(const char* title) override; + void SetStatusText(const char* text) override; + void SetProgressRange(u32 range) override; + void SetProgressValue(u32 value) override; + + void DisplayError(const char* message) override; + void DisplayWarning(const char* message) override; + void DisplayInformation(const char* message) override; + void DisplayDebugMessage(const char* message) override; + + void ModalError(const char* message) override; + bool ModalConfirmation(const char* message) override; + void ModalInformation(const char* message) override; + + void SetCancelled(); + +private: + struct SharedData + { + QProgressDialog* dialog = nullptr; + QString init_title; + QString init_status_text; + std::atomic_bool cancelled{false}; + bool cancellable = true; + bool was_fullscreen = false; + }; + + void EnsureHasData(); + static void EnsureDialogVisible(const std::shared_ptr& data); + void Redraw(bool force); + + std::string m_name; + std::shared_ptr m_data; + int m_last_progress_percent = -1; +}; +} + +QtHostProgressCallback::QtHostProgressCallback() + : BaseProgressCallback() +{ +} + +QtHostProgressCallback::~QtHostProgressCallback() +{ + if (m_data) + { + QtHost::RunOnUIThread([data = m_data]() { + if (!data->dialog) + return; + + data->dialog->close(); + delete data->dialog; + if (data->was_fullscreen) + g_emu_thread->setFullscreen(true, false); + }); + } +} + +void QtHostProgressCallback::PushState() +{ + BaseProgressCallback::PushState(); +} + +void QtHostProgressCallback::PopState() +{ + BaseProgressCallback::PopState(); + Redraw(true); +} + +void QtHostProgressCallback::SetCancellable(bool cancellable) +{ + BaseProgressCallback::SetCancellable(cancellable); + EnsureHasData(); + m_data->cancellable = cancellable; +} + +void QtHostProgressCallback::SetTitle(const char* title) +{ + EnsureHasData(); + QtHost::RunOnUIThread([data = m_data, title = QString::fromUtf8(title)]() { + if (data->dialog) + data->dialog->setWindowTitle(title); + else + data->init_title = title; + }); +} + +void QtHostProgressCallback::SetStatusText(const char* text) +{ + BaseProgressCallback::SetStatusText(text); + + EnsureHasData(); + QtHost::RunOnUIThread([data = m_data, text = QString::fromUtf8(text)]() { + if (data->dialog) + data->dialog->setLabelText(text); + else + data->init_status_text = text; + }); +} + +void QtHostProgressCallback::SetProgressRange(u32 range) +{ + u32 last_range = m_progress_range; + + BaseProgressCallback::SetProgressRange(range); + + if (m_progress_range != last_range) + Redraw(false); +} + +void QtHostProgressCallback::SetProgressValue(u32 value) +{ + u32 lastValue = m_progress_value; + + BaseProgressCallback::SetProgressValue(value); + + if (m_progress_value != lastValue) + Redraw(false); +} + +void QtHostProgressCallback::Redraw(bool force) +{ + const int percent = static_cast((static_cast(m_progress_value) / static_cast(m_progress_range)) * 100.0f); + if (percent == m_last_progress_percent && !force) + return; + + // If this is the emu uthread, we need to process the un-fullscreen message. + if (g_emu_thread->isOnEmuThread()) + Host::PumpMessagesOnCPUThread(); + + m_last_progress_percent = percent; + EnsureHasData(); + QtHost::RunOnUIThread([data = m_data, percent]() { + EnsureDialogVisible(data); + data->dialog->setValue(percent); + }); +} + +void QtHostProgressCallback::DisplayError(const char* message) +{ + Console.Error(message); + Host::ReportErrorAsync("Error", message); +} + +void QtHostProgressCallback::DisplayWarning(const char* message) +{ + Console.Warning(message); +} + +void QtHostProgressCallback::DisplayInformation(const char* message) +{ + Console.WriteLn(message); +} + +void QtHostProgressCallback::DisplayDebugMessage(const char* message) +{ + DevCon.WriteLn(message); +} + +void QtHostProgressCallback::ModalError(const char* message) +{ + Console.Error(message); + Host::ReportErrorAsync("Error", message); +} + +bool QtHostProgressCallback::ModalConfirmation(const char* message) +{ + return false; +} + +void QtHostProgressCallback::ModalInformation(const char* message) +{ + Console.WriteLn(message); +} + +void QtHostProgressCallback::SetCancelled() +{ + // not done here +} + +bool QtHostProgressCallback::IsCancelled() const +{ + return m_data && m_data->cancelled.load(std::memory_order_acquire); +} + +void QtHostProgressCallback::EnsureHasData() +{ + if (!m_data) + m_data = std::make_shared(); +} + +void QtHostProgressCallback::EnsureDialogVisible(const std::shared_ptr& data) +{ + pxAssert(data); + if (data->dialog) + return; + + data->was_fullscreen = g_emu_thread->isFullscreen(); + if (data->was_fullscreen) + g_emu_thread->setFullscreen(false, true); + + data->dialog = new QProgressDialog(data->init_status_text, + data->cancellable ? qApp->translate("QtHost", "Cancel") : QString(), + 0, 100, g_main_window); + if (data->cancellable) + { + data->dialog->connect(data->dialog, &QProgressDialog::canceled, + [data]() { data->cancelled.store(true, std::memory_order_release); }); + } + data->dialog->setWindowIcon(QtHost::GetAppIcon()); + data->dialog->setMinimumWidth(400); + data->dialog->show(); + data->dialog->raise(); + data->dialog->activateWindow(); + if (!data->init_title.isEmpty()) + { + data->dialog->setWindowTitle(data->init_title); + data->init_title = QString(); + } +} + +std::unique_ptr Host::CreateHostProgressCallback() +{ + return std::make_unique(); +} + ////////////////////////////////////////////////////////////////////////// // Hotkeys ////////////////////////////////////////////////////////////////////////// diff --git a/pcsx2-qt/QtHost.h b/pcsx2-qt/QtHost.h index fc74e6ec27..a3ec396bb1 100644 --- a/pcsx2-qt/QtHost.h +++ b/pcsx2-qt/QtHost.h @@ -114,7 +114,7 @@ public Q_SLOTS: void endCapture(); void setAudioOutputVolume(int volume, int fast_forward_volume); void setAudioOutputMuted(bool muted); - + Q_SIGNALS: bool messageConfirmed(const QString& title, const QString& message); void statusMessage(const QString& message); diff --git a/pcsx2-qt/Settings/EmulationSettingsWidget.cpp b/pcsx2-qt/Settings/EmulationSettingsWidget.cpp index bfa82b1dd8..81cf8c1d80 100644 --- a/pcsx2-qt/Settings/EmulationSettingsWidget.cpp +++ b/pcsx2-qt/Settings/EmulationSettingsWidget.cpp @@ -45,6 +45,7 @@ EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.MTVU, "EmuCore/Speedhacks", "vuThread", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.instantVU1, "EmuCore/Speedhacks", "vu1Instant", true); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.fastCDVD, "EmuCore/Speedhacks", "fastCDVD", false); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.precacheCDVD, "EmuCore", "CdvdPrecache", false); if (m_dialog->isPerGameSettings()) { @@ -124,6 +125,9 @@ EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget "Safe for most games, but a few games may exhibit graphical errors.")); dialog->registerWidgetHelp(m_ui.fastCDVD, tr("Enable Fast CDVD"), tr("Unchecked"), tr("Fast disc access, less loading times. Check HDLoader compatibility lists for known games that have issues with this.")); + dialog->registerWidgetHelp(m_ui.precacheCDVD, tr("Enable CDVD Precaching"), tr("Unchecked"), + tr("Loads the disc image into RAM before starting the virtual machine. Can reduce stutter on systems with hard drives that " + "have long wake times, but significantly increases boot times.")); dialog->registerWidgetHelp(m_ui.cheats, tr("Enable Cheats"), tr("Unchecked"), tr("Automatically loads and applies cheats on game start.")); dialog->registerWidgetHelp(m_ui.hostFilesystem, tr("Enable Host Filesystem"), tr("Unchecked"), diff --git a/pcsx2-qt/Settings/EmulationSettingsWidget.ui b/pcsx2-qt/Settings/EmulationSettingsWidget.ui index 7723f9615b..8f49f935b4 100644 --- a/pcsx2-qt/Settings/EmulationSettingsWidget.ui +++ b/pcsx2-qt/Settings/EmulationSettingsWidget.ui @@ -98,13 +98,20 @@ - + Enable Fast CDVD + + + + Enable CDVD Precaching + + + diff --git a/pcsx2/CDVD/CDVDcommon.cpp b/pcsx2/CDVD/CDVDcommon.cpp index d2f60bf367..e5c9598cd0 100644 --- a/pcsx2/CDVD/CDVDcommon.cpp +++ b/pcsx2/CDVD/CDVDcommon.cpp @@ -12,8 +12,10 @@ #include "common/Assertions.h" #include "common/Console.h" #include "common/EnumOps.h" +#include "common/Error.h" #include "common/FileSystem.h" #include "common/Path.h" +#include "common/ProgressCallback.h" #include "common/StringUtil.h" #include @@ -413,6 +415,13 @@ bool DoCDVDopen(Error* error) return true; } +bool DoCDVDprecache(ProgressCallback* progress, Error* error) +{ + CheckNullCDVD(); + progress->SetTitle(TRANSLATE("CDVD", "Precaching CDVD")); + return CDVD->precache(progress, error); +} + void DoCDVDclose() { CheckNullCDVD(); @@ -525,6 +534,11 @@ static bool NODISCopen(std::string filename, Error* error) return true; } +static bool NODISCprecache(ProgressCallback* progress, Error* error) +{ + return true; +} + static void NODISCclose() { } @@ -592,6 +606,7 @@ const CDVD_API CDVDapi_NoDisc = { NODISCclose, NODISCopen, + NODISCprecache, NODISCreadTrack, NODISCgetBuffer, NODISCreadSubQ, diff --git a/pcsx2/CDVD/CDVDcommon.h b/pcsx2/CDVD/CDVDcommon.h index c9a8727be5..3d5a6afa35 100644 --- a/pcsx2/CDVD/CDVDcommon.h +++ b/pcsx2/CDVD/CDVDcommon.h @@ -8,6 +8,7 @@ #include class Error; +class ProgressCallback; typedef struct _cdvdSubQ { @@ -79,6 +80,7 @@ typedef struct _cdvdTN // CDVD typedef bool (*_CDVDopen)(std::string filename, Error* error); +typedef bool (*_CDVDprecache)(ProgressCallback* progress, Error* error); // Initiates an asynchronous track read operation. // Returns -1 on error (invalid track) @@ -118,6 +120,7 @@ struct CDVD_API // Don't need init or shutdown. iso/nodisc have no init/shutdown. _CDVDopen open; + _CDVDprecache precache; _CDVDreadTrack readTrack; _CDVDgetBuffer getBuffer; _CDVDreadSubQ readSubQ; @@ -152,6 +155,7 @@ extern CDVD_SourceType CDVDsys_GetSourceType(); extern void CDVDsys_ClearFiles(); extern bool DoCDVDopen(Error* error); +extern bool DoCDVDprecache(ProgressCallback* progress, Error* error); extern void DoCDVDclose(); extern s32 DoCDVDreadSector(u8* buffer, u32 lsn, int mode); extern s32 DoCDVDreadTrack(u32 lsn, int mode); diff --git a/pcsx2/CDVD/CDVDdiscReader.cpp b/pcsx2/CDVD/CDVDdiscReader.cpp index a49c02b334..26d28535a3 100644 --- a/pcsx2/CDVD/CDVDdiscReader.cpp +++ b/pcsx2/CDVD/CDVDdiscReader.cpp @@ -1,8 +1,9 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ #include "CDVDdiscReader.h" #include "CDVD/CDVD.h" +#include "Host.h" #include "common/Error.h" @@ -192,6 +193,12 @@ static bool DISCopen(std::string filename, Error* error) return true; } +static bool DISCprecache(ProgressCallback* progress, Error* error) +{ + Error::SetStringView(error, TRANSLATE_SV("CDVD", "Precaching is not supported for discs.")); + return false; +} + static void DISCclose() { StopKeepAliveThread(); @@ -531,6 +538,7 @@ const CDVD_API CDVDapi_Disc = { DISCclose, DISCopen, + DISCprecache, DISCreadTrack, DISCgetBuffer, DISCreadSubQ, diff --git a/pcsx2/CDVD/CDVDisoReader.cpp b/pcsx2/CDVD/CDVDisoReader.cpp index 5f16e5a207..c862080a6c 100644 --- a/pcsx2/CDVD/CDVDisoReader.cpp +++ b/pcsx2/CDVD/CDVDisoReader.cpp @@ -55,6 +55,11 @@ static bool ISOopen(std::string filename, Error* error) return true; } +static bool ISOprecache(ProgressCallback* progress, Error* error) +{ + return iso.Precache(progress, error); +} + static s32 ISOreadSubQ(u32 lsn, cdvdSubQ* subq) { // fake it @@ -400,6 +405,7 @@ const CDVD_API CDVDapi_Iso = ISOclose, ISOopen, + ISOprecache, ISOreadTrack, ISOgetBuffer, ISOreadSubQ, diff --git a/pcsx2/CDVD/ChdFileReader.cpp b/pcsx2/CDVD/ChdFileReader.cpp index 64443e4a12..c38b55371c 100644 --- a/pcsx2/CDVD/ChdFileReader.cpp +++ b/pcsx2/CDVD/ChdFileReader.cpp @@ -8,6 +8,8 @@ #include "common/Error.h" #include "common/FileSystem.h" #include "common/Path.h" +#include "common/ProgressCallback.h" +#include "common/SmallString.h" #include "common/StringUtil.h" #include "libchdr/chd.h" @@ -188,6 +190,32 @@ bool ChdFileReader::Open2(std::string filename, Error* error) return true; } +bool ChdFileReader::Precache2(ProgressCallback* progress, Error* error) +{ + if (!CheckAvailableMemoryForPrecaching(chd_get_compressed_size(ChdFile), error)) + return false; + + progress->SetProgressRange(100); + + const auto callback = [](size_t pos, size_t total, void* param) -> bool { + ProgressCallback* progress = static_cast(param); + const u32 percent = static_cast((pos * 100) / total); + progress->SetProgressValue(std::min(percent, 100)); + return !progress->IsCancelled(); + }; + + const chd_error cerror = chd_precache_progress(ChdFile, callback, progress); + if (cerror != CHDERR_NONE) + { + if (cerror != CHDERR_CANCELLED) + Error::SetStringView(error, "Failed to read part of the file."); + + return false; + } + + return true; +} + ThreadedFileReader::Chunk ChdFileReader::ChunkForOffset(u64 offset) { Chunk chunk = {0}; diff --git a/pcsx2/CDVD/ChdFileReader.h b/pcsx2/CDVD/ChdFileReader.h index 84adb62495..dca13b794f 100644 --- a/pcsx2/CDVD/ChdFileReader.h +++ b/pcsx2/CDVD/ChdFileReader.h @@ -16,6 +16,8 @@ public: ~ChdFileReader() override; bool Open2(std::string filename, Error* error) override; + + bool Precache2(ProgressCallback* progress, Error* error) override; Chunk ChunkForOffset(u64 offset) override; int ReadChunk(void* dst, s64 blockID) override; diff --git a/pcsx2/CDVD/CsoFileReader.cpp b/pcsx2/CDVD/CsoFileReader.cpp index 7289b035c1..a46f5bde81 100644 --- a/pcsx2/CDVD/CsoFileReader.cpp +++ b/pcsx2/CDVD/CsoFileReader.cpp @@ -84,6 +84,31 @@ bool CsoFileReader::Open2(std::string filename, Error* error) return true; } +bool CsoFileReader::Precache2(ProgressCallback* progress, Error* error) +{ + if (!m_src) + return false; + + const s64 size = FileSystem::FSize64(m_src); + if (size < 0 || !CheckAvailableMemoryForPrecaching(static_cast(size), error)) + return false; + + m_file_cache_size = static_cast(size); + m_file_cache = std::make_unique_for_overwrite(m_file_cache_size); + if (FileSystem::FSeek64(m_src, 0, SEEK_SET) != 0 || + FileSystem::ReadFileWithProgress( + m_src, m_file_cache.get(), m_file_cache_size, progress, error) != m_file_cache_size) + { + m_file_cache.reset(); + return false; + } + + m_readBuffer.reset(); + std::fclose(m_src); + m_src = nullptr; + return true; +} + bool CsoFileReader::ReadFileHeader(Error* error) { CsoHeader hdr; @@ -141,11 +166,7 @@ bool CsoFileReader::InitializeBuffers(Error* error) // initialize zlib if not a ZSO if (!m_uselz4) { - m_z_stream = std::make_unique(); - m_z_stream->zalloc = Z_NULL; - m_z_stream->zfree = Z_NULL; - m_z_stream->opaque = Z_NULL; - if (inflateInit2(m_z_stream.get(), -15) != Z_OK) + if (inflateInit2(&m_z_stream, -15) != Z_OK) { Error::SetString(error, "Unable to initialize zlib for CSO decompression."); return false; @@ -164,11 +185,10 @@ void CsoFileReader::Close2() fclose(m_src); m_src = nullptr; } - if (m_z_stream) - { - inflateEnd(m_z_stream.get()); - m_z_stream.reset(); - } + if (m_file_cache) + m_file_cache.reset(); + if (!m_uselz4) + inflateEnd(&m_z_stream); m_readBuffer.reset(); m_index.reset(); @@ -213,6 +233,16 @@ int CsoFileReader::ReadChunk(void* dst, s64 chunkID) if (!compressed) { + if (m_file_cache) + { + if (frameRawPos >= m_file_cache_size) + return 0; + + const size_t read_count = std::min(m_file_cache_size - frameRawPos, frameRawSize); + std::memcpy(dst, &m_file_cache[frameRawPos], read_count); + return static_cast(read_count); + } + // Just read directly, easy. if (FileSystem::FSeek64(m_src, frameRawPos, SEEK_SET) != 0) { @@ -223,21 +253,36 @@ int CsoFileReader::ReadChunk(void* dst, s64 chunkID) } else { - if (FileSystem::FSeek64(m_src, frameRawPos, SEEK_SET) != 0) - { - Console.Error("Unable to seek to compressed CSO data."); - return 0; - } // This might be less bytes than frameRawSize in case of padding on the last frame. // This is because the index positions must be aligned. - const u32 readRawBytes = fread(m_readBuffer.get(), 1, frameRawSize, m_src); + u32 readRawBytes; + u8* readBuffer; + if (m_file_cache) + { + if (frameRawPos >= m_file_cache_size) + return 0; + + readRawBytes = static_cast(std::min(m_file_cache_size - frameRawPos, frameRawSize)); + readBuffer = &m_file_cache[frameRawPos]; + } + else + { + if (FileSystem::FSeek64(m_src, frameRawPos, SEEK_SET) != 0) + { + Console.Error("Unable to seek to compressed CSO data."); + return 0; + } + readBuffer = m_readBuffer.get(); + readRawBytes = fread(m_readBuffer.get(), 1, frameRawSize, m_src); + } + bool success = false; if (m_uselz4) { const int src_size = static_cast(readRawBytes); const int dst_size = static_cast(m_frameSize); - const char* src_buf = reinterpret_cast(m_readBuffer.get()); + const char* src_buf = reinterpret_cast(readBuffer); char* dst_buf = static_cast(dst); const int res = LZ4_decompress_safe_partial(src_buf, dst_buf, src_size, dst_size, dst_size); @@ -245,20 +290,20 @@ int CsoFileReader::ReadChunk(void* dst, s64 chunkID) } else { - m_z_stream->next_in = m_readBuffer.get(); - m_z_stream->avail_in = readRawBytes; - m_z_stream->next_out = static_cast(dst); - m_z_stream->avail_out = m_frameSize; + m_z_stream.next_in = readBuffer; + m_z_stream.avail_in = readRawBytes; + m_z_stream.next_out = static_cast(dst); + m_z_stream.avail_out = m_frameSize; - const int status = inflate(m_z_stream.get(), Z_FINISH); - success = (status == Z_STREAM_END && m_z_stream->total_out == m_frameSize); + const int status = inflate(&m_z_stream, Z_FINISH); + success = (status == Z_STREAM_END && m_z_stream.total_out == m_frameSize); } if (!success) Console.Error(fmt::format("Unable to decompress CSO frame using {}", (m_uselz4)? "lz4":"zlib")); if (!m_uselz4) - inflateReset(m_z_stream.get()); + inflateReset(&m_z_stream); return success ? m_frameSize : 0; } diff --git a/pcsx2/CDVD/CsoFileReader.h b/pcsx2/CDVD/CsoFileReader.h index 3b063ca04c..49216f270b 100644 --- a/pcsx2/CDVD/CsoFileReader.h +++ b/pcsx2/CDVD/CsoFileReader.h @@ -7,7 +7,6 @@ #include struct CsoHeader; -typedef struct z_stream_s z_stream; class CsoFileReader final : public ThreadedFileReader { @@ -19,6 +18,8 @@ public: bool Open2(std::string filename, Error* error) override; + bool Precache2(ProgressCallback* progress, Error* error) override; + Chunk ChunkForOffset(u64 offset) override; int ReadChunk(void* dst, s64 chunkID) override; @@ -44,5 +45,7 @@ private: u64 m_totalSize = 0; // The actual source cso file handle. std::FILE* m_src = nullptr; - std::unique_ptr m_z_stream; + std::unique_ptr m_file_cache; + size_t m_file_cache_size = 0; + z_stream m_z_stream = {}; }; diff --git a/pcsx2/CDVD/FlatFileReader.cpp b/pcsx2/CDVD/FlatFileReader.cpp index e7a31216f3..071a9e3583 100644 --- a/pcsx2/CDVD/FlatFileReader.cpp +++ b/pcsx2/CDVD/FlatFileReader.cpp @@ -38,6 +38,25 @@ bool FlatFileReader::Open2(std::string filename, Error* error) return true; } +bool FlatFileReader::Precache2(ProgressCallback* progress, Error* error) +{ + if (!m_file || !CheckAvailableMemoryForPrecaching(m_file_size, error)) + return false; + + m_file_cache = std::make_unique_for_overwrite(m_file_size); + if (FileSystem::FSeek64(m_file, 0, SEEK_SET) != 0 || + FileSystem::ReadFileWithProgress( + m_file, m_file_cache.get(), m_file_size, progress, error) != m_file_size) + { + m_file_cache.reset(); + return false; + } + + std::fclose(m_file); + m_file = nullptr; + return true; +} + ThreadedFileReader::Chunk FlatFileReader::ChunkForOffset(u64 offset) { ThreadedFileReader::Chunk chunk = {}; @@ -61,6 +80,16 @@ int FlatFileReader::ReadChunk(void* dst, s64 blockID) return -1; const u64 file_offset = static_cast(blockID) * CHUNK_SIZE; + if (m_file_cache) + { + if (file_offset >= m_file_size) + return -1; + + const u64 read_size = std::min(m_file_size - file_offset, CHUNK_SIZE); + std::memcpy(dst, &m_file_cache[file_offset], read_size); + return static_cast(read_size); + } + if (FileSystem::FSeek64(m_file, file_offset, SEEK_SET) != 0) return -1; diff --git a/pcsx2/CDVD/FlatFileReader.h b/pcsx2/CDVD/FlatFileReader.h index cfa3729485..75db3ed981 100644 --- a/pcsx2/CDVD/FlatFileReader.h +++ b/pcsx2/CDVD/FlatFileReader.h @@ -12,6 +12,7 @@ class FlatFileReader final : public ThreadedFileReader DeclareNoncopyableObject(FlatFileReader); std::FILE* m_file = nullptr; + std::unique_ptr m_file_cache; u64 m_file_size = 0; public: @@ -20,6 +21,8 @@ public: bool Open2(std::string filename, Error* error) override; + bool Precache2(ProgressCallback* progress, Error* error) override; + Chunk ChunkForOffset(u64 offset) override; int ReadChunk(void* dst, s64 blockID) override; diff --git a/pcsx2/CDVD/InputIsoFile.cpp b/pcsx2/CDVD/InputIsoFile.cpp index ea19260a66..a518bea3ae 100644 --- a/pcsx2/CDVD/InputIsoFile.cpp +++ b/pcsx2/CDVD/InputIsoFile.cpp @@ -221,6 +221,11 @@ bool InputIsoFile::Open(std::string srcfile, Error* error) return true; } +bool InputIsoFile::Precache(ProgressCallback* progress, Error* error) +{ + return m_reader->Precache(progress, error); +} + void InputIsoFile::Close() { if (m_reader) diff --git a/pcsx2/CDVD/IsoFileFormats.h b/pcsx2/CDVD/IsoFileFormats.h index 8b343618d5..7e3d0ebdfd 100644 --- a/pcsx2/CDVD/IsoFileFormats.h +++ b/pcsx2/CDVD/IsoFileFormats.h @@ -10,6 +10,7 @@ #include class Error; +class ProgressCallback; enum isoType { @@ -65,6 +66,7 @@ public: } bool Open(std::string srcfile, Error* error); + bool Precache(ProgressCallback* progress, Error* error); void Close(); bool Detect(bool readType = true); diff --git a/pcsx2/CDVD/ThreadedFileReader.cpp b/pcsx2/CDVD/ThreadedFileReader.cpp index 51773f92ad..a1fabf8dc0 100644 --- a/pcsx2/CDVD/ThreadedFileReader.cpp +++ b/pcsx2/CDVD/ThreadedFileReader.cpp @@ -2,8 +2,15 @@ // SPDX-License-Identifier: LGPL-3.0+ #include "ThreadedFileReader.h" +#include "Host.h" +#include "common/Error.h" +#include "common/HostSys.h" +#include "common/Path.h" +#include "common/ProgressCallback.h" +#include "common/SmallString.h" #include "common/Threading.h" + #include // Make sure buffer size is bigger than the cutoff where PCSX2 emulates a seek @@ -244,6 +251,36 @@ bool ThreadedFileReader::TryCachedRead(void*& buffer, u64& offset, u32& size, co return allDone; } +bool ThreadedFileReader::Precache(ProgressCallback* progress, Error* error) +{ + CancelAndWaitUntilStopped(); + progress->SetStatusText(SmallString::from_format("Precaching {}...", Path::GetFileName(m_filename)).c_str()); + return Precache2(progress, error); +} + +bool ThreadedFileReader::Precache2(ProgressCallback* progress, Error* error) +{ + Error::SetStringView(error, "Precaching is not supported for this file format."); + return false; +} + +bool ThreadedFileReader::CheckAvailableMemoryForPrecaching(u64 required_size, Error* error) +{ + // Don't allow precaching to use more than 50% of system memory. + // Hopefully nobody's running 2-4GB potatoes anymore.... + const u64 memory_size = GetPhysicalMemory(); + const u64 max_precache_size = memory_size / 2; + if (required_size > max_precache_size) + { + Error::SetStringFmt(error, + TRANSLATE_FS("CDVD", "Required memory ({}GB) is the above the maximum allowed ({}GB)."), + required_size / _1gb, max_precache_size / _1gb); + return false; + } + + return true; +} + bool ThreadedFileReader::Open(std::string filename, Error* error) { CancelAndWaitUntilStopped(); diff --git a/pcsx2/CDVD/ThreadedFileReader.h b/pcsx2/CDVD/ThreadedFileReader.h index ce90726042..5cb05198d7 100644 --- a/pcsx2/CDVD/ThreadedFileReader.h +++ b/pcsx2/CDVD/ThreadedFileReader.h @@ -11,6 +11,7 @@ #include class Error; +class ProgressCallback; /// A file reader for use with compressed formats /// Calls decompression code on a separate thread to make a synchronous decompression API async @@ -42,8 +43,12 @@ protected: virtual int ReadChunk(void* dst, s64 chunkID) = 0; /// AsyncFileReader open but ThreadedFileReader needs prep work first virtual bool Open2(std::string filename, Error* error) = 0; + /// AsyncFileReader precache but ThreadedFileReader needs prep work first + virtual bool Precache2(ProgressCallback* progress, Error* error); /// AsyncFileReader close but ThreadedFileReader needs prep work first virtual void Close2() = 0; + /// Checks system memory, to ensure that precaching would not exceed a reasonable amount. + bool CheckAvailableMemoryForPrecaching(u64 required_size, Error* error); ThreadedFileReader(); @@ -109,7 +114,9 @@ public: virtual u32 GetBlockCount() const = 0; + bool Open(std::string filename, Error* error); + bool Precache(ProgressCallback* progress, Error* error); int ReadSync(void* pBuffer, u32 sector, u32 count); void BeginRead(void* pBuffer, u32 sector, u32 count); int FinishRead(); diff --git a/pcsx2/Config.h b/pcsx2/Config.h index cc4299a03b..2790d22e68 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -1111,6 +1111,7 @@ struct Pcsx2Config bool CdvdVerboseReads : 1, // enables cdvd read activity verbosely dumped to the console CdvdDumpBlocks : 1, // enables cdvd block dumping + CdvdPrecache : 1, // enables cdvd precaching of compressed images EnablePatches : 1, // enables patch detection and application EnableCheats : 1, // enables cheat detection and application EnablePINE : 1, // enables inter-process communication diff --git a/pcsx2/Host.h b/pcsx2/Host.h index da7c87b28b..29c28a1c74 100644 --- a/pcsx2/Host.h +++ b/pcsx2/Host.h @@ -16,6 +16,7 @@ #include #include +class ProgressCallback; class SettingsInterface; namespace Host @@ -131,6 +132,9 @@ namespace Host /// Sets host-specific default settings. void SetDefaultUISettings(SettingsInterface& si); + /// Creates a progress callback that displays in the host. + std::unique_ptr CreateHostProgressCallback(); + namespace Internal { /// Retrieves the base settings layer. Must call with lock held. diff --git a/pcsx2/ImGui/FullscreenUI.cpp b/pcsx2/ImGui/FullscreenUI.cpp index 6604573c07..94e0c35d9d 100644 --- a/pcsx2/ImGui/FullscreenUI.cpp +++ b/pcsx2/ImGui/FullscreenUI.cpp @@ -3330,6 +3330,9 @@ void FullscreenUI::DrawEmulationSettingsPage() "EmuCore/Speedhacks", "fastCDVD", false); } + DrawToggleSetting(bsi, FSUI_CSTR("Enable CDVD Precaching"), FSUI_CSTR("Loads the disc image into RAM before starting the virtual machine."), + "EmuCore", "CdvdPrecache", false); + MenuHeading(FSUI_CSTR("Frame Pacing/Latency Control")); bool optimal_frame_pacing = (bsi->GetIntValue("EmuCore/GS", "VsyncQueueSize", DEFAULT_FRAME_LATENCY) == 0); @@ -6949,6 +6952,8 @@ TRANSLATE_NOOP("FullscreenUI", "Enable Host Filesystem"); TRANSLATE_NOOP("FullscreenUI", "Enables access to files from the host: namespace in the virtual machine."); TRANSLATE_NOOP("FullscreenUI", "Enable Fast CDVD"); TRANSLATE_NOOP("FullscreenUI", "Fast disc access, less loading times. Not recommended."); +TRANSLATE_NOOP("FullscreenUI", "Enable CDVD Precaching"); +TRANSLATE_NOOP("FullscreenUI", "Loads the disc image into RAM before starting the virtual machine."); TRANSLATE_NOOP("FullscreenUI", "Frame Pacing/Latency Control"); TRANSLATE_NOOP("FullscreenUI", "Maximum Frame Latency"); TRANSLATE_NOOP("FullscreenUI", "Sets the number of frames which can be queued."); diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index 0d1e6813f8..ffda233026 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -1706,6 +1706,7 @@ void Pcsx2Config::LoadSaveCore(SettingsWrapper& wrap) SettingsWrapBitBool(CdvdVerboseReads); SettingsWrapBitBool(CdvdDumpBlocks); + SettingsWrapBitBool(CdvdPrecache); SettingsWrapBitBool(EnablePatches); SettingsWrapBitBool(EnableCheats); SettingsWrapBitBool(EnablePINE); diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index c40bd0bf12..487ec6ea0b 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -113,6 +113,7 @@ namespace VMManager static void ReportGameChangeToHost(); static bool HasBootedELF(); static bool HasValidOrInitializingVM(); + static void PrecacheCDVDFile(); static std::string GetCurrentSaveStateFileName(s32 slot); static bool DoLoadState(const char* filename); @@ -1223,6 +1224,27 @@ bool VMManager::AutoDetectSource(const std::string& filename) } } +void VMManager::PrecacheCDVDFile() +{ + Error error; + std::unique_ptr progress = Host::CreateHostProgressCallback(); + if (!DoCDVDprecache(progress.get(), &error)) + { + if (progress->IsCancelled()) + { + Host::AddIconOSDMessage("PrecacheCDVDFile", ICON_FA_COMPACT_DISC, + TRANSLATE_STR("VMManager", "CDVD precaching was cancelled."), + Host::OSD_WARNING_DURATION); + } + else + { + Host::AddIconOSDMessage("PrecacheCDVDFile", ICON_FA_EXCLAMATION_TRIANGLE, + fmt::format(TRANSLATE_FS("VMManager", "CDVD precaching failed: {}"), error.GetDescription()), + Host::OSD_ERROR_DURATION); + } + } +} + bool VMManager::Initialize(VMBootParameters boot_params) { const Common::Timer init_timer; @@ -1522,6 +1544,9 @@ bool VMManager::Initialize(VMBootParameters boot_params) close_cdvd_files.Cancel(); close_state.Cancel(); + if (EmuConfig.CdvdPrecache) + PrecacheCDVDFile(); + hwReset(); Console.WriteLn("VM subsystems initialized in %.2f ms", init_timer.GetTimeMilliseconds()); diff --git a/tests/ctest/core/StubHost.cpp b/tests/ctest/core/StubHost.cpp index 5982b75b1e..ce2d95fb7e 100644 --- a/tests/ctest/core/StubHost.cpp +++ b/tests/ctest/core/StubHost.cpp @@ -11,6 +11,7 @@ #include "pcsx2/ImGui/ImGuiManager.h" #include "pcsx2/Input/InputManager.h" #include "pcsx2/VMManager.h" +#include "common/ProgressCallback.h" void Host::CommitBaseSettingChanges() { @@ -33,6 +34,11 @@ void Host::SetDefaultUISettings(SettingsInterface& si) { } +std::unique_ptr Host::CreateHostProgressCallback() +{ + return ProgressCallback::CreateNullProgressCallback(); +} + void Host::ReportErrorAsync(const std::string_view title, const std::string_view message) { }