From 7522c8470ee27d50a68ba662ae721b69018f3a8f Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:24:46 +0200 Subject: [PATCH 01/35] resource: move fontawesome to .rodata (#1259) --- src/resource/embedded/fontawesome.S | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resource/embedded/fontawesome.S b/src/resource/embedded/fontawesome.S index 04da3b24..29b4f93a 100644 --- a/src/resource/embedded/fontawesome.S +++ b/src/resource/embedded/fontawesome.S @@ -1,4 +1,4 @@ -.section .text +.rodata .global g_fontawesome_data, g_fontawesome_size g_fontawesome_data: From 64232ffdbddd39cf14eed0ef457273af67277f3c Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 23 Jul 2024 03:13:36 +0200 Subject: [PATCH 02/35] Windows default to non-portable + Reworked MLC handling and related UI (#1252) --- src/config/ActiveSettings.cpp | 52 ++-- src/config/ActiveSettings.h | 23 +- src/config/CMakeLists.txt | 4 - src/config/CemuConfig.cpp | 18 -- src/config/CemuConfig.h | 2 +- src/config/PermanentConfig.cpp | 65 ----- src/config/PermanentConfig.h | 18 -- src/config/PermanentStorage.cpp | 76 ------ src/config/PermanentStorage.h | 27 -- src/gui/CemuApp.cpp | 407 +++++++++++++++++++------------ src/gui/CemuApp.h | 13 +- src/gui/GeneralSettings2.cpp | 154 ++++++------ src/gui/GeneralSettings2.h | 3 +- src/gui/GettingStartedDialog.cpp | 224 +++++++---------- src/gui/GettingStartedDialog.h | 34 +-- src/gui/MainWindow.cpp | 30 +-- src/gui/MainWindow.h | 2 - src/main.cpp | 14 +- 18 files changed, 515 insertions(+), 651 deletions(-) delete mode 100644 src/config/PermanentConfig.cpp delete mode 100644 src/config/PermanentConfig.h delete mode 100644 src/config/PermanentStorage.cpp delete mode 100644 src/config/PermanentStorage.h diff --git a/src/config/ActiveSettings.cpp b/src/config/ActiveSettings.cpp index 07e6f16d..560f2986 100644 --- a/src/config/ActiveSettings.cpp +++ b/src/config/ActiveSettings.cpp @@ -7,41 +7,47 @@ #include "config/LaunchSettings.h" #include "util/helpers/helpers.h" -std::set -ActiveSettings::LoadOnce( - const fs::path& executablePath, - const fs::path& userDataPath, - const fs::path& configPath, - const fs::path& cachePath, - const fs::path& dataPath) +void ActiveSettings::SetPaths(bool isPortableMode, + const fs::path& executablePath, + const fs::path& userDataPath, + const fs::path& configPath, + const fs::path& cachePath, + const fs::path& dataPath, + std::set& failedWriteAccess) { + cemu_assert_debug(!s_setPathsCalled); // can only change paths before loading + s_isPortableMode = isPortableMode; s_executable_path = executablePath; s_user_data_path = userDataPath; s_config_path = configPath; s_cache_path = cachePath; s_data_path = dataPath; - std::set failed_write_access; + failedWriteAccess.clear(); for (auto&& path : {userDataPath, configPath, cachePath}) { - if (!fs::exists(path)) - { - std::error_code ec; + std::error_code ec; + if (!fs::exists(path, ec)) fs::create_directories(path, ec); - } if (!TestWriteAccess(path)) { cemuLog_log(LogType::Force, "Failed to write to {}", _pathToUtf8(path)); - failed_write_access.insert(path); + failedWriteAccess.insert(path); } } - s_executable_filename = s_executable_path.filename(); + s_setPathsCalled = true; +} - g_config.SetFilename(GetConfigPath("settings.xml").generic_wstring()); - g_config.Load(); +[[nodiscard]] bool ActiveSettings::IsPortableMode() +{ + return s_isPortableMode; +} + +void ActiveSettings::Init() +{ + cemu_assert_debug(s_setPathsCalled); std::string additionalErrorInfo; s_has_required_online_files = iosuCrypt_checkRequirementsForOnlineMode(additionalErrorInfo) == IOS_CRYPTO_ONLINE_REQ_OK; - return failed_write_access; } bool ActiveSettings::LoadSharedLibrariesEnabled() @@ -229,6 +235,7 @@ bool ActiveSettings::ForceSamplerRoundToPrecision() fs::path ActiveSettings::GetMlcPath() { + cemu_assert_debug(s_setPathsCalled); if(const auto launch_mlc = LaunchSettings::GetMLCPath(); launch_mlc.has_value()) return launch_mlc.value(); @@ -238,6 +245,17 @@ fs::path ActiveSettings::GetMlcPath() return GetDefaultMLCPath(); } +bool ActiveSettings::IsCustomMlcPath() +{ + cemu_assert_debug(s_setPathsCalled); + return !GetConfig().mlc_path.GetValue().empty(); +} + +bool ActiveSettings::IsCommandLineMlcPath() +{ + return LaunchSettings::GetMLCPath().has_value(); +} + fs::path ActiveSettings::GetDefaultMLCPath() { return GetUserDataPath("mlc01"); diff --git a/src/config/ActiveSettings.h b/src/config/ActiveSettings.h index 54052741..e672fbee 100644 --- a/src/config/ActiveSettings.h +++ b/src/config/ActiveSettings.h @@ -34,12 +34,16 @@ private: public: // Set directories and return all directories that failed write access test - static std::set - LoadOnce(const fs::path& executablePath, - const fs::path& userDataPath, - const fs::path& configPath, - const fs::path& cachePath, - const fs::path& dataPath); + static void + SetPaths(bool isPortableMode, + const fs::path& executablePath, + const fs::path& userDataPath, + const fs::path& configPath, + const fs::path& cachePath, + const fs::path& dataPath, + std::set& failedWriteAccess); + + static void Init(); [[nodiscard]] static fs::path GetExecutablePath() { return s_executable_path; } [[nodiscard]] static fs::path GetExecutableFilename() { return s_executable_filename; } @@ -56,11 +60,14 @@ public: template [[nodiscard]] static fs::path GetMlcPath(TArgs&&... args){ return GetPath(GetMlcPath(), std::forward(args)...); }; + static bool IsCustomMlcPath(); + static bool IsCommandLineMlcPath(); // get mlc path to default cemu root dir/mlc01 [[nodiscard]] static fs::path GetDefaultMLCPath(); private: + inline static bool s_isPortableMode{false}; inline static fs::path s_executable_path; inline static fs::path s_user_data_path; inline static fs::path s_config_path; @@ -70,6 +77,9 @@ private: inline static fs::path s_mlc_path; public: + // can be called before Init + [[nodiscard]] static bool IsPortableMode(); + // general [[nodiscard]] static bool LoadSharedLibrariesEnabled(); [[nodiscard]] static bool DisplayDRCEnabled(); @@ -111,6 +121,7 @@ public: [[nodiscard]] static bool ForceSamplerRoundToPrecision(); private: + inline static bool s_setPathsCalled = false; // dump options inline static bool s_dump_shaders = false; inline static bool s_dump_textures = false; diff --git a/src/config/CMakeLists.txt b/src/config/CMakeLists.txt index f02b95d4..d53e8574 100644 --- a/src/config/CMakeLists.txt +++ b/src/config/CMakeLists.txt @@ -8,10 +8,6 @@ add_library(CemuConfig LaunchSettings.h NetworkSettings.cpp NetworkSettings.h - PermanentConfig.cpp - PermanentConfig.h - PermanentStorage.cpp - PermanentStorage.h XMLConfig.h ) diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 8e7cf398..03b12731 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -5,7 +5,6 @@ #include -#include "PermanentConfig.h" #include "ActiveSettings.h" XMLCemuConfig_t g_config(L"settings.xml"); @@ -15,23 +14,6 @@ void CemuConfig::SetMLCPath(fs::path path, bool save) mlc_path.SetValue(_pathToUtf8(path)); if(save) g_config.Save(); - - // if custom mlc path has been selected, store it in permanent config - if (path != ActiveSettings::GetDefaultMLCPath()) - { - try - { - auto pconfig = PermanentConfig::Load(); - pconfig.custom_mlc_path = _pathToUtf8(path); - pconfig.Store(); - } - catch (const PSDisabledException&) {} - catch (const std::exception& ex) - { - cemuLog_log(LogType::Force, "can't store custom mlc path in permanent storage: {}", ex.what()); - } - } - Account::RefreshAccounts(); } diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index d0776d2e..3f3da953 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -417,7 +417,7 @@ struct CemuConfig ConfigValue save_screenshot{true}; ConfigValue did_show_vulkan_warning{false}; - ConfigValue did_show_graphic_pack_download{false}; + ConfigValue did_show_graphic_pack_download{false}; // no longer used but we keep the config value around in case people downgrade Cemu. Despite the name this was used for the Getting Started dialog ConfigValue did_show_macos_disclaimer{false}; ConfigValue show_icon_column{ true }; diff --git a/src/config/PermanentConfig.cpp b/src/config/PermanentConfig.cpp deleted file mode 100644 index 20a44d28..00000000 --- a/src/config/PermanentConfig.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "PermanentConfig.h" - -#include "pugixml.hpp" - -#include "PermanentStorage.h" - -struct xml_string_writer : pugi::xml_writer -{ - std::string result; - - void write(const void* data, size_t size) override - { - result.append(static_cast(data), size); - } -}; - -std::string PermanentConfig::ToXMLString() const noexcept -{ - pugi::xml_document doc; - doc.append_child(pugi::node_declaration).append_attribute("encoding") = "UTF-8"; - auto root = doc.append_child("config"); - root.append_child("MlcPath").text().set(this->custom_mlc_path.c_str()); - - xml_string_writer writer; - doc.save(writer); - return writer.result; -} - -PermanentConfig PermanentConfig::FromXMLString(std::string_view str) noexcept -{ - PermanentConfig result{}; - - pugi::xml_document doc; - if(doc.load_buffer(str.data(), str.size())) - { - result.custom_mlc_path = doc.select_node("/config/MlcPath").node().text().as_string(); - } - - return result; -} - -PermanentConfig PermanentConfig::Load() -{ - PermanentStorage storage; - - const auto str = storage.ReadFile(kFileName); - if (!str.empty()) - return FromXMLString(str); - - return {}; -} - -bool PermanentConfig::Store() noexcept -{ - try - { - PermanentStorage storage; - storage.WriteStringToFile(kFileName, ToXMLString()); - } - catch (...) - { - return false; - } - return true; -} diff --git a/src/config/PermanentConfig.h b/src/config/PermanentConfig.h deleted file mode 100644 index 8c134747..00000000 --- a/src/config/PermanentConfig.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "PermanentStorage.h" - -struct PermanentConfig -{ - static constexpr const char* kFileName = "perm_setting.xml"; - - std::string custom_mlc_path; - - [[nodiscard]] std::string ToXMLString() const noexcept; - static PermanentConfig FromXMLString(std::string_view str) noexcept; - - // gets from permanent storage - static PermanentConfig Load(); - // saves to permanent storage - bool Store() noexcept; -}; diff --git a/src/config/PermanentStorage.cpp b/src/config/PermanentStorage.cpp deleted file mode 100644 index e095ff4b..00000000 --- a/src/config/PermanentStorage.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "PermanentStorage.h" -#include "config/CemuConfig.h" -#include "util/helpers/SystemException.h" - -PermanentStorage::PermanentStorage() -{ - if (!GetConfig().permanent_storage) - throw PSDisabledException(); - - const char* appdata = std::getenv("LOCALAPPDATA"); - if (!appdata) - throw std::runtime_error("can't get LOCALAPPDATA"); - m_storage_path = appdata; - m_storage_path /= "Cemu"; - - fs::create_directories(m_storage_path); -} - -PermanentStorage::~PermanentStorage() -{ - if (m_remove_storage) - { - std::error_code ec; - fs::remove_all(m_storage_path, ec); - if (ec) - { - SystemException ex(ec); - cemuLog_log(LogType::Force, "can't remove permanent storage: {}", ex.what()); - } - } -} - -void PermanentStorage::ClearAllFiles() const -{ - fs::remove_all(m_storage_path); - fs::create_directories(m_storage_path); -} - -void PermanentStorage::RemoveStorage() -{ - m_remove_storage = true; -} - -void PermanentStorage::WriteStringToFile(std::string_view filename, std::string_view content) -{ - const auto name = m_storage_path.append(filename.data(), filename.data() + filename.size()); - std::ofstream file(name.string()); - file.write(content.data(), (uint32_t)content.size()); -} - -std::string PermanentStorage::ReadFile(std::string_view filename) noexcept -{ - try - { - const auto name = m_storage_path.append(filename.data(), filename.data() + filename.size()); - std::ifstream file(name, std::ios::in | std::ios::ate); - if (!file.is_open()) - return {}; - - const auto end = file.tellg(); - file.seekg(0, std::ios::beg); - const auto file_size = end - file.tellg(); - if (file_size == 0) - return {}; - - std::string result; - result.resize(file_size); - file.read(result.data(), file_size); - return result; - } - catch (...) - { - return {}; - } - -} diff --git a/src/config/PermanentStorage.h b/src/config/PermanentStorage.h deleted file mode 100644 index 3cda3d6d..00000000 --- a/src/config/PermanentStorage.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -// disabled by config -class PSDisabledException : public std::runtime_error -{ -public: - PSDisabledException() - : std::runtime_error("permanent storage is disabled by user") {} -}; - -class PermanentStorage -{ -public: - PermanentStorage(); - ~PermanentStorage(); - - void ClearAllFiles() const; - // flags storage to be removed on destruction - void RemoveStorage(); - - void WriteStringToFile(std::string_view filename, std::string_view content); - std::string ReadFile(std::string_view filename) noexcept; - -private: - fs::path m_storage_path; - bool m_remove_storage = false; -}; \ No newline at end of file diff --git a/src/gui/CemuApp.cpp b/src/gui/CemuApp.cpp index 86d81e43..baa83888 100644 --- a/src/gui/CemuApp.cpp +++ b/src/gui/CemuApp.cpp @@ -3,11 +3,11 @@ #include "gui/wxgui.h" #include "config/CemuConfig.h" #include "Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h" +#include "Cafe/HW/Latte/Core/LatteOverlay.h" #include "gui/guiWrapper.h" #include "config/ActiveSettings.h" +#include "config/LaunchSettings.h" #include "gui/GettingStartedDialog.h" -#include "config/PermanentConfig.h" -#include "config/PermanentStorage.h" #include "input/InputManager.h" #include "gui/helpers/wxHelpers.h" #include "Cemu/ncrypto/ncrypto.h" @@ -30,7 +30,10 @@ wxIMPLEMENT_APP_NO_MAIN(CemuApp); extern WindowInfo g_window_info; extern std::shared_mutex g_mutex; -int mainEmulatorHLE(); +// forward declarations from main.cpp +void UnitTests(); +void CemuCommonInit(); + void HandlePostUpdate(); // Translation strings to extract for gettext: void unused_translation_dummy() @@ -54,34 +57,86 @@ void unused_translation_dummy() void(_("unknown")); } -bool CemuApp::OnInit() +#if BOOST_OS_WINDOWS +#include +fs::path GetAppDataRoamingPath() { + PWSTR path = nullptr; + HRESULT result = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &path); + if (result != S_OK || !path) + { + if (path) + CoTaskMemFree(path); + return {}; + } + std::string appDataPath = boost::nowide::narrow(path); + CoTaskMemFree(path); + return _utf8ToPath(appDataPath); +} +#endif + +#if BOOST_OS_WINDOWS +void CemuApp::DeterminePaths(std::set& failedWriteAccess) // for Windows +{ + std::error_code ec; + bool isPortable = false; fs::path user_data_path, config_path, cache_path, data_path; auto standardPaths = wxStandardPaths::Get(); fs::path exePath(wxHelper::MakeFSPath(standardPaths.GetExecutablePath())); + fs::path portablePath = exePath.parent_path() / "portable"; + data_path = exePath.parent_path(); // the data path is always the same as the exe path + if (fs::exists(portablePath, ec)) + { + isPortable = true; + user_data_path = config_path = cache_path = portablePath; + } + else + { + fs::path roamingPath = GetAppDataRoamingPath() / "Cemu"; + user_data_path = config_path = cache_path = roamingPath; + } + // on Windows Cemu used to be portable by default prior to 2.0-89 + // to remain backwards compatible with old installations we check for settings.xml in the Cemu directory + // if it exists, we use the exe path as the portable directory + if(!isPortable) // lower priority than portable directory + { + if (fs::exists(exePath.parent_path() / "settings.xml", ec)) + { + isPortable = true; + user_data_path = config_path = cache_path = exePath.parent_path(); + } + } + ActiveSettings::SetPaths(isPortable, exePath, user_data_path, config_path, cache_path, data_path, failedWriteAccess); +} +#endif + #if BOOST_OS_LINUX +void CemuApp::DeterminePaths(std::set& failedWriteAccess) // for Linux +{ + std::error_code ec; + bool isPortable = false; + fs::path user_data_path, config_path, cache_path, data_path; + auto standardPaths = wxStandardPaths::Get(); + fs::path exePath(wxHelper::MakeFSPath(standardPaths.GetExecutablePath())); + fs::path portablePath = exePath.parent_path() / "portable"; // GetExecutablePath returns the AppImage's temporary mount location wxString appImagePath; if (wxGetEnv(("APPIMAGE"), &appImagePath)) - exePath = wxHelper::MakeFSPath(appImagePath); -#endif - // Try a portable path first, if it exists. - user_data_path = config_path = cache_path = data_path = exePath.parent_path() / "portable"; -#if BOOST_OS_MACOS - // If run from an app bundle, use its parent directory. - fs::path appPath = exePath.parent_path().parent_path().parent_path(); - if (appPath.extension() == ".app") - user_data_path = config_path = cache_path = data_path = appPath.parent_path() / "portable"; -#endif - - if (!fs::exists(user_data_path)) { -#if BOOST_OS_WINDOWS - user_data_path = config_path = cache_path = data_path = exePath.parent_path(); -#else + exePath = wxHelper::MakeFSPath(appImagePath); + portablePath = exePath.parent_path() / "portable"; + } + if (fs::exists(portablePath, ec)) + { + isPortable = true; + user_data_path = config_path = cache_path = portablePath; + // in portable mode assume the data directories (resources, gameProfiles/default/) are next to the executable + data_path = exePath.parent_path(); + } + else + { SetAppName("Cemu"); - wxString appName=GetAppName(); -#if BOOST_OS_LINUX + wxString appName = GetAppName(); standardPaths.SetFileLayout(wxStandardPaths::FileLayout::FileLayout_XDG); auto getEnvDir = [&](const wxString& varName, const wxString& defaultValue) { @@ -90,33 +145,151 @@ bool CemuApp::OnInit() return defaultValue; return dir; }; - wxString homeDir=wxFileName::GetHomeDir(); + wxString homeDir = wxFileName::GetHomeDir(); user_data_path = (getEnvDir(wxS("XDG_DATA_HOME"), homeDir + wxS("/.local/share")) + "/" + appName).ToStdString(); config_path = (getEnvDir(wxS("XDG_CONFIG_HOME"), homeDir + wxS("/.config")) + "/" + appName).ToStdString(); -#else - user_data_path = config_path = standardPaths.GetUserDataDir().ToStdString(); -#endif data_path = standardPaths.GetDataDir().ToStdString(); cache_path = standardPaths.GetUserDir(wxStandardPaths::Dir::Dir_Cache).ToStdString(); cache_path /= appName.ToStdString(); -#endif } + ActiveSettings::SetPaths(isPortable, exePath, user_data_path, config_path, cache_path, data_path, failedWriteAccess); +} +#endif - auto failed_write_access = ActiveSettings::LoadOnce(exePath, user_data_path, config_path, cache_path, data_path); - for (auto&& path : failed_write_access) - wxMessageBox(formatWxString(_("Cemu can't write to {}!"), wxString::FromUTF8(_pathToUtf8(path))), - _("Warning"), wxOK | wxCENTRE | wxICON_EXCLAMATION, nullptr); +#if BOOST_OS_MACOS +void CemuApp::DeterminePaths(std::set& failedWriteAccess) // for MacOS +{ + std::error_code ec; + bool isPortable = false; + fs::path user_data_path, config_path, cache_path, data_path; + auto standardPaths = wxStandardPaths::Get(); + fs::path exePath(wxHelper::MakeFSPath(standardPaths.GetExecutablePath())); + // If run from an app bundle, use its parent directory + fs::path appPath = exePath.parent_path().parent_path().parent_path(); + fs::path portablePath = appPath.extension() == ".app" ? appPath.parent_path() / "portable" : exePath.parent_path() / "portable"; + if (fs::exists(portablePath, ec)) + { + isPortable = true; + user_data_path = config_path = cache_path = portablePath; + data_path = exePath.parent_path(); + } + else + { + SetAppName("Cemu"); + wxString appName = GetAppName(); + user_data_path = config_path = standardPaths.GetUserDataDir().ToStdString(); + data_path = standardPaths.GetDataDir().ToStdString(); + cache_path = standardPaths.GetUserDir(wxStandardPaths::Dir::Dir_Cache).ToStdString(); + cache_path /= appName.ToStdString(); + } + ActiveSettings::SetPaths(isPortable, exePath, user_data_path, config_path, cache_path, data_path, failedWriteAccess); +} +#endif + +// create default MLC files or quit if it fails +void CemuApp::InitializeNewMLCOrFail(fs::path mlc) +{ + if( CemuApp::CreateDefaultMLCFiles(mlc) ) + return; // all good + cemu_assert_debug(!ActiveSettings::IsCustomMlcPath()); // should not be possible? + + if(ActiveSettings::IsCommandLineMlcPath() || ActiveSettings::IsCustomMlcPath()) + { + // tell user that the custom path is not writable + wxMessageBox(formatWxString(_("Cemu failed to write to the custom mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); + exit(0); + } + wxMessageBox(formatWxString(_("Cemu failed to write to the mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); + exit(0); +} + +void CemuApp::InitializeExistingMLCOrFail(fs::path mlc) +{ + if(CreateDefaultMLCFiles(mlc)) + return; // all good + // failed to write mlc files + if(ActiveSettings::IsCommandLineMlcPath() || ActiveSettings::IsCustomMlcPath()) + { + // tell user that the custom path is not writable + // if it's a command line path then just quit. Otherwise ask if user wants to reset the path + if(ActiveSettings::IsCommandLineMlcPath()) + { + wxMessageBox(formatWxString(_("Cemu failed to write to the custom mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); + exit(0); + } + // ask user if they want to reset the path + const wxString message = formatWxString(_("Cemu failed to write to the custom mlc directory.\n\nThe path is:\n{}\n\nCemu cannot start without a valid mlc path. Do you want to reset the path? You can later change it again in the General Settings."), + _pathToUtf8(mlc)); + wxMessageDialog dialog(nullptr, message, _("Error"), wxCENTRE | wxYES_NO | wxICON_WARNING); + dialog.SetYesNoLabels(_("Reset path"), _("Exit")); + const auto dialogResult = dialog.ShowModal(); + if (dialogResult == wxID_NO) + exit(0); + else // reset path + { + GetConfig().mlc_path = ""; + g_config.Save(); + } + } +} + +bool CemuApp::OnInit() +{ + std::set failedWriteAccess; + DeterminePaths(failedWriteAccess); + // make sure default cemu directories exist + CreateDefaultCemuFiles(); + + g_config.SetFilename(ActiveSettings::GetConfigPath("settings.xml").generic_wstring()); + + std::error_code ec; + bool isFirstStart = !fs::exists(ActiveSettings::GetConfigPath("settings.xml"), ec); NetworkConfig::LoadOnce(); - g_config.Load(); + if(!isFirstStart) + { + g_config.Load(); + LocalizeUI(static_cast(GetConfig().language == wxLANGUAGE_DEFAULT ? wxLocale::GetSystemLanguage() : GetConfig().language.GetValue())); + } + else + { + LocalizeUI(static_cast(wxLocale::GetSystemLanguage())); + } + for (auto&& path : failedWriteAccess) + { + wxMessageBox(formatWxString(_("Cemu can't write to {}!"), wxString::FromUTF8(_pathToUtf8(path))), + _("Warning"), wxOK | wxCENTRE | wxICON_EXCLAMATION, nullptr); + } + + if (isFirstStart) + { + // show the getting started dialog + GettingStartedDialog dia(nullptr); + dia.ShowModal(); + // make sure config is created. Gfx pack UI and input UI may create it earlier already, but we still want to update it + g_config.Save(); + // create mlc, on failure the user can quit here. So do this after the Getting Started dialog + InitializeNewMLCOrFail(ActiveSettings::GetMlcPath()); + } + else + { + // check if mlc is valid and recreate default files + InitializeExistingMLCOrFail(ActiveSettings::GetMlcPath()); + } + + ActiveSettings::Init(); // this is a bit of a misnomer, right now this call only loads certs for online play. In the future we should move the logic to a more appropriate place HandlePostUpdate(); - mainEmulatorHLE(); + + LatteOverlay_init(); + // run a couple of tests if in non-release mode +#ifdef CEMU_DEBUG_ASSERT + UnitTests(); +#endif + CemuCommonInit(); wxInitAllImageHandlers(); - LocalizeUI(); - // fill colour db wxTheColourDatabase->AddColour("ERROR", wxColour(0xCC, 0, 0)); wxTheColourDatabase->AddColour("SUCCESS", wxColour(0, 0xbb, 0)); @@ -135,15 +308,8 @@ bool CemuApp::OnInit() Bind(wxEVT_ACTIVATE_APP, &CemuApp::ActivateApp, this); auto& config = GetConfig(); - const bool first_start = !config.did_show_graphic_pack_download; - - CreateDefaultFiles(first_start); - m_mainFrame = new MainWindow(); - if (first_start) - m_mainFrame->ShowGettingStartedDialog(); - std::unique_lock lock(g_mutex); g_window_info.app_active = true; @@ -230,22 +396,22 @@ std::vector CemuApp::GetLanguages() const { return availableLanguages; } -void CemuApp::LocalizeUI() +void CemuApp::LocalizeUI(wxLanguage languageToUse) { std::unique_ptr translationsMgr(new wxTranslations()); m_availableTranslations = GetAvailableTranslationLanguages(translationsMgr.get()); - const sint32 configuredLanguage = GetConfig().language; bool isTranslationAvailable = std::any_of(m_availableTranslations.begin(), m_availableTranslations.end(), - [configuredLanguage](const wxLanguageInfo* info) { return info->Language == configuredLanguage; }); - if (configuredLanguage == wxLANGUAGE_DEFAULT || isTranslationAvailable) + [languageToUse](const wxLanguageInfo* info) { return info->Language == languageToUse; }); + if (languageToUse == wxLANGUAGE_DEFAULT || isTranslationAvailable) { - translationsMgr->SetLanguage(static_cast(configuredLanguage)); + translationsMgr->SetLanguage(static_cast(languageToUse)); translationsMgr->AddCatalog("cemu"); - if (translationsMgr->IsLoaded("cemu") && wxLocale::IsAvailable(configuredLanguage)) - m_locale.Init(configuredLanguage); - + if (translationsMgr->IsLoaded("cemu") && wxLocale::IsAvailable(languageToUse)) + { + m_locale.Init(languageToUse); + } // This must be run after wxLocale::Init, as the latter sets up its own wxTranslations instance which we want to override wxTranslations::Set(translationsMgr.release()); } @@ -264,55 +430,47 @@ std::vector CemuApp::GetAvailableTranslationLanguages(wxT return languages; } -void CemuApp::CreateDefaultFiles(bool first_start) +bool CemuApp::CheckMLCPath(const fs::path& mlc) { std::error_code ec; - fs::path mlc = ActiveSettings::GetMlcPath(); - // check for mlc01 folder missing if custom path has been set - if (!fs::exists(mlc, ec) && !first_start) - { - const wxString message = formatWxString(_("Your mlc01 folder seems to be missing.\n\nThis is where Cemu stores save files, game updates and other Wii U files.\n\nThe expected path is:\n{}\n\nDo you want to create the folder at the expected path?"), - _pathToUtf8(mlc)); - - wxMessageDialog dialog(nullptr, message, _("Error"), wxCENTRE | wxYES_NO | wxCANCEL| wxICON_WARNING); - dialog.SetYesNoCancelLabels(_("Yes"), _("No"), _("Select a custom path")); - const auto dialogResult = dialog.ShowModal(); - if (dialogResult == wxID_NO) - exit(0); - else if(dialogResult == wxID_CANCEL) - { - if (!SelectMLCPath()) - return; - mlc = ActiveSettings::GetMlcPath(); - } - else - { - GetConfig().mlc_path = ""; - g_config.Save(); - } - } + if (!fs::exists(mlc, ec)) + return false; + if (!fs::exists(mlc / "usr", ec) || !fs::exists(mlc / "sys", ec)) + return false; + return true; +} +bool CemuApp::CreateDefaultMLCFiles(const fs::path& mlc) +{ + auto CreateDirectoriesIfNotExist = [](const fs::path& path) + { + std::error_code ec; + if (!fs::exists(path, ec)) + return fs::create_directories(path, ec); + return true; + }; + // list of directories to create + const fs::path directories[] = { + mlc, + mlc / "sys", + mlc / "usr", + mlc / "usr/title/00050000", // base + mlc / "usr/title/0005000c", // dlc + mlc / "usr/title/0005000e", // update + mlc / "usr/save/00050010/1004a000/user/common/db", // Mii Maker save folders {0x500101004A000, 0x500101004A100, 0x500101004A200} + mlc / "usr/save/00050010/1004a100/user/common/db", + mlc / "usr/save/00050010/1004a200/user/common/db", + mlc / "sys/title/0005001b/1005c000/content" // lang files + }; + for(auto& path : directories) + { + if(!CreateDirectoriesIfNotExist(path)) + return false; + } // create sys/usr folder in mlc01 try { - const auto sysFolder = fs::path(mlc).append("sys"); - fs::create_directories(sysFolder); - - const auto usrFolder = fs::path(mlc).append("usr"); - fs::create_directories(usrFolder); - fs::create_directories(fs::path(usrFolder).append("title/00050000")); // base - fs::create_directories(fs::path(usrFolder).append("title/0005000c")); // dlc - fs::create_directories(fs::path(usrFolder).append("title/0005000e")); // update - - // Mii Maker save folders {0x500101004A000, 0x500101004A100, 0x500101004A200}, - fs::create_directories(fs::path(mlc).append("usr/save/00050010/1004a000/user/common/db")); - fs::create_directories(fs::path(mlc).append("usr/save/00050010/1004a100/user/common/db")); - fs::create_directories(fs::path(mlc).append("usr/save/00050010/1004a200/user/common/db")); - - // lang files const auto langDir = fs::path(mlc).append("sys/title/0005001b/1005c000/content"); - fs::create_directories(langDir); - auto langFile = fs::path(langDir).append("language.txt"); if (!fs::exists(langFile)) { @@ -346,18 +504,13 @@ void CemuApp::CreateDefaultFiles(bool first_start) } catch (const std::exception& ex) { - wxString errorMsg = formatWxString(_("Couldn't create a required mlc01 subfolder or file!\n\nError: {0}\nTarget path:\n{1}"), ex.what(), _pathToUtf8(mlc)); - -#if BOOST_OS_WINDOWS - const DWORD lastError = GetLastError(); - if (lastError != ERROR_SUCCESS) - errorMsg << fmt::format("\n\n{}", GetSystemErrorMessage(lastError)); -#endif - - wxMessageBox(errorMsg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - exit(0); + return false; } + return true; +} +void CemuApp::CreateDefaultCemuFiles() +{ // cemu directories try { @@ -384,58 +537,6 @@ void CemuApp::CreateDefaultFiles(bool first_start) } } - -bool CemuApp::TrySelectMLCPath(fs::path path) -{ - if (path.empty()) - path = ActiveSettings::GetDefaultMLCPath(); - - if (!TestWriteAccess(path)) - return false; - - GetConfig().SetMLCPath(path); - CemuApp::CreateDefaultFiles(); - - // update TitleList and SaveList scanner with new MLC path - CafeTitleList::SetMLCPath(path); - CafeTitleList::Refresh(); - CafeSaveList::SetMLCPath(path); - CafeSaveList::Refresh(); - return true; -} - -bool CemuApp::SelectMLCPath(wxWindow* parent) -{ - auto& config = GetConfig(); - - fs::path default_path; - if (fs::exists(_utf8ToPath(config.mlc_path.GetValue()))) - default_path = _utf8ToPath(config.mlc_path.GetValue()); - - // try until users selects a valid path or aborts - while(true) - { - wxDirDialog path_dialog(parent, _("Select a mlc directory"), wxHelper::FromPath(default_path), wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); - if (path_dialog.ShowModal() != wxID_OK || path_dialog.GetPath().empty()) - return false; - - const auto path = path_dialog.GetPath().ToStdWstring(); - - if (!TrySelectMLCPath(path)) - { - const auto result = wxMessageBox(_("Cemu can't write to the selected mlc path!\nDo you want to select another path?"), _("Error"), wxYES_NO | wxCENTRE | wxICON_ERROR); - if (result == wxYES) - continue; - - break; - } - - return true; - } - - return false; -} - void CemuApp::ActivateApp(wxActivateEvent& event) { g_window_info.app_active = event.GetActive(); diff --git a/src/gui/CemuApp.h b/src/gui/CemuApp.h index cfdab0a2..b73d627d 100644 --- a/src/gui/CemuApp.h +++ b/src/gui/CemuApp.h @@ -15,13 +15,18 @@ public: std::vector GetLanguages() const; - static void CreateDefaultFiles(bool first_start = false); - static bool TrySelectMLCPath(fs::path path); - static bool SelectMLCPath(wxWindow* parent = nullptr); + static bool CheckMLCPath(const fs::path& mlc); + static bool CreateDefaultMLCFiles(const fs::path& mlc); + static void CreateDefaultCemuFiles(); + static void InitializeNewMLCOrFail(fs::path mlc); + static void InitializeExistingMLCOrFail(fs::path mlc); private: + void LocalizeUI(wxLanguage languageToUse); + + void DeterminePaths(std::set& failedWriteAccess); + void ActivateApp(wxActivateEvent& event); - void LocalizeUI(); static std::vector GetAvailableTranslationLanguages(wxTranslations* translationsMgr); MainWindow* m_mainFrame = nullptr; diff --git a/src/gui/GeneralSettings2.cpp b/src/gui/GeneralSettings2.cpp index c0b54949..08395cd3 100644 --- a/src/gui/GeneralSettings2.cpp +++ b/src/gui/GeneralSettings2.cpp @@ -32,7 +32,6 @@ #include #include "util/helpers/SystemException.h" #include "gui/dialogs/CreateAccount/wxCreateAccountDialog.h" -#include "config/PermanentStorage.h" #if BOOST_OS_WINDOWS #include @@ -176,19 +175,15 @@ wxPanel* GeneralSettings2::AddGeneralPage(wxNotebook* notebook) m_save_screenshot->SetToolTip(_("Pressing the screenshot key (F12) will save a screenshot directly to the screenshots folder")); second_row->Add(m_save_screenshot, 0, botflag, 5); - m_permanent_storage = new wxCheckBox(box, wxID_ANY, _("Use permanent storage")); - m_permanent_storage->SetToolTip(_("Cemu will remember your custom mlc path in %LOCALAPPDATA%/Cemu for new installations.")); - second_row->Add(m_permanent_storage, 0, botflag, 5); - second_row->AddSpacer(10); m_disable_screensaver = new wxCheckBox(box, wxID_ANY, _("Disable screen saver")); m_disable_screensaver->SetToolTip(_("Prevents the system from activating the screen saver or going to sleep while running a game.")); second_row->Add(m_disable_screensaver, 0, botflag, 5); - // Enable/disable feral interactive gamemode + // Enable/disable feral interactive gamemode #if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) - m_feral_gamemode = new wxCheckBox(box, wxID_ANY, _("Enable Feral GameMode")); - m_feral_gamemode->SetToolTip(_("Use FeralInteractive GameMode if installed.")); - second_row->Add(m_feral_gamemode, 0, botflag, 5); + m_feral_gamemode = new wxCheckBox(box, wxID_ANY, _("Enable Feral GameMode")); + m_feral_gamemode->SetToolTip(_("Use FeralInteractive GameMode if installed.")); + second_row->Add(m_feral_gamemode, 0, botflag, 5); #endif // temporary workaround because feature crashes on macOS @@ -203,23 +198,33 @@ wxPanel* GeneralSettings2::AddGeneralPage(wxNotebook* notebook) } { - auto* box = new wxStaticBox(panel, wxID_ANY, _("MLC Path")); - auto* box_sizer = new wxStaticBoxSizer(box, wxHORIZONTAL); + auto* outerMlcBox = new wxStaticBox(panel, wxID_ANY, _("Custom MLC path")); - m_mlc_path = new wxTextCtrl(box, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY); + auto* box_sizer_mlc = new wxStaticBoxSizer(outerMlcBox, wxVERTICAL); + box_sizer_mlc->Add(new wxStaticText(box_sizer_mlc->GetStaticBox(), wxID_ANY, _("You can configure a custom path for the emulated internal Wii U storage (MLC).\nThis is where Cemu stores saves, accounts and other Wii U system files."), wxDefaultPosition, wxDefaultSize, 0), 0, wxALL, 5); + + auto* mlcPathLineSizer = new wxBoxSizer(wxHORIZONTAL); + + m_mlc_path = new wxTextCtrl(outerMlcBox, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY); m_mlc_path->SetMinSize(wxSize(150, -1)); - m_mlc_path->Bind(wxEVT_CHAR, &GeneralSettings2::OnMLCPathChar, this); m_mlc_path->SetToolTip(_("The mlc directory contains your save games and installed game update/dlc data")); - box_sizer->Add(m_mlc_path, 1, wxALL | wxEXPAND, 5); + mlcPathLineSizer->Add(m_mlc_path, 1, wxALL | wxEXPAND, 5); - auto* change_path = new wxButton(box, wxID_ANY, "..."); - change_path->Bind(wxEVT_BUTTON, &GeneralSettings2::OnMLCPathSelect, this); - change_path->SetToolTip(_("Select a custom mlc path\nThe mlc path is used to store Wii U related files like save games, game updates and dlc data")); - box_sizer->Add(change_path, 0, wxALL, 5); + auto* changePath = new wxButton(outerMlcBox, wxID_ANY, "Change"); + changePath->Bind(wxEVT_BUTTON, &GeneralSettings2::OnMLCPathSelect, this); + mlcPathLineSizer->Add(changePath, 0, wxALL, 5); if (LaunchSettings::GetMLCPath().has_value()) - change_path->Disable(); - general_panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); + changePath->Disable(); + + auto* clearPath = new wxButton(outerMlcBox, wxID_ANY, "Clear custom path"); + clearPath->Bind(wxEVT_BUTTON, &GeneralSettings2::OnMLCPathClear, this); + mlcPathLineSizer->Add(clearPath, 0, wxALL, 5); + if (LaunchSettings::GetMLCPath().has_value() || !ActiveSettings::IsCustomMlcPath()) + clearPath->Disable(); + + box_sizer_mlc->Add(mlcPathLineSizer, 0, wxEXPAND, 5); + general_panel_sizer->Add(box_sizer_mlc, 0, wxEXPAND | wxALL, 5); } { @@ -897,39 +902,12 @@ void GeneralSettings2::StoreConfig() #if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) config.feral_gamemode = m_feral_gamemode->IsChecked(); #endif - const bool use_ps = m_permanent_storage->IsChecked(); - if(use_ps) - { - config.permanent_storage = use_ps; - try - { - - PermanentStorage storage; - storage.RemoveStorage(); - } - catch (...) {} - } - else - { - try - { - // delete permanent storage - PermanentStorage storage; - storage.RemoveStorage(); - } - catch (...) {} - config.permanent_storage = use_ps; - } - config.disable_screensaver = m_disable_screensaver->IsChecked(); // Toggle while a game is running if (CafeSystem::IsTitleRunning()) { ScreenSaver::SetInhibit(config.disable_screensaver); } - - if (!LaunchSettings::GetMLCPath().has_value()) - config.SetMLCPath(wxHelper::MakeFSPath(m_mlc_path->GetValue()), false); // -1 is default wx widget value -> set to dummy 0 so mainwindow and padwindow will update it config.window_position = m_save_window_position_size->IsChecked() ? Vector2i{ 0,0 } : Vector2i{-1,-1}; @@ -1560,7 +1538,6 @@ void GeneralSettings2::ApplyConfig() m_auto_update->SetValue(config.check_update); m_save_screenshot->SetValue(config.save_screenshot); - m_permanent_storage->SetValue(config.permanent_storage); m_disable_screensaver->SetValue(config.disable_screensaver); #if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) m_feral_gamemode->SetValue(config.feral_gamemode); @@ -1570,6 +1547,7 @@ void GeneralSettings2::ApplyConfig() m_disable_screensaver->SetValue(false); #endif + m_game_paths->Clear(); for (auto& path : config.game_paths) { m_game_paths->Append(to_wxString(path)); @@ -1985,34 +1963,70 @@ void GeneralSettings2::OnAccountServiceChanged(wxCommandEvent& event) void GeneralSettings2::OnMLCPathSelect(wxCommandEvent& event) { - if (!CemuApp::SelectMLCPath(this)) + if(CafeSystem::IsTitleRunning()) + { + wxMessageBox(_("Can't change MLC path while a game is running!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); return; - - m_mlc_path->SetValue(wxHelper::FromPath(ActiveSettings::GetMlcPath())); - m_reload_gamelist = true; - m_mlc_modified = true; + } + // show directory dialog + wxDirDialog path_dialog(this, _("Select MLC directory"), wxEmptyString, wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); + if (path_dialog.ShowModal() != wxID_OK || path_dialog.GetPath().empty()) + return; + // check if the choosen MLC path is an already initialized MLC location + fs::path newMlc = wxHelper::MakeFSPath(path_dialog.GetPath()); + if(CemuApp::CheckMLCPath(newMlc)) + { + // ask user if they are sure they want to use this folder and let them know that accounts and saves wont transfer + wxString message = _("Note that changing the MLC location will not transfer any accounts or save files. Are you sure you want to change the path?"); + wxMessageDialog dialog(this, message, _("Warning"), wxYES_NO | wxCENTRE | wxICON_WARNING); + if(dialog.ShowModal() == wxID_NO) + return; + if( !CemuApp::CreateDefaultMLCFiles(newMlc) ) // creating also acts as a check for read+write access + { + wxMessageBox(_("Failed to create default MLC files in the selected directory. The MLC path has not been changed"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); + return; + } + } + else + { + // ask user if they want to create a new mlc structure at the choosen location + wxString message = _("The selected directory does not contain the expected MLC structure. Do you want to create a new MLC structure in this directory?\nNote that changing the MLC location will not transfer any accounts or save files."); + wxMessageDialog dialog(this, message, _("Warning"), wxYES_NO | wxCENTRE | wxICON_WARNING); + if( !CemuApp::CreateDefaultMLCFiles(newMlc) ) + { + wxMessageBox(_("Failed to create default MLC files in the selected directory. The MLC path has not been changed"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); + return; + } + } + // update MLC path and store any other modified settings + GetConfig().SetMLCPath(newMlc); + StoreConfig(); + wxMessageBox(_("Cemu needs to be restarted for the changes to take effect."), _("Information"), wxOK | wxCENTRE | wxICON_INFORMATION, this); + // close settings and then cemu + wxCloseEvent closeEvent(wxEVT_CLOSE_WINDOW); + wxPostEvent(this, closeEvent); + wxPostEvent(GetParent(), closeEvent); } -void GeneralSettings2::OnMLCPathChar(wxKeyEvent& event) +void GeneralSettings2::OnMLCPathClear(wxCommandEvent& event) { - if (LaunchSettings::GetMLCPath().has_value()) - return; - - if(event.GetKeyCode() == WXK_DELETE || event.GetKeyCode() == WXK_BACK) + if(CafeSystem::IsTitleRunning()) { - fs::path newPath = ""; - if(!CemuApp::TrySelectMLCPath(newPath)) - { - const auto res = wxMessageBox(_("The default MLC path is inaccessible.\nDo you want to select a different path?"), _("Error"), wxYES_NO | wxCENTRE | wxICON_ERROR); - if (res == wxYES && CemuApp::SelectMLCPath(this)) - newPath = ActiveSettings::GetMlcPath(); - else - return; - } - m_mlc_path->SetValue(wxHelper::FromPath(newPath)); - m_reload_gamelist = true; - m_mlc_modified = true; + wxMessageBox(_("Can't change MLC path while a game is running!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); + return; } + wxString message = _("Note that changing the MLC location will not transfer any accounts or save files. Are you sure you want to change the path?"); + wxMessageDialog dialog(this, message, _("Warning"), wxYES_NO | wxCENTRE | wxICON_WARNING); + if(dialog.ShowModal() == wxID_NO) + return; + GetConfig().SetMLCPath(""); + StoreConfig(); + g_config.Save(); + wxMessageBox(_("Cemu needs to be restarted for the changes to take effect."), _("Information"), wxOK | wxCENTRE | wxICON_INFORMATION, this); + // close settings and then cemu + wxCloseEvent closeEvent(wxEVT_CLOSE_WINDOW); + wxPostEvent(this, closeEvent); + wxPostEvent(GetParent(), closeEvent); } void GeneralSettings2::OnShowOnlineValidator(wxCommandEvent& event) diff --git a/src/gui/GeneralSettings2.h b/src/gui/GeneralSettings2.h index b34c9222..a3429fa1 100644 --- a/src/gui/GeneralSettings2.h +++ b/src/gui/GeneralSettings2.h @@ -42,7 +42,6 @@ private: wxCheckBox* m_save_padwindow_position_size; wxCheckBox* m_discord_presence, *m_fullscreen_menubar; wxCheckBox* m_auto_update, *m_save_screenshot; - wxCheckBox* m_permanent_storage; wxCheckBox* m_disable_screensaver; #if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) wxCheckBox* m_feral_gamemode; @@ -96,7 +95,7 @@ private: void OnRemovePathClicked(wxCommandEvent& event); void OnActiveAccountChanged(wxCommandEvent& event); void OnMLCPathSelect(wxCommandEvent& event); - void OnMLCPathChar(wxKeyEvent& event); + void OnMLCPathClear(wxCommandEvent& event); void OnShowOnlineValidator(wxCommandEvent& event); void OnAccountServiceChanged(wxCommandEvent& event); static wxString GetOnlineAccountErrorMessage(OnlineAccountError error); diff --git a/src/gui/GettingStartedDialog.cpp b/src/gui/GettingStartedDialog.cpp index bfd206b1..22426cf2 100644 --- a/src/gui/GettingStartedDialog.cpp +++ b/src/gui/GettingStartedDialog.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "config/ActiveSettings.h" #include "gui/CemuApp.h" @@ -11,7 +12,6 @@ #include "gui/GraphicPacksWindow2.h" #include "gui/input/InputSettings2.h" #include "config/CemuConfig.h" -#include "config/PermanentConfig.h" #include "Cafe/TitleList/TitleList.h" @@ -21,75 +21,100 @@ #include "wxHelper.h" +wxDEFINE_EVENT(EVT_REFRESH_FIRST_PAGE, wxCommandEvent); // used to refresh the first page after the language change + wxPanel* GettingStartedDialog::CreatePage1() { - auto* result = new wxPanel(m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + auto* mainPanel = new wxPanel(m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); auto* page1_sizer = new wxBoxSizer(wxVERTICAL); { auto* sizer = new wxBoxSizer(wxHORIZONTAL); - - sizer->Add(new wxStaticBitmap(result, wxID_ANY, wxICON(M_WND_ICON128)), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - auto* m_staticText11 = new wxStaticText(result, wxID_ANY, _("It looks like you're starting Cemu for the first time.\nThis quick setup assistant will help you get the best experience"), wxDefaultPosition, wxDefaultSize, 0); - m_staticText11->Wrap(-1); - sizer->Add(m_staticText11, 0, wxALL, 5); - + sizer->Add(new wxStaticBitmap(mainPanel, wxID_ANY, wxICON(M_WND_ICON128)), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); + m_page1.staticText11 = new wxStaticText(mainPanel, wxID_ANY, _("It looks like you're starting Cemu for the first time.\nThis quick setup assistant will help you get the best experience"), wxDefaultPosition, wxDefaultSize, 0); + m_page1.staticText11->Wrap(-1); + sizer->Add(m_page1.staticText11, 0, wxALL, 5); page1_sizer->Add(sizer, 0, wxALL | wxEXPAND, 5); } + if(ActiveSettings::IsPortableMode()) { - m_mlc_box_sizer = new wxStaticBoxSizer(wxVERTICAL, result, _("mlc01 path")); - m_mlc_box_sizer->Add(new wxStaticText(m_mlc_box_sizer->GetStaticBox(), wxID_ANY, _("The mlc path is the root folder of the emulated Wii U internal flash storage. It contains all your saves, installed updates and DLCs.\nIt is strongly recommend that you create a dedicated folder for it (example: C:\\wiiu\\mlc\\) \nIf left empty, the mlc folder will be created inside the Cemu folder.")), 0, wxALL, 5); + m_page1.portableModeInfoText = new wxStaticText(mainPanel, wxID_ANY, _("Cemu is running in portable mode")); + m_page1.portableModeInfoText->Show(true); + page1_sizer->Add(m_page1.portableModeInfoText, 0, wxALL, 5); - m_prev_mlc_warning = new wxStaticText(m_mlc_box_sizer->GetStaticBox(), wxID_ANY, _("A custom mlc path from a previous Cemu installation has been found and filled in.")); - m_prev_mlc_warning->SetForegroundColour(*wxRED); - m_prev_mlc_warning->Show(false); - m_mlc_box_sizer->Add(m_prev_mlc_warning, 0, wxALL, 5); - - auto* mlc_path_sizer = new wxBoxSizer(wxHORIZONTAL); - mlc_path_sizer->Add(new wxStaticText(m_mlc_box_sizer->GetStaticBox(), wxID_ANY, _("Custom mlc01 path")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - // workaround since we can't specify our own browse label? >> _("Browse") - m_mlc_folder = new wxDirPickerCtrl(m_mlc_box_sizer->GetStaticBox(), wxID_ANY, wxEmptyString, _("Select a folder"), wxDefaultPosition, wxDefaultSize, wxDIRP_DEFAULT_STYLE); - auto tTest1 = m_mlc_folder->GetTextCtrl(); - if(m_mlc_folder->HasTextCtrl()) - { - m_mlc_folder->GetTextCtrl()->SetEditable(false); - m_mlc_folder->GetTextCtrl()->Bind(wxEVT_CHAR, &GettingStartedDialog::OnMLCPathChar, this); - } - mlc_path_sizer->Add(m_mlc_folder, 1, wxALL, 5); - - mlc_path_sizer->Add(new wxStaticText(m_mlc_box_sizer->GetStaticBox(), wxID_ANY, _("(optional)")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_mlc_box_sizer->Add(mlc_path_sizer, 0, wxEXPAND, 5); - - page1_sizer->Add(m_mlc_box_sizer, 0, wxALL | wxEXPAND, 5); } + // language selection +#if 0 { - auto* sizer = new wxStaticBoxSizer(wxVERTICAL, result, _("Game paths")); + m_page1.languageBoxSizer = new wxStaticBoxSizer(wxVERTICAL, mainPanel, _("Language")); + m_page1.languageText = new wxStaticText(m_page1.languageBoxSizer->GetStaticBox(), wxID_ANY, _("Select the language you want to use in Cemu")); + m_page1.languageBoxSizer->Add(m_page1.languageText, 0, wxALL, 5); - sizer->Add(new wxStaticText(sizer->GetStaticBox(), wxID_ANY, _("The game path is scanned by Cemu to locate your games. We recommend creating a dedicated directory in which\nyou place all your Wii U games. (example: C:\\wiiu\\games\\)\n\nYou can also set additional paths in the general settings of Cemu.")), 0, wxALL, 5); + wxString language_choices[] = { _("Default") }; + wxChoice* m_language = new wxChoice(m_page1.languageBoxSizer->GetStaticBox(), wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(language_choices), language_choices); + m_language->SetSelection(0); + + for (const auto& language : wxGetApp().GetLanguages()) + { + m_language->Append(language->DescriptionNative); + } + + m_language->SetSelection(0); + m_page1.languageBoxSizer->Add(m_language, 0, wxALL | wxEXPAND, 5); + + page1_sizer->Add(m_page1.languageBoxSizer, 0, wxALL | wxEXPAND, 5); + + m_language->Bind(wxEVT_CHOICE, [this, m_language](const auto&) + { + const auto language = m_language->GetStringSelection(); + auto selection = m_language->GetSelection(); + if (selection == 0) + GetConfig().language = wxLANGUAGE_DEFAULT; + else + { + auto* app = (CemuApp*)wxTheApp; + const auto language = m_language->GetStringSelection(); + for (const auto& lang : app->GetLanguages()) + { + if (lang->DescriptionNative == language) + { + app->LocalizeUI(static_cast(lang->Language)); + wxCommandEvent event(EVT_REFRESH_FIRST_PAGE); + wxPostEvent(this, event); + break; + } + } + } + }); + } +#endif + + { + m_page1.gamePathBoxSizer = new wxStaticBoxSizer(wxVERTICAL, mainPanel, _("Game paths")); + m_page1.gamePathText = new wxStaticText(m_page1.gamePathBoxSizer->GetStaticBox(), wxID_ANY, _("The game path is scanned by Cemu to automatically locate your games, game updates and DLCs. We recommend creating a dedicated directory in which\nyou place all your Wii U game files. Additional paths can be set later in Cemu's general settings. All common Wii U game formats are supported by Cemu.")); + m_page1.gamePathBoxSizer->Add(m_page1.gamePathText, 0, wxALL, 5); auto* game_path_sizer = new wxBoxSizer(wxHORIZONTAL); - game_path_sizer->Add(new wxStaticText(sizer->GetStaticBox(), wxID_ANY, _("Game path")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); + m_page1.gamePathText2 = new wxStaticText(m_page1.gamePathBoxSizer->GetStaticBox(), wxID_ANY, _("Game path")); + game_path_sizer->Add(m_page1.gamePathText2, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_game_path = new wxDirPickerCtrl(sizer->GetStaticBox(), wxID_ANY, wxEmptyString, _("Select a folder")); - game_path_sizer->Add(m_game_path, 1, wxALL, 5); + m_page1.gamePathPicker = new wxDirPickerCtrl(m_page1.gamePathBoxSizer->GetStaticBox(), wxID_ANY, wxEmptyString, _("Select a folder")); + game_path_sizer->Add(m_page1.gamePathPicker, 1, wxALL, 5); - sizer->Add(game_path_sizer, 0, wxEXPAND, 5); + m_page1.gamePathBoxSizer->Add(game_path_sizer, 0, wxEXPAND, 5); - page1_sizer->Add(sizer, 0, wxALL | wxEXPAND, 5); + page1_sizer->Add(m_page1.gamePathBoxSizer, 0, wxALL | wxEXPAND, 5); } { - auto* sizer = new wxStaticBoxSizer(wxVERTICAL, result, _("Graphic packs")); + auto* sizer = new wxStaticBoxSizer(wxVERTICAL, mainPanel, _("Graphic packs && mods")); - sizer->Add(new wxStaticText(sizer->GetStaticBox(), wxID_ANY, _("Graphic packs improve games by offering the possibility to change resolution, tweak FPS or add other visual or gameplay modifications.\nDownload the community graphic packs to get started.\n")), 0, wxALL, 5); + sizer->Add(new wxStaticText(sizer->GetStaticBox(), wxID_ANY, _("Graphic packs improve games by offering the ability to change resolution, increase FPS, tweak visuals or add gameplay modifications.\nGet started by opening the graphic packs configuration window.\n")), 0, wxALL, 5); - auto* download_gp = new wxButton(sizer->GetStaticBox(), wxID_ANY, _("Download community graphic packs")); - download_gp->Bind(wxEVT_BUTTON, &GettingStartedDialog::OnDownloadGPs, this); + auto* download_gp = new wxButton(sizer->GetStaticBox(), wxID_ANY, _("Download and configure graphic packs")); + download_gp->Bind(wxEVT_BUTTON, &GettingStartedDialog::OnConfigureGPs, this); sizer->Add(download_gp, 0, wxALIGN_CENTER | wxALL, 5); page1_sizer->Add(sizer, 0, wxALL | wxEXPAND, 5); @@ -102,16 +127,15 @@ wxPanel* GettingStartedDialog::CreatePage1() sizer->SetFlexibleDirection(wxBOTH); sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_ALL); - auto* next = new wxButton(result, wxID_ANY, _("Next"), wxDefaultPosition, wxDefaultSize, 0); + auto* next = new wxButton(mainPanel, wxID_ANY, _("Next"), wxDefaultPosition, wxDefaultSize, 0); next->Bind(wxEVT_BUTTON, [this](const auto&){m_notebook->SetSelection(1); }); sizer->Add(next, 0, wxALIGN_BOTTOM | wxALIGN_RIGHT | wxALL, 5); page1_sizer->Add(sizer, 1, wxEXPAND, 5); } - - result->SetSizer(page1_sizer); - return result; + mainPanel->SetSizer(page1_sizer); + return mainPanel; } wxPanel* GettingStartedDialog::CreatePage2() @@ -138,17 +162,17 @@ wxPanel* GettingStartedDialog::CreatePage2() option_sizer->SetFlexibleDirection(wxBOTH); option_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - m_fullscreen = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Start games with fullscreen")); - option_sizer->Add(m_fullscreen, 0, wxALL, 5); + m_page2.fullscreenCheckbox = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Start games with fullscreen")); + option_sizer->Add(m_page2.fullscreenCheckbox, 0, wxALL, 5); - m_separate = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Open separate pad screen")); - option_sizer->Add(m_separate, 0, wxALL, 5); + m_page2.separateCheckbox = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Open separate pad screen")); + option_sizer->Add(m_page2.separateCheckbox, 0, wxALL, 5); - m_update = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Automatically check for updates")); - option_sizer->Add(m_update, 0, wxALL, 5); + m_page2.updateCheckbox = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Automatically check for updates")); + option_sizer->Add(m_page2.updateCheckbox, 0, wxALL, 5); #if BOOST_OS_LINUX if (!std::getenv("APPIMAGE")) { - m_update->Disable(); + m_page2.updateCheckbox->Disable(); } #endif sizer->Add(option_sizer, 1, wxEXPAND, 5); @@ -162,10 +186,6 @@ wxPanel* GettingStartedDialog::CreatePage2() sizer->SetFlexibleDirection(wxBOTH); sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_ALL); - m_dont_show = new wxCheckBox(result, wxID_ANY, _("Don't show this again")); - m_dont_show->SetValue(true); - sizer->Add(m_dont_show, 0, wxALIGN_BOTTOM | wxALL, 5); - auto* previous = new wxButton(result, wxID_ANY, _("Previous")); previous->Bind(wxEVT_BUTTON, [this](const auto&) {m_notebook->SetSelection(0); }); sizer->Add(previous, 0, wxALIGN_BOTTOM | wxALIGN_RIGHT | wxALL, 5); @@ -184,23 +204,9 @@ wxPanel* GettingStartedDialog::CreatePage2() void GettingStartedDialog::ApplySettings() { auto& config = GetConfig(); - m_fullscreen->SetValue(config.fullscreen.GetValue()); - m_update->SetValue(config.check_update.GetValue()); - m_separate->SetValue(config.pad_open.GetValue()); - m_dont_show->SetValue(true); // we want it always enabled by default - m_mlc_folder->SetPath(config.mlc_path.GetValue()); - - try - { - const auto pconfig = PermanentConfig::Load(); - if(!pconfig.custom_mlc_path.empty()) - { - m_mlc_folder->SetPath(wxString::FromUTF8(pconfig.custom_mlc_path)); - m_prev_mlc_warning->Show(true); - } - } - catch (const PSDisabledException&) {} - catch (...) {} + m_page2.fullscreenCheckbox->SetValue(config.fullscreen.GetValue()); + m_page2.updateCheckbox->SetValue(config.check_update.GetValue()); + m_page2.separateCheckbox->SetValue(config.pad_open.GetValue()); } void GettingStartedDialog::UpdateWindowSize() @@ -219,46 +225,25 @@ void GettingStartedDialog::OnClose(wxCloseEvent& event) event.Skip(); auto& config = GetConfig(); - config.fullscreen = m_fullscreen->GetValue(); - config.check_update = m_update->GetValue(); - config.pad_open = m_separate->GetValue(); - config.did_show_graphic_pack_download = m_dont_show->GetValue(); + config.fullscreen = m_page2.fullscreenCheckbox->GetValue(); + config.check_update = m_page2.updateCheckbox->GetValue(); + config.pad_open = m_page2.separateCheckbox->GetValue(); - const fs::path gamePath = wxHelper::MakeFSPath(m_game_path->GetPath()); - if (!gamePath.empty() && fs::exists(gamePath)) + const fs::path gamePath = wxHelper::MakeFSPath(m_page1.gamePathPicker->GetPath()); + std::error_code ec; + if (!gamePath.empty() && fs::exists(gamePath, ec)) { const auto it = std::find(config.game_paths.cbegin(), config.game_paths.cend(), gamePath); if (it == config.game_paths.cend()) { config.game_paths.emplace_back(_pathToUtf8(gamePath)); - m_game_path_changed = true; } } - - const fs::path mlcPath = wxHelper::MakeFSPath(m_mlc_folder->GetPath()); - if(config.mlc_path.GetValue() != mlcPath && (mlcPath.empty() || fs::exists(mlcPath))) - { - config.SetMLCPath(mlcPath, false); - m_mlc_changed = true; - } - - g_config.Save(); - - if(m_mlc_changed) - CemuApp::CreateDefaultFiles(); - - CafeTitleList::ClearScanPaths(); - for (auto& it : GetConfig().game_paths) - CafeTitleList::AddScanPath(_utf8ToPath(it)); - CafeTitleList::Refresh(); } - GettingStartedDialog::GettingStartedDialog(wxWindow* parent) : wxDialog(parent, wxID_ANY, _("Getting started"), wxDefaultPosition, { 740,530 }, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { - //this->SetSizeHints(wxDefaultSize, { 740,530 }); - auto* sizer = new wxBoxSizer(wxVERTICAL); m_notebook = new wxSimplebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0); @@ -274,24 +259,18 @@ GettingStartedDialog::GettingStartedDialog(wxWindow* parent) this->SetSizer(sizer); this->Centre(wxBOTH); this->Bind(wxEVT_CLOSE_WINDOW, &GettingStartedDialog::OnClose, this); - + ApplySettings(); UpdateWindowSize(); } -void GettingStartedDialog::OnDownloadGPs(wxCommandEvent& event) +void GettingStartedDialog::OnConfigureGPs(wxCommandEvent& event) { DownloadGraphicPacksWindow dialog(this); dialog.ShowModal(); - GraphicPacksWindow2::RefreshGraphicPacks(); - - wxMessageDialog ask_dialog(this, _("Do you want to view the downloaded graphic packs?"), _("Graphic packs"), wxCENTRE | wxYES_NO); - if (ask_dialog.ShowModal() == wxID_YES) - { - GraphicPacksWindow2 window(this, 0); - window.ShowModal(); - } + GraphicPacksWindow2 window(this, 0); + window.ShowModal(); } void GettingStartedDialog::OnInputSettings(wxCommandEvent& event) @@ -299,20 +278,3 @@ void GettingStartedDialog::OnInputSettings(wxCommandEvent& event) InputSettings2 dialog(this); dialog.ShowModal(); } - -void GettingStartedDialog::OnMLCPathChar(wxKeyEvent& event) -{ - //if (LaunchSettings::GetMLCPath().has_value()) - // return; - - if (event.GetKeyCode() == WXK_DELETE || event.GetKeyCode() == WXK_BACK) - { - m_mlc_folder->GetTextCtrl()->SetValue(wxEmptyString); - if(m_prev_mlc_warning->IsShown()) - { - m_prev_mlc_warning->Show(false); - UpdateWindowSize(); - } - } -} - diff --git a/src/gui/GettingStartedDialog.h b/src/gui/GettingStartedDialog.h index ec122eab..9dfd69b4 100644 --- a/src/gui/GettingStartedDialog.h +++ b/src/gui/GettingStartedDialog.h @@ -13,9 +13,6 @@ class GettingStartedDialog : public wxDialog public: GettingStartedDialog(wxWindow* parent = nullptr); - [[nodiscard]] bool HasGamePathChanged() const { return m_game_path_changed; } - [[nodiscard]] bool HasMLCChanged() const { return m_mlc_changed; } - private: wxPanel* CreatePage1(); wxPanel* CreatePage2(); @@ -23,22 +20,29 @@ private: void UpdateWindowSize(); void OnClose(wxCloseEvent& event); - void OnDownloadGPs(wxCommandEvent& event); + void OnConfigureGPs(wxCommandEvent& event); void OnInputSettings(wxCommandEvent& event); - void OnMLCPathChar(wxKeyEvent& event); wxSimplebook* m_notebook; - wxCheckBox* m_fullscreen; - wxCheckBox* m_separate; - wxCheckBox* m_update; - wxCheckBox* m_dont_show; - wxStaticBoxSizer* m_mlc_box_sizer; - wxStaticText* m_prev_mlc_warning; - wxDirPickerCtrl* m_mlc_folder; - wxDirPickerCtrl* m_game_path; + struct + { + // header + wxStaticText* staticText11{}; + wxStaticText* portableModeInfoText{}; - bool m_game_path_changed = false; - bool m_mlc_changed = false; + // game path box + wxStaticBoxSizer* gamePathBoxSizer{}; + wxStaticText* gamePathText{}; + wxStaticText* gamePathText2{}; + wxDirPickerCtrl* gamePathPicker{}; + }m_page1; + + struct + { + wxCheckBox* fullscreenCheckbox; + wxCheckBox* separateCheckbox; + wxCheckBox* updateCheckbox; + }m_page2; }; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 7a4f3174..c83ab16b 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -149,8 +149,6 @@ enum // help MAINFRAME_MENU_ID_HELP_ABOUT = 21700, MAINFRAME_MENU_ID_HELP_UPDATE, - MAINFRAME_MENU_ID_HELP_GETTING_STARTED, - // custom MAINFRAME_ID_TIMER1 = 21800, }; @@ -225,7 +223,6 @@ EVT_MENU(MAINFRAME_MENU_ID_DEBUG_VIEW_TEXTURE_RELATIONS, MainWindow::OnDebugView // help menu EVT_MENU(MAINFRAME_MENU_ID_HELP_ABOUT, MainWindow::OnHelpAbout) EVT_MENU(MAINFRAME_MENU_ID_HELP_UPDATE, MainWindow::OnHelpUpdate) -EVT_MENU(MAINFRAME_MENU_ID_HELP_GETTING_STARTED, MainWindow::OnHelpGettingStarted) // misc EVT_COMMAND(wxID_ANY, wxEVT_REQUEST_GAMELIST_REFRESH, MainWindow::OnRequestGameListRefresh) @@ -418,25 +415,6 @@ wxString MainWindow::GetInitialWindowTitle() return BUILD_VERSION_WITH_NAME_STRING; } -void MainWindow::ShowGettingStartedDialog() -{ - GettingStartedDialog dia(this); - dia.ShowModal(); - if (dia.HasGamePathChanged() || dia.HasMLCChanged()) - m_game_list->ReloadGameEntries(); - - TogglePadView(); - - auto& config = GetConfig(); - m_padViewMenuItem->Check(config.pad_open.GetValue()); - m_fullscreenMenuItem->Check(config.fullscreen.GetValue()); -} - -namespace coreinit -{ - void OSSchedulerEnd(); -}; - void MainWindow::OnClose(wxCloseEvent& event) { wxTheClipboard->Flush(); @@ -2075,11 +2053,6 @@ void MainWindow::OnHelpUpdate(wxCommandEvent& event) test.ShowModal(); } -void MainWindow::OnHelpGettingStarted(wxCommandEvent& event) -{ - ShowGettingStartedDialog(); -} - void MainWindow::RecreateMenu() { if (m_menuBar) @@ -2303,8 +2276,7 @@ void MainWindow::RecreateMenu() if (!std::getenv("APPIMAGE")) { m_check_update_menu->Enable(false); } -#endif - helpMenu->Append(MAINFRAME_MENU_ID_HELP_GETTING_STARTED, _("&Getting started")); +#endif helpMenu->AppendSeparator(); helpMenu->Append(MAINFRAME_MENU_ID_HELP_ABOUT, _("&About Cemu")); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index dd4d0d0d..beb86f98 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -103,7 +103,6 @@ public: void OnAccountSelect(wxCommandEvent& event); void OnConsoleLanguage(wxCommandEvent& event); void OnHelpAbout(wxCommandEvent& event); - void OnHelpGettingStarted(wxCommandEvent& event); void OnHelpUpdate(wxCommandEvent& event); void OnDebugSetting(wxCommandEvent& event); void OnDebugLoggingToggleFlagGeneric(wxCommandEvent& event); @@ -150,7 +149,6 @@ private: void RecreateMenu(); void UpdateChildWindowTitleRunningState(); static wxString GetInitialWindowTitle(); - void ShowGettingStartedDialog(); bool InstallUpdate(const fs::path& metaFilePath); diff --git a/src/main.cpp b/src/main.cpp index 1ccc2805..ea1df684 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,6 @@ #include "Cafe/OS/RPL/rpl.h" #include "Cafe/OS/libs/gx2/GX2.h" #include "Cafe/OS/libs/coreinit/coreinit_Thread.h" -#include "Cafe/HW/Latte/Core/LatteOverlay.h" #include "Cafe/GameProfile/GameProfile.h" #include "Cafe/GraphicPack/GraphicPack2.h" #include "config/CemuConfig.h" @@ -160,7 +159,7 @@ void ExpressionParser_test(); void FSTVolumeTest(); void CRCTest(); -void unitTests() +void UnitTests() { ExpressionParser_test(); gx2CopySurfaceTest(); @@ -169,17 +168,6 @@ void unitTests() CRCTest(); } -int mainEmulatorHLE() -{ - LatteOverlay_init(); - // run a couple of tests if in non-release mode -#ifdef CEMU_DEBUG_ASSERT - unitTests(); -#endif - CemuCommonInit(); - return 0; -} - bool isConsoleConnected = false; void requireConsole() { From a1c1a608d77e6c1f7989127d472c655d3159df62 Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Tue, 23 Jul 2024 02:18:48 +0100 Subject: [PATCH 03/35] nsyshid: Emulate Infinity Base (#1246) --- src/Cafe/CMakeLists.txt | 2 + src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp | 8 + src/Cafe/OS/libs/nsyshid/Infinity.cpp | 1102 +++++++++++++++++ src/Cafe/OS/libs/nsyshid/Infinity.h | 105 ++ src/Cafe/OS/libs/nsyshid/Skylander.cpp | 2 +- src/Cafe/OS/libs/nsyshid/Skylander.h | 8 +- src/config/CemuConfig.cpp | 2 + src/config/CemuConfig.h | 1 + .../EmulatedUSBDeviceFrame.cpp | 252 +++- .../EmulatedUSBDeviceFrame.h | 18 + 10 files changed, 1478 insertions(+), 22 deletions(-) create mode 100644 src/Cafe/OS/libs/nsyshid/Infinity.cpp create mode 100644 src/Cafe/OS/libs/nsyshid/Infinity.h diff --git a/src/Cafe/CMakeLists.txt b/src/Cafe/CMakeLists.txt index 1583bdd7..0fb7a44b 100644 --- a/src/Cafe/CMakeLists.txt +++ b/src/Cafe/CMakeLists.txt @@ -463,6 +463,8 @@ add_library(CemuCafe OS/libs/nsyshid/BackendLibusb.h OS/libs/nsyshid/BackendWindowsHID.cpp OS/libs/nsyshid/BackendWindowsHID.h + OS/libs/nsyshid/Infinity.cpp + OS/libs/nsyshid/Infinity.h OS/libs/nsyshid/Skylander.cpp OS/libs/nsyshid/Skylander.h OS/libs/nsyskbd/nsyskbd.cpp diff --git a/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp b/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp index 11a299ed..95eaf06a 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp +++ b/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp @@ -1,4 +1,5 @@ #include "BackendEmulated.h" +#include "Infinity.h" #include "Skylander.h" #include "config/CemuConfig.h" @@ -25,5 +26,12 @@ namespace nsyshid::backend::emulated auto device = std::make_shared(); AttachDevice(device); } + if (GetConfig().emulated_usb_devices.emulate_infinity_base && !FindDeviceById(0x0E6F, 0x0129)) + { + cemuLog_logDebug(LogType::Force, "Attaching Emulated Base"); + // Add Infinity Base + auto device = std::make_shared(); + AttachDevice(device); + } } } // namespace nsyshid::backend::emulated \ No newline at end of file diff --git a/src/Cafe/OS/libs/nsyshid/Infinity.cpp b/src/Cafe/OS/libs/nsyshid/Infinity.cpp new file mode 100644 index 00000000..ab44ef4a --- /dev/null +++ b/src/Cafe/OS/libs/nsyshid/Infinity.cpp @@ -0,0 +1,1102 @@ +#include "Infinity.h" + +#include + +#include "nsyshid.h" +#include "Backend.h" + +#include "util/crypto/aes128.h" + +#include +#include "openssl/sha.h" + +namespace nsyshid +{ + static constexpr std::array SHA1_CONSTANT = { + 0xAF, 0x62, 0xD2, 0xEC, 0x04, 0x91, 0x96, 0x8C, 0xC5, 0x2A, 0x1A, 0x71, 0x65, 0xF8, 0x65, 0xFE, + 0x28, 0x63, 0x29, 0x20, 0x44, 0x69, 0x73, 0x6e, 0x65, 0x79, 0x20, 0x32, 0x30, 0x31, 0x33}; + + InfinityUSB g_infinitybase; + + const std::map> s_listFigures = { + {0x0F4241, {1, "Mr. Incredible"}}, + {0x0F4242, {1, "Sulley"}}, + {0x0F4243, {1, "Jack Sparrow"}}, + {0x0F4244, {1, "Lone Ranger"}}, + {0x0F4245, {1, "Tonto"}}, + {0x0F4246, {1, "Lightning McQueen"}}, + {0x0F4247, {1, "Holley Shiftwell"}}, + {0x0F4248, {1, "Buzz Lightyear"}}, + {0x0F4249, {1, "Jessie"}}, + {0x0F424A, {1, "Mike"}}, + {0x0F424B, {1, "Mrs. Incredible"}}, + {0x0F424C, {1, "Hector Barbossa"}}, + {0x0F424D, {1, "Davy Jones"}}, + {0x0F424E, {1, "Randy"}}, + {0x0F424F, {1, "Syndrome"}}, + {0x0F4250, {1, "Woody"}}, + {0x0F4251, {1, "Mater"}}, + {0x0F4252, {1, "Dash"}}, + {0x0F4253, {1, "Violet"}}, + {0x0F4254, {1, "Francesco Bernoulli"}}, + {0x0F4255, {1, "Sorcerer's Apprentice Mickey"}}, + {0x0F4256, {1, "Jack Skellington"}}, + {0x0F4257, {1, "Rapunzel"}}, + {0x0F4258, {1, "Anna"}}, + {0x0F4259, {1, "Elsa"}}, + {0x0F425A, {1, "Phineas"}}, + {0x0F425B, {1, "Agent P"}}, + {0x0F425C, {1, "Wreck-It Ralph"}}, + {0x0F425D, {1, "Vanellope"}}, + {0x0F425E, {1, "Mr. Incredible (Crystal)"}}, + {0x0F425F, {1, "Jack Sparrow (Crystal)"}}, + {0x0F4260, {1, "Sulley (Crystal)"}}, + {0x0F4261, {1, "Lightning McQueen (Crystal)"}}, + {0x0F4262, {1, "Lone Ranger (Crystal)"}}, + {0x0F4263, {1, "Buzz Lightyear (Crystal)"}}, + {0x0F4264, {1, "Agent P (Crystal)"}}, + {0x0F4265, {1, "Sorcerer's Apprentice Mickey (Crystal)"}}, + {0x0F4266, {1, "Buzz Lightyear (Glowing)"}}, + {0x0F42A4, {2, "Captain America"}}, + {0x0F42A5, {2, "Hulk"}}, + {0x0F42A6, {2, "Iron Man"}}, + {0x0F42A7, {2, "Thor"}}, + {0x0F42A8, {2, "Groot"}}, + {0x0F42A9, {2, "Rocket Raccoon"}}, + {0x0F42AA, {2, "Star-Lord"}}, + {0x0F42AB, {2, "Spider-Man"}}, + {0x0F42AC, {2, "Nick Fury"}}, + {0x0F42AD, {2, "Black Widow"}}, + {0x0F42AE, {2, "Hawkeye"}}, + {0x0F42AF, {2, "Drax"}}, + {0x0F42B0, {2, "Gamora"}}, + {0x0F42B1, {2, "Iron Fist"}}, + {0x0F42B2, {2, "Nova"}}, + {0x0F42B3, {2, "Venom"}}, + {0x0F42B4, {2, "Donald Duck"}}, + {0x0F42B5, {2, "Aladdin"}}, + {0x0F42B6, {2, "Stitch"}}, + {0x0F42B7, {2, "Merida"}}, + {0x0F42B8, {2, "Tinker Bell"}}, + {0x0F42B9, {2, "Maleficent"}}, + {0x0F42BA, {2, "Hiro"}}, + {0x0F42BB, {2, "Baymax"}}, + {0x0F42BC, {2, "Loki"}}, + {0x0F42BD, {2, "Ronan"}}, + {0x0F42BE, {2, "Green Goblin"}}, + {0x0F42BF, {2, "Falcon"}}, + {0x0F42C0, {2, "Yondu"}}, + {0x0F42C1, {2, "Jasmine"}}, + {0x0F42C6, {2, "Black Suit Spider-Man"}}, + {0x0F42D6, {3, "Sam Flynn"}}, + {0x0F42D7, {3, "Quorra"}}, + {0x0F4308, {3, "Anakin Skywalker"}}, + {0x0F4309, {3, "Obi-Wan Kenobi"}}, + {0x0F430A, {3, "Yoda"}}, + {0x0F430B, {3, "Ahsoka Tano"}}, + {0x0F430C, {3, "Darth Maul"}}, + {0x0F430E, {3, "Luke Skywalker"}}, + {0x0F430F, {3, "Han Solo"}}, + {0x0F4310, {3, "Princess Leia"}}, + {0x0F4311, {3, "Chewbacca"}}, + {0x0F4312, {3, "Darth Vader"}}, + {0x0F4313, {3, "Boba Fett"}}, + {0x0F4314, {3, "Ezra Bridger"}}, + {0x0F4315, {3, "Kanan Jarrus"}}, + {0x0F4316, {3, "Sabine Wren"}}, + {0x0F4317, {3, "Zeb Orrelios"}}, + {0x0F4318, {3, "Joy"}}, + {0x0F4319, {3, "Anger"}}, + {0x0F431A, {3, "Fear"}}, + {0x0F431B, {3, "Sadness"}}, + {0x0F431C, {3, "Disgust"}}, + {0x0F431D, {3, "Mickey Mouse"}}, + {0x0F431E, {3, "Minnie Mouse"}}, + {0x0F431F, {3, "Mulan"}}, + {0x0F4320, {3, "Olaf"}}, + {0x0F4321, {3, "Vision"}}, + {0x0F4322, {3, "Ultron"}}, + {0x0F4323, {3, "Ant-Man"}}, + {0x0F4325, {3, "Captain America - The First Avenger"}}, + {0x0F4326, {3, "Finn"}}, + {0x0F4327, {3, "Kylo Ren"}}, + {0x0F4328, {3, "Poe Dameron"}}, + {0x0F4329, {3, "Rey"}}, + {0x0F432B, {3, "Spot"}}, + {0x0F432C, {3, "Nick Wilde"}}, + {0x0F432D, {3, "Judy Hopps"}}, + {0x0F432E, {3, "Hulkbuster"}}, + {0x0F432F, {3, "Anakin Skywalker (Light FX)"}}, + {0x0F4330, {3, "Obi-Wan Kenobi (Light FX)"}}, + {0x0F4331, {3, "Yoda (Light FX)"}}, + {0x0F4332, {3, "Luke Skywalker (Light FX)"}}, + {0x0F4333, {3, "Darth Vader (Light FX)"}}, + {0x0F4334, {3, "Kanan Jarrus (Light FX)"}}, + {0x0F4335, {3, "Kylo Ren (Light FX)"}}, + {0x0F4336, {3, "Black Panther"}}, + {0x0F436C, {3, "Nemo"}}, + {0x0F436D, {3, "Dory"}}, + {0x0F436E, {3, "Baloo"}}, + {0x0F436F, {3, "Alice"}}, + {0x0F4370, {3, "Mad Hatter"}}, + {0x0F4371, {3, "Time"}}, + {0x0F4372, {3, "Peter Pan"}}, + {0x1E8481, {1, "Starter Play Set"}}, + {0x1E8482, {1, "Lone Ranger Play Set"}}, + {0x1E8483, {1, "Cars Play Set"}}, + {0x1E8484, {1, "Toy Story in Space Play Set"}}, + {0x1E84E4, {2, "Marvel's The Avengers Play Set"}}, + {0x1E84E5, {2, "Marvel's Spider-Man Play Set"}}, + {0x1E84E6, {2, "Marvel's Guardians of the Galaxy Play Set"}}, + {0x1E84E7, {2, "Assault on Asgard"}}, + {0x1E84E8, {2, "Escape from the Kyln"}}, + {0x1E84E9, {2, "Stitch's Tropical Rescue"}}, + {0x1E84EA, {2, "Brave Forest Siege"}}, + {0x1E8548, {3, "Inside Out Play Set"}}, + {0x1E8549, {3, "Star Wars: Twilight of the Republic Play Set"}}, + {0x1E854A, {3, "Star Wars: Rise Against the Empire Play Set"}}, + {0x1E854B, {3, "Star Wars: The Force Awakens Play Set"}}, + {0x1E854C, {3, "Marvel Battlegrounds Play Set"}}, + {0x1E854D, {3, "Toy Box Speedway"}}, + {0x1E854E, {3, "Toy Box Takeover"}}, + {0x1E85AC, {3, "Finding Dory Play Set"}}, + {0x2DC6C3, {1, "Bolt's Super Strength"}}, + {0x2DC6C4, {1, "Ralph's Power of Destruction"}}, + {0x2DC6C5, {1, "Chernabog's Power"}}, + {0x2DC6C6, {1, "C.H.R.O.M.E. Damage Increaser"}}, + {0x2DC6C7, {1, "Dr. Doofenshmirtz's Damage-Inator!"}}, + {0x2DC6C8, {1, "Electro-Charge"}}, + {0x2DC6C9, {1, "Fix-It Felix's Repair Power"}}, + {0x2DC6CA, {1, "Rapunzel's Healing"}}, + {0x2DC6CB, {1, "C.H.R.O.M.E. Armor Shield"}}, + {0x2DC6CC, {1, "Star Command Shield"}}, + {0x2DC6CD, {1, "Violet's Force Field"}}, + {0x2DC6CE, {1, "Pieces of Eight"}}, + {0x2DC6CF, {1, "Scrooge McDuck's Lucky Dime"}}, + {0x2DC6D0, {1, "User Control"}}, + {0x2DC6D1, {1, "Sorcerer Mickey's Hat"}}, + {0x2DC6FE, {1, "Emperor Zurg's Wrath"}}, + {0x2DC6FF, {1, "Merlin's Summon"}}, + {0x2DC765, {2, "Enchanted Rose"}}, + {0x2DC766, {2, "Mulan's Training Uniform"}}, + {0x2DC767, {2, "Flubber"}}, + {0x2DC768, {2, "S.H.I.E.L.D. Helicarrier Strike"}}, + {0x2DC769, {2, "Zeus' Thunderbolts"}}, + {0x2DC76A, {2, "King Louie's Monkeys"}}, + {0x2DC76B, {2, "Infinity Gauntlet"}}, + {0x2DC76D, {2, "Sorcerer Supreme"}}, + {0x2DC76E, {2, "Maleficent's Spell Cast"}}, + {0x2DC76F, {2, "Chernabog's Spirit Cyclone"}}, + {0x2DC770, {2, "Marvel Team-Up: Capt. Marvel"}}, + {0x2DC771, {2, "Marvel Team-Up: Iron Patriot"}}, + {0x2DC772, {2, "Marvel Team-Up: Ant-Man"}}, + {0x2DC773, {2, "Marvel Team-Up: White Tiger"}}, + {0x2DC774, {2, "Marvel Team-Up: Yondu"}}, + {0x2DC775, {2, "Marvel Team-Up: Winter Soldier"}}, + {0x2DC776, {2, "Stark Arc Reactor"}}, + {0x2DC777, {2, "Gamma Rays"}}, + {0x2DC778, {2, "Alien Symbiote"}}, + {0x2DC779, {2, "All for One"}}, + {0x2DC77A, {2, "Sandy Claws Surprise"}}, + {0x2DC77B, {2, "Glory Days"}}, + {0x2DC77C, {2, "Cursed Pirate Gold"}}, + {0x2DC77D, {2, "Sentinel of Liberty"}}, + {0x2DC77E, {2, "The Immortal Iron Fist"}}, + {0x2DC77F, {2, "Space Armor"}}, + {0x2DC780, {2, "Rags to Riches"}}, + {0x2DC781, {2, "Ultimate Falcon"}}, + {0x2DC788, {3, "Tomorrowland Time Bomb"}}, + {0x2DC78E, {3, "Galactic Team-Up: Mace Windu"}}, + {0x2DC791, {3, "Luke's Rebel Alliance Flight Suit Costume"}}, + {0x2DC798, {3, "Finn's Stormtrooper Costume"}}, + {0x2DC799, {3, "Poe's Resistance Jacket"}}, + {0x2DC79A, {3, "Resistance Tactical Strike"}}, + {0x2DC79E, {3, "Officer Nick Wilde"}}, + {0x2DC79F, {3, "Meter Maid Judy"}}, + {0x2DC7A2, {3, "Darkhawk's Blast"}}, + {0x2DC7A3, {3, "Cosmic Cube Blast"}}, + {0x2DC7A4, {3, "Princess Leia's Boushh Disguise"}}, + {0x2DC7A6, {3, "Nova Corps Strike"}}, + {0x2DC7A7, {3, "King Mickey"}}, + {0x3D0912, {1, "Mickey's Car"}}, + {0x3D0913, {1, "Cinderella's Coach"}}, + {0x3D0914, {1, "Electric Mayhem Bus"}}, + {0x3D0915, {1, "Cruella De Vil's Car"}}, + {0x3D0916, {1, "Pizza Planet Delivery Truck"}}, + {0x3D0917, {1, "Mike's New Car"}}, + {0x3D0919, {1, "Parking Lot Tram"}}, + {0x3D091A, {1, "Captain Hook's Ship"}}, + {0x3D091B, {1, "Dumbo"}}, + {0x3D091C, {1, "Calico Helicopter"}}, + {0x3D091D, {1, "Maximus"}}, + {0x3D091E, {1, "Angus"}}, + {0x3D091F, {1, "Abu the Elephant"}}, + {0x3D0920, {1, "Headless Horseman's Horse"}}, + {0x3D0921, {1, "Phillipe"}}, + {0x3D0922, {1, "Khan"}}, + {0x3D0923, {1, "Tantor"}}, + {0x3D0924, {1, "Dragon Firework Cannon"}}, + {0x3D0925, {1, "Stitch's Blaster"}}, + {0x3D0926, {1, "Toy Story Mania Blaster"}}, + {0x3D0927, {1, "Flamingo Croquet Mallet"}}, + {0x3D0928, {1, "Carl Fredricksen's Cane"}}, + {0x3D0929, {1, "Hangin' Ten Stitch With Surfboard"}}, + {0x3D092A, {1, "Condorman Glider"}}, + {0x3D092B, {1, "WALL-E's Fire Extinguisher"}}, + {0x3D092C, {1, "On the Grid"}}, + {0x3D092D, {1, "WALL-E's Collection"}}, + {0x3D092E, {1, "King Candy's Dessert Toppings"}}, + {0x3D0930, {1, "Victor's Experiments"}}, + {0x3D0931, {1, "Jack's Scary Decorations"}}, + {0x3D0933, {1, "Frozen Flourish"}}, + {0x3D0934, {1, "Rapunzel's Kingdom"}}, + {0x3D0935, {1, "TRON Interface"}}, + {0x3D0936, {1, "Buy N Large Atmosphere"}}, + {0x3D0937, {1, "Sugar Rush Sky"}}, + {0x3D0939, {1, "New Holland Skyline"}}, + {0x3D093A, {1, "Halloween Town Sky"}}, + {0x3D093C, {1, "Chill in the Air"}}, + {0x3D093D, {1, "Rapunzel's Birthday Sky"}}, + {0x3D0940, {1, "Astro Blasters Space Cruiser"}}, + {0x3D0941, {1, "Marlin's Reef"}}, + {0x3D0942, {1, "Nemo's Seascape"}}, + {0x3D0943, {1, "Alice's Wonderland"}}, + {0x3D0944, {1, "Tulgey Wood"}}, + {0x3D0945, {1, "Tri-State Area Terrain"}}, + {0x3D0946, {1, "Danville Sky"}}, + {0x3D0965, {2, "Stark Tech"}}, + {0x3D0966, {2, "Spider-Streets"}}, + {0x3D0967, {2, "World War Hulk"}}, + {0x3D0968, {2, "Gravity Falls Forest"}}, + {0x3D0969, {2, "Neverland"}}, + {0x3D096A, {2, "Simba's Pridelands"}}, + {0x3D096C, {2, "Calhoun's Command"}}, + {0x3D096D, {2, "Star-Lord's Galaxy"}}, + {0x3D096E, {2, "Dinosaur World"}}, + {0x3D096F, {2, "Groot's Roots"}}, + {0x3D0970, {2, "Mulan's Countryside"}}, + {0x3D0971, {2, "The Sands of Agrabah"}}, + {0x3D0974, {2, "A Small World"}}, + {0x3D0975, {2, "View from the Suit"}}, + {0x3D0976, {2, "Spider-Sky"}}, + {0x3D0977, {2, "World War Hulk Sky"}}, + {0x3D0978, {2, "Gravity Falls Sky"}}, + {0x3D0979, {2, "Second Star to the Right"}}, + {0x3D097A, {2, "The King's Domain"}}, + {0x3D097C, {2, "CyBug Swarm"}}, + {0x3D097D, {2, "The Rip"}}, + {0x3D097E, {2, "Forgotten Skies"}}, + {0x3D097F, {2, "Groot's View"}}, + {0x3D0980, {2, "The Middle Kingdom"}}, + {0x3D0984, {2, "Skies of the World"}}, + {0x3D0985, {2, "S.H.I.E.L.D. Containment Truck"}}, + {0x3D0986, {2, "Main Street Electrical Parade Float"}}, + {0x3D0987, {2, "Mr. Toad's Motorcar"}}, + {0x3D0988, {2, "Le Maximum"}}, + {0x3D0989, {2, "Alice in Wonderland's Caterpillar"}}, + {0x3D098A, {2, "Eglantine's Motorcycle"}}, + {0x3D098B, {2, "Medusa's Swamp Mobile"}}, + {0x3D098C, {2, "Hydra Motorcycle"}}, + {0x3D098D, {2, "Darkwing Duck's Ratcatcher"}}, + {0x3D098F, {2, "The USS Swinetrek"}}, + {0x3D0991, {2, "Spider-Copter"}}, + {0x3D0992, {2, "Aerial Area Rug"}}, + {0x3D0993, {2, "Jack-O-Lantern's Glider"}}, + {0x3D0994, {2, "Spider-Buggy"}}, + {0x3D0995, {2, "Jack Skellington's Reindeer"}}, + {0x3D0996, {2, "Fantasyland Carousel Horse"}}, + {0x3D0997, {2, "Odin's Horse"}}, + {0x3D0998, {2, "Gus the Mule"}}, + {0x3D099A, {2, "Darkwing Duck's Grappling Gun"}}, + {0x3D099C, {2, "Ghost Rider's Chain Whip"}}, + {0x3D099D, {2, "Lew Zealand's Boomerang Fish"}}, + {0x3D099E, {2, "Sergeant Calhoun's Blaster"}}, + {0x3D09A0, {2, "Falcon's Wings"}}, + {0x3D09A1, {2, "Mabel's Kittens for Fists"}}, + {0x3D09A2, {2, "Jim Hawkins' Solar Board"}}, + {0x3D09A3, {2, "Black Panther's Vibranium Knives"}}, + {0x3D09A4, {2, "Cloak of Levitation"}}, + {0x3D09A5, {2, "Aladdin's Magic Carpet"}}, + {0x3D09A6, {2, "Honey Lemon's Ice Capsules"}}, + {0x3D09A7, {2, "Jasmine's Palace View"}}, + {0x3D09C1, {2, "Lola"}}, + {0x3D09C2, {2, "Spider-Cycle"}}, + {0x3D09C3, {2, "The Avenjet"}}, + {0x3D09C4, {2, "Spider-Glider"}}, + {0x3D09C5, {2, "Light Cycle"}}, + {0x3D09C6, {2, "Light Jet"}}, + {0x3D09C9, {3, "Retro Ray Gun"}}, + {0x3D09CA, {3, "Tomorrowland Futurescape"}}, + {0x3D09CB, {3, "Tomorrowland Stratosphere"}}, + {0x3D09CC, {3, "Skies Over Felucia"}}, + {0x3D09CD, {3, "Forests of Felucia"}}, + {0x3D09CF, {3, "General Grievous' Wheel Bike"}}, + {0x3D09D2, {3, "Slave I Flyer"}}, + {0x3D09D3, {3, "Y-Wing Fighter"}}, + {0x3D09D4, {3, "Arlo"}}, + {0x3D09D5, {3, "Nash"}}, + {0x3D09D6, {3, "Butch"}}, + {0x3D09D7, {3, "Ramsey"}}, + {0x3D09DC, {3, "Stars Over Sahara Square"}}, + {0x3D09DD, {3, "Sahara Square Sands"}}, + {0x3D09E0, {3, "Ghost Rider's Motorcycle"}}, + {0x3D09E5, {3, "Quad Jumper"}}}; + + InfinityBaseDevice::InfinityBaseDevice() + : Device(0x0E6F, 0x0129, 1, 2, 0) + { + m_IsOpened = false; + } + + bool InfinityBaseDevice::Open() + { + if (!IsOpened()) + { + m_IsOpened = true; + } + return true; + } + + void InfinityBaseDevice::Close() + { + if (IsOpened()) + { + m_IsOpened = false; + } + } + + bool InfinityBaseDevice::IsOpened() + { + return m_IsOpened; + } + + Device::ReadResult InfinityBaseDevice::Read(ReadMessage* message) + { + memcpy(message->data, g_infinitybase.GetStatus().data(), message->length); + message->bytesRead = message->length; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return Device::ReadResult::Success; + } + + Device::WriteResult InfinityBaseDevice::Write(WriteMessage* message) + { + g_infinitybase.SendCommand(message->data, message->length); + message->bytesWritten = message->length; + return Device::WriteResult::Success; + } + + bool InfinityBaseDevice::GetDescriptor(uint8 descType, + uint8 descIndex, + uint8 lang, + uint8* output, + uint32 outputMaxLength) + { + uint8 configurationDescriptor[0x29]; + + uint8* currentWritePtr; + + // configuration descriptor + currentWritePtr = configurationDescriptor + 0; + *(uint8*)(currentWritePtr + 0) = 9; // bLength + *(uint8*)(currentWritePtr + 1) = 2; // bDescriptorType + *(uint16be*)(currentWritePtr + 2) = 0x0029; // wTotalLength + *(uint8*)(currentWritePtr + 4) = 1; // bNumInterfaces + *(uint8*)(currentWritePtr + 5) = 1; // bConfigurationValue + *(uint8*)(currentWritePtr + 6) = 0; // iConfiguration + *(uint8*)(currentWritePtr + 7) = 0x80; // bmAttributes + *(uint8*)(currentWritePtr + 8) = 0xFA; // MaxPower + currentWritePtr = currentWritePtr + 9; + // configuration descriptor + *(uint8*)(currentWritePtr + 0) = 9; // bLength + *(uint8*)(currentWritePtr + 1) = 0x04; // bDescriptorType + *(uint8*)(currentWritePtr + 2) = 0; // bInterfaceNumber + *(uint8*)(currentWritePtr + 3) = 0; // bAlternateSetting + *(uint8*)(currentWritePtr + 4) = 2; // bNumEndpoints + *(uint8*)(currentWritePtr + 5) = 3; // bInterfaceClass + *(uint8*)(currentWritePtr + 6) = 0; // bInterfaceSubClass + *(uint8*)(currentWritePtr + 7) = 0; // bInterfaceProtocol + *(uint8*)(currentWritePtr + 8) = 0; // iInterface + currentWritePtr = currentWritePtr + 9; + // configuration descriptor + *(uint8*)(currentWritePtr + 0) = 9; // bLength + *(uint8*)(currentWritePtr + 1) = 0x21; // bDescriptorType + *(uint16be*)(currentWritePtr + 2) = 0x0111; // bcdHID + *(uint8*)(currentWritePtr + 4) = 0x00; // bCountryCode + *(uint8*)(currentWritePtr + 5) = 0x01; // bNumDescriptors + *(uint8*)(currentWritePtr + 6) = 0x22; // bDescriptorType + *(uint16be*)(currentWritePtr + 7) = 0x001D; // wDescriptorLength + currentWritePtr = currentWritePtr + 9; + // endpoint descriptor 1 + *(uint8*)(currentWritePtr + 0) = 7; // bLength + *(uint8*)(currentWritePtr + 1) = 0x05; // bDescriptorType + *(uint8*)(currentWritePtr + 2) = 0x81; // bEndpointAddress + *(uint8*)(currentWritePtr + 3) = 0x03; // bmAttributes + *(uint16be*)(currentWritePtr + 4) = 0x40; // wMaxPacketSize + *(uint8*)(currentWritePtr + 6) = 0x01; // bInterval + currentWritePtr = currentWritePtr + 7; + // endpoint descriptor 2 + *(uint8*)(currentWritePtr + 0) = 7; // bLength + *(uint8*)(currentWritePtr + 1) = 0x05; // bDescriptorType + *(uint8*)(currentWritePtr + 1) = 0x02; // bEndpointAddress + *(uint8*)(currentWritePtr + 2) = 0x03; // bmAttributes + *(uint16be*)(currentWritePtr + 3) = 0x40; // wMaxPacketSize + *(uint8*)(currentWritePtr + 5) = 0x01; // bInterval + currentWritePtr = currentWritePtr + 7; + + cemu_assert_debug((currentWritePtr - configurationDescriptor) == 0x29); + + memcpy(output, configurationDescriptor, + std::min(outputMaxLength, sizeof(configurationDescriptor))); + return true; + } + + bool InfinityBaseDevice::SetProtocol(uint8 ifIndex, uint8 protocol) + { + return true; + } + + bool InfinityBaseDevice::SetReport(ReportMessage* message) + { + return true; + } + + std::array InfinityUSB::GetStatus() + { + std::array response = {}; + + bool responded = false; + + do + { + if (!m_figureAddedRemovedResponses.empty()) + { + memcpy(response.data(), m_figureAddedRemovedResponses.front().data(), + 0x20); + m_figureAddedRemovedResponses.pop(); + responded = true; + } + else if (!m_queries.empty()) + { + memcpy(response.data(), m_queries.front().data(), 0x20); + m_queries.pop(); + responded = true; + } + else + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + /* code */ + } + while (!responded); + + return response; + } + + void InfinityUSB::SendCommand(uint8* buf, sint32 originalLength) + { + const uint8 command = buf[2]; + const uint8 sequence = buf[3]; + + std::array q_result{}; + + switch (command) + { + case 0x80: + { + q_result = {0xaa, 0x15, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x03, + 0x02, 0x09, 0x09, 0x43, 0x20, 0x32, 0x62, 0x36, + 0x36, 0x4b, 0x34, 0x99, 0x67, 0x31, 0x93, 0x8c}; + break; + } + case 0x81: + { + // Initiate Challenge + g_infinitybase.DescrambleAndSeed(buf, sequence, q_result); + break; + } + case 0x83: + { + // Challenge Response + g_infinitybase.GetNextAndScramble(sequence, q_result); + break; + } + case 0x90: + case 0x92: + case 0x93: + case 0x95: + case 0x96: + { + // Color commands + g_infinitybase.GetBlankResponse(sequence, q_result); + break; + } + case 0xA1: + { + // Get Present Figures + g_infinitybase.GetPresentFigures(sequence, q_result); + break; + } + case 0xA2: + { + // Read Block from Figure + g_infinitybase.QueryBlock(buf[4], buf[5], q_result, sequence); + break; + } + case 0xA3: + { + // Write block to figure + g_infinitybase.WriteBlock(buf[4], buf[5], &buf[7], q_result, sequence); + break; + } + case 0xB4: + { + // Get figure ID + g_infinitybase.GetFigureIdentifier(buf[4], sequence, q_result); + break; + } + case 0xB5: + { + // Get status? + g_infinitybase.GetBlankResponse(sequence, q_result); + break; + } + default: + cemu_assert_error(); + break; + } + + m_queries.push(q_result); + } + + uint8 InfinityUSB::GenerateChecksum(const std::array& data, + int numOfBytes) const + { + int checksum = 0; + for (int i = 0; i < numOfBytes; i++) + { + checksum += data[i]; + } + return (checksum & 0xFF); + } + + void InfinityUSB::GetBlankResponse(uint8 sequence, + std::array& replyBuf) + { + replyBuf[0] = 0xaa; + replyBuf[1] = 0x01; + replyBuf[2] = sequence; + replyBuf[3] = GenerateChecksum(replyBuf, 3); + } + + void InfinityUSB::DescrambleAndSeed(uint8* buf, uint8 sequence, + std::array& replyBuf) + { + uint64 value = uint64(buf[4]) << 56 | uint64(buf[5]) << 48 | + uint64(buf[6]) << 40 | uint64(buf[7]) << 32 | + uint64(buf[8]) << 24 | uint64(buf[9]) << 16 | + uint64(buf[10]) << 8 | uint64(buf[11]); + uint32 seed = Descramble(value); + GenerateSeed(seed); + GetBlankResponse(sequence, replyBuf); + } + + void InfinityUSB::GetNextAndScramble(uint8 sequence, + std::array& replyBuf) + { + const uint32 nextRandom = GetNext(); + const uint64 scrambledNextRandom = Scramble(nextRandom, 0); + replyBuf = {0xAA, 0x09, sequence}; + replyBuf[3] = uint8((scrambledNextRandom >> 56) & 0xFF); + replyBuf[4] = uint8((scrambledNextRandom >> 48) & 0xFF); + replyBuf[5] = uint8((scrambledNextRandom >> 40) & 0xFF); + replyBuf[6] = uint8((scrambledNextRandom >> 32) & 0xFF); + replyBuf[7] = uint8((scrambledNextRandom >> 24) & 0xFF); + replyBuf[8] = uint8((scrambledNextRandom >> 16) & 0xFF); + replyBuf[9] = uint8((scrambledNextRandom >> 8) & 0xFF); + replyBuf[10] = uint8(scrambledNextRandom & 0xFF); + replyBuf[11] = GenerateChecksum(replyBuf, 11); + } + + uint32 InfinityUSB::Descramble(uint64 numToDescramble) + { + uint64 mask = 0x8E55AA1B3999E8AA; + uint32 ret = 0; + + for (int i = 0; i < 64; i++) + { + if (mask & 0x8000000000000000) + { + ret = (ret << 1) | (numToDescramble & 0x01); + } + + numToDescramble >>= 1; + mask <<= 1; + } + + return ret; + } + + uint64 InfinityUSB::Scramble(uint32 numToScramble, uint32 garbage) + { + uint64 mask = 0x8E55AA1B3999E8AA; + uint64 ret = 0; + + for (int i = 0; i < 64; i++) + { + ret <<= 1; + + if ((mask & 1) != 0) + { + ret |= (numToScramble & 1); + numToScramble >>= 1; + } + else + { + ret |= (garbage & 1); + garbage >>= 1; + } + + mask >>= 1; + } + + return ret; + } + + void InfinityUSB::GenerateSeed(uint32 seed) + { + m_randomA = 0xF1EA5EED; + m_randomB = seed; + m_randomC = seed; + m_randomD = seed; + + for (int i = 0; i < 23; i++) + { + GetNext(); + } + } + + uint32 InfinityUSB::GetNext() + { + uint32 a = m_randomA; + uint32 b = m_randomB; + uint32 c = m_randomC; + uint32 ret = std::rotl(m_randomB, 27); + + const uint32 temp = (a + ((ret ^ 0xFFFFFFFF) + 1)); + b ^= std::rotl(c, 17); + a = m_randomD; + c += a; + ret = b + temp; + a += temp; + + m_randomC = a; + m_randomA = b; + m_randomB = c; + m_randomD = ret; + + return ret; + } + + void InfinityUSB::GetPresentFigures(uint8 sequence, + std::array& replyBuf) + { + int x = 3; + for (uint8 i = 0; i < m_figures.size(); i++) + { + uint8 slot = i == 0 ? 0x10 : (i < 4) ? 0x20 + : 0x30; + if (m_figures[i].present) + { + replyBuf[x] = slot + m_figures[i].orderAdded; + replyBuf[x + 1] = 0x09; + x += 2; + } + } + replyBuf[0] = 0xaa; + replyBuf[1] = x - 2; + replyBuf[2] = sequence; + replyBuf[x] = GenerateChecksum(replyBuf, x); + } + + InfinityUSB::InfinityFigure& + InfinityUSB::GetFigureByOrder(uint8 orderAdded) + { + for (uint8 i = 0; i < m_figures.size(); i++) + { + if (m_figures[i].orderAdded == orderAdded) + { + return m_figures[i]; + } + } + return m_figures[0]; + } + + void InfinityUSB::QueryBlock(uint8 fig_num, uint8 block, + std::array& replyBuf, + uint8 sequence) + { + std::lock_guard lock(m_infinityMutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + replyBuf[0] = 0xaa; + replyBuf[1] = 0x12; + replyBuf[2] = sequence; + replyBuf[3] = 0x00; + const uint8 file_block = (block == 0) ? 1 : (block * 4); + if (figure.present && file_block < 20) + { + memcpy(&replyBuf[4], figure.data.data() + (16 * file_block), 16); + } + replyBuf[20] = GenerateChecksum(replyBuf, 20); + } + + void InfinityUSB::WriteBlock(uint8 fig_num, uint8 block, + const uint8* to_write_buf, + std::array& replyBuf, + uint8 sequence) + { + std::lock_guard lock(m_infinityMutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + replyBuf[0] = 0xaa; + replyBuf[1] = 0x02; + replyBuf[2] = sequence; + replyBuf[3] = 0x00; + const uint8 file_block = (block == 0) ? 1 : (block * 4); + if (figure.present && file_block < 20) + { + memcpy(figure.data.data() + (file_block * 16), to_write_buf, 16); + figure.Save(); + } + replyBuf[4] = GenerateChecksum(replyBuf, 4); + } + + void InfinityUSB::GetFigureIdentifier(uint8 fig_num, uint8 sequence, + std::array& replyBuf) + { + std::lock_guard lock(m_infinityMutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + replyBuf[0] = 0xaa; + replyBuf[1] = 0x09; + replyBuf[2] = sequence; + replyBuf[3] = 0x00; + + if (figure.present) + { + memcpy(&replyBuf[4], figure.data.data(), 7); + } + replyBuf[11] = GenerateChecksum(replyBuf, 11); + } + + std::pair InfinityUSB::FindFigure(uint32 figNum) + { + for (const auto& it : GetFigureList()) + { + if (it.first == figNum) + { + return it.second; + } + } + return {0, fmt::format("Unknown Figure ({})", figNum)}; + } + + std::map> InfinityUSB::GetFigureList() + { + return s_listFigures; + } + + void InfinityUSB::InfinityFigure::Save() + { + if (!infFile) + return; + + infFile->SetPosition(0); + infFile->writeData(data.data(), data.size()); + } + + bool InfinityUSB::RemoveFigure(uint8 position) + { + std::lock_guard lock(m_infinityMutex); + InfinityFigure& figure = m_figures[position]; + + figure.Save(); + figure.infFile.reset(); + + if (figure.present) + { + figure.present = false; + + position = DeriveFigurePosition(position); + if (position == 0) + { + return false; + } + + std::array figureChangeResponse = {0xab, 0x04, position, 0x09, figure.orderAdded, + 0x01}; + figureChangeResponse[6] = GenerateChecksum(figureChangeResponse, 6); + m_figureAddedRemovedResponses.push(figureChangeResponse); + + return true; + } + return false; + } + + uint32 + InfinityUSB::LoadFigure(const std::array& buf, + std::unique_ptr inFile, uint8 position) + { + std::lock_guard lock(m_infinityMutex); + uint8 orderAdded; + + std::vector sha1Calc = {SHA1_CONSTANT.begin(), SHA1_CONSTANT.end() - 1}; + for (int i = 0; i < 7; i++) + { + sha1Calc.push_back(buf[i]); + } + + std::array key = GenerateInfinityFigureKey(sha1Calc); + + std::array infinity_decrypted_block = {}; + std::array encryptedBlock = {}; + memcpy(encryptedBlock.data(), &buf[16], 16); + + AES128_ECB_decrypt(encryptedBlock.data(), key.data(), infinity_decrypted_block.data()); + + uint32 number = uint32(infinity_decrypted_block[1]) << 16 | uint32(infinity_decrypted_block[2]) << 8 | + uint32(infinity_decrypted_block[3]); + + InfinityFigure& figure = m_figures[position]; + + figure.infFile = std::move(inFile); + memcpy(figure.data.data(), buf.data(), figure.data.size()); + figure.present = true; + if (figure.orderAdded == 255) + { + figure.orderAdded = m_figureOrder; + m_figureOrder++; + } + orderAdded = figure.orderAdded; + + position = DeriveFigurePosition(position); + if (position == 0) + { + return 0; + } + + std::array figureChangeResponse = {0xab, 0x04, position, 0x09, orderAdded, 0x00}; + figureChangeResponse[6] = GenerateChecksum(figureChangeResponse, 6); + m_figureAddedRemovedResponses.push(figureChangeResponse); + + return number; + } + + static uint32 InfinityCRC32(const std::array& buffer) + { + static constexpr std::array CRC32_TABLE{ + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, + 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, + 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, + 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, + 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, + 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, + 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, + 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, + 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, + 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, + 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, + 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, + 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, + 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, + 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, + 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, + 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, + 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, + 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, + 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, + 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, + 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, + 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, + 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, + 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, + 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, + 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, + 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, + 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, + 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, + 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d}; + + // Infinity m_figures calculate their CRC32 based on 12 bytes in the block of 16 + uint32 ret = 0; + for (uint32 i = 0; i < 12; ++i) + { + uint8 index = uint8(ret & 0xFF) ^ buffer[i]; + ret = ((ret >> 8) ^ CRC32_TABLE[index]); + } + + return ret; + } + + bool InfinityUSB::CreateFigure(fs::path pathName, uint32 figureNum, uint8 series) + { + FileStream* infFile(FileStream::createFile2(pathName)); + if (!infFile) + { + return false; + } + std::array fileData{}; + uint32 firstBlock = 0x17878E; + uint32 otherBlocks = 0x778788; + for (sint8 i = 2; i >= 0; i--) + { + fileData[0x38 - i] = uint8((firstBlock >> i * 8) & 0xFF); + } + for (uint32 index = 1; index < 0x05; index++) + { + for (sint8 i = 2; i >= 0; i--) + { + fileData[((index * 0x40) + 0x38) - i] = uint8((otherBlocks >> i * 8) & 0xFF); + } + } + // Create the vector to calculate the SHA1 hash with + std::vector sha1Calc = {SHA1_CONSTANT.begin(), SHA1_CONSTANT.end() - 1}; + + // Generate random UID, used for AES encrypt/decrypt + std::random_device rd; + std::mt19937 mt(rd()); + std::uniform_int_distribution dist(0, 255); + std::array uid_data = {0, 0, 0, 0, 0, 0, 0, 0x89, 0x44, 0x00, 0xC2}; + uid_data[0] = dist(mt); + uid_data[1] = dist(mt); + uid_data[2] = dist(mt); + uid_data[3] = dist(mt); + uid_data[4] = dist(mt); + uid_data[5] = dist(mt); + uid_data[6] = dist(mt); + for (sint8 i = 0; i < 7; i++) + { + sha1Calc.push_back(uid_data[i]); + } + std::array figureData = GenerateBlankFigureData(figureNum, series); + if (figureData[1] == 0x00) + return false; + + std::array key = GenerateInfinityFigureKey(sha1Calc); + + std::array encryptedBlock = {}; + std::array blankBlock = {}; + std::array encryptedBlank = {}; + + AES128_ECB_encrypt(figureData.data(), key.data(), encryptedBlock.data()); + AES128_ECB_encrypt(blankBlock.data(), key.data(), encryptedBlank.data()); + + memcpy(&fileData[0], uid_data.data(), uid_data.size()); + memcpy(&fileData[16], encryptedBlock.data(), encryptedBlock.size()); + memcpy(&fileData[16 * 0x04], encryptedBlank.data(), encryptedBlank.size()); + memcpy(&fileData[16 * 0x08], encryptedBlank.data(), encryptedBlank.size()); + memcpy(&fileData[16 * 0x0C], encryptedBlank.data(), encryptedBlank.size()); + memcpy(&fileData[16 * 0x0D], encryptedBlank.data(), encryptedBlank.size()); + + infFile->writeData(fileData.data(), fileData.size()); + + delete infFile; + + return true; + } + + std::array InfinityUSB::GenerateInfinityFigureKey(const std::vector& sha1Data) + { + std::array digest = {}; + SHA_CTX ctx; + SHA1_Init(&ctx); + SHA1_Update(&ctx, sha1Data.data(), sha1Data.size()); + SHA1_Final(digest.data(), &ctx); + OPENSSL_cleanse(&ctx, sizeof(ctx)); + // Infinity AES keys are the first 16 bytes of the SHA1 Digest, every set of 4 bytes need to be + // reversed due to endianness + std::array key = {}; + for (int i = 0; i < 4; i++) + { + for (int x = 3; x >= 0; x--) + { + key[(3 - x) + (i * 4)] = digest[x + (i * 4)]; + } + } + return key; + } + + std::array InfinityUSB::GenerateBlankFigureData(uint32 figureNum, uint8 series) + { + std::array figureData = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0xD1, 0x1F}; + + // Figure Number, input by end user + figureData[1] = uint8((figureNum >> 16) & 0xFF); + figureData[2] = uint8((figureNum >> 8) & 0xFF); + figureData[3] = uint8(figureNum & 0xFF); + + // Manufacture date, formatted as YY/MM/DD. Set to release date of figure's series + if (series == 1) + { + figureData[4] = 0x0D; + figureData[5] = 0x08; + figureData[6] = 0x12; + } + else if (series == 2) + { + figureData[4] = 0x0E; + figureData[5] = 0x09; + figureData[6] = 0x12; + } + else if (series == 3) + { + figureData[4] = 0x0F; + figureData[5] = 0x08; + figureData[6] = 0x1C; + } + + uint32 checksum = InfinityCRC32(figureData); + for (sint8 i = 3; i >= 0; i--) + { + figureData[15 - i] = uint8((checksum >> i * 8) & 0xFF); + } + return figureData; + } + + uint8 InfinityUSB::DeriveFigurePosition(uint8 position) + { + // In the added/removed response, position needs to be 1 for the hexagon, 2 for Player 1 and + // Player 1's abilities, and 3 for Player 2 and Player 2's abilities. In the UI, positions 0, 1 + // and 2 represent the hexagon slot, 3, 4 and 5 represent Player 1's slot and 6, 7 and 8 represent + // Player 2's slot. + + switch (position) + { + case 0: + case 1: + case 2: + return 1; + case 3: + case 4: + case 5: + return 2; + case 6: + case 7: + case 8: + return 3; + + default: + return 0; + } + } +} // namespace nsyshid \ No newline at end of file diff --git a/src/Cafe/OS/libs/nsyshid/Infinity.h b/src/Cafe/OS/libs/nsyshid/Infinity.h new file mode 100644 index 00000000..aa98fd15 --- /dev/null +++ b/src/Cafe/OS/libs/nsyshid/Infinity.h @@ -0,0 +1,105 @@ +#pragma once + +#include + +#include "nsyshid.h" +#include "Backend.h" + +#include "Common/FileStream.h" + +namespace nsyshid +{ + class InfinityBaseDevice final : public Device { + public: + InfinityBaseDevice(); + ~InfinityBaseDevice() = default; + + bool Open() override; + + void Close() override; + + bool IsOpened() override; + + ReadResult Read(ReadMessage* message) override; + + WriteResult Write(WriteMessage* message) override; + + bool GetDescriptor(uint8 descType, + uint8 descIndex, + uint8 lang, + uint8* output, + uint32 outputMaxLength) override; + + bool SetProtocol(uint8 ifIndex, uint8 protocol) override; + + bool SetReport(ReportMessage* message) override; + + private: + bool m_IsOpened; + }; + + constexpr uint16 INF_BLOCK_COUNT = 0x14; + constexpr uint16 INF_BLOCK_SIZE = 0x10; + constexpr uint16 INF_FIGURE_SIZE = INF_BLOCK_COUNT * INF_BLOCK_SIZE; + constexpr uint8 MAX_FIGURES = 9; + class InfinityUSB { + public: + struct InfinityFigure final + { + std::unique_ptr infFile; + std::array data{}; + bool present = false; + uint8 orderAdded = 255; + void Save(); + }; + + void SendCommand(uint8* buf, sint32 originalLength); + std::array GetStatus(); + + void GetBlankResponse(uint8 sequence, std::array& replyBuf); + void DescrambleAndSeed(uint8* buf, uint8 sequence, + std::array& replyBuf); + void GetNextAndScramble(uint8 sequence, std::array& replyBuf); + void GetPresentFigures(uint8 sequence, std::array& replyBuf); + void QueryBlock(uint8 figNum, uint8 block, std::array& replyBuf, + uint8 sequence); + void WriteBlock(uint8 figNum, uint8 block, const uint8* toWriteBuf, + std::array& replyBuf, uint8 sequence); + void GetFigureIdentifier(uint8 figNum, uint8 sequence, + std::array& replyBuf); + + bool RemoveFigure(uint8 position); + uint32 LoadFigure(const std::array& buf, + std::unique_ptr, uint8 position); + bool CreateFigure(fs::path pathName, uint32 figureNum, uint8 series); + static std::map> GetFigureList(); + std::pair FindFigure(uint32 figNum); + + protected: + std::shared_mutex m_infinityMutex; + std::array m_figures; + + private: + uint8 GenerateChecksum(const std::array& data, + int numOfBytes) const; + uint32 Descramble(uint64 numToDescramble); + uint64 Scramble(uint32 numToScramble, uint32 garbage); + void GenerateSeed(uint32 seed); + uint32 GetNext(); + InfinityFigure& GetFigureByOrder(uint8 orderAdded); + uint8 DeriveFigurePosition(uint8 position); + std::array GenerateInfinityFigureKey(const std::vector& sha1Data); + std::array GenerateBlankFigureData(uint32 figureNum, uint8 series); + + uint32 m_randomA; + uint32 m_randomB; + uint32 m_randomC; + uint32 m_randomD; + + uint8 m_figureOrder = 0; + std::queue> m_figureAddedRemovedResponses; + std::queue> m_queries; + }; + extern InfinityUSB g_infinitybase; + +} // namespace nsyshid \ No newline at end of file diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.cpp b/src/Cafe/OS/libs/nsyshid/Skylander.cpp index 7f17f8a3..a9888787 100644 --- a/src/Cafe/OS/libs/nsyshid/Skylander.cpp +++ b/src/Cafe/OS/libs/nsyshid/Skylander.cpp @@ -855,7 +855,7 @@ namespace nsyshid return false; } - std::array data{}; + std::array data{}; uint32 first_block = 0x690F0F0F; uint32 other_blocks = 0x69080F7F; diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.h b/src/Cafe/OS/libs/nsyshid/Skylander.h index ae8b5d92..95eaff0c 100644 --- a/src/Cafe/OS/libs/nsyshid/Skylander.h +++ b/src/Cafe/OS/libs/nsyshid/Skylander.h @@ -38,9 +38,9 @@ namespace nsyshid bool m_IsOpened; }; - constexpr uint16 BLOCK_COUNT = 0x40; - constexpr uint16 BLOCK_SIZE = 0x10; - constexpr uint16 FIGURE_SIZE = BLOCK_COUNT * BLOCK_SIZE; + constexpr uint16 SKY_BLOCK_COUNT = 0x40; + constexpr uint16 SKY_BLOCK_SIZE = 0x10; + constexpr uint16 SKY_FIGURE_SIZE = SKY_BLOCK_COUNT * SKY_BLOCK_SIZE; constexpr uint8 MAX_SKYLANDERS = 16; class SkylanderUSB { @@ -50,7 +50,7 @@ namespace nsyshid std::unique_ptr skyFile; uint8 status = 0; std::queue queuedStatus; - std::array data{}; + std::array data{}; uint32 lastId = 0; void Save(); diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 03b12731..338392dd 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -344,6 +344,7 @@ void CemuConfig::Load(XMLConfigParser& parser) // emulatedusbdevices auto usbdevices = parser.get("EmulatedUsbDevices"); emulated_usb_devices.emulate_skylander_portal = usbdevices.get("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal); + emulated_usb_devices.emulate_infinity_base = usbdevices.get("EmulateInfinityBase", emulated_usb_devices.emulate_infinity_base); } void CemuConfig::Save(XMLConfigParser& parser) @@ -541,6 +542,7 @@ void CemuConfig::Save(XMLConfigParser& parser) // emulated usb devices auto usbdevices = config.set("EmulatedUsbDevices"); usbdevices.set("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal.GetValue()); + usbdevices.set("EmulateInfinityBase", emulated_usb_devices.emulate_infinity_base.GetValue()); } GameEntry* CemuConfig::GetGameEntryByTitleId(uint64 titleId) diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index 3f3da953..2a1d29cb 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -519,6 +519,7 @@ struct CemuConfig struct { ConfigValue emulate_skylander_portal{false}; + ConfigValue emulate_infinity_base{true}; }emulated_usb_devices{}; private: diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp index f43c3690..f4784f35 100644 --- a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp @@ -43,6 +43,7 @@ EmulatedUSBDeviceFrame::EmulatedUSBDeviceFrame(wxWindow* parent) auto* notebook = new wxNotebook(this, wxID_ANY); notebook->AddPage(AddSkylanderPage(notebook), _("Skylanders Portal")); + notebook->AddPage(AddInfinityPage(notebook), _("Infinity Base")); sizer->Add(notebook, 1, wxEXPAND | wxALL, 2); @@ -83,32 +84,98 @@ wxPanel* EmulatedUSBDeviceFrame::AddSkylanderPage(wxNotebook* notebook) return panel; } -wxBoxSizer* EmulatedUSBDeviceFrame::AddSkylanderRow(uint8 row_number, +wxPanel* EmulatedUSBDeviceFrame::AddInfinityPage(wxNotebook* notebook) +{ + auto* panel = new wxPanel(notebook); + auto* panelSizer = new wxBoxSizer(wxBOTH); + auto* box = new wxStaticBox(panel, wxID_ANY, _("Infinity Manager")); + auto* boxSizer = new wxStaticBoxSizer(box, wxBOTH); + + auto* row = new wxBoxSizer(wxHORIZONTAL); + + m_emulateBase = + new wxCheckBox(box, wxID_ANY, _("Emulate Infinity Base")); + m_emulateBase->SetValue( + GetConfig().emulated_usb_devices.emulate_infinity_base); + m_emulateBase->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { + GetConfig().emulated_usb_devices.emulate_infinity_base = + m_emulateBase->IsChecked(); + g_config.Save(); + }); + row->Add(m_emulateBase, 1, wxEXPAND | wxALL, 2); + boxSizer->Add(row, 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Play Set/Power Disc", 0, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Power Disc Two", 1, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Power Disc Three", 2, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Player One", 3, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Player One Ability One", 4, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Player One Ability Two", 5, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Player Two", 6, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Player Two Ability One", 7, box), 1, wxEXPAND | wxALL, 2); + boxSizer->Add(AddInfinityRow("Player Two Ability Two", 8, box), 1, wxEXPAND | wxALL, 2); + + panelSizer->Add(boxSizer, 1, wxEXPAND | wxALL, 2); + panel->SetSizerAndFit(panelSizer); + + return panel; +} + +wxBoxSizer* EmulatedUSBDeviceFrame::AddSkylanderRow(uint8 rowNumber, wxStaticBox* box) { auto* row = new wxBoxSizer(wxHORIZONTAL); row->Add(new wxStaticText(box, wxID_ANY, fmt::format("{} {}", _("Skylander").ToStdString(), - (row_number + 1))), + (rowNumber + 1))), 1, wxEXPAND | wxALL, 2); - m_skylanderSlots[row_number] = + m_skylanderSlots[rowNumber] = new wxTextCtrl(box, wxID_ANY, _("None"), wxDefaultPosition, wxDefaultSize, wxTE_READONLY); - m_skylanderSlots[row_number]->SetMinSize(wxSize(150, -1)); - m_skylanderSlots[row_number]->Disable(); - row->Add(m_skylanderSlots[row_number], 1, wxEXPAND | wxALL, 2); + m_skylanderSlots[rowNumber]->SetMinSize(wxSize(150, -1)); + m_skylanderSlots[rowNumber]->Disable(); + row->Add(m_skylanderSlots[rowNumber], 1, wxEXPAND | wxALL, 2); auto* loadButton = new wxButton(box, wxID_ANY, _("Load")); - loadButton->Bind(wxEVT_BUTTON, [row_number, this](wxCommandEvent&) { - LoadSkylander(row_number); + loadButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { + LoadSkylander(rowNumber); }); auto* createButton = new wxButton(box, wxID_ANY, _("Create")); - createButton->Bind(wxEVT_BUTTON, [row_number, this](wxCommandEvent&) { - CreateSkylander(row_number); + createButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { + CreateSkylander(rowNumber); }); auto* clearButton = new wxButton(box, wxID_ANY, _("Clear")); - clearButton->Bind(wxEVT_BUTTON, [row_number, this](wxCommandEvent&) { - ClearSkylander(row_number); + clearButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { + ClearSkylander(rowNumber); + }); + row->Add(loadButton, 1, wxEXPAND | wxALL, 2); + row->Add(createButton, 1, wxEXPAND | wxALL, 2); + row->Add(clearButton, 1, wxEXPAND | wxALL, 2); + + return row; +} + +wxBoxSizer* EmulatedUSBDeviceFrame::AddInfinityRow(wxString name, uint8 rowNumber, wxStaticBox* box) +{ + auto* row = new wxBoxSizer(wxHORIZONTAL); + + row->Add(new wxStaticText(box, wxID_ANY, name), 1, wxEXPAND | wxALL, 2); + m_infinitySlots[rowNumber] = + new wxTextCtrl(box, wxID_ANY, _("None"), wxDefaultPosition, wxDefaultSize, + wxTE_READONLY); + m_infinitySlots[rowNumber]->SetMinSize(wxSize(150, -1)); + m_infinitySlots[rowNumber]->Disable(); + row->Add(m_infinitySlots[rowNumber], 1, wxALL | wxEXPAND, 5); + auto* loadButton = new wxButton(box, wxID_ANY, _("Load")); + loadButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { + LoadFigure(rowNumber); + }); + auto* createButton = new wxButton(box, wxID_ANY, _("Create")); + createButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { + CreateFigure(rowNumber); + }); + auto* clearButton = new wxButton(box, wxID_ANY, _("Clear")); + clearButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { + ClearFigure(rowNumber); }); row->Add(loadButton, 1, wxEXPAND | wxALL, 2); row->Add(createButton, 1, wxEXPAND | wxALL, 2); @@ -138,7 +205,7 @@ void EmulatedUSBDeviceFrame::LoadSkylanderPath(uint8 slot, wxString path) return; } - std::array fileData; + std::array fileData; if (skyFile->readData(fileData.data(), fileData.size()) != fileData.size()) { wxMessageDialog open_error(this, "Failed to read file! File was too small"); @@ -218,15 +285,15 @@ CreateSkylanderDialog::CreateSkylanderDialog(wxWindow* parent, uint8 slot) long longSkyId; if (!editId->GetValue().ToLong(&longSkyId) || longSkyId > 0xFFFF) { - wxMessageDialog id_error(this, "Error Converting ID!", "ID Entered is Invalid"); - id_error.ShowModal(); + wxMessageDialog idError(this, "Error Converting ID!", "ID Entered is Invalid"); + idError.ShowModal(); return; } long longSkyVar; if (!editVar->GetValue().ToLong(&longSkyVar) || longSkyVar > 0xFFFF) { - wxMessageDialog id_error(this, "Error Converting Variant!", "Variant Entered is Invalid"); - id_error.ShowModal(); + wxMessageDialog idError(this, "Error Converting Variant!", "Variant Entered is Invalid"); + idError.ShowModal(); return; } uint16 skyId = longSkyId & 0xFFFF; @@ -284,6 +351,157 @@ wxString CreateSkylanderDialog::GetFilePath() const return m_filePath; } +CreateInfinityFigureDialog::CreateInfinityFigureDialog(wxWindow* parent, uint8 slot) + : wxDialog(parent, wxID_ANY, _("Infinity Figure Creator"), wxDefaultPosition, wxSize(500, 150)) +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + auto* comboRow = new wxBoxSizer(wxHORIZONTAL); + + auto* comboBox = new wxComboBox(this, wxID_ANY); + comboBox->Append("---Select---", reinterpret_cast(0xFFFFFF)); + wxArrayString filterlist; + for (const auto& it : nsyshid::g_infinitybase.GetFigureList()) + { + const uint32 figure = it.first; + if ((slot == 0 && + ((figure > 0x1E8480 && figure < 0x2DC6BF) || (figure > 0x3D0900 && figure < 0x4C4B3F))) || + ((slot == 1 || slot == 2) && (figure > 0x3D0900 && figure < 0x4C4B3F)) || + ((slot == 3 || slot == 6) && figure < 0x1E847F) || + ((slot == 4 || slot == 5 || slot == 7 || slot == 8) && + (figure > 0x2DC6C0 && figure < 0x3D08FF))) + { + comboBox->Append(it.second.second, reinterpret_cast(figure)); + filterlist.Add(it.second.second); + } + } + comboBox->SetSelection(0); + bool enabled = comboBox->AutoComplete(filterlist); + comboRow->Add(comboBox, 1, wxEXPAND | wxALL, 2); + + auto* figNumRow = new wxBoxSizer(wxHORIZONTAL); + + wxIntegerValidator validator; + + auto* labelFigNum = new wxStaticText(this, wxID_ANY, "Figure Number:"); + auto* editFigNum = new wxTextCtrl(this, wxID_ANY, _("0"), wxDefaultPosition, wxDefaultSize, 0, validator); + + figNumRow->Add(labelFigNum, 1, wxALL, 5); + figNumRow->Add(editFigNum, 1, wxALL, 5); + + auto* buttonRow = new wxBoxSizer(wxHORIZONTAL); + + auto* createButton = new wxButton(this, wxID_ANY, _("Create")); + createButton->Bind(wxEVT_BUTTON, [editFigNum, this](wxCommandEvent&) { + long longFigNum; + if (!editFigNum->GetValue().ToLong(&longFigNum)) + { + wxMessageDialog idError(this, "Error Converting Figure Number!", "Number Entered is Invalid"); + idError.ShowModal(); + this->EndModal(0);; + } + uint32 figNum = longFigNum & 0xFFFFFFFF; + auto figure = nsyshid::g_infinitybase.FindFigure(figNum); + wxString predefName = figure.second + ".bin"; + wxFileDialog + saveFileDialog(this, _("Create Infinity Figure file"), "", predefName, + "BIN files (*.bin)|*.bin", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + + if (saveFileDialog.ShowModal() == wxID_CANCEL) + this->EndModal(0);; + + m_filePath = saveFileDialog.GetPath(); + + nsyshid::g_infinitybase.CreateFigure(_utf8ToPath(m_filePath.utf8_string()), figNum, figure.first); + + this->EndModal(1); + }); + auto* cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); + cancelButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + this->EndModal(0); + }); + + comboBox->Bind(wxEVT_COMBOBOX, [comboBox, editFigNum, this](wxCommandEvent&) { + const uint64 fig_info = reinterpret_cast(comboBox->GetClientData(comboBox->GetSelection())); + if (fig_info != 0xFFFFFF) + { + const uint32 figNum = fig_info & 0xFFFFFFFF; + + editFigNum->SetValue(wxString::Format(wxT("%i"), figNum)); + } + }); + + buttonRow->Add(createButton, 1, wxALL, 5); + buttonRow->Add(cancelButton, 1, wxALL, 5); + + sizer->Add(comboRow, 1, wxEXPAND | wxALL, 2); + sizer->Add(figNumRow, 1, wxEXPAND | wxALL, 2); + sizer->Add(buttonRow, 1, wxEXPAND | wxALL, 2); + + this->SetSizer(sizer); + this->Centre(wxBOTH); +} + +wxString CreateInfinityFigureDialog::GetFilePath() const +{ + return m_filePath; +} + +void EmulatedUSBDeviceFrame::LoadFigure(uint8 slot) +{ + wxFileDialog openFileDialog(this, _("Open Infinity Figure dump"), "", "", + "BIN files (*.bin)|*.bin", + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (openFileDialog.ShowModal() != wxID_OK || openFileDialog.GetPath().empty()) + { + wxMessageDialog errorMessage(this, "File Okay Error"); + errorMessage.ShowModal(); + return; + } + + LoadFigurePath(slot, openFileDialog.GetPath()); +} + +void EmulatedUSBDeviceFrame::LoadFigurePath(uint8 slot, wxString path) +{ + std::unique_ptr infFile(FileStream::openFile2(_utf8ToPath(path.utf8_string()), true)); + if (!infFile) + { + wxMessageDialog errorMessage(this, "File Open Error"); + errorMessage.ShowModal(); + return; + } + + std::array fileData; + if (infFile->readData(fileData.data(), fileData.size()) != fileData.size()) + { + wxMessageDialog open_error(this, "Failed to read file! File was too small"); + open_error.ShowModal(); + return; + } + ClearFigure(slot); + + uint32 number = nsyshid::g_infinitybase.LoadFigure(fileData, std::move(infFile), slot); + m_infinitySlots[slot]->ChangeValue(nsyshid::g_infinitybase.FindFigure(number).second); +} + +void EmulatedUSBDeviceFrame::CreateFigure(uint8 slot) +{ + cemuLog_log(LogType::Force, "Create Figure: {}", slot); + CreateInfinityFigureDialog create_dlg(this, slot); + create_dlg.ShowModal(); + if (create_dlg.GetReturnCode() == 1) + { + LoadFigurePath(slot, create_dlg.GetFilePath()); + } +} + +void EmulatedUSBDeviceFrame::ClearFigure(uint8 slot) +{ + m_infinitySlots[slot]->ChangeValue("None"); + nsyshid::g_infinitybase.RemoveFigure(slot); +} + void EmulatedUSBDeviceFrame::UpdateSkylanderEdits() { for (auto i = 0; i < nsyshid::MAX_SKYLANDERS; i++) diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h index 8988cb8a..ae29a036 100644 --- a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h @@ -5,6 +5,7 @@ #include #include +#include "Cafe/OS/libs/nsyshid/Infinity.h" #include "Cafe/OS/libs/nsyshid/Skylander.h" class wxBoxSizer; @@ -23,15 +24,23 @@ class EmulatedUSBDeviceFrame : public wxFrame { private: wxCheckBox* m_emulatePortal; + wxCheckBox* m_emulateBase; std::array m_skylanderSlots; + std::array m_infinitySlots; std::array>, nsyshid::MAX_SKYLANDERS> m_skySlots; wxPanel* AddSkylanderPage(wxNotebook* notebook); + wxPanel* AddInfinityPage(wxNotebook* notebook); wxBoxSizer* AddSkylanderRow(uint8 row_number, wxStaticBox* box); + wxBoxSizer* AddInfinityRow(wxString name, uint8 row_number, wxStaticBox* box); void LoadSkylander(uint8 slot); void LoadSkylanderPath(uint8 slot, wxString path); void CreateSkylander(uint8 slot); void ClearSkylander(uint8 slot); + void LoadFigure(uint8 slot); + void LoadFigurePath(uint8 slot, wxString path); + void CreateFigure(uint8 slot); + void ClearFigure(uint8 slot); void UpdateSkylanderEdits(); }; class CreateSkylanderDialog : public wxDialog { @@ -39,6 +48,15 @@ class CreateSkylanderDialog : public wxDialog { explicit CreateSkylanderDialog(wxWindow* parent, uint8 slot); wxString GetFilePath() const; + protected: + wxString m_filePath; +}; + +class CreateInfinityFigureDialog : public wxDialog { + public: + explicit CreateInfinityFigureDialog(wxWindow* parent, uint8 slot); + wxString GetFilePath() const; + protected: wxString m_filePath; }; \ No newline at end of file From e65abf48983f92a8de5259362f9842dbf3c28fb4 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Tue, 23 Jul 2024 21:18:55 +0100 Subject: [PATCH 04/35] Suppress unnecessary GTK messages (#1267) --- src/gui/CemuApp.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gui/CemuApp.cpp b/src/gui/CemuApp.cpp index baa83888..f91c1e3a 100644 --- a/src/gui/CemuApp.cpp +++ b/src/gui/CemuApp.cpp @@ -235,6 +235,9 @@ void CemuApp::InitializeExistingMLCOrFail(fs::path mlc) bool CemuApp::OnInit() { +#if __WXGTK__ + GTKSuppressDiagnostics(G_LOG_LEVEL_MASK & ~G_LOG_FLAG_FATAL); +#endif std::set failedWriteAccess; DeterminePaths(failedWriteAccess); // make sure default cemu directories exist From 4b9c7c0d307495c679127381d6f00bab9f0c2933 Mon Sep 17 00:00:00 2001 From: Exverge Date: Wed, 24 Jul 2024 02:32:40 -0400 Subject: [PATCH 05/35] Update Fedora build instructions (#1269) --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 3ff2254f..1e92527e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -57,7 +57,7 @@ At Step 3 in [Build Cemu using cmake and clang](#build-cemu-using-cmake-and-clan `cmake -S . -B build -DCMAKE_BUILD_TYPE=release -DCMAKE_C_COMPILER=/usr/bin/clang-15 -DCMAKE_CXX_COMPILER=/usr/bin/clang++-15 -G Ninja -DCMAKE_MAKE_PROGRAM=/usr/bin/ninja` #### For Fedora and derivatives: -`sudo dnf install clang cmake cubeb-devel freeglut-devel git glm-devel gtk3-devel kernel-headers libgcrypt-devel libsecret-devel libtool libusb1-devel llvm nasm ninja-build perl-core systemd-devel zlib-devel` +`sudo dnf install clang cmake cubeb-devel freeglut-devel git glm-devel gtk3-devel kernel-headers libgcrypt-devel libsecret-devel libtool libusb1-devel llvm nasm ninja-build perl-core systemd-devel zlib-devel zlib-static` ### Build Cemu From f1685eab665e1b262b47d6ea0c47d691fcc0f4a6 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Fri, 26 Jul 2024 05:48:42 +0200 Subject: [PATCH 06/35] h264: Use asynchronous decoding when possible (#1257) --- src/Cafe/CMakeLists.txt | 2 + .../OS/libs/coreinit/coreinit_SysHeap.cpp | 12 +- src/Cafe/OS/libs/coreinit/coreinit_SysHeap.h | 3 + src/Cafe/OS/libs/h264_avc/H264Dec.cpp | 755 +++--------------- .../OS/libs/h264_avc/H264DecBackendAVC.cpp | 502 ++++++++++++ src/Cafe/OS/libs/h264_avc/H264DecInternal.h | 139 ++++ .../OS/libs/h264_avc/parser/H264Parser.cpp | 17 +- src/Cafe/OS/libs/h264_avc/parser/H264Parser.h | 2 + 8 files changed, 787 insertions(+), 645 deletions(-) create mode 100644 src/Cafe/OS/libs/h264_avc/H264DecBackendAVC.cpp create mode 100644 src/Cafe/OS/libs/h264_avc/H264DecInternal.h diff --git a/src/Cafe/CMakeLists.txt b/src/Cafe/CMakeLists.txt index 0fb7a44b..91d257b2 100644 --- a/src/Cafe/CMakeLists.txt +++ b/src/Cafe/CMakeLists.txt @@ -374,7 +374,9 @@ add_library(CemuCafe OS/libs/gx2/GX2_Texture.h OS/libs/gx2/GX2_TilingAperture.cpp OS/libs/h264_avc/H264Dec.cpp + OS/libs/h264_avc/H264DecBackendAVC.cpp OS/libs/h264_avc/h264dec.h + OS/libs/h264_avc/H264DecInternal.h OS/libs/h264_avc/parser OS/libs/h264_avc/parser/H264Parser.cpp OS/libs/h264_avc/parser/H264Parser.h diff --git a/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.cpp b/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.cpp index e37949d7..2f819c50 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.cpp @@ -14,13 +14,10 @@ namespace coreinit return coreinit::MEMAllocFromExpHeapEx(_sysHeapHandle, size, alignment); } - void export_OSAllocFromSystem(PPCInterpreter_t* hCPU) + void OSFreeToSystem(void* ptr) { - ppcDefineParamU32(size, 0); - ppcDefineParamS32(alignment, 1); - MEMPTR mem = OSAllocFromSystem(size, alignment); - cemuLog_logDebug(LogType::Force, "OSAllocFromSystem(0x{:x}, {}) -> 0x{:08x}", size, alignment, mem.GetMPTR()); - osLib_returnFromFunction(hCPU, mem.GetMPTR()); + _sysHeapFreeCounter++; + coreinit::MEMFreeToExpHeap(_sysHeapHandle, ptr); } void InitSysHeap() @@ -34,7 +31,8 @@ namespace coreinit void InitializeSysHeap() { - osLib_addFunction("coreinit", "OSAllocFromSystem", export_OSAllocFromSystem); + cafeExportRegister("h264", OSAllocFromSystem, LogType::CoreinitMem); + cafeExportRegister("h264", OSFreeToSystem, LogType::CoreinitMem); } } diff --git a/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.h b/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.h index 428224af..ad115754 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.h +++ b/src/Cafe/OS/libs/coreinit/coreinit_SysHeap.h @@ -4,5 +4,8 @@ namespace coreinit { void InitSysHeap(); + void* OSAllocFromSystem(uint32 size, uint32 alignment); + void OSFreeToSystem(void* ptr); + void InitializeSysHeap(); } \ No newline at end of file diff --git a/src/Cafe/OS/libs/h264_avc/H264Dec.cpp b/src/Cafe/OS/libs/h264_avc/H264Dec.cpp index 024965fd..82db039b 100644 --- a/src/Cafe/OS/libs/h264_avc/H264Dec.cpp +++ b/src/Cafe/OS/libs/h264_avc/H264Dec.cpp @@ -1,17 +1,12 @@ #include "Cafe/OS/common/OSCommon.h" #include "Cafe/HW/Espresso/PPCCallback.h" #include "Cafe/OS/libs/h264_avc/parser/H264Parser.h" +#include "Cafe/OS/libs/h264_avc/H264DecInternal.h" #include "util/highresolutiontimer/HighResolutionTimer.h" #include "Cafe/CafeSystem.h" #include "h264dec.h" -extern "C" -{ -#include "../dependencies/ih264d/common/ih264_typedefs.h" -#include "../dependencies/ih264d/decoder/ih264d.h" -}; - enum class H264DEC_STATUS : uint32 { SUCCESS = 0x0, @@ -33,10 +28,35 @@ namespace H264 return false; } + struct H264Context + { + struct + { + MEMPTR ptr{ nullptr }; + uint32be length{ 0 }; + float64be timestamp; + }BitStream; + struct + { + MEMPTR outputFunc{ nullptr }; + uint8be outputPerFrame{ 0 }; // whats the default? + MEMPTR userMemoryParam{ nullptr }; + }Param; + // misc + uint32be sessionHandle; + + // decoder state + struct + { + uint32 numFramesInFlight{0}; + }decoderState; + }; + uint32 H264DECMemoryRequirement(uint32 codecProfile, uint32 codecLevel, uint32 width, uint32 height, uint32be* sizeRequirementOut) { if (H264_IsBotW()) { + static_assert(sizeof(H264Context) < 256); *sizeRequirementOut = 256; return 0; } @@ -169,590 +189,47 @@ namespace H264 return H264DEC_STATUS::BAD_STREAM; } - struct H264Context - { - struct - { - MEMPTR ptr{ nullptr }; - uint32be length{ 0 }; - float64be timestamp; - }BitStream; - struct - { - MEMPTR outputFunc{ nullptr }; - uint8be outputPerFrame{ 0 }; // whats the default? - MEMPTR userMemoryParam{ nullptr }; - }Param; - // misc - uint32be sessionHandle; - }; - - class H264AVCDecoder - { - static void* ivd_aligned_malloc(void* ctxt, WORD32 alignment, WORD32 size) - { -#ifdef _WIN32 - return _aligned_malloc(size, alignment); -#else - // alignment is atleast sizeof(void*) - alignment = std::max(alignment, sizeof(void*)); - - //smallest multiple of 2 at least as large as alignment - alignment--; - alignment |= alignment << 1; - alignment |= alignment >> 1; - alignment |= alignment >> 2; - alignment |= alignment >> 4; - alignment |= alignment >> 8; - alignment |= alignment >> 16; - alignment ^= (alignment >> 1); - - void* temp; - posix_memalign(&temp, (size_t)alignment, (size_t)size); - return temp; -#endif - } - - static void ivd_aligned_free(void* ctxt, void* buf) - { -#ifdef _WIN32 - _aligned_free(buf); -#else - free(buf); -#endif - return; - } - - public: - struct DecodeResult - { - bool frameReady{ false }; - double timestamp; - void* imageOutput; - ivd_video_decode_op_t decodeOutput; - }; - - void Init(bool isBufferedMode) - { - ih264d_create_ip_t s_create_ip{ 0 }; - ih264d_create_op_t s_create_op{ 0 }; - - s_create_ip.s_ivd_create_ip_t.u4_size = sizeof(ih264d_create_ip_t); - s_create_ip.s_ivd_create_ip_t.e_cmd = IVD_CMD_CREATE; - s_create_ip.s_ivd_create_ip_t.u4_share_disp_buf = 1; // shared display buffer mode -> We give the decoder a list of buffers that it will use (?) - - s_create_op.s_ivd_create_op_t.u4_size = sizeof(ih264d_create_op_t); - s_create_ip.s_ivd_create_ip_t.e_output_format = IV_YUV_420SP_UV; - s_create_ip.s_ivd_create_ip_t.pf_aligned_alloc = ivd_aligned_malloc; - s_create_ip.s_ivd_create_ip_t.pf_aligned_free = ivd_aligned_free; - s_create_ip.s_ivd_create_ip_t.pv_mem_ctxt = NULL; - - WORD32 status = ih264d_api_function(m_codecCtx, &s_create_ip, &s_create_op); - cemu_assert(!status); - - m_codecCtx = (iv_obj_t*)s_create_op.s_ivd_create_op_t.pv_handle; - m_codecCtx->pv_fxns = (void*)&ih264d_api_function; - m_codecCtx->u4_size = sizeof(iv_obj_t); - - SetDecoderCoreCount(1); - - m_isBufferedMode = isBufferedMode; - - UpdateParameters(false); - - m_bufferedResults.clear(); - m_numDecodedFrames = 0; - m_hasBufferSizeInfo = false; - m_timestampIndex = 0; - } - - void Destroy() - { - if (!m_codecCtx) - return; - ih264d_delete_ip_t s_delete_ip{ 0 }; - ih264d_delete_op_t s_delete_op{ 0 }; - s_delete_ip.s_ivd_delete_ip_t.u4_size = sizeof(ih264d_delete_ip_t); - s_delete_ip.s_ivd_delete_ip_t.e_cmd = IVD_CMD_DELETE; - s_delete_op.s_ivd_delete_op_t.u4_size = sizeof(ih264d_delete_op_t); - WORD32 status = ih264d_api_function(m_codecCtx, &s_delete_ip, &s_delete_op); - cemu_assert_debug(!status); - m_codecCtx = nullptr; - } - - void SetDecoderCoreCount(uint32 coreCount) - { - ih264d_ctl_set_num_cores_ip_t s_set_cores_ip; - ih264d_ctl_set_num_cores_op_t s_set_cores_op; - s_set_cores_ip.e_cmd = IVD_CMD_VIDEO_CTL; - s_set_cores_ip.e_sub_cmd = (IVD_CONTROL_API_COMMAND_TYPE_T)IH264D_CMD_CTL_SET_NUM_CORES; - s_set_cores_ip.u4_num_cores = coreCount; // valid numbers are 1-4 - s_set_cores_ip.u4_size = sizeof(ih264d_ctl_set_num_cores_ip_t); - s_set_cores_op.u4_size = sizeof(ih264d_ctl_set_num_cores_op_t); - IV_API_CALL_STATUS_T status = ih264d_api_function(m_codecCtx, (void *)&s_set_cores_ip, (void *)&s_set_cores_op); - cemu_assert(status == IV_SUCCESS); - } - - static bool GetImageInfo(uint8* stream, uint32 length, uint32& imageWidth, uint32& imageHeight) - { - // create temporary decoder - ih264d_create_ip_t s_create_ip{ 0 }; - ih264d_create_op_t s_create_op{ 0 }; - s_create_ip.s_ivd_create_ip_t.u4_size = sizeof(ih264d_create_ip_t); - s_create_ip.s_ivd_create_ip_t.e_cmd = IVD_CMD_CREATE; - s_create_ip.s_ivd_create_ip_t.u4_share_disp_buf = 0; - s_create_op.s_ivd_create_op_t.u4_size = sizeof(ih264d_create_op_t); - s_create_ip.s_ivd_create_ip_t.e_output_format = IV_YUV_420SP_UV; - s_create_ip.s_ivd_create_ip_t.pf_aligned_alloc = ivd_aligned_malloc; - s_create_ip.s_ivd_create_ip_t.pf_aligned_free = ivd_aligned_free; - s_create_ip.s_ivd_create_ip_t.pv_mem_ctxt = NULL; - iv_obj_t* ctx = nullptr; - WORD32 status = ih264d_api_function(ctx, &s_create_ip, &s_create_op); - cemu_assert_debug(!status); - if (status != IV_SUCCESS) - return false; - ctx = (iv_obj_t*)s_create_op.s_ivd_create_op_t.pv_handle; - ctx->pv_fxns = (void*)&ih264d_api_function; - ctx->u4_size = sizeof(iv_obj_t); - // set header-only mode - ih264d_ctl_set_config_ip_t s_h264d_ctl_ip{ 0 }; - ih264d_ctl_set_config_op_t s_h264d_ctl_op{ 0 }; - ivd_ctl_set_config_ip_t* ps_ctl_ip = &s_h264d_ctl_ip.s_ivd_ctl_set_config_ip_t; - ivd_ctl_set_config_op_t* ps_ctl_op = &s_h264d_ctl_op.s_ivd_ctl_set_config_op_t; - ps_ctl_ip->u4_disp_wd = 0; - ps_ctl_ip->e_frm_skip_mode = IVD_SKIP_NONE; - ps_ctl_ip->e_frm_out_mode = IVD_DISPLAY_FRAME_OUT; - ps_ctl_ip->e_vid_dec_mode = IVD_DECODE_HEADER; - ps_ctl_ip->e_cmd = IVD_CMD_VIDEO_CTL; - ps_ctl_ip->e_sub_cmd = IVD_CMD_CTL_SETPARAMS; - ps_ctl_ip->u4_size = sizeof(ih264d_ctl_set_config_ip_t); - ps_ctl_op->u4_size = sizeof(ih264d_ctl_set_config_op_t); - status = ih264d_api_function(ctx, &s_h264d_ctl_ip, &s_h264d_ctl_op); - cemu_assert(!status); - // decode stream - ivd_video_decode_ip_t s_dec_ip{ 0 }; - ivd_video_decode_op_t s_dec_op{ 0 }; - s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); - s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); - s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; - s_dec_ip.pv_stream_buffer = stream; - s_dec_ip.u4_num_Bytes = length; - s_dec_ip.s_out_buffer.u4_num_bufs = 0; - - s_dec_op.u4_raw_wd = 0; - s_dec_op.u4_raw_ht = 0; - - status = ih264d_api_function(ctx, &s_dec_ip, &s_dec_op); - //cemu_assert(status == 0); -> This errors when not both the headers are present, but it will still set the parameters we need - bool isValid = false; - if (true)//status == 0) - { - imageWidth = s_dec_op.u4_raw_wd; - imageHeight = s_dec_op.u4_raw_ht; - cemu_assert_debug(imageWidth != 0 && imageHeight != 0); - isValid = true; - } - // destroy decoder - ih264d_delete_ip_t s_delete_ip{ 0 }; - ih264d_delete_op_t s_delete_op{ 0 }; - s_delete_ip.s_ivd_delete_ip_t.u4_size = sizeof(ih264d_delete_ip_t); - s_delete_ip.s_ivd_delete_ip_t.e_cmd = IVD_CMD_DELETE; - s_delete_op.s_ivd_delete_op_t.u4_size = sizeof(ih264d_delete_op_t); - status = ih264d_api_function(ctx, &s_delete_ip, &s_delete_op); - cemu_assert_debug(!status); - return isValid; - } - - void Decode(void* data, uint32 length, double timestamp, void* imageOutput, DecodeResult& decodeResult) - { - if (!m_hasBufferSizeInfo) - { - uint32 numByteConsumed = 0; - if (!DetermineBufferSizes(data, length, numByteConsumed)) - { - cemuLog_log(LogType::Force, "H264: Unable to determine picture size. Ignoring decode input"); - decodeResult.frameReady = false; - return; - } - length -= numByteConsumed; - data = (uint8*)data + numByteConsumed; - m_hasBufferSizeInfo = true; - } - - ivd_video_decode_ip_t s_dec_ip{ 0 }; - ivd_video_decode_op_t s_dec_op{ 0 }; - s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); - s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); - - s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; - - // remember timestamp and associated output buffer - m_timestamps[m_timestampIndex] = timestamp; - m_imageBuffers[m_timestampIndex] = imageOutput; - s_dec_ip.u4_ts = m_timestampIndex; - m_timestampIndex = (m_timestampIndex + 1) % 64; - - s_dec_ip.pv_stream_buffer = (uint8*)data; - s_dec_ip.u4_num_Bytes = length; - - s_dec_ip.s_out_buffer.u4_min_out_buf_size[0] = 0; - s_dec_ip.s_out_buffer.u4_min_out_buf_size[1] = 0; - s_dec_ip.s_out_buffer.u4_num_bufs = 0; - - BenchmarkTimer bt; - bt.Start(); - WORD32 status = ih264d_api_function(m_codecCtx, &s_dec_ip, &s_dec_op); - if (status != 0 && (s_dec_op.u4_error_code&0xFF) == IVD_RES_CHANGED) - { - // resolution change - ResetDecoder(); - m_hasBufferSizeInfo = false; - Decode(data, length, timestamp, imageOutput, decodeResult); - return; - } - else if (status != 0) - { - cemuLog_log(LogType::Force, "H264: Failed to decode frame (error 0x{:08x})", status); - decodeResult.frameReady = false; - return; - } - - bt.Stop(); - double decodeTime = bt.GetElapsedMilliseconds(); - - cemu_assert(s_dec_op.u4_frame_decoded_flag); - cemu_assert_debug(s_dec_op.u4_num_bytes_consumed == length); - - cemu_assert_debug(m_isBufferedMode || s_dec_op.u4_output_present); // if buffered mode is disabled, then every input should output a frame (except for partial slices?) - - if (s_dec_op.u4_output_present) - { - cemu_assert(s_dec_op.e_output_format == IV_YUV_420SP_UV); - if (H264_IsBotW()) - { - if (s_dec_op.s_disp_frm_buf.u4_y_wd == 1920 && s_dec_op.s_disp_frm_buf.u4_y_ht == 1088) - s_dec_op.s_disp_frm_buf.u4_y_ht = 1080; - } - DecodeResult tmpResult; - tmpResult.frameReady = s_dec_op.u4_output_present != 0; - tmpResult.timestamp = m_timestamps[s_dec_op.u4_ts]; - tmpResult.imageOutput = m_imageBuffers[s_dec_op.u4_ts]; - tmpResult.decodeOutput = s_dec_op; - AddBufferedResult(tmpResult); - // transfer image to PPC output buffer and also correct stride - bt.Start(); - CopyImageToResultBuffer((uint8*)s_dec_op.s_disp_frm_buf.pv_y_buf, (uint8*)s_dec_op.s_disp_frm_buf.pv_u_buf, (uint8*)m_imageBuffers[s_dec_op.u4_ts], s_dec_op); - bt.Stop(); - double copyTime = bt.GetElapsedMilliseconds(); - // release buffer - sint32 bufferId = -1; - for (size_t i = 0; i < m_displayBuf.size(); i++) - { - if (s_dec_op.s_disp_frm_buf.pv_y_buf >= m_displayBuf[i].data() && s_dec_op.s_disp_frm_buf.pv_y_buf < (m_displayBuf[i].data() + m_displayBuf[i].size())) - { - bufferId = (sint32)i; - break; - } - } - cemu_assert_debug(bufferId == s_dec_op.u4_disp_buf_id); - cemu_assert(bufferId >= 0); - ivd_rel_display_frame_ip_t s_video_rel_disp_ip{ 0 }; - ivd_rel_display_frame_op_t s_video_rel_disp_op{ 0 }; - s_video_rel_disp_ip.e_cmd = IVD_CMD_REL_DISPLAY_FRAME; - s_video_rel_disp_ip.u4_size = sizeof(ivd_rel_display_frame_ip_t); - s_video_rel_disp_op.u4_size = sizeof(ivd_rel_display_frame_op_t); - s_video_rel_disp_ip.u4_disp_buf_id = bufferId; - status = ih264d_api_function(m_codecCtx, &s_video_rel_disp_ip, &s_video_rel_disp_op); - cemu_assert(!status); - - cemuLog_log(LogType::H264, "H264Bench | DecodeTime {}ms CopyTime {}ms", decodeTime, copyTime); - } - else - { - cemuLog_log(LogType::H264, "H264Bench | DecodeTime{}ms", decodeTime); - } - - if (s_dec_op.u4_frame_decoded_flag) - m_numDecodedFrames++; - - if (m_isBufferedMode) - { - // in buffered mode, always buffer 5 frames regardless of actual reordering and decoder latency - if (m_numDecodedFrames > 5) - GetCurrentBufferedResult(decodeResult); - } - else if(m_numDecodedFrames > 0) - GetCurrentBufferedResult(decodeResult); - - // get VUI - //ih264d_ctl_get_vui_params_ip_t s_ctl_get_vui_params_ip; - //ih264d_ctl_get_vui_params_op_t s_ctl_get_vui_params_op; - - //s_ctl_get_vui_params_ip.e_cmd = IVD_CMD_VIDEO_CTL; - //s_ctl_get_vui_params_ip.e_sub_cmd = (IVD_CONTROL_API_COMMAND_TYPE_T)IH264D_CMD_CTL_GET_VUI_PARAMS; - //s_ctl_get_vui_params_ip.u4_size = sizeof(ih264d_ctl_get_vui_params_ip_t); - //s_ctl_get_vui_params_op.u4_size = sizeof(ih264d_ctl_get_vui_params_op_t); - - //status = ih264d_api_function(mCodecCtx, &s_ctl_get_vui_params_ip, &s_ctl_get_vui_params_op); - //cemu_assert(status == 0); - } - - std::vector Flush() - { - std::vector results; - // set flush mode - ivd_ctl_flush_ip_t s_video_flush_ip{ 0 }; - ivd_ctl_flush_op_t s_video_flush_op{ 0 }; - s_video_flush_ip.e_cmd = IVD_CMD_VIDEO_CTL; - s_video_flush_ip.e_sub_cmd = IVD_CMD_CTL_FLUSH; - s_video_flush_ip.u4_size = sizeof(ivd_ctl_flush_ip_t); - s_video_flush_op.u4_size = sizeof(ivd_ctl_flush_op_t); - WORD32 status = ih264d_api_function(m_codecCtx, &s_video_flush_ip, &s_video_flush_op); - if (status != 0) - cemuLog_log(LogType::Force, "H264Dec: Unexpected error during flush ({})", status); - // get all frames from the codec - while (true) - { - ivd_video_decode_ip_t s_dec_ip{ 0 }; - ivd_video_decode_op_t s_dec_op{ 0 }; - s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); - s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); - s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; - s_dec_ip.pv_stream_buffer = NULL; - s_dec_ip.u4_num_Bytes = 0; - s_dec_ip.s_out_buffer.u4_min_out_buf_size[0] = 0; - s_dec_ip.s_out_buffer.u4_min_out_buf_size[1] = 0; - s_dec_ip.s_out_buffer.u4_num_bufs = 0; - status = ih264d_api_function(m_codecCtx, &s_dec_ip, &s_dec_op); - if (status != 0) - break; - cemu_assert_debug(s_dec_op.u4_output_present != 0); // should never be zero? - if(s_dec_op.u4_output_present == 0) - continue; - if (H264_IsBotW()) - { - if (s_dec_op.s_disp_frm_buf.u4_y_wd == 1920 && s_dec_op.s_disp_frm_buf.u4_y_ht == 1088) - s_dec_op.s_disp_frm_buf.u4_y_ht = 1080; - } - DecodeResult tmpResult; - tmpResult.frameReady = s_dec_op.u4_output_present != 0; - tmpResult.timestamp = m_timestamps[s_dec_op.u4_ts]; - tmpResult.imageOutput = m_imageBuffers[s_dec_op.u4_ts]; - tmpResult.decodeOutput = s_dec_op; - AddBufferedResult(tmpResult); - CopyImageToResultBuffer((uint8*)s_dec_op.s_disp_frm_buf.pv_y_buf, (uint8*)s_dec_op.s_disp_frm_buf.pv_u_buf, (uint8*)m_imageBuffers[s_dec_op.u4_ts], s_dec_op); - } - results = std::move(m_bufferedResults); - return results; - } - - void CopyImageToResultBuffer(uint8* yIn, uint8* uvIn, uint8* bufOut, ivd_video_decode_op_t& decodeInfo) - { - uint32 imageWidth = decodeInfo.s_disp_frm_buf.u4_y_wd; - uint32 imageHeight = decodeInfo.s_disp_frm_buf.u4_y_ht; - - size_t inputStride = decodeInfo.s_disp_frm_buf.u4_y_strd; - size_t outputStride = (imageWidth + 0xFF) & ~0xFF; - - // copy Y - uint8* yOut = bufOut; - for (uint32 row = 0; row < imageHeight; row++) - { - memcpy(yOut, yIn, imageWidth); - yIn += inputStride; - yOut += outputStride; - } - - // copy UV - uint8* uvOut = bufOut + outputStride * imageHeight; - for (uint32 row = 0; row < imageHeight/2; row++) - { - memcpy(uvOut, uvIn, imageWidth); - uvIn += inputStride; - uvOut += outputStride; - } - } - - private: - - bool DetermineBufferSizes(void* data, uint32 length, uint32& numByteConsumed) - { - numByteConsumed = 0; - UpdateParameters(true); - - ivd_video_decode_ip_t s_dec_ip{ 0 }; - ivd_video_decode_op_t s_dec_op{ 0 }; - s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); - s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); - - s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; - s_dec_ip.pv_stream_buffer = (uint8*)data; - s_dec_ip.u4_num_Bytes = length; - s_dec_ip.s_out_buffer.u4_num_bufs = 0; - WORD32 status = ih264d_api_function(m_codecCtx, &s_dec_ip, &s_dec_op); - if (status != 0) - { - cemuLog_log(LogType::Force, "H264: Unable to determine buffer sizes for stream"); - return false; - } - numByteConsumed = s_dec_op.u4_num_bytes_consumed; - cemu_assert(status == 0); - if (s_dec_op.u4_pic_wd == 0 || s_dec_op.u4_pic_ht == 0) - return false; - UpdateParameters(false); - ReinitBuffers(); - return true; - } - - void ReinitBuffers() - { - ivd_ctl_getbufinfo_ip_t s_ctl_ip{ 0 }; - ivd_ctl_getbufinfo_op_t s_ctl_op{ 0 }; - WORD32 outlen = 0; - - s_ctl_ip.e_cmd = IVD_CMD_VIDEO_CTL; - s_ctl_ip.e_sub_cmd = IVD_CMD_CTL_GETBUFINFO; - s_ctl_ip.u4_size = sizeof(ivd_ctl_getbufinfo_ip_t); - s_ctl_op.u4_size = sizeof(ivd_ctl_getbufinfo_op_t); - - WORD32 status = ih264d_api_function(m_codecCtx, &s_ctl_ip, &s_ctl_op); - cemu_assert(!status); - - // allocate - for (uint32 i = 0; i < s_ctl_op.u4_num_disp_bufs; i++) - { - m_displayBuf.emplace_back().resize(s_ctl_op.u4_min_out_buf_size[0] + s_ctl_op.u4_min_out_buf_size[1]); - } - // set - ivd_set_display_frame_ip_t s_set_display_frame_ip{ 0 }; // make sure to zero-initialize this. The codec seems to check the first 3 pointers/sizes per frame, regardless of the value of u4_num_bufs - ivd_set_display_frame_op_t s_set_display_frame_op{ 0 }; - - s_set_display_frame_ip.e_cmd = IVD_CMD_SET_DISPLAY_FRAME; - s_set_display_frame_ip.u4_size = sizeof(ivd_set_display_frame_ip_t); - s_set_display_frame_op.u4_size = sizeof(ivd_set_display_frame_op_t); - - cemu_assert_debug(s_ctl_op.u4_min_num_out_bufs == 2); - cemu_assert_debug(s_ctl_op.u4_min_out_buf_size[0] != 0 && s_ctl_op.u4_min_out_buf_size[1] != 0); - - s_set_display_frame_ip.num_disp_bufs = s_ctl_op.u4_num_disp_bufs; - - for (uint32 i = 0; i < s_ctl_op.u4_num_disp_bufs; i++) - { - s_set_display_frame_ip.s_disp_buffer[i].u4_num_bufs = 2; - s_set_display_frame_ip.s_disp_buffer[i].u4_min_out_buf_size[0] = s_ctl_op.u4_min_out_buf_size[0]; - s_set_display_frame_ip.s_disp_buffer[i].u4_min_out_buf_size[1] = s_ctl_op.u4_min_out_buf_size[1]; - s_set_display_frame_ip.s_disp_buffer[i].pu1_bufs[0] = m_displayBuf[i].data() + 0; - s_set_display_frame_ip.s_disp_buffer[i].pu1_bufs[1] = m_displayBuf[i].data() + s_ctl_op.u4_min_out_buf_size[0]; - } - - status = ih264d_api_function(m_codecCtx, &s_set_display_frame_ip, &s_set_display_frame_op); - cemu_assert(!status); - - - // mark all as released (available) - for (uint32 i = 0; i < s_ctl_op.u4_num_disp_bufs; i++) - { - ivd_rel_display_frame_ip_t s_video_rel_disp_ip{ 0 }; - ivd_rel_display_frame_op_t s_video_rel_disp_op{ 0 }; - - s_video_rel_disp_ip.e_cmd = IVD_CMD_REL_DISPLAY_FRAME; - s_video_rel_disp_ip.u4_size = sizeof(ivd_rel_display_frame_ip_t); - s_video_rel_disp_op.u4_size = sizeof(ivd_rel_display_frame_op_t); - s_video_rel_disp_ip.u4_disp_buf_id = i; - - status = ih264d_api_function(m_codecCtx, &s_video_rel_disp_ip, &s_video_rel_disp_op); - cemu_assert(!status); - } - } - - void ResetDecoder() - { - ivd_ctl_reset_ip_t s_ctl_ip; - ivd_ctl_reset_op_t s_ctl_op; - - s_ctl_ip.e_cmd = IVD_CMD_VIDEO_CTL; - s_ctl_ip.e_sub_cmd = IVD_CMD_CTL_RESET; - s_ctl_ip.u4_size = sizeof(ivd_ctl_reset_ip_t); - s_ctl_op.u4_size = sizeof(ivd_ctl_reset_op_t); - - WORD32 status = ih264d_api_function(m_codecCtx, (void*)&s_ctl_ip, (void*)&s_ctl_op); - cemu_assert_debug(status == 0); - } - - void UpdateParameters(bool headerDecodeOnly) - { - ih264d_ctl_set_config_ip_t s_h264d_ctl_ip{ 0 }; - ih264d_ctl_set_config_op_t s_h264d_ctl_op{ 0 }; - ivd_ctl_set_config_ip_t* ps_ctl_ip = &s_h264d_ctl_ip.s_ivd_ctl_set_config_ip_t; - ivd_ctl_set_config_op_t* ps_ctl_op = &s_h264d_ctl_op.s_ivd_ctl_set_config_op_t; - - ps_ctl_ip->u4_disp_wd = 0; - ps_ctl_ip->e_frm_skip_mode = IVD_SKIP_NONE; - ps_ctl_ip->e_frm_out_mode = m_isBufferedMode ? IVD_DISPLAY_FRAME_OUT : IVD_DECODE_FRAME_OUT; - ps_ctl_ip->e_vid_dec_mode = headerDecodeOnly ? IVD_DECODE_HEADER : IVD_DECODE_FRAME; - ps_ctl_ip->e_cmd = IVD_CMD_VIDEO_CTL; - ps_ctl_ip->e_sub_cmd = IVD_CMD_CTL_SETPARAMS; - ps_ctl_ip->u4_size = sizeof(ih264d_ctl_set_config_ip_t); - ps_ctl_op->u4_size = sizeof(ih264d_ctl_set_config_op_t); - - WORD32 status = ih264d_api_function(m_codecCtx, &s_h264d_ctl_ip, &s_h264d_ctl_op); - cemu_assert(status == 0); - } - - /* In non-flush mode we have a delay of (at least?) 5 frames */ - void AddBufferedResult(DecodeResult& decodeResult) - { - if (decodeResult.frameReady) - m_bufferedResults.emplace_back(decodeResult); - } - - void GetCurrentBufferedResult(DecodeResult& decodeResult) - { - cemu_assert(!m_bufferedResults.empty()); - if (m_bufferedResults.empty()) - { - decodeResult.frameReady = false; - return; - } - decodeResult = m_bufferedResults.front(); - m_bufferedResults.erase(m_bufferedResults.begin()); - } - private: - iv_obj_t* m_codecCtx{nullptr}; - bool m_hasBufferSizeInfo{ false }; - bool m_isBufferedMode{ false }; - double m_timestamps[64]; - void* m_imageBuffers[64]; - uint32 m_timestampIndex{0}; - std::vector m_bufferedResults; - uint32 m_numDecodedFrames{0}; - std::vector> m_displayBuf; - }; - H264DEC_STATUS H264DECGetImageSize(uint8* stream, uint32 length, uint32 offset, uint32be* outputWidth, uint32be* outputHeight) { - cemu_assert(offset <= length); - - uint32 imageWidth, imageHeight; - - if (H264AVCDecoder::GetImageInfo(stream, length, imageWidth, imageHeight)) + if(!stream || length < 4 || !outputWidth || !outputHeight) + return H264DEC_STATUS::INVALID_PARAM; + if( (offset+4) > length ) + return H264DEC_STATUS::INVALID_PARAM; + uint8* cur = stream + offset; + uint8* end = stream + length; + cur += 2; // we access cur[-2] and cur[-1] so we need to start at offset 2 + while(cur < end-2) { - if (H264_IsBotW()) + // check for start code + if(*cur != 1) { - if (imageWidth == 1920 && imageHeight == 1088) - imageHeight = 1080; + cur++; + continue; } - *outputWidth = imageWidth; - *outputHeight = imageHeight; + // check if this is a valid NAL header + if(cur[-2] != 0 || cur[-1] != 0 || cur[0] != 1) + { + cur++; + continue; + } + uint8 nalHeader = cur[1]; + if((nalHeader & 0x1F) != 7) + { + cur++; + continue; + } + h264State_seq_parameter_set_t psp; + bool r = h264Parser_ParseSPS(cur+2, end-cur-2, psp); + if(!r) + { + cemu_assert_suspicious(); // should not happen + return H264DEC_STATUS::BAD_STREAM; + } + *outputWidth = (psp.pic_width_in_mbs_minus1 + 1) * 16; + *outputHeight = (psp.pic_height_in_map_units_minus1 + 1) * 16; // affected by frame_mbs_only_flag? + return H264DEC_STATUS::SUCCESS; } - else - { - *outputWidth = 0; - *outputHeight = 0; - return H264DEC_STATUS::BAD_STREAM; - } - - return H264DEC_STATUS::SUCCESS; + return H264DEC_STATUS::BAD_STREAM; } uint32 H264DECInitParam(uint32 workMemorySize, void* workMemory) @@ -762,26 +239,28 @@ namespace H264 return 0; } - std::unordered_map sDecoderSessions; + std::unordered_map sDecoderSessions; std::mutex sDecoderSessionsMutex; std::atomic_uint32_t sCurrentSessionHandle{ 1 }; - static H264AVCDecoder* _CreateDecoderSession(uint32& handleOut) + H264DecoderBackend* CreateAVCDecoder(); + + static H264DecoderBackend* _CreateDecoderSession(uint32& handleOut) { std::unique_lock _lock(sDecoderSessionsMutex); handleOut = sCurrentSessionHandle.fetch_add(1); - H264AVCDecoder* session = new H264AVCDecoder(); + H264DecoderBackend* session = CreateAVCDecoder(); sDecoderSessions.try_emplace(handleOut, session); return session; } - static H264AVCDecoder* _AcquireDecoderSession(uint32 handle) + static H264DecoderBackend* _AcquireDecoderSession(uint32 handle) { std::unique_lock _lock(sDecoderSessionsMutex); auto it = sDecoderSessions.find(handle); if (it == sDecoderSessions.end()) return nullptr; - H264AVCDecoder* session = it->second; + H264DecoderBackend* session = it->second; if (sDecoderSessions.size() >= 5) { cemuLog_log(LogType::Force, "H264: Warning - more than 5 active sessions"); @@ -790,7 +269,7 @@ namespace H264 return session; } - static void _ReleaseDecoderSession(H264AVCDecoder* session) + static void _ReleaseDecoderSession(H264DecoderBackend* session) { std::unique_lock _lock(sDecoderSessionsMutex); @@ -802,7 +281,7 @@ namespace H264 auto it = sDecoderSessions.find(handle); if (it == sDecoderSessions.end()) return; - H264AVCDecoder* session = it->second; + H264DecoderBackend* session = it->second; session->Destroy(); delete session; sDecoderSessions.erase(it); @@ -830,45 +309,44 @@ namespace H264 uint32 H264DECBegin(void* workMemory) { H264Context* ctx = (H264Context*)workMemory; - H264AVCDecoder* session = _AcquireDecoderSession(ctx->sessionHandle); + H264DecoderBackend* session = _AcquireDecoderSession(ctx->sessionHandle); if (!session) { cemuLog_log(LogType::Force, "H264DECBegin(): Invalid session"); return 0; } session->Init(ctx->Param.outputPerFrame == 0); + ctx->decoderState.numFramesInFlight = 0; _ReleaseDecoderSession(session); return 0; } - void H264DoFrameOutputCallback(H264Context* ctx, H264AVCDecoder::DecodeResult& decodeResult); - - void _async_H264DECEnd(coreinit::OSEvent* executeDoneEvent, H264AVCDecoder* session, H264Context* ctx, std::vector* decodeResultsOut) - { - *decodeResultsOut = session->Flush(); - coreinit::OSSignalEvent(executeDoneEvent); - } + void H264DoFrameOutputCallback(H264Context* ctx, H264DecoderBackend::DecodeResult& decodeResult); H264DEC_STATUS H264DECEnd(void* workMemory) { H264Context* ctx = (H264Context*)workMemory; - H264AVCDecoder* session = _AcquireDecoderSession(ctx->sessionHandle); + H264DecoderBackend* session = _AcquireDecoderSession(ctx->sessionHandle); if (!session) { cemuLog_log(LogType::Force, "H264DECEnd(): Invalid session"); return H264DEC_STATUS::SUCCESS; } - StackAllocator executeDoneEvent; - coreinit::OSInitEvent(&executeDoneEvent, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_MANUAL); - std::vector results; - auto asyncTask = std::async(std::launch::async, _async_H264DECEnd, executeDoneEvent.GetPointer(), session, ctx, &results); - coreinit::OSWaitEvent(&executeDoneEvent); - _ReleaseDecoderSession(session); - if (!results.empty()) + coreinit::OSEvent* flushEvt = &session->GetFlushEvent(); + coreinit::OSResetEvent(flushEvt); + session->QueueFlush(); + coreinit::OSWaitEvent(flushEvt); + while(true) { - for (auto& itr : results) - H264DoFrameOutputCallback(ctx, itr); + H264DecoderBackend::DecodeResult decodeResult; + if( !session->GetFrameOutputIfReady(decodeResult) ) + break; + // todo - output all frames in a single callback? + H264DoFrameOutputCallback(ctx, decodeResult); + ctx->decoderState.numFramesInFlight--; } + cemu_assert_debug(ctx->decoderState.numFramesInFlight == 0); // no frames should be in flight anymore. Exact behavior is not well understood but we may have to output dummy frames if necessary + _ReleaseDecoderSession(session); return H264DEC_STATUS::SUCCESS; } @@ -930,7 +408,6 @@ namespace H264 return 0; } - struct H264DECFrameOutput { /* +0x00 */ uint32be result; @@ -967,7 +444,7 @@ namespace H264 static_assert(sizeof(H264OutputCBStruct) == 12); - void H264DoFrameOutputCallback(H264Context* ctx, H264AVCDecoder::DecodeResult& decodeResult) + void H264DoFrameOutputCallback(H264Context* ctx, H264DecoderBackend::DecodeResult& decodeResult) { sint32 outputFrameCount = 1; @@ -984,14 +461,14 @@ namespace H264 frameOutput->imagePtr = (uint8*)decodeResult.imageOutput; frameOutput->result = 100; frameOutput->timestamp = decodeResult.timestamp; - frameOutput->frameWidth = decodeResult.decodeOutput.u4_pic_wd; - frameOutput->frameHeight = decodeResult.decodeOutput.u4_pic_ht; - frameOutput->bytesPerRow = (decodeResult.decodeOutput.u4_pic_wd + 0xFF) & ~0xFF; - frameOutput->cropEnable = decodeResult.decodeOutput.u1_frame_cropping_flag; - frameOutput->cropTop = decodeResult.decodeOutput.u1_frame_cropping_rect_top_ofst; - frameOutput->cropBottom = decodeResult.decodeOutput.u1_frame_cropping_rect_bottom_ofst; - frameOutput->cropLeft = decodeResult.decodeOutput.u1_frame_cropping_rect_left_ofst; - frameOutput->cropRight = decodeResult.decodeOutput.u1_frame_cropping_rect_right_ofst; + frameOutput->frameWidth = decodeResult.frameWidth; + frameOutput->frameHeight = decodeResult.frameHeight; + frameOutput->bytesPerRow = decodeResult.bytesPerRow; + frameOutput->cropEnable = decodeResult.cropEnable; + frameOutput->cropTop = decodeResult.cropTop; + frameOutput->cropBottom = decodeResult.cropBottom; + frameOutput->cropLeft = decodeResult.cropLeft; + frameOutput->cropRight = decodeResult.cropRight; StackAllocator stack_fptrOutputData; stack_fptrOutputData->frameCount = outputFrameCount; @@ -1006,29 +483,41 @@ namespace H264 } } - void _async_H264DECExecute(coreinit::OSEvent* executeDoneEvent, H264AVCDecoder* session, H264Context* ctx, void* imageOutput, H264AVCDecoder::DecodeResult* decodeResult) - { - session->Decode(ctx->BitStream.ptr.GetPtr(), ctx->BitStream.length, ctx->BitStream.timestamp, imageOutput, *decodeResult); - coreinit::OSSignalEvent(executeDoneEvent); - } - uint32 H264DECExecute(void* workMemory, void* imageOutput) { + BenchmarkTimer bt; + bt.Start(); H264Context* ctx = (H264Context*)workMemory; - H264AVCDecoder* session = _AcquireDecoderSession(ctx->sessionHandle); + H264DecoderBackend* session = _AcquireDecoderSession(ctx->sessionHandle); if (!session) { cemuLog_log(LogType::Force, "H264DECExecute(): Invalid session"); return 0; } - StackAllocator executeDoneEvent; - coreinit::OSInitEvent(&executeDoneEvent, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_MANUAL); - H264AVCDecoder::DecodeResult decodeResult; - auto asyncTask = std::async(std::launch::async, _async_H264DECExecute, &executeDoneEvent, session, ctx, imageOutput , &decodeResult); - coreinit::OSWaitEvent(&executeDoneEvent); + // feed data to backend + session->QueueForDecode((uint8*)ctx->BitStream.ptr.GetPtr(), ctx->BitStream.length, ctx->BitStream.timestamp, imageOutput); + ctx->decoderState.numFramesInFlight++; + // H264DECExecute is synchronous and will return a frame after either every call (non-buffered) or after 6 calls (buffered) + // normally frame decoding happens only during H264DECExecute, but in order to hide the latency of our CPU decoder we will decode asynchronously in buffered mode + uint32 numFramesToBuffer = (ctx->Param.outputPerFrame == 0) ? 5 : 0; + if(ctx->decoderState.numFramesInFlight > numFramesToBuffer) + { + ctx->decoderState.numFramesInFlight--; + while(true) + { + coreinit::OSEvent& evt = session->GetFrameOutputEvent(); + coreinit::OSWaitEvent(&evt); + H264DecoderBackend::DecodeResult decodeResult; + if( !session->GetFrameOutputIfReady(decodeResult) ) + continue; + H264DoFrameOutputCallback(ctx, decodeResult); + break; + } + } _ReleaseDecoderSession(session); - if(decodeResult.frameReady) - H264DoFrameOutputCallback(ctx, decodeResult); + bt.Stop(); + double callTime = bt.GetElapsedMilliseconds(); + cemuLog_log(LogType::H264, "H264Bench | H264DECExecute took {}ms", callTime); return 0x80 | 100; } diff --git a/src/Cafe/OS/libs/h264_avc/H264DecBackendAVC.cpp b/src/Cafe/OS/libs/h264_avc/H264DecBackendAVC.cpp new file mode 100644 index 00000000..228f65a5 --- /dev/null +++ b/src/Cafe/OS/libs/h264_avc/H264DecBackendAVC.cpp @@ -0,0 +1,502 @@ +#include "H264DecInternal.h" +#include "util/highresolutiontimer/HighResolutionTimer.h" + +extern "C" +{ +#include "../dependencies/ih264d/common/ih264_typedefs.h" +#include "../dependencies/ih264d/decoder/ih264d.h" +}; + +namespace H264 +{ + bool H264_IsBotW(); + + class H264AVCDecoder : public H264DecoderBackend + { + static void* ivd_aligned_malloc(void* ctxt, WORD32 alignment, WORD32 size) + { +#ifdef _WIN32 + return _aligned_malloc(size, alignment); +#else + // alignment is atleast sizeof(void*) + alignment = std::max(alignment, sizeof(void*)); + + //smallest multiple of 2 at least as large as alignment + alignment--; + alignment |= alignment << 1; + alignment |= alignment >> 1; + alignment |= alignment >> 2; + alignment |= alignment >> 4; + alignment |= alignment >> 8; + alignment |= alignment >> 16; + alignment ^= (alignment >> 1); + + void* temp; + posix_memalign(&temp, (size_t)alignment, (size_t)size); + return temp; +#endif + } + + static void ivd_aligned_free(void* ctxt, void* buf) + { +#ifdef _WIN32 + _aligned_free(buf); +#else + free(buf); +#endif + } + + public: + H264AVCDecoder() + { + m_decoderThread = std::thread(&H264AVCDecoder::DecoderThread, this); + } + + ~H264AVCDecoder() + { + m_threadShouldExit = true; + m_decodeSem.increment(); + if (m_decoderThread.joinable()) + m_decoderThread.join(); + } + + void Init(bool isBufferedMode) + { + ih264d_create_ip_t s_create_ip{ 0 }; + ih264d_create_op_t s_create_op{ 0 }; + + s_create_ip.s_ivd_create_ip_t.u4_size = sizeof(ih264d_create_ip_t); + s_create_ip.s_ivd_create_ip_t.e_cmd = IVD_CMD_CREATE; + s_create_ip.s_ivd_create_ip_t.u4_share_disp_buf = 1; // shared display buffer mode -> We give the decoder a list of buffers that it will use (?) + + s_create_op.s_ivd_create_op_t.u4_size = sizeof(ih264d_create_op_t); + s_create_ip.s_ivd_create_ip_t.e_output_format = IV_YUV_420SP_UV; + s_create_ip.s_ivd_create_ip_t.pf_aligned_alloc = ivd_aligned_malloc; + s_create_ip.s_ivd_create_ip_t.pf_aligned_free = ivd_aligned_free; + s_create_ip.s_ivd_create_ip_t.pv_mem_ctxt = NULL; + + WORD32 status = ih264d_api_function(m_codecCtx, &s_create_ip, &s_create_op); + cemu_assert(!status); + + m_codecCtx = (iv_obj_t*)s_create_op.s_ivd_create_op_t.pv_handle; + m_codecCtx->pv_fxns = (void*)&ih264d_api_function; + m_codecCtx->u4_size = sizeof(iv_obj_t); + + SetDecoderCoreCount(1); + + m_isBufferedMode = isBufferedMode; + + UpdateParameters(false); + + m_numDecodedFrames = 0; + m_hasBufferSizeInfo = false; + } + + void Destroy() + { + if (!m_codecCtx) + return; + ih264d_delete_ip_t s_delete_ip{ 0 }; + ih264d_delete_op_t s_delete_op{ 0 }; + s_delete_ip.s_ivd_delete_ip_t.u4_size = sizeof(ih264d_delete_ip_t); + s_delete_ip.s_ivd_delete_ip_t.e_cmd = IVD_CMD_DELETE; + s_delete_op.s_ivd_delete_op_t.u4_size = sizeof(ih264d_delete_op_t); + WORD32 status = ih264d_api_function(m_codecCtx, &s_delete_ip, &s_delete_op); + cemu_assert_debug(!status); + m_codecCtx = nullptr; + } + + void PushDecodedFrame(ivd_video_decode_op_t& s_dec_op) + { + // copy image data outside of lock since its an expensive operation + CopyImageToResultBuffer((uint8*)s_dec_op.s_disp_frm_buf.pv_y_buf, (uint8*)s_dec_op.s_disp_frm_buf.pv_u_buf, (uint8*)m_decodedSliceArray[s_dec_op.u4_ts].result.imageOutput, s_dec_op); + + std::unique_lock _l(m_decodeQueueMtx); + cemu_assert(s_dec_op.u4_ts < m_decodedSliceArray.size()); + auto& result = m_decodedSliceArray[s_dec_op.u4_ts]; + cemu_assert_debug(result.isUsed); + cemu_assert_debug(s_dec_op.u4_output_present != 0); + + result.result.isDecoded = true; + result.result.hasFrame = s_dec_op.u4_output_present != 0; + result.result.frameWidth = s_dec_op.u4_pic_wd; + result.result.frameHeight = s_dec_op.u4_pic_ht; + result.result.bytesPerRow = (s_dec_op.u4_pic_wd + 0xFF) & ~0xFF; + result.result.cropEnable = s_dec_op.u1_frame_cropping_flag; + result.result.cropTop = s_dec_op.u1_frame_cropping_rect_top_ofst; + result.result.cropBottom = s_dec_op.u1_frame_cropping_rect_bottom_ofst; + result.result.cropLeft = s_dec_op.u1_frame_cropping_rect_left_ofst; + result.result.cropRight = s_dec_op.u1_frame_cropping_rect_right_ofst; + + m_displayQueue.push_back(s_dec_op.u4_ts); + + _l.unlock(); + coreinit::OSSignalEvent(m_displayQueueEvt); + } + + // called from async worker thread + void Decode(DecodedSlice& decodedSlice) + { + if (!m_hasBufferSizeInfo) + { + uint32 numByteConsumed = 0; + if (!DetermineBufferSizes(decodedSlice.dataToDecode.m_data, decodedSlice.dataToDecode.m_length, numByteConsumed)) + { + cemuLog_log(LogType::Force, "H264AVC: Unable to determine picture size. Ignoring decode input"); + std::unique_lock _l(m_decodeQueueMtx); + decodedSlice.result.isDecoded = true; + decodedSlice.result.hasFrame = false; + coreinit::OSSignalEvent(m_displayQueueEvt); + return; + } + decodedSlice.dataToDecode.m_length -= numByteConsumed; + decodedSlice.dataToDecode.m_data = (uint8*)decodedSlice.dataToDecode.m_data + numByteConsumed; + m_hasBufferSizeInfo = true; + } + + ivd_video_decode_ip_t s_dec_ip{ 0 }; + ivd_video_decode_op_t s_dec_op{ 0 }; + s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); + s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); + + s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; + + s_dec_ip.u4_ts = std::distance(m_decodedSliceArray.data(), &decodedSlice); + cemu_assert_debug(s_dec_ip.u4_ts < m_decodedSliceArray.size()); + + s_dec_ip.pv_stream_buffer = (uint8*)decodedSlice.dataToDecode.m_data; + s_dec_ip.u4_num_Bytes = decodedSlice.dataToDecode.m_length; + + s_dec_ip.s_out_buffer.u4_min_out_buf_size[0] = 0; + s_dec_ip.s_out_buffer.u4_min_out_buf_size[1] = 0; + s_dec_ip.s_out_buffer.u4_num_bufs = 0; + + BenchmarkTimer bt; + bt.Start(); + WORD32 status = ih264d_api_function(m_codecCtx, &s_dec_ip, &s_dec_op); + if (status != 0 && (s_dec_op.u4_error_code&0xFF) == IVD_RES_CHANGED) + { + // resolution change + ResetDecoder(); + m_hasBufferSizeInfo = false; + Decode(decodedSlice); + return; + } + else if (status != 0) + { + cemuLog_log(LogType::Force, "H264: Failed to decode frame (error 0x{:08x})", status); + decodedSlice.result.hasFrame = false; + cemu_assert_unimplemented(); + return; + } + + bt.Stop(); + double decodeTime = bt.GetElapsedMilliseconds(); + + cemu_assert(s_dec_op.u4_frame_decoded_flag); + cemu_assert_debug(s_dec_op.u4_num_bytes_consumed == decodedSlice.dataToDecode.m_length); + + cemu_assert_debug(m_isBufferedMode || s_dec_op.u4_output_present); // if buffered mode is disabled, then every input should output a frame (except for partial slices?) + + if (s_dec_op.u4_output_present) + { + cemu_assert(s_dec_op.e_output_format == IV_YUV_420SP_UV); + if (H264_IsBotW()) + { + if (s_dec_op.s_disp_frm_buf.u4_y_wd == 1920 && s_dec_op.s_disp_frm_buf.u4_y_ht == 1088) + s_dec_op.s_disp_frm_buf.u4_y_ht = 1080; + } + bt.Start(); + PushDecodedFrame(s_dec_op); + bt.Stop(); + double copyTime = bt.GetElapsedMilliseconds(); + // release buffer + sint32 bufferId = -1; + for (size_t i = 0; i < m_displayBuf.size(); i++) + { + if (s_dec_op.s_disp_frm_buf.pv_y_buf >= m_displayBuf[i].data() && s_dec_op.s_disp_frm_buf.pv_y_buf < (m_displayBuf[i].data() + m_displayBuf[i].size())) + { + bufferId = (sint32)i; + break; + } + } + cemu_assert_debug(bufferId == s_dec_op.u4_disp_buf_id); + cemu_assert(bufferId >= 0); + ivd_rel_display_frame_ip_t s_video_rel_disp_ip{ 0 }; + ivd_rel_display_frame_op_t s_video_rel_disp_op{ 0 }; + s_video_rel_disp_ip.e_cmd = IVD_CMD_REL_DISPLAY_FRAME; + s_video_rel_disp_ip.u4_size = sizeof(ivd_rel_display_frame_ip_t); + s_video_rel_disp_op.u4_size = sizeof(ivd_rel_display_frame_op_t); + s_video_rel_disp_ip.u4_disp_buf_id = bufferId; + status = ih264d_api_function(m_codecCtx, &s_video_rel_disp_ip, &s_video_rel_disp_op); + cemu_assert(!status); + + cemuLog_log(LogType::H264, "H264Bench | DecodeTime {}ms CopyTime {}ms", decodeTime, copyTime); + } + else + { + cemuLog_log(LogType::H264, "H264Bench | DecodeTime {}ms (no frame output)", decodeTime); + } + + if (s_dec_op.u4_frame_decoded_flag) + m_numDecodedFrames++; + // get VUI + //ih264d_ctl_get_vui_params_ip_t s_ctl_get_vui_params_ip; + //ih264d_ctl_get_vui_params_op_t s_ctl_get_vui_params_op; + + //s_ctl_get_vui_params_ip.e_cmd = IVD_CMD_VIDEO_CTL; + //s_ctl_get_vui_params_ip.e_sub_cmd = (IVD_CONTROL_API_COMMAND_TYPE_T)IH264D_CMD_CTL_GET_VUI_PARAMS; + //s_ctl_get_vui_params_ip.u4_size = sizeof(ih264d_ctl_get_vui_params_ip_t); + //s_ctl_get_vui_params_op.u4_size = sizeof(ih264d_ctl_get_vui_params_op_t); + + //status = ih264d_api_function(mCodecCtx, &s_ctl_get_vui_params_ip, &s_ctl_get_vui_params_op); + //cemu_assert(status == 0); + } + + void Flush() + { + // set flush mode + ivd_ctl_flush_ip_t s_video_flush_ip{ 0 }; + ivd_ctl_flush_op_t s_video_flush_op{ 0 }; + s_video_flush_ip.e_cmd = IVD_CMD_VIDEO_CTL; + s_video_flush_ip.e_sub_cmd = IVD_CMD_CTL_FLUSH; + s_video_flush_ip.u4_size = sizeof(ivd_ctl_flush_ip_t); + s_video_flush_op.u4_size = sizeof(ivd_ctl_flush_op_t); + WORD32 status = ih264d_api_function(m_codecCtx, &s_video_flush_ip, &s_video_flush_op); + if (status != 0) + cemuLog_log(LogType::Force, "H264Dec: Unexpected error during flush ({})", status); + // get all frames from the decoder + while (true) + { + ivd_video_decode_ip_t s_dec_ip{ 0 }; + ivd_video_decode_op_t s_dec_op{ 0 }; + s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); + s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); + s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; + s_dec_ip.pv_stream_buffer = NULL; + s_dec_ip.u4_num_Bytes = 0; + s_dec_ip.s_out_buffer.u4_min_out_buf_size[0] = 0; + s_dec_ip.s_out_buffer.u4_min_out_buf_size[1] = 0; + s_dec_ip.s_out_buffer.u4_num_bufs = 0; + status = ih264d_api_function(m_codecCtx, &s_dec_ip, &s_dec_op); + if (status != 0) + break; + cemu_assert_debug(s_dec_op.u4_output_present != 0); // should never be false? + if(s_dec_op.u4_output_present == 0) + continue; + if (H264_IsBotW()) + { + if (s_dec_op.s_disp_frm_buf.u4_y_wd == 1920 && s_dec_op.s_disp_frm_buf.u4_y_ht == 1088) + s_dec_op.s_disp_frm_buf.u4_y_ht = 1080; + } + PushDecodedFrame(s_dec_op); + } + } + + void CopyImageToResultBuffer(uint8* yIn, uint8* uvIn, uint8* bufOut, ivd_video_decode_op_t& decodeInfo) + { + uint32 imageWidth = decodeInfo.s_disp_frm_buf.u4_y_wd; + uint32 imageHeight = decodeInfo.s_disp_frm_buf.u4_y_ht; + + size_t inputStride = decodeInfo.s_disp_frm_buf.u4_y_strd; + size_t outputStride = (imageWidth + 0xFF) & ~0xFF; + + // copy Y + uint8* yOut = bufOut; + for (uint32 row = 0; row < imageHeight; row++) + { + memcpy(yOut, yIn, imageWidth); + yIn += inputStride; + yOut += outputStride; + } + + // copy UV + uint8* uvOut = bufOut + outputStride * imageHeight; + for (uint32 row = 0; row < imageHeight/2; row++) + { + memcpy(uvOut, uvIn, imageWidth); + uvIn += inputStride; + uvOut += outputStride; + } + } + private: + void SetDecoderCoreCount(uint32 coreCount) + { + ih264d_ctl_set_num_cores_ip_t s_set_cores_ip; + ih264d_ctl_set_num_cores_op_t s_set_cores_op; + s_set_cores_ip.e_cmd = IVD_CMD_VIDEO_CTL; + s_set_cores_ip.e_sub_cmd = (IVD_CONTROL_API_COMMAND_TYPE_T)IH264D_CMD_CTL_SET_NUM_CORES; + s_set_cores_ip.u4_num_cores = coreCount; // valid numbers are 1-4 + s_set_cores_ip.u4_size = sizeof(ih264d_ctl_set_num_cores_ip_t); + s_set_cores_op.u4_size = sizeof(ih264d_ctl_set_num_cores_op_t); + IV_API_CALL_STATUS_T status = ih264d_api_function(m_codecCtx, (void *)&s_set_cores_ip, (void *)&s_set_cores_op); + cemu_assert(status == IV_SUCCESS); + } + + bool DetermineBufferSizes(void* data, uint32 length, uint32& numByteConsumed) + { + numByteConsumed = 0; + UpdateParameters(true); + + ivd_video_decode_ip_t s_dec_ip{ 0 }; + ivd_video_decode_op_t s_dec_op{ 0 }; + s_dec_ip.u4_size = sizeof(ivd_video_decode_ip_t); + s_dec_op.u4_size = sizeof(ivd_video_decode_op_t); + + s_dec_ip.e_cmd = IVD_CMD_VIDEO_DECODE; + s_dec_ip.pv_stream_buffer = (uint8*)data; + s_dec_ip.u4_num_Bytes = length; + s_dec_ip.s_out_buffer.u4_num_bufs = 0; + WORD32 status = ih264d_api_function(m_codecCtx, &s_dec_ip, &s_dec_op); + if (status != 0) + { + cemuLog_log(LogType::Force, "H264: Unable to determine buffer sizes for stream"); + return false; + } + numByteConsumed = s_dec_op.u4_num_bytes_consumed; + cemu_assert(status == 0); + if (s_dec_op.u4_pic_wd == 0 || s_dec_op.u4_pic_ht == 0) + return false; + UpdateParameters(false); + ReinitBuffers(); + return true; + } + + void ReinitBuffers() + { + ivd_ctl_getbufinfo_ip_t s_ctl_ip{ 0 }; + ivd_ctl_getbufinfo_op_t s_ctl_op{ 0 }; + WORD32 outlen = 0; + + s_ctl_ip.e_cmd = IVD_CMD_VIDEO_CTL; + s_ctl_ip.e_sub_cmd = IVD_CMD_CTL_GETBUFINFO; + s_ctl_ip.u4_size = sizeof(ivd_ctl_getbufinfo_ip_t); + s_ctl_op.u4_size = sizeof(ivd_ctl_getbufinfo_op_t); + + WORD32 status = ih264d_api_function(m_codecCtx, &s_ctl_ip, &s_ctl_op); + cemu_assert(!status); + + // allocate + for (uint32 i = 0; i < s_ctl_op.u4_num_disp_bufs; i++) + { + m_displayBuf.emplace_back().resize(s_ctl_op.u4_min_out_buf_size[0] + s_ctl_op.u4_min_out_buf_size[1]); + } + // set + ivd_set_display_frame_ip_t s_set_display_frame_ip{ 0 }; // make sure to zero-initialize this. The codec seems to check the first 3 pointers/sizes per frame, regardless of the value of u4_num_bufs + ivd_set_display_frame_op_t s_set_display_frame_op{ 0 }; + + s_set_display_frame_ip.e_cmd = IVD_CMD_SET_DISPLAY_FRAME; + s_set_display_frame_ip.u4_size = sizeof(ivd_set_display_frame_ip_t); + s_set_display_frame_op.u4_size = sizeof(ivd_set_display_frame_op_t); + + cemu_assert_debug(s_ctl_op.u4_min_num_out_bufs == 2); + cemu_assert_debug(s_ctl_op.u4_min_out_buf_size[0] != 0 && s_ctl_op.u4_min_out_buf_size[1] != 0); + + s_set_display_frame_ip.num_disp_bufs = s_ctl_op.u4_num_disp_bufs; + + for (uint32 i = 0; i < s_ctl_op.u4_num_disp_bufs; i++) + { + s_set_display_frame_ip.s_disp_buffer[i].u4_num_bufs = 2; + s_set_display_frame_ip.s_disp_buffer[i].u4_min_out_buf_size[0] = s_ctl_op.u4_min_out_buf_size[0]; + s_set_display_frame_ip.s_disp_buffer[i].u4_min_out_buf_size[1] = s_ctl_op.u4_min_out_buf_size[1]; + s_set_display_frame_ip.s_disp_buffer[i].pu1_bufs[0] = m_displayBuf[i].data() + 0; + s_set_display_frame_ip.s_disp_buffer[i].pu1_bufs[1] = m_displayBuf[i].data() + s_ctl_op.u4_min_out_buf_size[0]; + } + + status = ih264d_api_function(m_codecCtx, &s_set_display_frame_ip, &s_set_display_frame_op); + cemu_assert(!status); + + + // mark all as released (available) + for (uint32 i = 0; i < s_ctl_op.u4_num_disp_bufs; i++) + { + ivd_rel_display_frame_ip_t s_video_rel_disp_ip{ 0 }; + ivd_rel_display_frame_op_t s_video_rel_disp_op{ 0 }; + + s_video_rel_disp_ip.e_cmd = IVD_CMD_REL_DISPLAY_FRAME; + s_video_rel_disp_ip.u4_size = sizeof(ivd_rel_display_frame_ip_t); + s_video_rel_disp_op.u4_size = sizeof(ivd_rel_display_frame_op_t); + s_video_rel_disp_ip.u4_disp_buf_id = i; + + status = ih264d_api_function(m_codecCtx, &s_video_rel_disp_ip, &s_video_rel_disp_op); + cemu_assert(!status); + } + } + + void ResetDecoder() + { + ivd_ctl_reset_ip_t s_ctl_ip; + ivd_ctl_reset_op_t s_ctl_op; + + s_ctl_ip.e_cmd = IVD_CMD_VIDEO_CTL; + s_ctl_ip.e_sub_cmd = IVD_CMD_CTL_RESET; + s_ctl_ip.u4_size = sizeof(ivd_ctl_reset_ip_t); + s_ctl_op.u4_size = sizeof(ivd_ctl_reset_op_t); + + WORD32 status = ih264d_api_function(m_codecCtx, (void*)&s_ctl_ip, (void*)&s_ctl_op); + cemu_assert_debug(status == 0); + } + + void UpdateParameters(bool headerDecodeOnly) + { + ih264d_ctl_set_config_ip_t s_h264d_ctl_ip{ 0 }; + ih264d_ctl_set_config_op_t s_h264d_ctl_op{ 0 }; + ivd_ctl_set_config_ip_t* ps_ctl_ip = &s_h264d_ctl_ip.s_ivd_ctl_set_config_ip_t; + ivd_ctl_set_config_op_t* ps_ctl_op = &s_h264d_ctl_op.s_ivd_ctl_set_config_op_t; + + ps_ctl_ip->u4_disp_wd = 0; + ps_ctl_ip->e_frm_skip_mode = IVD_SKIP_NONE; + ps_ctl_ip->e_frm_out_mode = m_isBufferedMode ? IVD_DISPLAY_FRAME_OUT : IVD_DECODE_FRAME_OUT; + ps_ctl_ip->e_vid_dec_mode = headerDecodeOnly ? IVD_DECODE_HEADER : IVD_DECODE_FRAME; + ps_ctl_ip->e_cmd = IVD_CMD_VIDEO_CTL; + ps_ctl_ip->e_sub_cmd = IVD_CMD_CTL_SETPARAMS; + ps_ctl_ip->u4_size = sizeof(ih264d_ctl_set_config_ip_t); + ps_ctl_op->u4_size = sizeof(ih264d_ctl_set_config_op_t); + + WORD32 status = ih264d_api_function(m_codecCtx, &s_h264d_ctl_ip, &s_h264d_ctl_op); + cemu_assert(status == 0); + } + + private: + void DecoderThread() + { + while(!m_threadShouldExit) + { + m_decodeSem.decrementWithWait(); + std::unique_lock _l(m_decodeQueueMtx); + if (m_decodeQueue.empty()) + continue; + uint32 decodeIndex = m_decodeQueue.front(); + m_decodeQueue.erase(m_decodeQueue.begin()); + _l.unlock(); + if(decodeIndex == CMD_FLUSH) + { + Flush(); + _l.lock(); + cemu_assert_debug(m_decodeQueue.empty()); // after flushing the queue should be empty since the sender is waiting for the flush to complete + _l.unlock(); + coreinit::OSSignalEvent(m_flushEvt); + } + else + { + auto& decodedSlice = m_decodedSliceArray[decodeIndex]; + Decode(decodedSlice); + } + } + } + + iv_obj_t* m_codecCtx{nullptr}; + bool m_hasBufferSizeInfo{ false }; + bool m_isBufferedMode{ false }; + uint32 m_numDecodedFrames{0}; + std::vector> m_displayBuf; + + std::thread m_decoderThread; + std::atomic_bool m_threadShouldExit{false}; + }; + + H264DecoderBackend* CreateAVCDecoder() + { + return new H264AVCDecoder(); + } +}; diff --git a/src/Cafe/OS/libs/h264_avc/H264DecInternal.h b/src/Cafe/OS/libs/h264_avc/H264DecInternal.h new file mode 100644 index 00000000..498cccfa --- /dev/null +++ b/src/Cafe/OS/libs/h264_avc/H264DecInternal.h @@ -0,0 +1,139 @@ +#pragma once + +#include "util/helpers/Semaphore.h" +#include "Cafe/OS/libs/coreinit/coreinit_Thread.h" +#include "Cafe/OS/libs/coreinit/coreinit_SysHeap.h" + +#include "Cafe/OS/libs/h264_avc/parser/H264Parser.h" + +namespace H264 +{ + class H264DecoderBackend + { + protected: + struct DataToDecode + { + uint8* m_data; + uint32 m_length; + std::vector m_buffer; + }; + + static constexpr uint32 CMD_FLUSH = 0xFFFFFFFF; + + public: + struct DecodeResult + { + bool isDecoded{false}; + bool hasFrame{false}; // set to true if a full frame was successfully decoded + double timestamp{}; + void* imageOutput{nullptr}; + sint32 frameWidth{0}; + sint32 frameHeight{0}; + uint32 bytesPerRow{0}; + bool cropEnable{false}; + sint32 cropTop{0}; + sint32 cropBottom{0}; + sint32 cropLeft{0}; + sint32 cropRight{0}; + }; + + struct DecodedSlice + { + bool isUsed{false}; + DecodeResult result; + DataToDecode dataToDecode; + }; + + H264DecoderBackend() + { + m_displayQueueEvt = (coreinit::OSEvent*)coreinit::OSAllocFromSystem(sizeof(coreinit::OSEvent), 4); + coreinit::OSInitEvent(m_displayQueueEvt, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_AUTO); + m_flushEvt = (coreinit::OSEvent*)coreinit::OSAllocFromSystem(sizeof(coreinit::OSEvent), 4); + coreinit::OSInitEvent(m_flushEvt, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_AUTO); + }; + + virtual ~H264DecoderBackend() + { + coreinit::OSFreeToSystem(m_displayQueueEvt); + coreinit::OSFreeToSystem(m_flushEvt); + }; + + virtual void Init(bool isBufferedMode) = 0; + virtual void Destroy() = 0; + + void QueueForDecode(uint8* data, uint32 length, double timestamp, void* imagePtr) + { + std::unique_lock _l(m_decodeQueueMtx); + + DecodedSlice& ds = GetFreeDecodedSliceEntry(); + + ds.dataToDecode.m_buffer.assign(data, data + length); + ds.dataToDecode.m_data = ds.dataToDecode.m_buffer.data(); + ds.dataToDecode.m_length = length; + + ds.result.isDecoded = false; + ds.result.imageOutput = imagePtr; + ds.result.timestamp = timestamp; + + m_decodeQueue.push_back(std::distance(m_decodedSliceArray.data(), &ds)); + m_decodeSem.increment(); + } + + void QueueFlush() + { + std::unique_lock _l(m_decodeQueueMtx); + m_decodeQueue.push_back(CMD_FLUSH); + m_decodeSem.increment(); + } + + bool GetFrameOutputIfReady(DecodeResult& result) + { + std::unique_lock _l(m_decodeQueueMtx); + if(m_displayQueue.empty()) + return false; + uint32 sliceIndex = m_displayQueue.front(); + DecodedSlice& ds = m_decodedSliceArray[sliceIndex]; + cemu_assert_debug(ds.result.isDecoded); + std::swap(result, ds.result); + ds.isUsed = false; + m_displayQueue.erase(m_displayQueue.begin()); + return true; + } + + coreinit::OSEvent& GetFrameOutputEvent() + { + return *m_displayQueueEvt; + } + + coreinit::OSEvent& GetFlushEvent() + { + return *m_flushEvt; + } + + protected: + DecodedSlice& GetFreeDecodedSliceEntry() + { + for (auto& slice : m_decodedSliceArray) + { + if (!slice.isUsed) + { + slice.isUsed = true; + return slice; + } + } + cemu_assert_suspicious(); + return m_decodedSliceArray[0]; + } + + std::mutex m_decodeQueueMtx; + std::vector m_decodeQueue; // indices into m_decodedSliceArray, in order of decode input + CounterSemaphore m_decodeSem; + std::vector m_displayQueue; // indices into m_decodedSliceArray, in order of frame display output + coreinit::OSEvent* m_displayQueueEvt; // signalled when a new frame is ready for display + coreinit::OSEvent* m_flushEvt; // signalled after flush operation finished and all queued slices are decoded + + // frame output queue + std::mutex m_frameOutputMtx; + std::array m_decodedSliceArray; + }; +} \ No newline at end of file diff --git a/src/Cafe/OS/libs/h264_avc/parser/H264Parser.cpp b/src/Cafe/OS/libs/h264_avc/parser/H264Parser.cpp index d77e551f..36f70f81 100644 --- a/src/Cafe/OS/libs/h264_avc/parser/H264Parser.cpp +++ b/src/Cafe/OS/libs/h264_avc/parser/H264Parser.cpp @@ -319,6 +319,17 @@ bool parseNAL_pic_parameter_set_rbsp(h264ParserState_t* h264ParserState, h264Par return true; } +bool h264Parser_ParseSPS(uint8* data, uint32 length, h264State_seq_parameter_set_t& sps) +{ + h264ParserState_t parserState; + RBSPInputBitstream nalStream(data, length); + bool r = parseNAL_seq_parameter_set_rbsp(&parserState, nullptr, nalStream); + if(!r || !parserState.hasSPS) + return false; + sps = parserState.sps; + return true; +} + void parseNAL_ref_pic_list_modification(const h264State_seq_parameter_set_t& sps, const h264State_pic_parameter_set_t& pps, RBSPInputBitstream& nalStream, nal_slice_header_t* sliceHeader) { if (!sliceHeader->slice_type.isSliceTypeI() && !sliceHeader->slice_type.isSliceTypeSI()) @@ -688,9 +699,8 @@ void _calculateFrameOrder(h264ParserState_t* h264ParserState, const h264State_se else if (sps.pic_order_cnt_type == 2) { // display order matches decode order - uint32 prevFrameNum = h264ParserState->picture_order.prevFrameNum; - ; + uint32 FrameNumOffset; if (sliceHeader->IdrPicFlag) { @@ -706,9 +716,6 @@ void _calculateFrameOrder(h264ParserState_t* h264ParserState, const h264State_se FrameNumOffset = prevFrameNumOffset + sps.getMaxFrameNum(); else FrameNumOffset = prevFrameNumOffset; - - - } uint32 tempPicOrderCnt; diff --git a/src/Cafe/OS/libs/h264_avc/parser/H264Parser.h b/src/Cafe/OS/libs/h264_avc/parser/H264Parser.h index ee32ca8b..6f2b3cf6 100644 --- a/src/Cafe/OS/libs/h264_avc/parser/H264Parser.h +++ b/src/Cafe/OS/libs/h264_avc/parser/H264Parser.h @@ -513,6 +513,8 @@ typedef struct void h264Parse(h264ParserState_t* h264ParserState, h264ParserOutput_t* output, uint8* data, uint32 length, bool parseSlices = true); sint32 h264GetUnitLength(h264ParserState_t* h264ParserState, uint8* data, uint32 length); +bool h264Parser_ParseSPS(uint8* data, uint32 length, h264State_seq_parameter_set_t& sps); + void h264Parser_getScalingMatrix4x4(h264State_seq_parameter_set_t* sps, h264State_pic_parameter_set_t* pps, nal_slice_header_t* sliceHeader, sint32 index, uint8* matrix4x4); void h264Parser_getScalingMatrix8x8(h264State_seq_parameter_set_t* sps, h264State_pic_parameter_set_t* pps, nal_slice_header_t* sliceHeader, sint32 index, uint8* matrix8x8); From 026d547dccd568a67bd42214728b173811694a1e Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Fri, 26 Jul 2024 01:45:34 +0200 Subject: [PATCH 07/35] Use HTTP 1.1 in Nintendo API requests --- src/Cemu/napi/napi_helper.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Cemu/napi/napi_helper.cpp b/src/Cemu/napi/napi_helper.cpp index 164de7e5..182c5371 100644 --- a/src/Cemu/napi/napi_helper.cpp +++ b/src/Cemu/napi/napi_helper.cpp @@ -107,6 +107,7 @@ CurlRequestHelper::CurlRequestHelper() curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(m_curl, CURLOPT_MAXREDIRS, 2); + curl_easy_setopt(m_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); if(GetConfig().proxy_server.GetValue() != "") { @@ -263,6 +264,7 @@ CurlSOAPHelper::CurlSOAPHelper(NetworkService service) m_curl = curl_easy_init(); curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, __curlWriteCallback); curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, this); + curl_easy_setopt(m_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); // SSL if (!IsNetworkServiceSSLDisabled(service)) From 252429933f8ae8dde9443ee5cc2c17b83b7b9dc7 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Fri, 26 Jul 2024 03:31:42 +0200 Subject: [PATCH 08/35] debugger: Slightly optimize symbol list updates --- src/gui/debugger/SymbolCtrl.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/gui/debugger/SymbolCtrl.cpp b/src/gui/debugger/SymbolCtrl.cpp index cb1f3b1a..aa862987 100644 --- a/src/gui/debugger/SymbolCtrl.cpp +++ b/src/gui/debugger/SymbolCtrl.cpp @@ -46,25 +46,25 @@ SymbolListCtrl::SymbolListCtrl(wxWindow* parent, const wxWindowID& id, const wxP void SymbolListCtrl::OnGameLoaded() { m_data.clear(); - long itemId = 0; const auto symbol_map = rplSymbolStorage_lockSymbolMap(); for (auto const& [address, symbol_info] : symbol_map) { if (symbol_info == nullptr || symbol_info->symbolName == nullptr) continue; + wxString libNameWX = wxString::FromAscii((const char*)symbol_info->libName); + wxString symbolNameWX = wxString::FromAscii((const char*)symbol_info->symbolName); + wxString searchNameWX = libNameWX + symbolNameWX; + searchNameWX.MakeLower(); + auto new_entry = m_data.try_emplace( symbol_info->address, - (char*)(symbol_info->symbolName), - (char*)(symbol_info->libName), - "", + symbolNameWX, + libNameWX, + searchNameWX, false ); - new_entry.first->second.searchName += new_entry.first->second.name; - new_entry.first->second.searchName += new_entry.first->second.libName; - new_entry.first->second.searchName.MakeLower(); - if (m_list_filter.IsEmpty()) new_entry.first->second.visible = true; else if (new_entry.first->second.searchName.Contains(m_list_filter)) From 47f1dcf99691fbf0f8125f98f7df5ebf9eed221a Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Fri, 26 Jul 2024 05:08:38 +0200 Subject: [PATCH 09/35] debugger: Add symbol support to PPC stack traces Also moved the declaration to precompiled.h instead of redefining it wherever it is used --- src/Cafe/HW/Espresso/Debugger/Debugger.cpp | 2 -- src/Cafe/OS/libs/coreinit/coreinit.cpp | 14 +++++++++++--- src/Cafe/OS/libs/coreinit/coreinit_MEM_ExpHeap.cpp | 2 -- src/Common/ExceptionHandler/ExceptionHandler.cpp | 4 +--- src/Common/precompiled.h | 3 +++ .../PPCThreadsViewer/DebugPPCThreadsWindow.cpp | 4 +--- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Cafe/HW/Espresso/Debugger/Debugger.cpp b/src/Cafe/HW/Espresso/Debugger/Debugger.cpp index 62a5d592..e7369af6 100644 --- a/src/Cafe/HW/Espresso/Debugger/Debugger.cpp +++ b/src/Cafe/HW/Espresso/Debugger/Debugger.cpp @@ -501,8 +501,6 @@ void debugger_createPPCStateSnapshot(PPCInterpreter_t* hCPU) debuggerState.debugSession.ppcSnapshot.cr[i] = hCPU->cr[i]; } -void DebugLogStackTrace(OSThread_t* thread, MPTR sp); - void debugger_enterTW(PPCInterpreter_t* hCPU) { // handle logging points diff --git a/src/Cafe/OS/libs/coreinit/coreinit.cpp b/src/Cafe/OS/libs/coreinit/coreinit.cpp index 49d232f8..00327a97 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit.cpp @@ -1,6 +1,6 @@ #include "Cafe/OS/common/OSCommon.h" #include "Common/SysAllocator.h" -#include "Cafe/OS/RPL/rpl.h" +#include "Cafe/OS/RPL/rpl_symbol_storage.h" #include "Cafe/OS/libs/coreinit/coreinit_Misc.h" @@ -69,7 +69,7 @@ sint32 ScoreStackTrace(OSThread_t* thread, MPTR sp) return score; } -void DebugLogStackTrace(OSThread_t* thread, MPTR sp) +void DebugLogStackTrace(OSThread_t* thread, MPTR sp, bool printSymbols) { // sp might not point to a valid stackframe // scan stack and evaluate which sp is most likely the beginning of the stackframe @@ -107,7 +107,15 @@ void DebugLogStackTrace(OSThread_t* thread, MPTR sp) uint32 returnAddress = 0; returnAddress = memory_readU32(nextStackPtr + 4); - cemuLog_log(LogType::Force, fmt::format("SP {0:08x} ReturnAddr {1:08x}", nextStackPtr, returnAddress)); + + RPLStoredSymbol* symbol = nullptr; + if(printSymbols) + symbol = rplSymbolStorage_getByClosestAddress(returnAddress); + + if(symbol) + cemuLog_log(LogType::Force, fmt::format("SP {:08x} ReturnAddr {:08x} ({}.{}+0x{:x})", nextStackPtr, returnAddress, (const char*)symbol->libName, (const char*)symbol->symbolName, returnAddress - symbol->address)); + else + cemuLog_log(LogType::Force, fmt::format("SP {:08x} ReturnAddr {:08x}", nextStackPtr, returnAddress)); currentStackPtr = nextStackPtr; } diff --git a/src/Cafe/OS/libs/coreinit/coreinit_MEM_ExpHeap.cpp b/src/Cafe/OS/libs/coreinit/coreinit_MEM_ExpHeap.cpp index 7ddadcf1..552a610f 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_MEM_ExpHeap.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_MEM_ExpHeap.cpp @@ -2,8 +2,6 @@ #include "Cafe/HW/Espresso/PPCCallback.h" #include "Cafe/OS/libs/coreinit/coreinit_MEM_ExpHeap.h" -void DebugLogStackTrace(OSThread_t* thread, MPTR sp); - #define EXP_HEAP_GET_FROM_FREE_BLOCKCHAIN(__blockchain__) (MEMExpHeapHead2*)((uintptr_t)__blockchain__ - offsetof(MEMExpHeapHead2, expHeapHead) - offsetof(MEMExpHeapHead40_t, chainFreeBlocks)) namespace coreinit diff --git a/src/Common/ExceptionHandler/ExceptionHandler.cpp b/src/Common/ExceptionHandler/ExceptionHandler.cpp index b6755fd8..7530a2eb 100644 --- a/src/Common/ExceptionHandler/ExceptionHandler.cpp +++ b/src/Common/ExceptionHandler/ExceptionHandler.cpp @@ -6,8 +6,6 @@ #include "Cafe/HW/Espresso/Debugger/GDBStub.h" #include "ExceptionHandler.h" -void DebugLogStackTrace(OSThread_t* thread, MPTR sp); - bool crashLogCreated = false; bool CrashLog_Create() @@ -97,7 +95,7 @@ void ExceptionHandler_LogGeneralInfo() MPTR currentStackVAddr = hCPU->gpr[1]; CrashLog_WriteLine(""); CrashLog_WriteHeader("PPC stack trace"); - DebugLogStackTrace(currentThread, currentStackVAddr); + DebugLogStackTrace(currentThread, currentStackVAddr, true); // stack dump CrashLog_WriteLine(""); diff --git a/src/Common/precompiled.h b/src/Common/precompiled.h index 790a001a..61707519 100644 --- a/src/Common/precompiled.h +++ b/src/Common/precompiled.h @@ -552,6 +552,9 @@ inline uint32 GetTitleIdLow(uint64 titleId) #include "Cafe/HW/Espresso/PPCState.h" #include "Cafe/HW/Espresso/PPCCallback.h" +// PPC stack trace printer +void DebugLogStackTrace(struct OSThread_t* thread, MPTR sp, bool printSymbols = false); + // generic formatter for enums (to underlying) template requires std::is_enum_v diff --git a/src/gui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp b/src/gui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp index dfbaf76e..f4e5b7af 100644 --- a/src/gui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp +++ b/src/gui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp @@ -277,12 +277,10 @@ void DebugPPCThreadsWindow::RefreshThreadList() m_thread_list->SetScrollPos(0, scrollPos, true); } -void DebugLogStackTrace(OSThread_t* thread, MPTR sp); - void DebugPPCThreadsWindow::DumpStackTrace(OSThread_t* thread) { cemuLog_log(LogType::Force, "Dumping stack trace for thread {0:08x} LR: {1:08x}", memory_getVirtualOffsetFromPointer(thread), _swapEndianU32(thread->context.lr)); - DebugLogStackTrace(thread, _swapEndianU32(thread->context.gpr[1])); + DebugLogStackTrace(thread, _swapEndianU32(thread->context.gpr[1]), true); } void DebugPPCThreadsWindow::PresentProfileResults(OSThread_t* thread, const std::unordered_map& samples) From 5328e9eb10b2abeaf303310b06b37269d79dde12 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Fri, 26 Jul 2024 05:13:45 +0200 Subject: [PATCH 10/35] CPU: Fix overflow bit calculation in SUBFO instruction Since rD can overlap with rA or rB the result needs to be stored in a temporary --- src/Cafe/HW/Espresso/Interpreter/PPCInterpreterALU.hpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterALU.hpp b/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterALU.hpp index ed97288d..fe9316f0 100644 --- a/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterALU.hpp +++ b/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterALU.hpp @@ -212,11 +212,12 @@ static void PPCInterpreter_SUBF(PPCInterpreter_t* hCPU, uint32 opcode) static void PPCInterpreter_SUBFO(PPCInterpreter_t* hCPU, uint32 opcode) { - // untested (Don't Starve Giant Edition uses this) + // Seen in Don't Starve Giant Edition and Teslagrad // also used by DS Virtual Console (Super Mario 64 DS) PPC_OPC_TEMPL3_XO(); - hCPU->gpr[rD] = ~hCPU->gpr[rA] + hCPU->gpr[rB] + 1; - PPCInterpreter_setXerOV(hCPU, checkAdditionOverflow(~hCPU->gpr[rA], hCPU->gpr[rB], hCPU->gpr[rD])); + uint32 result = ~hCPU->gpr[rA] + hCPU->gpr[rB] + 1; + PPCInterpreter_setXerOV(hCPU, checkAdditionOverflow(~hCPU->gpr[rA], hCPU->gpr[rB], result)); + hCPU->gpr[rD] = result; if (opHasRC()) ppc_update_cr0(hCPU, hCPU->gpr[rD]); PPCInterpreter_nextInstruction(hCPU); From c73fa3761c9572db4d09cdb976a0f1510cda548a Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:45:36 +0200 Subject: [PATCH 11/35] Fix compatibility with GCC --- src/resource/embedded/fontawesome.S | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/resource/embedded/fontawesome.S b/src/resource/embedded/fontawesome.S index 29b4f93a..db23e7ae 100644 --- a/src/resource/embedded/fontawesome.S +++ b/src/resource/embedded/fontawesome.S @@ -1,4 +1,4 @@ -.rodata +.section .rodata,"",%progbits .global g_fontawesome_data, g_fontawesome_size g_fontawesome_data: @@ -6,3 +6,4 @@ g_fontawesome_data: g_fontawesome_size: .int g_fontawesome_size - g_fontawesome_data +.section .note.GNU-stack,"",%progbits \ No newline at end of file From 593da5ed79cab9ca175391d0ba6666a1f8a52500 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sat, 27 Jul 2024 18:33:01 +0200 Subject: [PATCH 12/35] CI: Workaround for MoltenVK crash 1.2.10 and later crash during descriptor set creation. So for now let's stick with the older version --- .github/workflows/build.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a2342c27..28efa833 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -239,7 +239,17 @@ jobs: - name: "Install system dependencies" run: | brew update - brew install llvm@15 ninja nasm molten-vk automake libtool + brew install llvm@15 ninja nasm automake libtool + brew install cmake python3 ninja + + - name: "Build and install molten-vk" + run: | + git clone https://github.com/KhronosGroup/MoltenVK.git + cd MoltenVK + git checkout bf097edc74ec3b6dfafdcd5a38d3ce14b11952d6 + ./fetchDependencies --macos + make macos + make install - name: "Setup cmake" uses: jwlawson/actions-setup-cmake@v2 From 517e68fe57ac1ac112f37c321d3d40a30ea5a8d6 Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Sun, 28 Jul 2024 17:50:20 +0100 Subject: [PATCH 13/35] nsyshid: Tidyups and Fixes (#1275) --- src/Cafe/OS/libs/nsyshid/Skylander.cpp | 2 +- src/Cafe/OS/libs/nsyshid/Skylander.h | 2 +- src/config/CemuConfig.h | 2 +- src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.cpp b/src/Cafe/OS/libs/nsyshid/Skylander.cpp index a9888787..1b4515ef 100644 --- a/src/Cafe/OS/libs/nsyshid/Skylander.cpp +++ b/src/Cafe/OS/libs/nsyshid/Skylander.cpp @@ -978,7 +978,7 @@ namespace nsyshid { for (const auto& it : GetListSkylanders()) { - if(it.first.first == skyId && it.first.second == skyVar) + if (it.first.first == skyId && it.first.second == skyVar) { return it.second; } diff --git a/src/Cafe/OS/libs/nsyshid/Skylander.h b/src/Cafe/OS/libs/nsyshid/Skylander.h index 95eaff0c..986ef185 100644 --- a/src/Cafe/OS/libs/nsyshid/Skylander.h +++ b/src/Cafe/OS/libs/nsyshid/Skylander.h @@ -50,7 +50,7 @@ namespace nsyshid std::unique_ptr skyFile; uint8 status = 0; std::queue queuedStatus; - std::array data{}; + std::array data{}; uint32 lastId = 0; void Save(); diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index 2a1d29cb..ac861c9a 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -519,7 +519,7 @@ struct CemuConfig struct { ConfigValue emulate_skylander_portal{false}; - ConfigValue emulate_infinity_base{true}; + ConfigValue emulate_infinity_base{false}; }emulated_usb_devices{}; private: diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp index f4784f35..3a0f534a 100644 --- a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp @@ -398,7 +398,7 @@ CreateInfinityFigureDialog::CreateInfinityFigureDialog(wxWindow* parent, uint8 s { wxMessageDialog idError(this, "Error Converting Figure Number!", "Number Entered is Invalid"); idError.ShowModal(); - this->EndModal(0);; + this->EndModal(0); } uint32 figNum = longFigNum & 0xFFFFFFFF; auto figure = nsyshid::g_infinitybase.FindFigure(figNum); @@ -408,7 +408,7 @@ CreateInfinityFigureDialog::CreateInfinityFigureDialog(wxWindow* parent, uint8 s "BIN files (*.bin)|*.bin", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if (saveFileDialog.ShowModal() == wxID_CANCEL) - this->EndModal(0);; + this->EndModal(0); m_filePath = saveFileDialog.GetPath(); From 1575866eca8f84bbe94f2e3a5c2bc8938a5856bb Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sun, 4 Aug 2024 14:45:57 +0200 Subject: [PATCH 14/35] Vulkan: Add R32_X8_FLOAT format --- src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index 9209e3cd..b9922fc3 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -2439,6 +2439,11 @@ void VulkanRenderer::GetTextureFormatInfoVK(Latte::E_GX2SURFFMT format, bool isD // used by Color Splash and Resident Evil formatInfoOut->vkImageFormat = VK_FORMAT_R8G8B8A8_UINT; // todo - should we use ABGR format? formatInfoOut->decoder = TextureDecoder_X24_G8_UINT::getInstance(); // todo - verify + case Latte::E_GX2SURFFMT::R32_X8_FLOAT: + // seen in Disney Infinity 3.0 + formatInfoOut->vkImageFormat = VK_FORMAT_R32_SFLOAT; + formatInfoOut->decoder = TextureDecoder_NullData64::getInstance(); + break; default: cemuLog_log(LogType::Force, "Unsupported color texture format {:04x}", (uint32)format); cemu_assert_debug(false); From d81eb952a4c8273670c9a29fc3c7e69961282d41 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:58:23 +0200 Subject: [PATCH 15/35] nsyshid: Silence some logging in release builds --- src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp b/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp index 44e01399..267111b2 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp +++ b/src/Cafe/OS/libs/nsyshid/BackendWindowsHID.cpp @@ -67,13 +67,6 @@ namespace nsyshid::backend::windows device->m_productId); } } - else - { - cemuLog_log(LogType::Force, - "nsyshid::BackendWindowsHID: device not on whitelist: {:04x}:{:04x}", - device->m_vendorId, - device->m_productId); - } } CloseHandle(hHIDDevice); } @@ -125,14 +118,12 @@ namespace nsyshid::backend::windows } if (maxPacketInputLength <= 0 || maxPacketInputLength >= 0xF000) { - cemuLog_log(LogType::Force, "HID: Input packet length not available or out of range (length = {})", - maxPacketInputLength); + cemuLog_logDebug(LogType::Force, "HID: Input packet length not available or out of range (length = {})", maxPacketInputLength); maxPacketInputLength = 0x20; } if (maxPacketOutputLength <= 0 || maxPacketOutputLength >= 0xF000) { - cemuLog_log(LogType::Force, "HID: Output packet length not available or out of range (length = {})", - maxPacketOutputLength); + cemuLog_logDebug(LogType::Force, "HID: Output packet length not available or out of range (length = {})", maxPacketOutputLength); maxPacketOutputLength = 0x20; } From 21296447812794f8cde76db93cf3697a96da7ac9 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 6 Aug 2024 23:02:28 +0200 Subject: [PATCH 16/35] Remove shaderCache directory The location of the shaderCache path is different for non-portable cases so let's not confuse the user by shipping with a precreated directory that isn't actually used --- bin/shaderCache/info.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 bin/shaderCache/info.txt diff --git a/bin/shaderCache/info.txt b/bin/shaderCache/info.txt deleted file mode 100644 index 962cf88b..00000000 --- a/bin/shaderCache/info.txt +++ /dev/null @@ -1 +0,0 @@ -If you plan to transfer the shader cache to a different PC or Cemu installation you only need to copy the 'transferable' directory. \ No newline at end of file From b52b676413a5566adc4a5c1f2472fa6cc961a94c Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Wed, 7 Aug 2024 02:50:24 +0200 Subject: [PATCH 17/35] vcpkg: Automatically unshallow submodule --- CMakeLists.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b5f3881..48e18637 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,24 @@ if (EXPERIMENTAL_VERSION) endif() if (ENABLE_VCPKG) + # check if vcpkg is shallow and unshallow it if necessary + execute_process( + COMMAND git rev-parse --is-shallow-repository + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/dependencies/vcpkg + OUTPUT_VARIABLE is_vcpkg_shallow + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + if(is_vcpkg_shallow STREQUAL "true") + message(STATUS "vcpkg is shallow. Unshallowing it now...") + execute_process( + COMMAND git fetch --unshallow + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/dependencies/vcpkg" + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ) + endif() + if(UNIX AND NOT APPLE) set(VCPKG_OVERLAY_PORTS "${CMAKE_CURRENT_LIST_DIR}/dependencies/vcpkg_overlay_ports_linux") elseif(APPLE) From bf2208145b21505f5ebe3b1245e4c32bfc6f0d45 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Wed, 7 Aug 2024 16:18:40 +0200 Subject: [PATCH 18/35] Enable async shader compile by default --- src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp | 4 +++- src/config/CemuConfig.h | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index b9922fc3..09515993 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -7,6 +7,7 @@ #include "Cafe/HW/Latte/Core/LatteBufferCache.h" #include "Cafe/HW/Latte/Core/LattePerformanceMonitor.h" +#include "Cafe/HW/Latte/Core/LatteOverlay.h" #include "Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompiler.h" @@ -29,6 +30,7 @@ #include #include +#include // for localization #ifndef VK_API_VERSION_MAJOR #define VK_API_VERSION_MAJOR(version) (((uint32_t)(version) >> 22) & 0x7FU) @@ -285,7 +287,7 @@ void VulkanRenderer::GetDeviceFeatures() cemuLog_log(LogType::Force, "VK_EXT_pipeline_creation_cache_control not supported. Cannot use asynchronous shader and pipeline compilation"); // if async shader compilation is enabled show warning message if (GetConfig().async_compile) - wxMessageBox(_("The currently installed graphics driver does not support the Vulkan extension necessary for asynchronous shader compilation. Asynchronous compilation cannot be used.\n \nRequired extension: VK_EXT_pipeline_creation_cache_control\n\nInstalling the latest graphics driver may solve this error."), _("Information"), wxOK | wxCENTRE); + LatteOverlay_pushNotification(_("Async shader compile is enabled but not supported by the graphics driver\nCemu will use synchronous compilation which can cause additional stutter").utf8_string(), 10000); } if (!m_featureControl.deviceExtensions.custom_border_color_without_format) { diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index ac861c9a..5db8f58c 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -441,7 +441,7 @@ struct CemuConfig ConfigValue vsync{ 0 }; // 0 = off, 1+ = on depending on render backend ConfigValue gx2drawdone_sync {true}; ConfigValue render_upside_down{ false }; - ConfigValue async_compile{ false }; + ConfigValue async_compile{ true }; ConfigValue vk_accurate_barriers{ true }; From 54e695a6e81efd4fb9a632c62abdb4585f8f776e Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:58:24 +0200 Subject: [PATCH 19/35] git: unshallow vcpkg, shallow vulkan-headers and imgui (#1282) --- .gitmodules | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index f352d478..dc69c441 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,10 +9,12 @@ [submodule "dependencies/vcpkg"] path = dependencies/vcpkg url = https://github.com/microsoft/vcpkg - shallow = true + shallow = false [submodule "dependencies/Vulkan-Headers"] path = dependencies/Vulkan-Headers url = https://github.com/KhronosGroup/Vulkan-Headers + shallow = true [submodule "dependencies/imgui"] path = dependencies/imgui url = https://github.com/ocornut/imgui + shallow = true From 598298cb3d28fd608878f13ef1e76add75173692 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:07:08 +0200 Subject: [PATCH 20/35] Vulkan: Fix stencil front mask --- src/Cafe/HW/Latte/Renderer/Vulkan/VulkanPipelineCompiler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanPipelineCompiler.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanPipelineCompiler.cpp index ce582b9a..ba094a84 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanPipelineCompiler.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanPipelineCompiler.cpp @@ -826,7 +826,7 @@ void PipelineCompiler::InitDepthStencilState() depthStencilState.front.reference = stencilRefFront; depthStencilState.front.compareMask = stencilCompareMaskFront; - depthStencilState.front.writeMask = stencilWriteMaskBack; + depthStencilState.front.writeMask = stencilWriteMaskFront; depthStencilState.front.compareOp = vkDepthCompareTable[(size_t)frontStencilFunc]; depthStencilState.front.depthFailOp = stencilOpTable[(size_t)frontStencilZFail]; depthStencilState.front.failOp = stencilOpTable[(size_t)frontStencilFail]; From 7fd532436d5af65af5a27a532d7ea5cb6ac5895c Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:07:36 +0200 Subject: [PATCH 21/35] CI: Manual unshallow of vcpkg is no longer needed --- .github/workflows/build.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 28efa833..015ef367 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,11 +24,6 @@ jobs: submodules: "recursive" fetch-depth: 0 - - name: "Fetch full history for vcpkg submodule" - run: | - cd dependencies/vcpkg - git fetch --unshallow - - name: Setup release mode parameters (for deploy) if: ${{ inputs.deploymode == 'release' }} run: | @@ -133,11 +128,6 @@ jobs: with: submodules: "recursive" - - name: "Fetch full history for vcpkg submodule" - run: | - cd dependencies/vcpkg - git fetch --unshallow - - name: Setup release mode parameters (for deploy) if: ${{ inputs.deploymode == 'release' }} run: | @@ -212,11 +202,6 @@ jobs: with: submodules: "recursive" - - name: "Fetch full history for vcpkg submodule" - run: | - cd dependencies/vcpkg - git fetch --unshallow - - name: Setup release mode parameters (for deploy) if: ${{ inputs.deploymode == 'release' }} run: | From 9812a47cb182331f7c7c7a6a16eff014098a6206 Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Thu, 8 Aug 2024 19:35:50 +0200 Subject: [PATCH 22/35] clang-format: Put class braces on a new line (#1283) --- .clang-format | 1 + 1 file changed, 1 insertion(+) diff --git a/.clang-format b/.clang-format index b22a1048..0cef9ae4 100644 --- a/.clang-format +++ b/.clang-format @@ -15,6 +15,7 @@ BinPackArguments: true BinPackParameters: true BraceWrapping: AfterCaseLabel: true + AfterClass: true AfterControlStatement: Always AfterEnum: true AfterExternBlock: true From e02cc42d675ffe203c3f047f60669583934841ad Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 13 Aug 2024 01:00:49 +0200 Subject: [PATCH 23/35] COS: Implement PPC va_list, va_arg and update related functions --- src/Cafe/OS/common/OSCommon.h | 83 +++++++++++++++ src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp | 112 ++++++++++---------- src/Cafe/OS/libs/coreinit/coreinit_Misc.h | 14 +++ src/Common/MemPtr.h | 19 ++-- src/Common/betype.h | 25 +++++ 5 files changed, 182 insertions(+), 71 deletions(-) diff --git a/src/Cafe/OS/common/OSCommon.h b/src/Cafe/OS/common/OSCommon.h index 4fb65a47..34f207bb 100644 --- a/src/Cafe/OS/common/OSCommon.h +++ b/src/Cafe/OS/common/OSCommon.h @@ -23,3 +23,86 @@ void osLib_returnFromFunction64(PPCInterpreter_t* hCPU, uint64 returnValue64); // utility functions #include "Cafe/OS/common/OSUtil.h" + +// va_list +struct ppc_va_list +{ + uint8be gprIndex; + uint8be fprIndex; + uint8be _padding2[2]; + MEMPTR overflow_arg_area; + MEMPTR reg_save_area; +}; +static_assert(sizeof(ppc_va_list) == 0xC); + +struct ppc_va_list_reg_storage +{ + uint32be gpr_save_area[8]; // 32 bytes, r3 to r10 + float64be fpr_save_area[8]; // 64 bytes, f1 to f8 + ppc_va_list vargs; + uint32be padding; +}; +static_assert(sizeof(ppc_va_list_reg_storage) == 0x70); + +// Equivalent of va_start for PPC HLE functions. Must be called before any StackAllocator<> definitions +#define ppc_define_va_list(__gprIndex, __fprIndex) \ + MPTR vaOriginalR1 = PPCInterpreter_getCurrentInstance()->gpr[1]; \ + StackAllocator va_list_storage; \ + for(int i=3; i<=10; i++) va_list_storage->gpr_save_area[i-3] = PPCInterpreter_getCurrentInstance()->gpr[i]; \ + for(int i=1; i<=8; i++) va_list_storage->fpr_save_area[i-1] = PPCInterpreter_getCurrentInstance()->fpr[i].fp0; \ + va_list_storage->vargs.gprIndex = __gprIndex; \ + va_list_storage->vargs.fprIndex = __fprIndex; \ + va_list_storage->vargs.reg_save_area = (uint8be*)&va_list_storage; \ + va_list_storage->vargs.overflow_arg_area = {vaOriginalR1 + 8}; \ + ppc_va_list& vargs = va_list_storage->vargs; + +enum class ppc_va_type +{ + INT32 = 1, + INT64 = 2, + FLOAT_OR_DOUBLE = 3, +}; + +static void* _ppc_va_arg(ppc_va_list* vargs, ppc_va_type argType) +{ + void* r; + switch ( argType ) + { + default: + cemu_assert_suspicious(); + case ppc_va_type::INT32: + if ( vargs[0].gprIndex < 8u ) + { + r = &vargs->reg_save_area[4 * vargs->gprIndex]; + vargs->gprIndex++; + return r; + } + r = vargs->overflow_arg_area; + vargs->overflow_arg_area += 4; + return r; + case ppc_va_type::INT64: + if ( (vargs->gprIndex & 1) != 0 ) + vargs->gprIndex++; + if ( vargs->gprIndex < 8 ) + { + r = &vargs->reg_save_area[4 * vargs->gprIndex]; + vargs->gprIndex += 2; + return r; + } + vargs->overflow_arg_area = {(vargs->overflow_arg_area.GetMPTR()+7) & 0xFFFFFFF8}; + r = vargs->overflow_arg_area; + vargs->overflow_arg_area += 8; + return r; + case ppc_va_type::FLOAT_OR_DOUBLE: + if ( vargs->fprIndex < 8 ) + { + r = &vargs->reg_save_area[0x20 + 8 * vargs->fprIndex]; + vargs->fprIndex++; + return r; + } + vargs->overflow_arg_area = {(vargs->overflow_arg_area.GetMPTR()+7) & 0xFFFFFFF8}; + r = vargs->overflow_arg_area; + vargs->overflow_arg_area += 8; + return r; + } +} \ No newline at end of file diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp b/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp index e2b50661..71a7d6e2 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_Misc.cpp @@ -7,14 +7,9 @@ namespace coreinit { - - /* coreinit logging and string format */ - - sint32 ppcSprintf(const char* formatStr, char* strOut, sint32 maxLength, PPCInterpreter_t* hCPU, sint32 initialParamIndex) + sint32 ppc_vprintf(const char* formatStr, char* strOut, sint32 maxLength, ppc_va_list* vargs) { char tempStr[4096]; - sint32 integerParamIndex = initialParamIndex; - sint32 floatParamIndex = 0; sint32 writeIndex = 0; while (*formatStr) { @@ -101,8 +96,7 @@ namespace coreinit tempFormat[(formatStr - formatStart)] = '\0'; else tempFormat[sizeof(tempFormat) - 1] = '\0'; - sint32 tempLen = sprintf(tempStr, tempFormat, PPCInterpreter_getCallParamU32(hCPU, integerParamIndex)); - integerParamIndex++; + sint32 tempLen = sprintf(tempStr, tempFormat, (uint32)*(uint32be*)_ppc_va_arg(vargs, ppc_va_type::INT32)); for (sint32 i = 0; i < tempLen; i++) { if (writeIndex >= maxLength) @@ -120,13 +114,12 @@ namespace coreinit tempFormat[(formatStr - formatStart)] = '\0'; else tempFormat[sizeof(tempFormat) - 1] = '\0'; - MPTR strOffset = PPCInterpreter_getCallParamU32(hCPU, integerParamIndex); + MPTR strOffset = *(uint32be*)_ppc_va_arg(vargs, ppc_va_type::INT32); sint32 tempLen = 0; if (strOffset == MPTR_NULL) tempLen = sprintf(tempStr, "NULL"); else tempLen = sprintf(tempStr, tempFormat, memory_getPointerFromVirtualOffset(strOffset)); - integerParamIndex++; for (sint32 i = 0; i < tempLen; i++) { if (writeIndex >= maxLength) @@ -136,25 +129,6 @@ namespace coreinit } strOut[std::min(maxLength - 1, writeIndex)] = '\0'; } - else if (*formatStr == 'f') - { - // float - formatStr++; - strncpy(tempFormat, formatStart, std::min((std::ptrdiff_t)sizeof(tempFormat) - 1, formatStr - formatStart)); - if ((formatStr - formatStart) < sizeof(tempFormat)) - tempFormat[(formatStr - formatStart)] = '\0'; - else - tempFormat[sizeof(tempFormat) - 1] = '\0'; - sint32 tempLen = sprintf(tempStr, tempFormat, (float)hCPU->fpr[1 + floatParamIndex].fp0); - floatParamIndex++; - for (sint32 i = 0; i < tempLen; i++) - { - if (writeIndex >= maxLength) - break; - strOut[writeIndex] = tempStr[i]; - writeIndex++; - } - } else if (*formatStr == 'c') { // character @@ -164,8 +138,24 @@ namespace coreinit tempFormat[(formatStr - formatStart)] = '\0'; else tempFormat[sizeof(tempFormat) - 1] = '\0'; - sint32 tempLen = sprintf(tempStr, tempFormat, PPCInterpreter_getCallParamU32(hCPU, integerParamIndex)); - integerParamIndex++; + sint32 tempLen = sprintf(tempStr, tempFormat, (uint32)*(uint32be*)_ppc_va_arg(vargs, ppc_va_type::INT32)); + for (sint32 i = 0; i < tempLen; i++) + { + if (writeIndex >= maxLength) + break; + strOut[writeIndex] = tempStr[i]; + writeIndex++; + } + } + else if (*formatStr == 'f' || *formatStr == 'g' || *formatStr == 'G') + { + formatStr++; + strncpy(tempFormat, formatStart, std::min((std::ptrdiff_t)sizeof(tempFormat) - 1, formatStr - formatStart)); + if ((formatStr - formatStart) < sizeof(tempFormat)) + tempFormat[(formatStr - formatStart)] = '\0'; + else + tempFormat[sizeof(tempFormat) - 1] = '\0'; + sint32 tempLen = sprintf(tempStr, tempFormat, (double)*(betype*)_ppc_va_arg(vargs, ppc_va_type::FLOAT_OR_DOUBLE)); for (sint32 i = 0; i < tempLen; i++) { if (writeIndex >= maxLength) @@ -183,8 +173,7 @@ namespace coreinit tempFormat[(formatStr - formatStart)] = '\0'; else tempFormat[sizeof(tempFormat) - 1] = '\0'; - sint32 tempLen = sprintf(tempStr, tempFormat, (double)hCPU->fpr[1 + floatParamIndex].fp0); - floatParamIndex++; + sint32 tempLen = sprintf(tempStr, tempFormat, (double)*(betype*)_ppc_va_arg(vargs, ppc_va_type::FLOAT_OR_DOUBLE)); for (sint32 i = 0; i < tempLen; i++) { if (writeIndex >= maxLength) @@ -196,16 +185,13 @@ namespace coreinit else if ((formatStr[0] == 'l' && formatStr[1] == 'l' && (formatStr[2] == 'x' || formatStr[2] == 'X'))) { formatStr += 3; - // double (64bit) + // 64bit int strncpy(tempFormat, formatStart, std::min((std::ptrdiff_t)sizeof(tempFormat) - 1, formatStr - formatStart)); if ((formatStr - formatStart) < sizeof(tempFormat)) tempFormat[(formatStr - formatStart)] = '\0'; else tempFormat[sizeof(tempFormat) - 1] = '\0'; - if (integerParamIndex & 1) - integerParamIndex++; - sint32 tempLen = sprintf(tempStr, tempFormat, PPCInterpreter_getCallParamU64(hCPU, integerParamIndex)); - integerParamIndex += 2; + sint32 tempLen = sprintf(tempStr, tempFormat, (uint64)*(uint64be*)_ppc_va_arg(vargs, ppc_va_type::INT64)); for (sint32 i = 0; i < tempLen; i++) { if (writeIndex >= maxLength) @@ -223,10 +209,7 @@ namespace coreinit tempFormat[(formatStr - formatStart)] = '\0'; else tempFormat[sizeof(tempFormat) - 1] = '\0'; - if (integerParamIndex & 1) - integerParamIndex++; - sint32 tempLen = sprintf(tempStr, tempFormat, PPCInterpreter_getCallParamU64(hCPU, integerParamIndex)); - integerParamIndex += 2; + sint32 tempLen = sprintf(tempStr, tempFormat, (sint64)*(sint64be*)_ppc_va_arg(vargs, ppc_va_type::INT64)); for (sint32 i = 0; i < tempLen; i++) { if (writeIndex >= maxLength) @@ -255,9 +238,12 @@ namespace coreinit return std::min(writeIndex, maxLength - 1); } + /* coreinit logging and string format */ + sint32 __os_snprintf(char* outputStr, sint32 maxLength, const char* formatStr) { - sint32 r = ppcSprintf(formatStr, outputStr, maxLength, PPCInterpreter_getCurrentInstance(), 3); + ppc_define_va_list(3, 0); + sint32 r = ppc_vprintf(formatStr, outputStr, maxLength, &vargs); return r; } @@ -322,32 +308,40 @@ namespace coreinit } } - void OSReport(const char* format) + void COSVReport(COSReportModule module, COSReportLevel level, const char* format, ppc_va_list* vargs) { - char buffer[1024 * 2]; - sint32 len = ppcSprintf(format, buffer, sizeof(buffer), PPCInterpreter_getCurrentInstance(), 1); - WriteCafeConsole(CafeLogType::OSCONSOLE, buffer, len); + char tmpBuffer[1024]; + sint32 len = ppc_vprintf(format, tmpBuffer, sizeof(tmpBuffer), vargs); + WriteCafeConsole(CafeLogType::OSCONSOLE, tmpBuffer, len); } - void OSVReport(const char* format, MPTR vaArgs) + void OSReport(const char* format) { - cemu_assert_unimplemented(); + ppc_define_va_list(1, 0); + COSVReport(COSReportModule::coreinit, COSReportLevel::Info, format, &vargs); + } + + void OSVReport(const char* format, ppc_va_list* vargs) + { + COSVReport(COSReportModule::coreinit, COSReportLevel::Info, format, vargs); } void COSWarn(int moduleId, const char* format) { - char buffer[1024 * 2]; - int prefixLen = sprintf(buffer, "[COSWarn-%d] ", moduleId); - sint32 len = ppcSprintf(format, buffer + prefixLen, sizeof(buffer) - prefixLen, PPCInterpreter_getCurrentInstance(), 2); - WriteCafeConsole(CafeLogType::OSCONSOLE, buffer, len + prefixLen); + ppc_define_va_list(2, 0); + char tmpBuffer[1024]; + int prefixLen = sprintf(tmpBuffer, "[COSWarn-%d] ", moduleId); + sint32 len = ppc_vprintf(format, tmpBuffer + prefixLen, sizeof(tmpBuffer) - prefixLen, &vargs); + WriteCafeConsole(CafeLogType::OSCONSOLE, tmpBuffer, len + prefixLen); } void OSLogPrintf(int ukn1, int ukn2, int ukn3, const char* format) { - char buffer[1024 * 2]; - int prefixLen = sprintf(buffer, "[OSLogPrintf-%d-%d-%d] ", ukn1, ukn2, ukn3); - sint32 len = ppcSprintf(format, buffer + prefixLen, sizeof(buffer) - prefixLen, PPCInterpreter_getCurrentInstance(), 4); - WriteCafeConsole(CafeLogType::OSCONSOLE, buffer, len + prefixLen); + ppc_define_va_list(4, 0); + char tmpBuffer[1024]; + int prefixLen = sprintf(tmpBuffer, "[OSLogPrintf-%d-%d-%d] ", ukn1, ukn2, ukn3); + sint32 len = ppc_vprintf(format, tmpBuffer + prefixLen, sizeof(tmpBuffer) - prefixLen, &vargs); + WriteCafeConsole(CafeLogType::OSCONSOLE, tmpBuffer, len + prefixLen); } void OSConsoleWrite(const char* strPtr, sint32 length) @@ -562,9 +556,11 @@ namespace coreinit s_transitionToForeground = false; cafeExportRegister("coreinit", __os_snprintf, LogType::Placeholder); + + cafeExportRegister("coreinit", COSVReport, LogType::Placeholder); + cafeExportRegister("coreinit", COSWarn, LogType::Placeholder); cafeExportRegister("coreinit", OSReport, LogType::Placeholder); cafeExportRegister("coreinit", OSVReport, LogType::Placeholder); - cafeExportRegister("coreinit", COSWarn, LogType::Placeholder); cafeExportRegister("coreinit", OSLogPrintf, LogType::Placeholder); cafeExportRegister("coreinit", OSConsoleWrite, LogType::Placeholder); diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Misc.h b/src/Cafe/OS/libs/coreinit/coreinit_Misc.h index 7abba92f..36f6b06a 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Misc.h +++ b/src/Cafe/OS/libs/coreinit/coreinit_Misc.h @@ -26,5 +26,19 @@ namespace coreinit uint32 OSDriver_Register(uint32 moduleHandle, sint32 priority, OSDriverInterface* driverCallbacks, sint32 driverId, uint32be* outUkn1, uint32be* outUkn2, uint32be* outUkn3); uint32 OSDriver_Deregister(uint32 moduleHandle, sint32 driverId); + enum class COSReportModule + { + coreinit = 0, + }; + + enum class COSReportLevel + { + Error = 0, + Warn = 1, + Info = 2 + }; + + sint32 ppc_vprintf(const char* formatStr, char* strOut, sint32 maxLength, ppc_va_list* vargs); + void miscInit(); }; \ No newline at end of file diff --git a/src/Common/MemPtr.h b/src/Common/MemPtr.h index 5fb73479..142da7e4 100644 --- a/src/Common/MemPtr.h +++ b/src/Common/MemPtr.h @@ -92,19 +92,6 @@ public: template explicit operator MEMPTR() const { return MEMPTR(this->m_value); } - //bool operator==(const MEMPTR& v) const { return m_value == v.m_value; } - //bool operator==(const T* rhs) const { return (T*)(m_value == 0 ? nullptr : memory_base + (uint32)m_value) == rhs; } -> ambigious (implicit cast to T* allows for T* == T*) - //bool operator==(std::nullptr_t rhs) const { return m_value == 0; } - - //bool operator!=(const MEMPTR& v) const { return !(*this == v); } - //bool operator!=(const void* rhs) const { return !(*this == rhs); } - //bool operator!=(int rhs) const { return !(*this == rhs); } - - //bool operator==(const void* rhs) const { return (void*)(m_value == 0 ? nullptr : memory_base + (uint32)m_value) == rhs; } - - //explicit bool operator==(int rhs) const { return *this == (const void*)(size_t)rhs; } - - MEMPTR operator+(const MEMPTR& ptr) { return MEMPTR(this->GetMPTR() + ptr.GetMPTR()); } MEMPTR operator-(const MEMPTR& ptr) { return MEMPTR(this->GetMPTR() - ptr.GetMPTR()); } @@ -120,6 +107,12 @@ public: return MEMPTR(this->GetMPTR() - v * 4); } + MEMPTR& operator+=(sint32 v) + { + m_value += v * sizeof(T); + return *this; + } + template typename std::enable_if::value, Q>::type& operator*() const { return *GetPtr(); } diff --git a/src/Common/betype.h b/src/Common/betype.h index e684fb93..60a64b7a 100644 --- a/src/Common/betype.h +++ b/src/Common/betype.h @@ -121,6 +121,12 @@ public: return *this; } + betype& operator+=(const T& v) requires std::integral + { + m_value = SwapEndian(T(value() + v)); + return *this; + } + betype& operator-=(const betype& v) { m_value = SwapEndian(T(value() - v.value())); @@ -188,17 +194,36 @@ public: return from_bevalue(T(~m_value)); } + // pre-increment betype& operator++() requires std::integral { m_value = SwapEndian(T(value() + 1)); return *this; } + // post-increment + betype operator++(int) requires std::integral + { + betype tmp(*this); + m_value = SwapEndian(T(value() + 1)); + return tmp; + } + + // pre-decrement betype& operator--() requires std::integral { m_value = SwapEndian(T(value() - 1)); return *this; } + + // post-decrement + betype operator--(int) requires std::integral + { + betype tmp(*this); + m_value = SwapEndian(T(value() - 1)); + return tmp; + } + private: //T m_value{}; // before 1.26.2 T m_value; From f52970c822b7f671f6f9a80e828bf53152feb783 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 13 Aug 2024 04:47:43 +0200 Subject: [PATCH 24/35] Vulkan: Allow RGBA16F texture format with SRGB bit --- src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index 09515993..81b0b0f1 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -2212,6 +2212,7 @@ void VulkanRenderer::GetTextureFormatInfoVK(Latte::E_GX2SURFFMT format, bool isD formatInfoOut->decoder = TextureDecoder_R32_G32_B32_A32_UINT::getInstance(); break; case Latte::E_GX2SURFFMT::R16_G16_B16_A16_FLOAT: + case Latte::E_GX2SURFFMT::R16_G16_B16_A16_FLOAT | Latte::E_GX2SURFFMT::FMT_BIT_SRGB: // Seen in Sonic Transformed level Starry Speedway. SRGB should just be ignored for native float formats? formatInfoOut->vkImageFormat = VK_FORMAT_R16G16B16A16_SFLOAT; formatInfoOut->decoder = TextureDecoder_R16_G16_B16_A16_FLOAT::getInstance(); break; From e551f8f5245f9e94f677094d56e29b11870cd3f4 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 13 Aug 2024 05:57:51 +0200 Subject: [PATCH 25/35] Fix clang compile error --- src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index 81b0b0f1..fb54a803 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -2200,6 +2200,8 @@ void VulkanRenderer::GetTextureFormatInfoVK(Latte::E_GX2SURFFMT format, bool isD else { formatInfoOut->vkImageAspect = VK_IMAGE_ASPECT_COLOR_BIT; + if(format == (Latte::E_GX2SURFFMT::R16_G16_B16_A16_FLOAT | Latte::E_GX2SURFFMT::FMT_BIT_SRGB)) // Seen in Sonic Transformed level Starry Speedway. SRGB should just be ignored for native float formats? + format = Latte::E_GX2SURFFMT::R16_G16_B16_A16_FLOAT; switch (format) { // RGBA formats @@ -2212,7 +2214,6 @@ void VulkanRenderer::GetTextureFormatInfoVK(Latte::E_GX2SURFFMT format, bool isD formatInfoOut->decoder = TextureDecoder_R32_G32_B32_A32_UINT::getInstance(); break; case Latte::E_GX2SURFFMT::R16_G16_B16_A16_FLOAT: - case Latte::E_GX2SURFFMT::R16_G16_B16_A16_FLOAT | Latte::E_GX2SURFFMT::FMT_BIT_SRGB: // Seen in Sonic Transformed level Starry Speedway. SRGB should just be ignored for native float formats? formatInfoOut->vkImageFormat = VK_FORMAT_R16G16B16A16_SFLOAT; formatInfoOut->decoder = TextureDecoder_R16_G16_B16_A16_FLOAT::getInstance(); break; From a6d8c0fb9f139817b90d82775e36a4f3d1a4ce76 Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:48:13 +0200 Subject: [PATCH 26/35] CI: Fix macOS build (#1291) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 015ef367..9fb775e2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -225,7 +225,7 @@ jobs: run: | brew update brew install llvm@15 ninja nasm automake libtool - brew install cmake python3 ninja + brew install cmake ninja - name: "Build and install molten-vk" run: | From c49296acdc4acf3998249d8f67e8cbc984b9e276 Mon Sep 17 00:00:00 2001 From: "Skyth (Asilkan)" <19259897+blueskythlikesclouds@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:53:04 +0300 Subject: [PATCH 27/35] Add support for iterating directories in graphics pack content folders. (#1288) --- src/Cafe/Filesystem/FST/fstUtil.h | 65 +++++++++++++++++++++-- src/Cafe/Filesystem/fsc.h | 2 +- src/Cafe/Filesystem/fscDeviceRedirect.cpp | 13 +++-- src/Cafe/GraphicPack/GraphicPack2.cpp | 2 +- 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/Cafe/Filesystem/FST/fstUtil.h b/src/Cafe/Filesystem/FST/fstUtil.h index 01283684..a432cc95 100644 --- a/src/Cafe/Filesystem/FST/fstUtil.h +++ b/src/Cafe/Filesystem/FST/fstUtil.h @@ -3,6 +3,8 @@ #include +#include "../fsc.h" + // path parser and utility class for Wii U paths // optimized to be allocation-free for common path lengths class FSCPath @@ -119,9 +121,7 @@ public: template class FSAFileTree { -public: - -private: + private: enum NODETYPE : uint8 { @@ -133,6 +133,7 @@ private: { std::string name; std::vector subnodes; + size_t fileSize; F* custom; NODETYPE type; }; @@ -179,13 +180,54 @@ private: return newNode; } + class DirectoryIterator : public FSCVirtualFile + { + public: + DirectoryIterator(node_t* node) + : m_node(node), m_subnodeIndex(0) + { + } + + sint32 fscGetType() override + { + return FSC_TYPE_DIRECTORY; + } + + bool fscDirNext(FSCDirEntry* dirEntry) override + { + if (m_subnodeIndex >= m_node->subnodes.size()) + return false; + + const node_t* subnode = m_node->subnodes[m_subnodeIndex]; + + strncpy(dirEntry->path, subnode->name.c_str(), sizeof(dirEntry->path) - 1); + dirEntry->path[sizeof(dirEntry->path) - 1] = '\0'; + dirEntry->isDirectory = subnode->type == FSAFileTree::NODETYPE_DIRECTORY; + dirEntry->isFile = subnode->type == FSAFileTree::NODETYPE_FILE; + dirEntry->fileSize = subnode->type == FSAFileTree::NODETYPE_FILE ? subnode->fileSize : 0; + + ++m_subnodeIndex; + return true; + } + + bool fscRewindDir() override + { + m_subnodeIndex = 0; + return true; + } + + private: + node_t* m_node; + size_t m_subnodeIndex; + }; + public: FSAFileTree() { rootNode.type = NODETYPE_DIRECTORY; } - bool addFile(std::string_view path, F* custom) + bool addFile(std::string_view path, size_t fileSize, F* custom) { FSCPath p(path); if (p.GetNodeCount() == 0) @@ -196,6 +238,7 @@ public: return false; // node already exists // add file node node_t* fileNode = newNode(directoryNode, NODETYPE_FILE, p.GetNodeName(p.GetNodeCount() - 1)); + fileNode->fileSize = fileSize; fileNode->custom = custom; return true; } @@ -214,6 +257,20 @@ public: return true; } + bool getDirectory(std::string_view path, FSCVirtualFile*& dirIterator) + { + FSCPath p(path); + if (p.GetNodeCount() == 0) + return false; + node_t* node = getByNodePath(p, p.GetNodeCount(), false); + if (node == nullptr) + return false; + if (node->type != NODETYPE_DIRECTORY) + return false; + dirIterator = new DirectoryIterator(node); + return true; + } + bool removeFile(std::string_view path) { FSCPath p(path); diff --git a/src/Cafe/Filesystem/fsc.h b/src/Cafe/Filesystem/fsc.h index a3df2af2..8b8ed5ef 100644 --- a/src/Cafe/Filesystem/fsc.h +++ b/src/Cafe/Filesystem/fsc.h @@ -212,4 +212,4 @@ bool FSCDeviceHostFS_Mount(std::string_view mountPath, std::string_view hostTarg // redirect device void fscDeviceRedirect_map(); -void fscDeviceRedirect_add(std::string_view virtualSourcePath, const fs::path& targetFilePath, sint32 priority); +void fscDeviceRedirect_add(std::string_view virtualSourcePath, size_t fileSize, const fs::path& targetFilePath, sint32 priority); diff --git a/src/Cafe/Filesystem/fscDeviceRedirect.cpp b/src/Cafe/Filesystem/fscDeviceRedirect.cpp index d25bff86..9c62d37a 100644 --- a/src/Cafe/Filesystem/fscDeviceRedirect.cpp +++ b/src/Cafe/Filesystem/fscDeviceRedirect.cpp @@ -11,7 +11,7 @@ struct RedirectEntry FSAFileTree redirectTree; -void fscDeviceRedirect_add(std::string_view virtualSourcePath, const fs::path& targetFilePath, sint32 priority) +void fscDeviceRedirect_add(std::string_view virtualSourcePath, size_t fileSize, const fs::path& targetFilePath, sint32 priority) { // check if source already has a redirection RedirectEntry* existingEntry; @@ -24,7 +24,7 @@ void fscDeviceRedirect_add(std::string_view virtualSourcePath, const fs::path& t delete existingEntry; } RedirectEntry* entry = new RedirectEntry(targetFilePath, priority); - redirectTree.addFile(virtualSourcePath, entry); + redirectTree.addFile(virtualSourcePath, fileSize, entry); } class fscDeviceTypeRedirect : public fscDeviceC @@ -32,8 +32,15 @@ class fscDeviceTypeRedirect : public fscDeviceC FSCVirtualFile* fscDeviceOpenByPath(std::string_view path, FSC_ACCESS_FLAG accessFlags, void* ctx, sint32* fscStatus) override { RedirectEntry* redirectionEntry; - if (redirectTree.getFile(path, redirectionEntry)) + + if (HAS_FLAG(accessFlags, FSC_ACCESS_FLAG::OPEN_FILE) && redirectTree.getFile(path, redirectionEntry)) return FSCVirtualFile_Host::OpenFile(redirectionEntry->dstPath, accessFlags, *fscStatus); + + FSCVirtualFile* dirIterator; + + if (HAS_FLAG(accessFlags, FSC_ACCESS_FLAG::OPEN_DIR) && redirectTree.getDirectory(path, dirIterator)) + return dirIterator; + return nullptr; } diff --git a/src/Cafe/GraphicPack/GraphicPack2.cpp b/src/Cafe/GraphicPack/GraphicPack2.cpp index 27d423b9..c54c31cb 100644 --- a/src/Cafe/GraphicPack/GraphicPack2.cpp +++ b/src/Cafe/GraphicPack/GraphicPack2.cpp @@ -830,7 +830,7 @@ void GraphicPack2::_iterateReplacedFiles(const fs::path& currentPath, bool isAOC { virtualMountPath = fs::path("vol/content/") / virtualMountPath; } - fscDeviceRedirect_add(virtualMountPath.generic_string(), it.path().generic_string(), m_fs_priority); + fscDeviceRedirect_add(virtualMountPath.generic_string(), it.file_size(), it.path().generic_string(), m_fs_priority); } } } From b0bab273e21f8f648de011688d96baf78e064526 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Thu, 15 Aug 2024 02:16:03 +0200 Subject: [PATCH 28/35] padscore: Simulate queue behaviour for KPADRead --- src/Cafe/OS/libs/padscore/padscore.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Cafe/OS/libs/padscore/padscore.cpp b/src/Cafe/OS/libs/padscore/padscore.cpp index 47f3bc4f..8ae4730d 100644 --- a/src/Cafe/OS/libs/padscore/padscore.cpp +++ b/src/Cafe/OS/libs/padscore/padscore.cpp @@ -12,6 +12,7 @@ enum class KPAD_ERROR : sint32 { NONE = 0, + NO_SAMPLE_DATA = -1, NO_CONTROLLER = -2, NOT_INITIALIZED = -5, }; @@ -106,6 +107,9 @@ void padscoreExport_WPADProbe(PPCInterpreter_t* hCPU) } else { + if(type) + *type = 253; + osLib_returnFromFunction(hCPU, WPAD_ERR_NO_CONTROLLER); } } @@ -420,9 +424,12 @@ void padscoreExport_KPADSetConnectCallback(PPCInterpreter_t* hCPU) osLib_returnFromFunction(hCPU, old_callback.GetMPTR()); } +uint64 g_kpadLastRead[InputManager::kMaxWPADControllers] = {0}; bool g_kpadIsInited = true; + sint32 _KPADRead(uint32 channel, KPADStatus_t* samplingBufs, uint32 length, betype* errResult) { + if (channel >= InputManager::kMaxWPADControllers) { debugBreakpoint(); @@ -446,6 +453,19 @@ sint32 _KPADRead(uint32 channel, KPADStatus_t* samplingBufs, uint32 length, bety return 0; } + // On console new input samples are only received every few ms and calling KPADRead(Ex) clears the internal queue regardless of length value + // thus calling KPADRead(Ex) again too soon on the same channel will result in no data being returned + // Games that depend on this: Affordable Space Adventures + uint64 currentTime = coreinit::OSGetTime(); + uint64 timeDif = currentTime - g_kpadLastRead[channel]; + if(length == 0 || timeDif < coreinit::EspressoTime::ConvertNsToTimerTicks(1000000)) + { + if (errResult) + *errResult = KPAD_ERROR::NO_SAMPLE_DATA; + return 0; + } + g_kpadLastRead[channel] = currentTime; + memset(samplingBufs, 0x00, sizeof(KPADStatus_t)); samplingBufs->wpadErr = WPAD_ERR_NONE; samplingBufs->data_format = controller->get_data_format(); From 2843da4479630e82d93ca0bb0c7e0c1748c86c48 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Thu, 15 Aug 2024 05:00:09 +0200 Subject: [PATCH 29/35] padscore: Invoke sampling callbacks every 5ms This fixes high input latency in games like Pokemon Rumble U which update input via the sampling callbacks --- src/Cafe/OS/libs/padscore/padscore.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Cafe/OS/libs/padscore/padscore.cpp b/src/Cafe/OS/libs/padscore/padscore.cpp index 8ae4730d..a83711fe 100644 --- a/src/Cafe/OS/libs/padscore/padscore.cpp +++ b/src/Cafe/OS/libs/padscore/padscore.cpp @@ -746,7 +746,8 @@ namespace padscore // call sampling callback for (auto i = 0; i < InputManager::kMaxWPADControllers; ++i) { - if (g_padscore.controller_data[i].sampling_callback) { + if (g_padscore.controller_data[i].sampling_callback) + { if (const auto controller = instance.get_wpad_controller(i)) { cemuLog_log(LogType::InputAPI, "Calling WPADsamplingCallback({})", i); @@ -761,7 +762,7 @@ namespace padscore { OSCreateAlarm(&g_padscore.alarm); const uint64 start_tick = coreinit::coreinit_getOSTime(); - const uint64 period_tick = coreinit::EspressoTime::GetTimerClock(); // once a second + const uint64 period_tick = coreinit::EspressoTime::GetTimerClock() / 200; // every 5ms MPTR handler = PPCInterpreter_makeCallableExportDepr(TickFunction); OSSetPeriodicAlarm(&g_padscore.alarm, start_tick, period_tick, handler); } From 294a6de779ddf9c6294dafa603ad23a404052ec3 Mon Sep 17 00:00:00 2001 From: 20943204920434 <160030054+20943204920434@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:22:41 +0200 Subject: [PATCH 30/35] Update appimage.sh to support runtime libstdc++.so.6 loading (#1292) Add checkrt plugin in order to detect the right libstdc++.so.6 version to load. --- dist/linux/appimage.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dist/linux/appimage.sh b/dist/linux/appimage.sh index 7bfc4701..e9081521 100755 --- a/dist/linux/appimage.sh +++ b/dist/linux/appimage.sh @@ -10,6 +10,8 @@ curl -sSfL https://github.com"$(curl https://github.com/probonopd/go-appimage/re chmod a+x mkappimage.AppImage curl -sSfLO "https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh" chmod a+x linuxdeploy-plugin-gtk.sh +curl -sSfLO "https://github.com/darealshinji/linuxdeploy-plugin-checkrt/releases/download/continuous/linuxdeploy-plugin-checkrt.sh" +chmod a+x linuxdeploy-plugin-checkrt.sh if [[ ! -e /usr/lib/x86_64-linux-gnu ]]; then sed -i 's#lib\/x86_64-linux-gnu#lib64#g' linuxdeploy-plugin-gtk.sh @@ -39,7 +41,8 @@ export NO_STRIP=1 -d "${GITHUB_WORKSPACE}"/AppDir/info.cemu.Cemu.desktop \ -i "${GITHUB_WORKSPACE}"/AppDir/info.cemu.Cemu.png \ -e "${GITHUB_WORKSPACE}"/AppDir/usr/bin/Cemu \ - --plugin gtk + --plugin gtk \ + --plugin checkrt if ! GITVERSION="$(git rev-parse --short HEAD 2>/dev/null)"; then GITVERSION=experimental From 958137a301208141b1e62f6b2f5b3ce2f04335d6 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Thu, 15 Aug 2024 18:26:58 +0200 Subject: [PATCH 31/35] vpad: Keep second channel empty if no extra GamePad is configured --- src/Cafe/OS/libs/padscore/padscore.cpp | 1 - src/Cafe/OS/libs/vpad/vpad.cpp | 24 +++++++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Cafe/OS/libs/padscore/padscore.cpp b/src/Cafe/OS/libs/padscore/padscore.cpp index a83711fe..0a577b97 100644 --- a/src/Cafe/OS/libs/padscore/padscore.cpp +++ b/src/Cafe/OS/libs/padscore/padscore.cpp @@ -494,7 +494,6 @@ void padscoreExport_KPADReadEx(PPCInterpreter_t* hCPU) osLib_returnFromFunction(hCPU, samplesRead); } -bool debugUseDRC1 = true; void padscoreExport_KPADRead(PPCInterpreter_t* hCPU) { ppcDefineParamU32(channel, 0); diff --git a/src/Cafe/OS/libs/vpad/vpad.cpp b/src/Cafe/OS/libs/vpad/vpad.cpp index 94bb0ca2..ded4304d 100644 --- a/src/Cafe/OS/libs/vpad/vpad.cpp +++ b/src/Cafe/OS/libs/vpad/vpad.cpp @@ -50,7 +50,6 @@ extern bool isLaunchTypeELF; -bool debugUseDRC = true; VPADDir g_vpadGyroDirOverwrite[VPAD_MAX_CONTROLLERS] = { {{1.0f,0.0f,0.0f}, {0.0f,1.0f,0.0f}, {0.0f, 0.0f, 0.1f}}, @@ -240,19 +239,20 @@ namespace vpad status->tpProcessed2.validity = VPAD_TP_VALIDITY_INVALID_XY; const auto controller = InputManager::instance().get_vpad_controller(channel); - if (!controller || debugUseDRC == false) + if (!controller) { - // no controller + // most games expect the Wii U GamePad to be connected, so even if the user has not set it up we should still return empty samples for channel 0 + if(channel != 0) + { + if (error) + *error = VPAD_READ_ERR_NO_CONTROLLER; + if (length > 0) + status->vpadErr = -1; + return 0; + } if (error) - *error = VPAD_READ_ERR_NONE; // VPAD_READ_ERR_NO_DATA; // VPAD_READ_ERR_NO_CONTROLLER; - + *error = VPAD_READ_ERR_NONE; return 1; - //osLib_returnFromFunction(hCPU, 1); return; - } - - if (channel != 0) - { - debugBreakpoint(); } const bool vpadDelayEnabled = ActiveSettings::VPADDelayEnabled(); @@ -274,9 +274,7 @@ namespace vpad // not ready yet if (error) *error = VPAD_READ_ERR_NONE; - return 0; - //osLib_returnFromFunction(hCPU, 0); return; } else if (dif <= ESPRESSO_TIMER_CLOCK) { From 9e53c1ce2760ac6a33d52f8813d79402b5183203 Mon Sep 17 00:00:00 2001 From: Cemu-Language CI Date: Thu, 22 Aug 2024 05:17:01 +0000 Subject: [PATCH 32/35] Update translation files --- bin/resources/es/cemu.mo | Bin 65733 -> 68744 bytes bin/resources/ko/cemu.mo | Bin 69784 -> 70573 bytes bin/resources/ru/cemu.mo | Bin 89284 -> 90752 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/resources/es/cemu.mo b/bin/resources/es/cemu.mo index 856049de037de70de90a2868e6da991fe25d1235..a43d4a1dc46c8db327cd91f2ae906dccb734169b 100644 GIT binary patch delta 20556 zcmX@w$kH*BrT(4}%Txvi1_o|s1_l`h28I#|1_pi>28J^RAW;Se4o3zCUIqpRPDcg? z9tH*maYqIQZUzPh6-NdJP6h@B11N0;rCp%3A5>k0BLjmp0|P^fBLf3J0|P^YBLf2; z0|UbZM+OEi28Mcu`A~^fPz!cBGBB_(Ffbf`(+%Kr=2$iTqw3~`{C zGXsM#0|SGmGsGNQXNZG6oFV!GogwChL-~o$5PQ;{85p?h85kJyp&F{4AwksQ46%3$ zRN*pbh|ktJL!w~6GXsMtC}f-=K6wPS;3L$6-%y7zxz-6wGAYShZ@##)i1_p5k28NTa3=Con3=9vU>VCLF5-+10#38(H^$>%^paQaP z5C^EaK@2c-gBa}T2Jv~Y8zjgR+#ve$-5?f}xj}s1>&Cz!&%nSi-wooS{ZR2sZjg|8 z;sy!PpHOu|_3jW0jocwVvvr3g8drA)23ZCM25+c*raQ#q5_d?@HM>KiV7fa4Lm&eK z!wPo>24+yMaEFAzKd3%F4~V&v9uR$s9t;d}3=9mW9uWQYAs!42RtyXbsU8drwG0dl z3p^m%O3o8vp@k&V8g(`Q0WZ`qHW#`42ld445z&zA@SB5 z5(S^VA&Kp;H^e|DA4rsl`aptQ$%lc#l!1Z4$p@mY!Uv+S)&~;g?LH8XEbxIic%2U@ z4b(F*Yy&eG820->g6IU4z5~_p!3UCP{y{C|@`Xg1xGyAeD)~a>Eqozq!o?R7Qr^B0 zhx+?M9FpV_>@=d;wI631BNmO@zA&Kw{)L(9WD#lXN&9>Bn0$-uzyC;;LknLq}HPzDADjX;PG>H--UW->4^bOeHej)6fb z2tsQIK@yco5F`YHgCJ3t9s~*M+#rYpOM@U$F&QepCWxUPT$XQvYS#u0<{pcMgz(K+28KpZr4-A+kO|8F$#D>gvvH8Pz8VJ! z^7nBN3)$i!iBCEnVvtEZB>%d{LmUzx4@oLImNWeNj>5(5LnmJ~?kb3FxO@Xr*8PgPPOw00^aaT%pT z9OwiU4@iaR3y1P6Qz22&kP1o6bD`olq4a}PNTPgRp9%@O\q`6CsQc!bj+A)=lJ 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 J%=d3Q69BbIgs%Vq delta 17814 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 delta 18861 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 diff --git a/bin/resources/ru/cemu.mo b/bin/resources/ru/cemu.mo index 619ba6783440be10ac2b2c0c77cda8523926e8da..4ff04e2bf7ba65f396cd0c2f640e8bc7df3c35cf 100644 GIT binary patch delta 22957 zcmX@IleOU}YyCYTmZ=O33=HPX3=A?13=CH!85lfS7#MntL81%{8(bI|co`TNHo7n{ z@Gvki>~~>c;AUW8IOD>=z{$YCa05y|g3@oG^f#zF23H0KX$A%c9#;kieg*~x4Oa#R zJ_ZH`3kbiS!QB;NaJVZ20}BHKLy9W{gE#{NL#8VOgAfA)L!&DL0|x^G!xUEr20;b} zhPkc`46F6@E@EI%aEFA5hC9Sy zQ+J3Dyxk!Vi*kqfAi*6Hq}lEa45|za4At%s2QGAnIAn`E#AAD*<{o#4MCEy?xp& zC&b_cPl&~to{+Rs>B+z##=yWZ5h}j|N^gYf+Xdwx^n`@mDX6}yp7jtPz4e3y)qhWj z&pEvyK`!P6u~^j$Vu6kqM7^^Y1A{yR14F16#3!Xt@eVIYNX+trgy?#xx)WXyi(hy_ zJoMQM5|zK}y%-o|85kHCy%`u785kJky&)DWdP73Mz#9?;F5V0b%peQBAwG}vh8URX z4Kc6;s=mgXfkBRefnfqv-+FHb273kuh6COV47H#f?Zdzz$-ux+=L4~)eufVu=$HFI zEZFM9z!1W~z_8N?;xj&9hy{wi3=HiI3=EpS3=B@7^1v73kT1Rvbu4}madAHe1{Vef zhA2M<21f=4hWUOB3^oi53=jPvAtUDx@wu@-IK=B2EFlaAXMac{^Yw=q7~~I$id26{ zP#5_#FqkqhFie7~I|0>q)*lko*ZmI zNtJ#95DTLMAVHfR07;BRQ2CAkNLrW@012tt0T74I4*>gwVN(F41lIFqqSQN(fk6zE|6>9nskk^0VnKHx#NbI#g-Zh=X=Ga+V9Fr)=TqCzKxfx(J_fgvx1fgy{5 zfnj9`14F$60|SFdCa6MlE^q>A-P5=mVp5j z#ZIvh2PMZcFa$6#FqFkIFsx-@V5mPH3-MWL90P+j0|P^I9K?eCagd-r76*yTYjKcN zeABqT)AH9ZNihusi{x zP9zaRt0poqs4y@vm?c6y7M%!68}$qf*@+N~Cnqv6NP$YML`Xi~l?Vyi>xqykd7B9F zId>97TrLS>ut^f6#B@!9ICNGLME&|CNXVT{Vqi#SU|_hM#K7PNYH1}y5@%g91A`za z|DQ;PD7=*nG5A3;BoTc}h8W170!cI~DG+&!6iATzr7$owGB7ZNr7$pLf|_Dbai>&B z8t_VmL`6y}#GL9>h`AF|As$(o%D_+$YHICCg*f0!DkPEJN`-{L(^LiqZ3YGgrZffy zBT&;R4Z=@MgQVu(G)VovBMnm0y-9-W=3^nNx{TtIE+5Aa5 z#2lFnhI(+D%p(JmIQ%mpX(0^CkIsPjI4J{?Xeu%w7ItMoQvbdTh(Wh9AW_Jc3Gtaw zCd9|8nUKV7l?f>yJTf7PI5d-iL6?DnAu_WbVo-Y~14BFm1Hh|id_7#Ng6KF)#! zVNezWgAS-sng#LMv@A%8cOVPW*u0+w339em(GKPm{K0tk)|Jcx_S z@*oy=LB%KKL3}Vj590Dod62YmAP-VkT+3r%IKsfdz@HB(Kd$6M%)6Tpwus?XJ|y-3 z%7>UMTmX^>#eV?<0~4qkEr0}_b^)YlG$?@h@LmBV@jWeoq?PXl5C;esLaJNELWs{T z3nBWw3L#P9UkI{*fg!pO5~3M}kS14AAw+*8NF0>^`wJm1oLLBoo0WwS3%3+P;_z4@ zBt#w-LQ?ftkVXau2CgEAMIuEE3~dYy3<^aI4CSDbuLx4$$QLs(1cQq7Vg?3BMh1rK z#SouMmV!z?1_s4a28Mc2yWO)ClK7tZ!~^HcAldH*RNwD1h{Hj_1M0l8mqQ#ZTwV_`Sh^e% z^a|w=7iyO?FhnyjFgTV&95A;WqHbY1B(9g0LoD77rH@19ua`p{aHkxSrk<8VqV7dG z#DRaxAyFt+0kx;T0%EXZ1w_KF0@4nMu7D(xSrw4%w4#E6p_+k#;Ve|#vl3!acqPQ5 zgi1)ckX#8VXy#W!9DcA8;?tv0b!RIf=3c0T6xsFnDj66Y7#J9Ms~~ahR|RoMdKJXQ zHB}HFOoP&^svuFd56VAV1xYIpq53~pK^)9e4YAm;8sbw+DBr&tk}JZi!BJe#kOCDb zsD>1&wbhWQm{Sdjv*pzgAFZo~l>HZ~AtA<614(3@HIR@Hu7O0E43u9{!@v*&8b7Fk zMAbd0_`4d2hrZW<99Yl5z+DURk#H>}Zk1{w1{&8w9AHxmvB{%J7#N%x7#M`=A@X6+Mr~X@!~;3?5c7-cA?8e}hj?&) zJp)5MXh>uQRA4>SfF00WaiAXJ;-mGDqW2Lfv=|r|zScv6l(7Mlm;@UjAtT=au~4-E zqEDv*QbKw*K+;rV1H_!N21pv|Y=Gwfbq$dGx~BnR@cjlz{rtWGk_crRA(e@FBP1w$Mr@?1B0&#QjWMaL3%!wO%V0Fn;=p5yb0nkw&r?BeJ|GxY0rB$ zL()Q3Geo?z8BzpqX@(S;=bIr8{oV}mfm92GZ{Gq5(xethoM*Q{Jkr<#v2bDwBn@qV zitlQHM8&E47D)EF3Ke+X0*U)iEf9zNZ-E3cUn|65nN~=mGirs%d$vLnZA2@?z|2-i z$D*Vak~UVgf}7b4?^_{0mS}_MH)w<8qI&l>h|7H1Ao)M64H8HBZ4iewwL#)^N*g3` z&2EGEY;hZ;Hrx!AKMxhZ2Bn`t#lN*d9QwBnoZlIE+97<2cCdN%4C?I=AL_S5d~Vwg z$)8>j1q^ZRkho84hXipKRD3d&p4|>{=whh;tx)}k+94(5wRVUHZni@_`T)v*1va;y zf#G92BjOWMI(ifF!mFoeT`i7#J8fbwW~gWEUhmm31*N$TKo9Oz(p9 z{kC^QdeQ&8AtN4^Jq!#%psrRAB<-B+f#~}PrGINJf|O$s&Do)F!V7nFi1~;G)`AdfTaHG6BrmSfm%Wn zA$`C{6Cph%rb&=oVloMmJF+J+Fw`$)U|^UxiGjhKk%7TwG9*>kOo8}x;S@-Lv~LO| z&VEgSL;>eiNJt4yg;=046;drbOob%Iw5gCpRz4LHvVBt_`esgrM8Tq|kVLs=DnmVZ zZ1?I^NYLGz3QiOZ64M|CDNKW;Qp0JG+RSVkBo|msV_;apz`zhN4KnETa~j0KHq#*? z6gi!Np&1mC(-|0=7#J9&WY0RN(V0h!1|wf&{tPY)B9X&4vVJ!fZ&;mCuGmQR8e#NG+QU@!7iBkPzAmrFYJT zgxp@JxiK;m%197svEVGbm2&d-6A2S4UOf=qBOM5EGN zh(#uI85pL4#_^%_d5}0014G(8NP8k{0ij`=A;RgEcZRoLmGc(Jn56 zM9p0&|M?&v>4)}kj0S1l(HBSWW|diE^S;4 zae4bwCD8VN z@Dc`w97YC)j3p3_SC>Kzyt@?QfG0~KasCQQzk|}Bp!Bz;5QqI*3UQd=GDrbtz6{b^ zj#vhXqNZh#G&XG+#6w$`G1P+x7oT`M6Dy|5DEq3{d*p*L$G?E~3$kThYr4&uQ0b&!Ita2;5GJwqQyyKf4}c-re;OAAg1F)7}7a zNWcbABCTg&NZJ62qvj0|4XZXlEIPCSV)54vka0VyjSzK-8zG6OV48;-LJ$WD}$>wr3M0s9tY^WH-Uh5SNN> zh9oxi&5-iJb~B`L>a!WrS1jKQF?ic%28L7y28LUk85nMXMnbnh%$42>b;XX*a<7xdqh=Na0T7EwR zgBJq>gU^15!PECcLSoZ?NN-l;0HjDQJOI%*`v3#OL`DXN69*U=av2#Iq7Fgk`A!^$ zsCPTUz%Uy$uz3U$CH6-d7+OI?vPU7K<@HR*AVHRS43bEik1;S@2WdFQz@Wv*z)*c0 z;?iX&AcrY3nv*E zCWGb!PlD4-Jwxd!28J3228N@jAc@53G$a=Uo`#fI<)w9Eh%GU=TY8 zu~7RQBxnQAK@?V>V_=A8WMG(l4wBm6pJ!lL$-uziegQHI_VEH_%(&|!BpbfE2+5Y3 zmmo!S$R$X5G5rz)Lj`Eg=n@0N2~fyfW?;w#O~qbeV6Xw@|GX=ZM78<~#HXjOK=S?d zE07R*c?FU^e_Vk$RO2cGcxu(}DkL#YxC#mKD^ObY8YD5NU4yiK`>!!Ds4+4y2wjIn zh2;&1c+d^#{NL3Zkf8c@0~90-3@SGv1(4-UNF3YVgtQCdZbCvP?Iy%wbvGeC?uF7j zpz@EQ^1p9F5-Ha$NDs;U7Nk6=zXeGP9k-zU|CzTS7B9aAiGn>)1;(o&?1_mxr{=fPO zQWigeDs=x0iPMnJkPw*q8B(+|e}Tlg*cV8Vt^EaJulBiF9fh1z) zuMk@CDL6nH^D80tZjzzl^yAVD?j z2P9jq`~hi#o&Etywf}xV(uUGc28IR(28M*63=FZ13=9u{LiE}HhWN7m^0z|3aGO{eK~;_Viy!Nq6%v zBz)BRGnlGctl}!D&p44CYJ>44TZ0 z;1yActc>8qS;5K(9wR=&$_QS@$HB%3UM*KI%Ekz8f~m1Ff@e5=*cib}vMSja!OQPj z*cib>G}GA_!7H8?Ks6pGJ+R0wnF)fI3XTc4^_8=laav?wEpifCnI<; z=_@D10rR*R!IRR9xF8|2l8X_%g7F%Z{=@~bh>sg$pd>d$y%IMgc&UXZl=k9g1ot7c zq4aF1y3^c@;K}Dp+>8t{p#0Ck1F;~MhY>U+!cfV>$RNzfz_6VM5~QbjAwGJ<%LtyR ze9y}WZs&jE1^Ix1A%c$)ycDyG529`gAH+cm`5@-5;9~@L&5rOff*T_8{16As;Adp0 z2Q8sE#t(7n4}M1QEVYIJ#35b+jNrv-9Rd&y%LE|mt_U!K7ap+)LgLt35E3GJf{+la z6of?KGC@Z0y1=c1ki>RM5MthKK}PUGq@RL}_29LhCPEOO`Uo+C7mFneF@h(V7YIRu zm{k}O_k6+-gEfU289)P<7Qztud%}$1GX1$QBY1UNiwML)^F$ztbfpMH-x(1|h};%o z1g{JFE>aJ1nY<_?cyWk^55rV)1=3NFskH#t2>qBr49xkPqsj)r&(C%?5FZ#TUgPKKLLG2|6wb zMsPJNFTu#r#lXN&D8a~31zM^p0g*44goI3wBqRivOF|s5Pm&S5NR>ef;s6mTh=-J= z7$G4J6|eV^g5=j|DMs)TtUM`5wpk_x$)|^<7{Mzc9z$t%X^4T^(vTpwl7@twt2892 zv!o&Fd!-@zH%c>t7a(4ghImX+1`<-*GT^AFXULLa1a}(SWEeqfg&FS1FoI{Tm1H6L zx=a?rKPJlv?rciQF@jexmB=xIhu4qFF@i_QCFB_ydO)k)yH^;I&=%q2lVwkPu5( zhWNZn8B&7IS7rpSb~&mHDX@M)#RXI#QER3G$*!R)kdSHziG%Y0Diugvo&*UnFfiOx zf#ia}P})irlI_Y>A=ziSDkO-HsxpEHkzcAZGITRAFl4AfO3W{65cRR@5V}_#l34eu zL&}L$>Wtt~w=3$54E3P3*`V16(9&ud4M-x<(}4KUUIQFQ3_%)<;FU{x8W4lFYCsa# zAq_|zUxSK&(}08ovnC`2G&La(wbz6s@?=d&8fbyi^E4Uj!IjPqO-N#Ts>ujmT>3#1 z;t*pkNa}Xcf+V_lEl76E(}LvRrCN~ebXJQIymtHtRNY@KNN(WKhLjJ=+Kk{usXE#a zhZREUcI|pbhAt)shPm31Y@?ya2%gi)(t{Y>r4K3jw(2u7{9$BZc&yI|US#sXkP+PS zS!%=xUTnr;%n0sQml{J-_itlJC1qv82wt?BX97v&OH3FU#F-cvewi?Wmu3ppn?vH_ zhB+g{dIkmt77Ip(0tN<#Ef$R6HJYlHjNlf}1WQIxR%Q^if`ovMH6-;XTQf4uU}Rv} zW(_HN{cRy0sj-FRl6|(2#LREU$l$=pz~FDk$Z(K}fnkC@q+L)SzNJ)3j z1rjB8u8iQ`Z>TFc%Ig^pyFx5D=L)H{zCh{Ut`HwFxiK>6GBPmeyFr3H)}0Z&Q1O5} zBSRUeBy?wFkYr?FDE44vP-bLc==Fr;rj1?@bB}pJJoLs3Qn?9xGeQ=-dV{iiJp)6& zHzZ#!@P@QhE_p+mQm?%sQB&swNeiodAc^y;4aYHgC}9g=WS9lY|84;g4G#k#`IRvcQUn_YLb9D- zASByO4TNONMS+lp%2p`7GZ2#c_d@j@3S?xkWMp8t9|(yO$zX`bEP^3X9T?0AZgsZ> zGcweJM!DAqLsISiV2DrTLLjt92*lu`5JrYC3=9nAA&}gW6UxX?0oo%H1}S1$qamrg zG8)nrJRi--PzoC3iH4;9@EAy6YQX=sXixlG-8CV$@7_{OU8Q4L+W~jJD zJj7#;@%4~;-7OxR8W~FCAwk>_4{^|BsK!O{kk;#}ct{bt4az?N)prKUzYe7zLoIqA z56LAz;vs26G6CX&s02uqm(@cV(-I&q-wiJC2NMIh@Y&78z!1#D zz;Kg^fnh2m1A{OV1A`$G1H)_3@;C+th9{sEjf@No8$g;s>p&P87#@InPfQF9i)p=Cz`)7Kz_5@3vewU#k%6I< ziGhI`suwi;?#Be_mV>mmF)=XkGBYr6GC`*BHZnq{X+UzIVVdtukWr9&8%73(+n`ZR zCI*HbAXx?mhGS4ep&mYEnBW-~G{oMU8Q z*a#J4WoBTw!^FVw5XzTiVqoBd*ip}L7o-@%fl4zltYBhbI1j2OLE|)_1P7J>3RM9b z&*gzSC?6`f9m)qOQDA0Z*vi1bV8RR;oVw1$z)-@-z%ZMMfuWxXGJ2W-3K8ad$Vdff z6a%DjB_jhvJSfN*85n#R85k0nAnX0lLk%}zW?;x#5ASMO|TP6mE$&8Ru zFQ^o_IVA=f9cN--xWLH3aF&UIVJj$j85kI5GBPmmGchpOFflMBF)=WFWMp7C$-uyH zn+dY{0Az+CGXuk1Q2qyTUV{iI{>j9^@Sll+;V2^m!xoTlLF+e|7#JopF)+MgU|?7R zn&Dz%05``#T23-DFtC7@>o7t_QYx4rGbn2q8Ne+bc4h{K+fZ|^GD3!XJfQp)jP?u+ zozMgXG7Q|m2F+xFb~rLIFsx!^U{GLWU|7$@0B&4>)PPn!9D^EgdGbPI;reGx3=F3j z85kykoW;PvAc|}vNDBi4c;ughnSp_ei2>YT`^&(j5 zP@BYEVCZIKU|7V+z)%G$9T*uHq@kJ%85tOi zpfNAN#K4daGKGPGfrF8O;XG*Ql8J#qp9wO}p~1|+5W@r+w*CiNMFiE?!N|aHgOPz@ zDH8)j1tSB)K}H6Kr(g%yL&m;9W|lKDFvv1NmLFR)F)+LZ34-Q67$F1rpk+z>KxHX2 z1H*Gh28JC>kcB!M7#SFxnHU(%7#SG47#SFn7$G}SbwH_(k%3_j)FYaV4B+N5$k;7R z3=Aij7#QU1nHd;57#J9yfm{fZVPIe=0?i6AF)$p5@>!S}7@DDm>}6tLaAjm*_yHQO z2KxZ22(&(iiGe|ynSsHJ5i*TE0~8XVkw#DplZk<$2}%7&1_p352fT&~H0bu6iGjfx zYKbv4*kDGhf_%XQnN0wRMKMCAB(jkV3}s?qxX;AEa0gV&FfcGAL*+oL&p?BHAbn?` z=7aWQfcWmPJXFgBnU)3#wlFa;tY%_hum@G<3=9mMpmGqDFQ5j7FfuSGLD`>~Ad|fy zeaILzn+V!TI0f2D*vZ7epvJ_&um(v{8Q3xP3=C~x1_Q$^sALf+Bp4VN_JTI#GD4;t zK@&9-Ky@or4Tu5_=7MMtKF9!B?$!x4>k1PCgB8?~Ao=f5aS$7{W}}b^vYf@X9!Wxt znSo&kD8qs*Wny4B22u^$B?M|^Kn?Q;$sy@k$i%=f1yuHeI)b1?2-p^qZQcx~}#Xo4`4WtINl!=j< zf#Dw15uhaskxUE>DNqAJyUIc8Kzk#9GeY)E9D=Id&A`C$0aTrVszOk&6I4)v6hkp+ z-{d(c8$^NjQ`Unz)bp7b7=)N1(-iNahCK#V&`|buMh1pyP&PVCV z!3h*4pdy$FvUxKFL^Clk`~~?KYB*?1q7104U}RvhW@KQg<(*+ zDo|PgRcxR(v^XOJLk}ZlP2*LlV$f#1?+lPlIUtRoHMJo63nK%A3Nr)4J_ZKxfFDRK zf{B4)F;t%el>P^$MW7af=Fp!&#Xx#M8$^ScKx^nh?OtXEhMQ1@yBHvov>-(w3|iXC z#mvC42b6+A0-$M3P?-Q~>@hGftYTtdXkcVuSOy9KMg|5As3oA|3Nn}&7_LC|fGA!j z28KRH1_nV;B?q;y9(3FQNOBV+WQGtlrk2A5+1LpZ2jPXF1-?*&K?^oOYo!!G^#LPf zI&(2d5fYvUYRG|FkVsbFsALUjn-QomVq{=QXJTM@#mK;*!wi{fe#6ATFb`xp0|Ubg2FQfmJy5Tb zk%6Hf)Yt9f(o}@uB8}7V|y;34#W8L8U1Z0|O7J_ovPXnK`UvU|=}J z$iT1$s^LGVbqLh}qkI`56T5Yc3=H8=-+)ep0L>_Z)MPS2RwFimvNY73k4y{fW(M#GbGKi?1;{as3=D~& z;uuu4GB7Z(f_jahD1fR3?L-8b%gDsQFo_Ye$O|-kzyoU3fZ~5669a=As9VCw!0-&z zkA_McpS9A8`Mr@Vqi#x8vFv3?HCytI+z$3?lLhjWHT}_uroq7`0Zf? z%_1@|2tdsOtte9lHQGTgVPIf54mAU`hL^#Uk%7S*s*nTJ5Mg9s_ybh{8dcl_Wp4)+ zHlUs6%nS^bj0_B$K#fsG1_owk28O4M3=CROb$N^o3}H+R40cfda!|JjG$H{ik{KbZ zNko|;Q=;LZ`2P=DtO+WFpc?!b85nkf`Z%DmPDTcX)1ZzQsK3m>!0?_CvXEyvsLR60 zz~BhglMiYYF*7i*F)}cmf%1sCF!cA*(v76E4^=@L5FSr>BT7Hp2YDGb6QAx3$f=_Br_w8YGu)D(r1e1-C&%#u_P zqa-6$A-S};BtKUnHz!#kC9^0sxg@`+Qjd!(BqJ4KaAvU;S9L8Hmvd@vsX}sMUS58Q zLUBoAQHer%W=TeVX^BFj0>twmBS3D^Q*g;wsLU@_C{N5Q0a;U&TAW%^uK=oI_xK$16*d|?mr z23{jT?nep&B)uT5lO2N#r4a!Oa%Dk&QAuJ_PAVt`rA}TG>>!($nwnA!@^VtDLQyI> zm{L=~FJ*kT|G#O%dN@`kSX-)|^ouy@hatSnc zic@n^lR-HH5n>9U5Q8VkjKpFEP+Cb!P0fQwamwbfj5923YHFYWn=F)@&X_p4E>~75 zGY@22ajJq_K(LNNNqK5wwnABEacN>su|j^4LSjnF`oRXb>}2=4 zg3agZ&M_+FB<7`;CZ?Be4*Ij}pl9+c)& zi}e&7Q&KWP(V3V7E|!WxMUg^MszPyU3A7l=%u@ilR=rpu9a?e}!%7YX$DAC6F<^YY@;3SgpX1quN{p)LU+ z6?q6@aQ<`*@MOp%5&zL-Gx%XtA6_X~fSfMca>`#qJD_zCsUf6nJ%Y}Ux_CvH@*l}Ucg?$&cPFDq!iI~@7q(p3 zabe$PwH*tYWiRZwu>Hawkck&|Tx`17qX6~{h~ewwJUQl!B%|Wyjk}qcH#_fN$H;VH z?_}w7YLidz;h$`GLUwY)RaT{47dBjMy08Hf1E5@|$HjGF{>5gHVW1kFi)*s(e(8Ex z2!VtzHeT#kxUliU)(hJ~)-_z%aADJh9T#?A?76TP9FO}z!p#?(Kw+~BY^K76Nf)~n zF1B9S2NJ#5bYZK)#l{O8L6LQ_5zXcH3K!;I*mq$&BuF=b^fj7-LuuQEtuU=1i#A=@ zpn&QBtrs?5*r#w|lfs30(|rsXWjUdZ2~5AMWAQt(*CFj7gg%7}1T;*3ePFI8s903E z2nv*qVBc;4IU4Mq{TKFu69g#0cU;&sdHx5{$(;w=7?)2za8P!-j}W8a^z&MbqLb$w zRAAJbp6Jdf#sV^I@y0`Z(;HkFg(inzEOBkQu;Iea3%eCAwp`e8Ve`f2iwzfcUDyF{ z$}n752oAd~;Aq_niWHDih#q*|Guh#i{^pk#moS6dNRzGxvrNvv6~eX)9H7$~y%~ik z2d+@x%y&DHH4l`2L55xIfm@;gZW~-c%8E#e;FdruGX`XJ1heesgaML%_&^i3(ArkFLqzp zjhsxjUf4TX@vYcoyLa51f4s8flHQ>JPF!0q>;Q);D2~DTn&HCa>4jR1!ju2s;@hnM zm6vg{=Qn4@<&!_26xReL<6WTCdtoOyMPAr%HTsLvFD6oR0_Z_9-Z7xrA(4@xqyU_{Lw;9~emMnDBSxY|P-UUe^kec>l6S&+5l}NAzb73Q> z8rv|reup?H7wx^U0aU_+swYV3Zo07HV#kFY(>=u)l~@&Ae4OiNCw*kN+zIq1s7!?dqJVl0E$9ToeOTT z>Tz9|GF{n_QB@ICk8guyNKh(+l_a1l7gDiIcQa(v%!KsU!J=C(Yys8R7q)@ZF|5#r zSlV!51IW3cJPmRZxNzSK&INnHt&fW>ppqSwp)NLIciHwlLq-m8uVQ+o5#y}si%l6t zx4WA%zGbSPgj|$e1g8;5@`BaL;2d_b5uC5LUTgrl5R}9Aff%4N64VH~ud)L2FM?b)`4mPmWP?ySD}7b%E{4o{YilYT!WL z2nt4UQMMPFCgDvgNc(QOt1+X|^af)_aUlgrTc7K~ba3ivyx226F`Q9ka>`G^kc<5~ zC?y`KFa@PWNQt*Y0n|9!0?t~S!6hxYtZ4)#_6vK!7H>sP?U3}neL?_Z3EOg)2u8-q z50~g@fm(#V0SeC0&H)36e_=nU47}J6D((?7lM9y0OzvH3&U9h#bO%*N)#=%hjEMpu z_3jZyV0|7&W+u~5M>2*agPOCuzzw-A7dC+9{M?+uG_->NW2Y4`fV%|>;LZX}G%u|H a#sqZ`z-+%@a0dax4oOVkZXLzg$pip^yY+tn delta 21666 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 bDk?ARy0GuUW>8{cn4Gv;WIIm+V+j)g<` Date: Fri, 23 Aug 2024 19:26:33 +0200 Subject: [PATCH 33/35] GfxPack: Workaround for invisible detail panel Fixes #1307 There is probably a better way to calculate the maximum width. But this suffices for now as a workaround --- src/gui/GraphicPacksWindow2.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/GraphicPacksWindow2.cpp b/src/gui/GraphicPacksWindow2.cpp index 29f4b865..c49cbeae 100644 --- a/src/gui/GraphicPacksWindow2.cpp +++ b/src/gui/GraphicPacksWindow2.cpp @@ -458,10 +458,10 @@ void GraphicPacksWindow2::OnTreeSelectionChanged(wxTreeEvent& event) m_shown_graphic_pack = gp; - m_graphic_pack_name->Wrap(m_graphic_pack_name->GetParent()->GetClientSize().GetWidth() - 10); + m_graphic_pack_name->Wrap(m_graphic_pack_name->GetParent()->GetClientSize().GetWidth() - 20); m_graphic_pack_name->GetGrandParent()->Layout(); - m_graphic_pack_description->Wrap(m_graphic_pack_description->GetParent()->GetClientSize().GetWidth() - 10); + m_graphic_pack_description->Wrap(m_graphic_pack_description->GetParent()->GetClientSize().GetWidth() - 20); m_graphic_pack_description->GetGrandParent()->Layout(); m_right_panel->FitInside(); From dc9d99b03b38f3cf427714ad1ebb4d6d29f645fa Mon Sep 17 00:00:00 2001 From: bl <147349656+squelchiee@users.noreply.github.com> Date: Sat, 24 Aug 2024 16:03:03 -0300 Subject: [PATCH 34/35] nn_fp: Implement GetMyComment and UpdateCommentAsync (#1173) --- src/Cafe/IOSU/legacy/iosu_fpd.cpp | 55 ++++++++++++++++++++++++++----- src/Cafe/IOSU/legacy/iosu_fpd.h | 2 ++ src/Cafe/OS/libs/nn_fp/nn_fp.cpp | 33 +++++++++++++++++++ src/Cemu/nex/nexFriends.cpp | 25 +++++++++++++- src/Cemu/nex/nexFriends.h | 7 +++- 5 files changed, 111 insertions(+), 11 deletions(-) diff --git a/src/Cafe/IOSU/legacy/iosu_fpd.cpp b/src/Cafe/IOSU/legacy/iosu_fpd.cpp index aca1a332..28d248ae 100644 --- a/src/Cafe/IOSU/legacy/iosu_fpd.cpp +++ b/src/Cafe/IOSU/legacy/iosu_fpd.cpp @@ -511,6 +511,8 @@ namespace iosu return CallHandler_GetBlackList(fpdClient, vecIn, numVecIn, vecOut, numVecOut); case FPD_REQUEST_ID::GetFriendListEx: return CallHandler_GetFriendListEx(fpdClient, vecIn, numVecIn, vecOut, numVecOut); + case FPD_REQUEST_ID::UpdateCommentAsync: + return CallHandler_UpdateCommentAsync(fpdClient, vecIn, numVecIn, vecOut, numVecOut); case FPD_REQUEST_ID::UpdatePreferenceAsync: return CallHandler_UpdatePreferenceAsync(fpdClient, vecIn, numVecIn, vecOut, numVecOut); case FPD_REQUEST_ID::AddFriendRequestByPlayRecordAsync: @@ -719,18 +721,23 @@ namespace iosu nnResult CallHandler_GetMyComment(FPDClient* fpdClient, IPCIoctlVector* vecIn, uint32 numVecIn, IPCIoctlVector* vecOut, uint32 numVecOut) { - static constexpr uint32 MY_COMMENT_LENGTH = 0x12; // are comments utf16? Buffer length is 0x24 if(numVecIn != 0 || numVecOut != 1) return FPResult_InvalidIPCParam; - if(vecOut->size != MY_COMMENT_LENGTH*sizeof(uint16be)) - { - cemuLog_log(LogType::Force, "GetMyComment: Unexpected output size"); - return FPResult_InvalidIPCParam; - } std::basic_string myComment; - myComment.resize(MY_COMMENT_LENGTH); - memcpy(vecOut->basePhys.GetPtr(), myComment.data(), MY_COMMENT_LENGTH*sizeof(uint16be)); - return 0; + if(g_fpd.nexFriendSession) + { + if(vecOut->size != MY_COMMENT_LENGTH * sizeof(uint16be)) + { + cemuLog_log(LogType::Force, "GetMyComment: Unexpected output size"); + return FPResult_InvalidIPCParam; + } + nexComment myNexComment; + g_fpd.nexFriendSession->getMyComment(myNexComment); + myComment = StringHelpers::FromUtf8(myNexComment.commentString); + } + myComment.insert(0, 1, '\0'); + memcpy(vecOut->basePhys.GetPtr(), myComment.c_str(), MY_COMMENT_LENGTH * sizeof(uint16be)); + return FPResult_Ok; } nnResult CallHandler_GetMyPreference(FPDClient* fpdClient, IPCIoctlVector* vecIn, uint32 numVecIn, IPCIoctlVector* vecOut, uint32 numVecOut) @@ -1143,6 +1150,36 @@ namespace iosu return FPResult_Ok; } + nnResult CallHandler_UpdateCommentAsync(FPDClient* fpdClient, IPCIoctlVector* vecIn, uint32 numVecIn, IPCIoctlVector* vecOut, uint32 numVecOut) + { + std::unique_lock _l(g_fpd.mtxFriendSession); + if (numVecIn != 1 || numVecOut != 0) + return FPResult_InvalidIPCParam; + if (!g_fpd.nexFriendSession) + return FPResult_RequestFailed; + uint32 messageLength = vecIn[0].size / sizeof(uint16be); + DeclareInputPtr(newComment, uint16be, messageLength, 0); + if (messageLength == 0 || newComment[messageLength-1] != 0) + { + cemuLog_log(LogType::Force, "UpdateCommentAsync: Message must contain at least a null-termination character"); + return FPResult_InvalidIPCParam; + } + IPCCommandBody* cmd = ServiceCallDelayCurrentResponse(); + + auto utf8_comment = StringHelpers::ToUtf8(newComment, messageLength); + nexComment temporaryComment; + temporaryComment.ukn0 = 0; + temporaryComment.commentString = utf8_comment; + temporaryComment.ukn1 = 0; + + g_fpd.nexFriendSession->updateCommentAsync(temporaryComment, [cmd](NexFriends::RpcErrorCode result) { + if (result != NexFriends::ERR_NONE) + return ServiceCallAsyncRespond(cmd, FPResult_RequestFailed); + ServiceCallAsyncRespond(cmd, FPResult_Ok); + }); + return FPResult_Ok; + } + nnResult CallHandler_UpdatePreferenceAsync(FPDClient* fpdClient, IPCIoctlVector* vecIn, uint32 numVecIn, IPCIoctlVector* vecOut, uint32 numVecOut) { std::unique_lock _l(g_fpd.mtxFriendSession); diff --git a/src/Cafe/IOSU/legacy/iosu_fpd.h b/src/Cafe/IOSU/legacy/iosu_fpd.h index 0a6f0885..b1c30765 100644 --- a/src/Cafe/IOSU/legacy/iosu_fpd.h +++ b/src/Cafe/IOSU/legacy/iosu_fpd.h @@ -212,6 +212,7 @@ namespace iosu static const int RELATIONSHIP_FRIEND = 3; static const int GAMEMODE_MAX_MESSAGE_LENGTH = 0x80; // limit includes null-terminator character, so only 0x7F actual characters can be used + static const int MY_COMMENT_LENGTH = 0x12; enum class FPD_REQUEST_ID { @@ -245,6 +246,7 @@ namespace iosu CheckSettingStatusAsync = 0x7596, GetFriendListEx = 0x75F9, GetFriendRequestListEx = 0x76C1, + UpdateCommentAsync = 0x7726, UpdatePreferenceAsync = 0x7727, RemoveFriendAsync = 0x7789, DeleteFriendFlagsAsync = 0x778A, diff --git a/src/Cafe/OS/libs/nn_fp/nn_fp.cpp b/src/Cafe/OS/libs/nn_fp/nn_fp.cpp index fc757ea9..86ca4708 100644 --- a/src/Cafe/OS/libs/nn_fp/nn_fp.cpp +++ b/src/Cafe/OS/libs/nn_fp/nn_fp.cpp @@ -464,6 +464,14 @@ namespace nn return ipcCtx->Submit(std::move(ipcCtx)); } + nnResult GetMyPlayingGame(iosu::fpd::GameKey* myPlayingGame) + { + FP_API_BASE(); + auto ipcCtx = std::make_unique(iosu::fpd::FPD_REQUEST_ID::GetMyPlayingGame); + ipcCtx->AddOutput(myPlayingGame, sizeof(iosu::fpd::GameKey)); + return ipcCtx->Submit(std::move(ipcCtx)); + } + nnResult GetMyPreference(iosu::fpd::FPDPreference* myPreference) { FP_API_BASE(); @@ -472,6 +480,14 @@ namespace nn return ipcCtx->Submit(std::move(ipcCtx)); } + nnResult GetMyComment(uint16be* myComment) + { + FP_API_BASE(); + auto ipcCtx = std::make_unique(iosu::fpd::FPD_REQUEST_ID::GetMyComment); + ipcCtx->AddOutput(myComment, iosu::fpd::MY_COMMENT_LENGTH * sizeof(uint16be)); + return ipcCtx->Submit(std::move(ipcCtx)); + } + nnResult GetMyMii(FFLData_t* fflData) { FP_API_BASE(); @@ -607,6 +623,20 @@ namespace nn return resultBuf != 0 ? 1 : 0; } + nnResult UpdateCommentAsync(uint16be* newComment, void* funcPtr, void* customParam) + { + FP_API_BASE(); + auto ipcCtx = std::make_unique(iosu::fpd::FPD_REQUEST_ID::UpdateCommentAsync); + uint32 commentLen = CafeStringHelpers::Length(newComment, iosu::fpd::MY_COMMENT_LENGTH-1); + if (commentLen >= iosu::fpd::MY_COMMENT_LENGTH-1) + { + cemuLog_log(LogType::Force, "UpdateCommentAsync: message too long"); + return FPResult_InvalidIPCParam; + } + ipcCtx->AddInput(newComment, sizeof(uint16be) * commentLen + 2); + return ipcCtx->SubmitAsync(std::move(ipcCtx), funcPtr, customParam); + } + nnResult UpdatePreferenceAsync(iosu::fpd::FPDPreference* newPreference, void* funcPtr, void* customParam) { FP_API_BASE(); @@ -763,7 +793,9 @@ namespace nn cafeExportRegisterFunc(GetMyAccountId, "nn_fp", "GetMyAccountId__Q2_2nn2fpFPc", LogType::NN_FP); cafeExportRegisterFunc(GetMyScreenName, "nn_fp", "GetMyScreenName__Q2_2nn2fpFPw", LogType::NN_FP); cafeExportRegisterFunc(GetMyMii, "nn_fp", "GetMyMii__Q2_2nn2fpFP12FFLStoreData", LogType::NN_FP); + cafeExportRegisterFunc(GetMyPlayingGame, "nn_fp", "GetMyPlayingGame__Q2_2nn2fpFPQ3_2nn2fp7GameKey", LogType::NN_FP); cafeExportRegisterFunc(GetMyPreference, "nn_fp", "GetMyPreference__Q2_2nn2fpFPQ3_2nn2fp10Preference", LogType::NN_FP); + cafeExportRegisterFunc(GetMyComment, "nn_fp", "GetMyComment__Q2_2nn2fpFPQ3_2nn2fp7Comment", LogType::NN_FP); cafeExportRegisterFunc(GetFriendAccountId, "nn_fp", "GetFriendAccountId__Q2_2nn2fpFPA17_cPCUiUi", LogType::NN_FP); cafeExportRegisterFunc(GetFriendScreenName, "nn_fp", "GetFriendScreenName__Q2_2nn2fpFPA11_wPCUiUibPUc", LogType::NN_FP); @@ -774,6 +806,7 @@ namespace nn cafeExportRegisterFunc(CheckSettingStatusAsync, "nn_fp", "CheckSettingStatusAsync__Q2_2nn2fpFPUcPFQ2_2nn6ResultPv_vPv", LogType::NN_FP); cafeExportRegisterFunc(IsPreferenceValid, "nn_fp", "IsPreferenceValid__Q2_2nn2fpFv", LogType::NN_FP); + cafeExportRegisterFunc(UpdateCommentAsync, "nn_fp", "UpdateCommentAsync__Q2_2nn2fpFPCwPFQ2_2nn6ResultPv_vPv", LogType::NN_FP); cafeExportRegisterFunc(UpdatePreferenceAsync, "nn_fp", "UpdatePreferenceAsync__Q2_2nn2fpFPCQ3_2nn2fp10PreferencePFQ2_2nn6ResultPv_vPv", LogType::NN_FP); cafeExportRegisterFunc(GetRequestBlockSettingAsync, "nn_fp", "GetRequestBlockSettingAsync__Q2_2nn2fpFPUcPCUiUiPFQ2_2nn6ResultPv_vPv", LogType::NN_FP); diff --git a/src/Cemu/nex/nexFriends.cpp b/src/Cemu/nex/nexFriends.cpp index 927418ca..36ba4a53 100644 --- a/src/Cemu/nex/nexFriends.cpp +++ b/src/Cemu/nex/nexFriends.cpp @@ -277,7 +277,8 @@ void NexFriends::handleResponse_getAllInformation(nexServiceResponse_t* response } NexFriends* session = (NexFriends*)nexFriends; session->myPreference = nexPrincipalPreference(&response->data); - nexComment comment(&response->data); + auto comment = nexComment(&response->data); + session->myComment = comment; if (response->data.hasReadOutOfBounds()) return; // acquire lock on lists @@ -391,6 +392,28 @@ void NexFriends::getMyPreference(nexPrincipalPreference& preference) preference = myPreference; } +bool NexFriends::updateCommentAsync(nexComment newComment, std::function cb) +{ + uint8 tempNexBufferArray[1024]; + nexPacketBuffer packetBuffer(tempNexBufferArray, sizeof(tempNexBufferArray), true); + newComment.writeData(&packetBuffer); + nexCon->callMethod( + NEX_PROTOCOL_FRIENDS_WIIU, 15, &packetBuffer, [this, cb, newComment](nexServiceResponse_t* response) -> void { + if (!response->isSuccessful) + return cb(NexFriends::ERR_RPC_FAILED); + this->myComment = newComment; + return cb(NexFriends::ERR_NONE); + }, + true); + // TEST + return true; +} + +void NexFriends::getMyComment(nexComment& comment) +{ + comment = myComment; +} + bool NexFriends::addProvisionalFriendByPidGuessed(uint32 principalId) { uint8 tempNexBufferArray[512]; diff --git a/src/Cemu/nex/nexFriends.h b/src/Cemu/nex/nexFriends.h index 1077b0d5..05cc433f 100644 --- a/src/Cemu/nex/nexFriends.h +++ b/src/Cemu/nex/nexFriends.h @@ -297,7 +297,9 @@ public: void writeData(nexPacketBuffer* pb) const override { - cemu_assert_unimplemented(); + pb->writeU8(ukn0); + pb->writeString(commentString.c_str()); + pb->writeU64(ukn1); } void readData(nexPacketBuffer* pb) override @@ -554,6 +556,7 @@ public: bool getFriendRequestByMessageId(nexFriendRequest& friendRequestData, bool* isIncoming, uint64 messageId); bool isOnline(); void getMyPreference(nexPrincipalPreference& preference); + void getMyComment(nexComment& comment); // asynchronous API (data has to be requested) bool addProvisionalFriend(char* name, std::function cb); @@ -565,6 +568,7 @@ public: void acceptFriendRequest(uint64 messageId, std::function cb); void deleteFriendRequest(uint64 messageId, std::function cb); // rejecting incoming friend request (differs from blocking friend requests) bool updatePreferencesAsync(const nexPrincipalPreference newPreferences, std::function cb); + bool updateCommentAsync(const nexComment newComment, std::function cb); void updateMyPresence(nexPresenceV2& myPresence); void setNotificationHandler(void(*notificationHandler)(NOTIFICATION_TYPE notificationType, uint32 pid)); @@ -619,6 +623,7 @@ private: // local friend state nexPresenceV2 myPresence; nexPrincipalPreference myPreference; + nexComment myComment; std::recursive_mutex mtx_lists; std::vector list_friends; From d7f39aab054a715e7b0481407298cbd654212fb9 Mon Sep 17 00:00:00 2001 From: Cemu-Language CI Date: Mon, 26 Aug 2024 09:16:11 +0000 Subject: [PATCH 35/35] Update translation files --- bin/resources/hu/cemu.mo | Bin 71267 -> 72404 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/resources/hu/cemu.mo b/bin/resources/hu/cemu.mo index dd3d2fbc74157e457fac7fefe3356bdbfbbe9e11..51b00d083cc320a55615d50774d60c74f71108d5 100644 GIT binary patch delta 22802 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 R&d*4sO&fRnbrHrMMgX}kLSFy? delta 21762 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