diff options
29 files changed, 239 insertions, 122 deletions
diff --git a/externals/SDL b/externals/SDL -Subproject c27f3ead7c37bcbef608f385baa9fce7232efc6 +Subproject 491fba1d06a4810645092b2559b9cc94abeb23b diff --git a/externals/nx_tzdb/tzdb_to_nx b/externals/nx_tzdb/tzdb_to_nx -Subproject 8c272f21d19c6e821345fd055f41b9640f9189d +Subproject 212afa2394a74226dcf1b7996a570aae17debb6 diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt index dd6c895fd..f54dccc69 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt @@ -29,7 +29,6 @@ import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel -import org.yuzu.yuzu_emu.utils.FileUtil class SearchFragment : Fragment() { private var _binding: FragmentSearchBinding? = null @@ -128,10 +127,7 @@ class SearchFragment : Fragment() { R.id.chip_homebrew -> baseList.filter { it.isHomebrew } - R.id.chip_retail -> baseList.filter { - FileUtil.hasExtension(it.path, "xci") || - FileUtil.hasExtension(it.path, "nsp") - } + R.id.chip_retail -> baseList.filter { !it.isHomebrew } else -> baseList } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt index 6a048e39f..6527c64ab 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt @@ -43,7 +43,7 @@ class Game( companion object { val extensions: Set<String> = HashSet( - listOf(".xci", ".nsp", ".nca", ".nro") + listOf("xci", "nsp", "nca", "nro") ) } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 3086cfad3..f7d7aed1e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -296,7 +296,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { return@registerForActivityResult } - if (!FileUtil.hasExtension(result, "keys")) { + if (FileUtil.getExtension(result) != "keys") { MessageDialogFragment.newInstance( R.string.reading_keys_failure, R.string.install_prod_keys_failure_extension_description @@ -393,7 +393,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { return@registerForActivityResult } - if (!FileUtil.hasExtension(result, "bin")) { + if (FileUtil.getExtension(result) != "bin") { MessageDialogFragment.newInstance( R.string.reading_keys_failure, R.string.install_amiibo_keys_failure_extension_description diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt index 9f3bbe56f..142af5f26 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt @@ -7,7 +7,6 @@ import android.content.Context import android.database.Cursor import android.net.Uri import android.provider.DocumentsContract -import android.provider.OpenableColumns import androidx.documentfile.provider.DocumentFile import java.io.BufferedInputStream import java.io.File @@ -185,19 +184,18 @@ object FileUtil { /** * Get file display name from given path - * @param path content uri path + * @param uri content uri * @return String display name */ - fun getFilename(context: Context, path: String): String { - val resolver = context.contentResolver + fun getFilename(uri: Uri): String { + val resolver = YuzuApplication.appContext.contentResolver val columns = arrayOf( DocumentsContract.Document.COLUMN_DISPLAY_NAME ) var filename = "" var c: Cursor? = null try { - val mUri = Uri.parse(path) - c = resolver.query(mUri, columns, null, null, null) + c = resolver.query(uri, columns, null, null, null) c!!.moveToNext() filename = c.getString(0) } catch (e: Exception) { @@ -326,25 +324,9 @@ object FileUtil { } } - fun hasExtension(path: String, extension: String): Boolean = - path.substring(path.lastIndexOf(".") + 1).contains(extension) - - fun hasExtension(uri: Uri, extension: String): Boolean { - val fileName: String? - val cursor = YuzuApplication.appContext.contentResolver.query(uri, null, null, null, null) - val nameIndex = cursor?.getColumnIndex(OpenableColumns.DISPLAY_NAME) - cursor?.moveToFirst() - - if (nameIndex == null) { - return false - } - - fileName = cursor.getString(nameIndex) - cursor.close() - - if (fileName == null) { - return false - } - return fileName.substring(fileName.lastIndexOf(".") + 1).contains(extension) + fun getExtension(uri: Uri): String { + val fileName = getFilename(uri) + return fileName.substring(fileName.lastIndexOf(".") + 1) + .lowercase() } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt index ee9f3e570..f8e7eeca7 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt @@ -6,7 +6,6 @@ package org.yuzu.yuzu_emu.utils import android.content.SharedPreferences import android.net.Uri import androidx.preference.PreferenceManager -import java.util.* import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.yuzu.yuzu_emu.NativeLibrary @@ -33,15 +32,9 @@ object GameHelper { val children = FileUtil.listFiles(context, gamesUri) for (file in children) { if (!file.isDirectory) { - val filename = file.uri.toString() - val extensionStart = filename.lastIndexOf('.') - if (extensionStart > 0) { - val fileExtension = filename.substring(extensionStart) - - // Check that the file has an extension we care about before trying to read out of it. - if (Game.extensions.contains(fileExtension.lowercase(Locale.getDefault()))) { - games.add(getGame(filename)) - } + // Check that the file has an extension we care about before trying to read out of it. + if (Game.extensions.contains(FileUtil.getExtension(file.uri))) { + games.add(getGame(file.uri)) } } } @@ -59,21 +52,19 @@ object GameHelper { return games.toList() } - private fun getGame(filePath: String): Game { + private fun getGame(uri: Uri): Game { + val filePath = uri.toString() var name = NativeLibrary.getTitle(filePath) // If the game's title field is empty, use the filename. if (name.isEmpty()) { - name = filePath.substring(filePath.lastIndexOf("/") + 1) + name = FileUtil.getFilename(uri) } var gameId = NativeLibrary.getGameId(filePath) // If the game's ID field is empty, use the filename without extension. if (gameId.isEmpty()) { - gameId = filePath.substring( - filePath.lastIndexOf("/") + 1, - filePath.lastIndexOf(".") - ) + gameId = name.substring(0, name.lastIndexOf(".")) } val newGame = Game( diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index d576aac50..8bc6a4a04 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -60,6 +60,9 @@ #include "video_core/rasterizer_interface.h" #include "video_core/renderer_base.h" +#define jconst [[maybe_unused]] const auto +#define jauto [[maybe_unused]] auto + namespace { class EmulationSession final { @@ -99,8 +102,8 @@ public: } int InstallFileToNand(std::string filename) { - const auto copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest, - std::size_t block_size) { + jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest, + std::size_t block_size) { if (src == nullptr || dest == nullptr) { return false; } @@ -109,10 +112,10 @@ public: } using namespace Common::Literals; - std::vector<u8> buffer(1_MiB); + [[maybe_unused]] std::vector<u8> buffer(1_MiB); for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) { - const auto read = src->Read(buffer.data(), buffer.size(), i); + jconst read = src->Read(buffer.data(), buffer.size(), i); dest->Write(buffer.data(), read, i); } return true; @@ -129,14 +132,14 @@ public: m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>()); m_system.GetFileSystemController().CreateFactories(*m_vfs); - std::shared_ptr<FileSys::NSP> nsp; + [[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp; if (filename.ends_with("nsp")) { nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read)); if (nsp->IsExtractedType()) { return InstallError; } } else if (filename.ends_with("xci")) { - const auto xci = + jconst xci = std::make_shared<FileSys::XCI>(m_vfs->OpenFile(filename, FileSys::Mode::Read)); nsp = xci->GetSecurePartitionNSP(); } else { @@ -151,7 +154,7 @@ public: return InstallError; } - const auto res = m_system.GetFileSystemController().GetUserNANDContents()->InstallEntry( + jconst res = m_system.GetFileSystemController().GetUserNANDContents()->InstallEntry( *nsp, true, copy_func); switch (res) { @@ -234,7 +237,7 @@ public: m_system.SetFilesystem(m_vfs); // Initialize system. - auto android_keyboard = std::make_unique<SoftwareKeyboard::AndroidKeyboard>(); + jauto android_keyboard = std::make_unique<SoftwareKeyboard::AndroidKeyboard>(); m_software_keyboard = android_keyboard.get(); m_system.SetShuttingDown(false); m_system.ApplySettings(); @@ -332,7 +335,7 @@ public: while (true) { { - std::unique_lock lock(m_mutex); + [[maybe_unused]] std::unique_lock lock(m_mutex); if (m_cv.wait_for(lock, std::chrono::milliseconds(800), [&]() { return !m_is_running; })) { // Emulation halted. @@ -364,7 +367,7 @@ public: } bool IsHandheldOnly() { - const auto npad_style_set = m_system.HIDCore().GetSupportedStyleTag(); + jconst npad_style_set = m_system.HIDCore().GetSupportedStyleTag(); if (npad_style_set.fullkey == 1) { return false; @@ -377,17 +380,17 @@ public: return !Settings::values.use_docked_mode.GetValue(); } - void SetDeviceType(int index, int type) { - auto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); + void SetDeviceType([[maybe_unused]] int index, int type) { + jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type)); } - void OnGamepadConnectEvent(int index) { - auto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); + void OnGamepadConnectEvent([[maybe_unused]] int index) { + jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); // Ensure that player1 is configured correctly and handheld disconnected if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) { - auto handheld = + jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld); if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) { @@ -399,7 +402,8 @@ public: // Ensure that handheld is configured correctly and player 1 disconnected if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) { - auto player1 = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); + jauto player1 = + m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) { player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld); @@ -413,8 +417,8 @@ public: } } - void OnGamepadDisconnectEvent(int index) { - auto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); + void OnGamepadDisconnectEvent([[maybe_unused]] int index) { + jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); controller->Disconnect(); } @@ -430,7 +434,7 @@ private: }; RomMetadata GetRomMetadata(const std::string& path) { - if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) { + if (jauto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) { return search->second; } @@ -438,14 +442,14 @@ private: } RomMetadata CacheRomMetadata(const std::string& path) { - const auto file = Core::GetGameFileFromPath(m_vfs, path); - auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0); + jconst file = Core::GetGameFileFromPath(m_vfs, path); + jauto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0); RomMetadata entry; loader->ReadTitle(entry.title); loader->ReadIcon(entry.icon); if (loader->GetFileType() == Loader::FileType::NRO) { - auto loader_nro = dynamic_cast<Loader::AppLoader_NRO*>(loader.get()); + jauto loader_nro = dynamic_cast<Loader::AppLoader_NRO*>(loader.get()); entry.isHomebrew = loader_nro->IsHomebrew(); } else { entry.isHomebrew = false; @@ -516,7 +520,7 @@ static Core::SystemResultStatus RunEmulation(const std::string& filepath) { SCOPE_EXIT({ EmulationSession::GetInstance().ShutdownEmulation(); }); - const auto result = EmulationSession::GetInstance().InitializeEmulation(filepath); + jconst result = EmulationSession::GetInstance().InitializeEmulation(filepath); if (result != Core::SystemResultStatus::Success) { return result; } @@ -528,24 +532,25 @@ static Core::SystemResultStatus RunEmulation(const std::string& filepath) { extern "C" { -void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceChanged(JNIEnv* env, jclass clazz, jobject surf) { +void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceChanged(JNIEnv* env, jobject instance, + [[maybe_unused]] jobject surf) { EmulationSession::GetInstance().SetNativeWindow(ANativeWindow_fromSurface(env, surf)); EmulationSession::GetInstance().SurfaceChanged(); } -void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceDestroyed(JNIEnv* env, jclass clazz) { +void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceDestroyed(JNIEnv* env, jobject instance) { ANativeWindow_release(EmulationSession::GetInstance().NativeWindow()); EmulationSession::GetInstance().SetNativeWindow(nullptr); EmulationSession::GetInstance().SurfaceChanged(); } -void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jclass clazz, - jstring j_directory) { +void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject instance, + [[maybe_unused]] jstring j_directory) { Common::FS::SetAppDirectory(GetJString(env, j_directory)); } -int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jclass clazz, - jstring j_file) { +int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance, + [[maybe_unused]] jstring j_file) { return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file)); } @@ -570,7 +575,7 @@ void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* e } jboolean JNICALL Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_supportsCustomDriverLoading( - JNIEnv* env, [[maybe_unused]] jobject instance) { + JNIEnv* env, jobject instance) { #ifdef ARCHITECTURE_arm64 // If the KGSL device exists custom drivers can be loaded using adrenotools return SupportsCustomDriver(); @@ -648,8 +653,8 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadDisconnectEvent(JNIEnv* return static_cast<jboolean>(true); } jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadButtonEvent(JNIEnv* env, jclass clazz, - [[maybe_unused]] jint j_device, - jint j_button, jint action) { + jint j_device, jint j_button, + jint action) { if (EmulationSession::GetInstance().IsRunning()) { // Ensure gamepad is connected EmulationSession::GetInstance().OnGamepadConnectEvent(j_device); @@ -718,8 +723,8 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased(JNIEnv* env, jclass c } jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getIcon(JNIEnv* env, jclass clazz, - [[maybe_unused]] jstring j_filename) { - auto icon_data = EmulationSession::GetInstance().GetRomIcon(GetJString(env, j_filename)); + jstring j_filename) { + jauto icon_data = EmulationSession::GetInstance().GetRomIcon(GetJString(env, j_filename)); jbyteArray icon = env->NewByteArray(static_cast<jsize>(icon_data.size())); env->SetByteArrayRegion(icon, 0, env->GetArrayLength(icon), reinterpret_cast<jbyte*>(icon_data.data())); @@ -727,8 +732,8 @@ jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getIcon(JNIEnv* env, jclass cla } jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getTitle(JNIEnv* env, jclass clazz, - [[maybe_unused]] jstring j_filename) { - auto title = EmulationSession::GetInstance().GetRomTitle(GetJString(env, j_filename)); + jstring j_filename) { + jauto title = EmulationSession::GetInstance().GetRomTitle(GetJString(env, j_filename)); return env->NewStringUTF(title.c_str()); } @@ -743,22 +748,21 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGameId(JNIEnv* env, jclass claz } jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getRegions(JNIEnv* env, jclass clazz, - [[maybe_unused]] jstring j_filename) { + jstring j_filename) { return env->NewStringUTF(""); } jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCompany(JNIEnv* env, jclass clazz, - [[maybe_unused]] jstring j_filename) { + jstring j_filename) { return env->NewStringUTF(""); } jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHomebrew(JNIEnv* env, jclass clazz, - [[maybe_unused]] jstring j_filename) { + jstring j_filename) { return EmulationSession::GetInstance().GetIsHomebrew(GetJString(env, j_filename)); } -void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmulation - [[maybe_unused]] (JNIEnv* env, jclass clazz) { +void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmulation(JNIEnv* env, jclass clazz) { // Create the default config.ini. Config{}; // Initialize the emulated system. @@ -770,8 +774,7 @@ jint Java_org_yuzu_yuzu_1emu_NativeLibrary_defaultCPUCore(JNIEnv* env, jclass cl } void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2Ljava_lang_String_2Z( - JNIEnv* env, jclass clazz, [[maybe_unused]] jstring j_file, - [[maybe_unused]] jstring j_savestate, [[maybe_unused]] jboolean j_delete_savestate) {} + JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate) {} void Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadSettings(JNIEnv* env, jclass clazz) { Config{}; @@ -816,7 +819,7 @@ jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jcl jdoubleArray j_stats = env->NewDoubleArray(4); if (EmulationSession::GetInstance().IsRunning()) { - const auto results = EmulationSession::GetInstance().PerfStats(); + jconst results = EmulationSession::GetInstance().PerfStats(); // Converting the structure into an array makes it easier to pass it to the frontend double stats[4] = {results.system_fps, results.average_game_fps, results.frametime, diff --git a/src/common/fs/fs.cpp b/src/common/fs/fs.cpp index 1baf6d746..36e67c145 100644 --- a/src/common/fs/fs.cpp +++ b/src/common/fs/fs.cpp @@ -605,6 +605,12 @@ fs::file_type GetEntryType(const fs::path& path) { } u64 GetSize(const fs::path& path) { +#ifdef ANDROID + if (Android::IsContentUri(path)) { + return Android::GetSize(path); + } +#endif + std::error_code ec; const auto file_size = fs::file_size(path, ec); diff --git a/src/core/hid/emulated_controller.cpp b/src/core/hid/emulated_controller.cpp index c937495f9..1ebc32c1e 100644 --- a/src/core/hid/emulated_controller.cpp +++ b/src/core/hid/emulated_controller.cpp @@ -1250,6 +1250,11 @@ Common::Input::DriverResult EmulatedController::SetPollingMode( const auto virtual_nfc_result = nfc_output_device->SetPollingMode(polling_mode); const auto mapped_nfc_result = right_output_device->SetPollingMode(polling_mode); + // Restore previous state + if (mapped_nfc_result != Common::Input::DriverResult::Success) { + right_output_device->SetPollingMode(Common::Input::PollingMode::Active); + } + if (virtual_nfc_result == Common::Input::DriverResult::Success) { return virtual_nfc_result; } @@ -1329,16 +1334,22 @@ bool EmulatedController::StartNfcPolling() { auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)]; auto& nfc_virtual_output_device = output_devices[3]; - return nfc_output_device->StartNfcPolling() == Common::Input::NfcState::Success || - nfc_virtual_output_device->StartNfcPolling() == Common::Input::NfcState::Success; + const auto device_result = nfc_output_device->StartNfcPolling(); + const auto virtual_device_result = nfc_virtual_output_device->StartNfcPolling(); + + return device_result == Common::Input::NfcState::Success || + virtual_device_result == Common::Input::NfcState::Success; } bool EmulatedController::StopNfcPolling() { auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)]; auto& nfc_virtual_output_device = output_devices[3]; - return nfc_output_device->StopNfcPolling() == Common::Input::NfcState::Success || - nfc_virtual_output_device->StopNfcPolling() == Common::Input::NfcState::Success; + const auto device_result = nfc_output_device->StopNfcPolling(); + const auto virtual_device_result = nfc_virtual_output_device->StopNfcPolling(); + + return device_result == Common::Input::NfcState::Success || + virtual_device_result == Common::Input::NfcState::Success; } bool EmulatedController::ReadAmiiboData(std::vector<u8>& data) { diff --git a/src/input_common/drivers/sdl_driver.cpp b/src/input_common/drivers/sdl_driver.cpp index ab64a64a2..9f26392b1 100644 --- a/src/input_common/drivers/sdl_driver.cpp +++ b/src/input_common/drivers/sdl_driver.cpp @@ -150,6 +150,8 @@ public: if (sdl_controller) { const auto type = SDL_GameControllerGetType(sdl_controller.get()); return (type == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO) || + (type == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT) || + (type == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT) || (type == SDL_CONTROLLER_TYPE_PS5); } return false; @@ -228,9 +230,8 @@ public: return false; } - Common::Input::BatteryLevel GetBatteryLevel() { - const auto level = SDL_JoystickCurrentPowerLevel(sdl_joystick.get()); - switch (level) { + Common::Input::BatteryLevel GetBatteryLevel(SDL_JoystickPowerLevel battery_level) { + switch (battery_level) { case SDL_JOYSTICK_POWER_EMPTY: return Common::Input::BatteryLevel::Empty; case SDL_JOYSTICK_POWER_LOW: @@ -378,7 +379,6 @@ void SDLDriver::InitJoystick(int joystick_index) { if (joystick_map.find(guid) == joystick_map.end()) { auto joystick = std::make_shared<SDLJoystick>(guid, 0, sdl_joystick, sdl_gamecontroller); PreSetController(joystick->GetPadIdentifier()); - SetBattery(joystick->GetPadIdentifier(), joystick->GetBatteryLevel()); joystick->EnableMotion(); joystick_map[guid].emplace_back(std::move(joystick)); return; @@ -398,7 +398,6 @@ void SDLDriver::InitJoystick(int joystick_index) { const int port = static_cast<int>(joystick_guid_list.size()); auto joystick = std::make_shared<SDLJoystick>(guid, port, sdl_joystick, sdl_gamecontroller); PreSetController(joystick->GetPadIdentifier()); - SetBattery(joystick->GetPadIdentifier(), joystick->GetBatteryLevel()); joystick->EnableMotion(); joystick_guid_list.emplace_back(std::move(joystick)); } @@ -438,8 +437,6 @@ void SDLDriver::HandleGameControllerEvent(const SDL_Event& event) { if (const auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) { const PadIdentifier identifier = joystick->GetPadIdentifier(); SetButton(identifier, event.jbutton.button, true); - // Battery doesn't trigger an event so just update every button press - SetBattery(identifier, joystick->GetBatteryLevel()); } break; } @@ -466,6 +463,13 @@ void SDLDriver::HandleGameControllerEvent(const SDL_Event& event) { } break; } + case SDL_JOYBATTERYUPDATED: { + if (auto joystick = GetSDLJoystickBySDLID(event.jbattery.which)) { + const PadIdentifier identifier = joystick->GetPadIdentifier(); + SetBattery(identifier, joystick->GetBatteryLevel(event.jbattery.level)); + } + break; + } case SDL_JOYDEVICEREMOVED: LOG_DEBUG(Input, "Controller removed with Instance_ID {}", event.jdevice.which); CloseJoystick(SDL_JoystickFromInstanceID(event.jdevice.which)); @@ -505,6 +509,9 @@ SDLDriver::SDLDriver(std::string input_engine_) : InputEngine(std::move(input_en SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "0"); } else { SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOYCON_HOME_LED, "0"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS, "0"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS, "1"); } // Disable hidapi drivers for pro controllers when the custom joycon driver is enabled @@ -512,8 +519,11 @@ SDLDriver::SDLDriver(std::string input_engine_) : InputEngine(std::move(input_en SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "0"); } else { SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH_HOME_LED, "0"); } + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH_PLAYER_LED, "1"); + // Disable hidapi driver for xbox. Already default on Windows, this causes conflict with native // driver on Linux. SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_XBOX, "0"); @@ -793,7 +803,9 @@ ButtonMapping SDLDriver::GetButtonMappingForDevice(const Common::ParamPackage& p // This list also excludes Screenshot since there's not really a mapping for that ButtonBindings switch_to_sdl_button; - if (SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO) { + if (SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO || + SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT || + SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT) { switch_to_sdl_button = GetNintendoButtonBinding(joystick); } else { switch_to_sdl_button = GetDefaultButtonBinding(); diff --git a/src/input_common/helpers/joycon_driver.cpp b/src/input_common/helpers/joycon_driver.cpp index 2c8c66951..ec984a647 100644 --- a/src/input_common/helpers/joycon_driver.cpp +++ b/src/input_common/helpers/joycon_driver.cpp @@ -72,6 +72,7 @@ DriverResult JoyconDriver::InitializeDevice() { nfc_enabled = false; passive_enabled = false; irs_enabled = false; + input_only_device = false; gyro_sensitivity = Joycon::GyroSensitivity::DPS2000; gyro_performance = Joycon::GyroPerformance::HZ833; accelerometer_sensitivity = Joycon::AccelerometerSensitivity::G8; @@ -86,16 +87,23 @@ DriverResult JoyconDriver::InitializeDevice() { rumble_protocol = std::make_unique<RumbleProtocol>(hidapi_handle); // Get fixed joycon info - generic_protocol->GetVersionNumber(version); - generic_protocol->SetLowPowerMode(false); - generic_protocol->GetColor(color); - if (handle_device_type == ControllerType::Pro) { - // Some 3rd party controllers aren't pro controllers - generic_protocol->GetControllerType(device_type); - } else { - device_type = handle_device_type; + if (generic_protocol->GetVersionNumber(version) != DriverResult::Success) { + // If this command fails the device doesn't accept configuration commands + input_only_device = true; } - generic_protocol->GetSerialNumber(serial_number); + + if (!input_only_device) { + generic_protocol->SetLowPowerMode(false); + generic_protocol->GetColor(color); + if (handle_device_type == ControllerType::Pro) { + // Some 3rd party controllers aren't pro controllers + generic_protocol->GetControllerType(device_type); + } else { + device_type = handle_device_type; + } + generic_protocol->GetSerialNumber(serial_number); + } + supported_features = GetSupportedFeatures(); // Get Calibration data @@ -261,6 +269,10 @@ DriverResult JoyconDriver::SetPollingMode() { generic_protocol->EnableImu(false); } + if (input_only_device) { + return DriverResult::NotSupported; + } + if (irs_protocol->IsEnabled()) { irs_protocol->DisableIrs(); } @@ -282,6 +294,7 @@ DriverResult JoyconDriver::SetPollingMode() { } irs_protocol->DisableIrs(); LOG_ERROR(Input, "Error enabling IRS"); + return result; } if (nfc_enabled && supported_features.nfc) { @@ -291,6 +304,7 @@ DriverResult JoyconDriver::SetPollingMode() { } nfc_protocol->DisableNfc(); LOG_ERROR(Input, "Error enabling NFC"); + return result; } if (hidbus_enabled && supported_features.hidbus) { @@ -305,6 +319,7 @@ DriverResult JoyconDriver::SetPollingMode() { ring_connected = false; ring_protocol->DisableRingCon(); LOG_ERROR(Input, "Error enabling Ringcon"); + return result; } if (passive_enabled && supported_features.passive) { @@ -333,6 +348,10 @@ JoyconDriver::SupportedFeatures JoyconDriver::GetSupportedFeatures() { .vibration = true, }; + if (input_only_device) { + return features; + } + if (device_type == ControllerType::Right) { features.nfc = true; features.irs = true; @@ -517,6 +536,11 @@ DriverResult JoyconDriver::StopNfcPolling() { const auto result = nfc_protocol->StopNFCPollingMode(); disable_input_thread = false; + if (amiibo_detected) { + amiibo_detected = false; + joycon_poller->UpdateAmiibo({}); + } + return result; } diff --git a/src/input_common/helpers/joycon_driver.h b/src/input_common/helpers/joycon_driver.h index bc7025a21..45b32d2f8 100644 --- a/src/input_common/helpers/joycon_driver.h +++ b/src/input_common/helpers/joycon_driver.h @@ -120,6 +120,7 @@ private: // Hardware configuration u8 leds{}; ReportMode mode{}; + bool input_only_device{}; bool passive_enabled{}; // Low power mode, Ideal for multiple controllers at the same time bool hidbus_enabled{}; // External device support bool irs_enabled{}; // Infrared camera input diff --git a/src/input_common/helpers/joycon_protocol/common_protocol.cpp b/src/input_common/helpers/joycon_protocol/common_protocol.cpp index 51669261a..88f4cec1c 100644 --- a/src/input_common/helpers/joycon_protocol/common_protocol.cpp +++ b/src/input_common/helpers/joycon_protocol/common_protocol.cpp @@ -73,7 +73,7 @@ DriverResult JoyconCommonProtocol::SendRawData(std::span<const u8> buffer) { DriverResult JoyconCommonProtocol::GetSubCommandResponse(SubCommand sc, SubCommandResponse& output) { constexpr int timeout_mili = 66; - constexpr int MaxTries = 15; + constexpr int MaxTries = 3; int tries = 0; do { @@ -113,9 +113,7 @@ DriverResult JoyconCommonProtocol::SendSubCommand(SubCommand sc, std::span<const return result; } - result = GetSubCommandResponse(sc, output); - - return DriverResult::Success; + return GetSubCommandResponse(sc, output); } DriverResult JoyconCommonProtocol::SendSubCommand(SubCommand sc, std::span<const u8> buffer) { @@ -158,7 +156,7 @@ DriverResult JoyconCommonProtocol::SendVibrationReport(std::span<const u8> buffe DriverResult JoyconCommonProtocol::ReadRawSPI(SpiAddress addr, std::span<u8> output) { constexpr std::size_t HeaderSize = 5; - constexpr std::size_t MaxTries = 10; + constexpr std::size_t MaxTries = 5; std::size_t tries = 0; SubCommandResponse response{}; std::array<u8, sizeof(ReadSpiPacket)> buffer{}; diff --git a/src/shader_recompiler/backend/glasm/emit_glasm.cpp b/src/shader_recompiler/backend/glasm/emit_glasm.cpp index fd4a61a4d..b795c0179 100644 --- a/src/shader_recompiler/backend/glasm/emit_glasm.cpp +++ b/src/shader_recompiler/backend/glasm/emit_glasm.cpp @@ -461,7 +461,7 @@ std::string EmitGLASM(const Profile& profile, const RuntimeInfo& runtime_info, I header += fmt::format("R{},", index); } if (program.local_memory_size > 0) { - header += fmt::format("lmem[{}],", program.local_memory_size); + header += fmt::format("lmem[{}],", Common::DivCeil(program.local_memory_size, 4U)); } if (program.info.uses_fswzadd) { header += "FSWZA[4],FSWZB[4],"; diff --git a/src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp b/src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp index 5a4195217..70292686f 100644 --- a/src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp +++ b/src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp @@ -424,6 +424,10 @@ void VisitUsages(Info& info, IR::Inst& inst) { info.used_constant_buffer_types |= IR::Type::U32 | IR::Type::U32x2; info.used_storage_buffer_types |= IR::Type::U32 | IR::Type::U32x2 | IR::Type::U32x4; break; + case IR::Opcode::LoadLocal: + case IR::Opcode::WriteLocal: + info.uses_local_memory = true; + break; default: break; } diff --git a/src/shader_recompiler/shader_info.h b/src/shader_recompiler/shader_info.h index d308db942..b4b4afd37 100644 --- a/src/shader_recompiler/shader_info.h +++ b/src/shader_recompiler/shader_info.h @@ -172,6 +172,7 @@ struct Info { bool stores_indexed_attributes{}; bool stores_global_memory{}; + bool uses_local_memory{}; bool uses_fp16{}; bool uses_fp64{}; diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 2442c3c29..e61d9af80 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -33,6 +33,7 @@ set(SHADER_FILES opengl_fidelityfx_fsr.frag opengl_fidelityfx_fsr_easu.frag opengl_fidelityfx_fsr_rcas.frag + opengl_lmem_warmup.comp opengl_present.frag opengl_present.vert opengl_present_scaleforce.frag diff --git a/src/video_core/host_shaders/opengl_lmem_warmup.comp b/src/video_core/host_shaders/opengl_lmem_warmup.comp new file mode 100644 index 000000000..518268477 --- /dev/null +++ b/src/video_core/host_shaders/opengl_lmem_warmup.comp @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// This shader is a workaround for a quirk in NVIDIA OpenGL drivers +// Shaders using local memory see a great performance benefit if a shader that was dispatched +// before it had more local memory allocated. +// This shader allocates the maximum local memory allowed on NVIDIA drivers to ensure that +// subsequent shaders see the performance boost. + +// NOTE: This shader does no actual meaningful work and returns immediately, +// it is simply a means to have the driver expect a shader using lots of local memory. + +#version 450 + +layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + +layout(location = 0) uniform uint uniform_data; + +layout(binding = 0, rgba8) uniform writeonly restrict image2DArray dest_image; + +#define MAX_LMEM_SIZE 4080 // Size chosen to avoid errors in Nvidia's GLSL compiler +#define NUM_LMEM_CONSTANTS 1 +#define ARRAY_SIZE MAX_LMEM_SIZE - NUM_LMEM_CONSTANTS + +uint lmem_0[ARRAY_SIZE]; +const uvec4 constant_values[NUM_LMEM_CONSTANTS] = uvec4[](uvec4(0)); + +void main() { + const uint global_id = gl_GlobalInvocationID.x; + if (global_id <= 128) { + // Since the shader is called with a dispatch of 1x1x1 + // This should always be the case, and this shader will not actually execute + return; + } + for (uint t = 0; t < uniform_data; t++) { + const uint offset = (t * uniform_data); + lmem_0[offset] = t; + } + const uint offset = (gl_GlobalInvocationID.y * uniform_data + gl_GlobalInvocationID.x); + const uint value = lmem_0[offset]; + const uint const_value = constant_values[offset / 4][offset % 4]; + const uvec4 color = uvec4(value + const_value); + + // A "side-effect" is needed so the variables don't get optimized out, + // but this should never execute so there should be no clobbering of previously bound state. + imageStore(dest_image, ivec3(gl_GlobalInvocationID), color); +} diff --git a/src/video_core/renderer_opengl/gl_compute_pipeline.cpp b/src/video_core/renderer_opengl/gl_compute_pipeline.cpp index 3151c0db8..f9ca55c36 100644 --- a/src/video_core/renderer_opengl/gl_compute_pipeline.cpp +++ b/src/video_core/renderer_opengl/gl_compute_pipeline.cpp @@ -63,6 +63,7 @@ ComputePipeline::ComputePipeline(const Device& device, TextureCache& texture_cac writes_global_memory = !use_storage_buffers && std::ranges::any_of(info.storage_buffers_descriptors, [](const auto& desc) { return desc.is_written; }); + uses_local_memory = info.uses_local_memory; if (force_context_flush) { std::scoped_lock lock{built_mutex}; built_fence.Create(); diff --git a/src/video_core/renderer_opengl/gl_compute_pipeline.h b/src/video_core/renderer_opengl/gl_compute_pipeline.h index 9bcc72b59..c26b4fa5e 100644 --- a/src/video_core/renderer_opengl/gl_compute_pipeline.h +++ b/src/video_core/renderer_opengl/gl_compute_pipeline.h @@ -59,6 +59,10 @@ public: return writes_global_memory; } + [[nodiscard]] bool UsesLocalMemory() const noexcept { + return uses_local_memory; + } + void SetEngine(Tegra::Engines::KeplerCompute* kepler_compute_, Tegra::MemoryManager* gpu_memory_) { kepler_compute = kepler_compute_; @@ -84,6 +88,7 @@ private: bool use_storage_buffers{}; bool writes_global_memory{}; + bool uses_local_memory{}; std::mutex built_mutex; std::condition_variable built_condvar; diff --git a/src/video_core/renderer_opengl/gl_device.cpp b/src/video_core/renderer_opengl/gl_device.cpp index 03d234f2f..33e63c17d 100644 --- a/src/video_core/renderer_opengl/gl_device.cpp +++ b/src/video_core/renderer_opengl/gl_device.cpp @@ -194,6 +194,7 @@ Device::Device(Core::Frontend::EmuWindow& emu_window) { has_bool_ref_bug = true; } } + has_lmem_perf_bug = is_nvidia; strict_context_required = emu_window.StrictContextRequired(); // Blocks AMD and Intel OpenGL drivers on Windows from using asynchronous shader compilation. diff --git a/src/video_core/renderer_opengl/gl_device.h b/src/video_core/renderer_opengl/gl_device.h index ad27264e5..a5a6bbbba 100644 --- a/src/video_core/renderer_opengl/gl_device.h +++ b/src/video_core/renderer_opengl/gl_device.h @@ -192,6 +192,10 @@ public: return supports_conditional_barriers; } + bool HasLmemPerfBug() const { + return has_lmem_perf_bug; + } + private: static bool TestVariableAoffi(); static bool TestPreciseBug(); @@ -238,6 +242,7 @@ private: bool can_report_memory{}; bool strict_context_required{}; bool supports_conditional_barriers{}; + bool has_lmem_perf_bug{}; std::string vendor_name; }; diff --git a/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp b/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp index c58f760b8..23a48c6fe 100644 --- a/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp +++ b/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp @@ -215,6 +215,7 @@ GraphicsPipeline::GraphicsPipeline(const Device& device, TextureCache& texture_c writes_global_memory |= std::ranges::any_of( info.storage_buffers_descriptors, [](const auto& desc) { return desc.is_written; }); + uses_local_memory |= info.uses_local_memory; } ASSERT(num_textures <= MAX_TEXTURES); ASSERT(num_images <= MAX_IMAGES); diff --git a/src/video_core/renderer_opengl/gl_graphics_pipeline.h b/src/video_core/renderer_opengl/gl_graphics_pipeline.h index 7bab3be0a..7b3d7eae8 100644 --- a/src/video_core/renderer_opengl/gl_graphics_pipeline.h +++ b/src/video_core/renderer_opengl/gl_graphics_pipeline.h @@ -98,6 +98,10 @@ public: return writes_global_memory; } + [[nodiscard]] bool UsesLocalMemory() const noexcept { + return uses_local_memory; + } + [[nodiscard]] bool IsBuilt() noexcept; template <typename Spec> @@ -146,6 +150,7 @@ private: bool use_storage_buffers{}; bool writes_global_memory{}; + bool uses_local_memory{}; static constexpr std::size_t XFB_ENTRY_STRIDE = 3; GLsizei num_xfb_attribs{}; diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index fc711c44a..edf527f2d 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -222,6 +222,9 @@ void RasterizerOpenGL::PrepareDraw(bool is_indexed, Func&& draw_func) { gpu.TickWork(); std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex}; + if (pipeline->UsesLocalMemory()) { + program_manager.LocalMemoryWarmup(); + } pipeline->SetEngine(maxwell3d, gpu_memory); pipeline->Configure(is_indexed); @@ -371,6 +374,9 @@ void RasterizerOpenGL::DispatchCompute() { if (!pipeline) { return; } + if (pipeline->UsesLocalMemory()) { + program_manager.LocalMemoryWarmup(); + } pipeline->SetEngine(kepler_compute, gpu_memory); pipeline->Configure(); const auto& qmd{kepler_compute->launch_description}; diff --git a/src/video_core/renderer_opengl/gl_shader_manager.cpp b/src/video_core/renderer_opengl/gl_shader_manager.cpp index 98841ae65..03d4b9d06 100644 --- a/src/video_core/renderer_opengl/gl_shader_manager.cpp +++ b/src/video_core/renderer_opengl/gl_shader_manager.cpp @@ -3,7 +3,9 @@ #include <glad/glad.h> +#include "video_core/host_shaders/opengl_lmem_warmup_comp.h" #include "video_core/renderer_opengl/gl_shader_manager.h" +#include "video_core/renderer_opengl/gl_shader_util.h" namespace OpenGL { @@ -17,6 +19,10 @@ ProgramManager::ProgramManager(const Device& device) { if (device.UseAssemblyShaders()) { glEnable(GL_COMPUTE_PROGRAM_NV); } + if (device.HasLmemPerfBug()) { + lmem_warmup_program = + CreateProgram(HostShaders::OPENGL_LMEM_WARMUP_COMP, GL_COMPUTE_SHADER); + } } void ProgramManager::BindComputeProgram(GLuint program) { @@ -98,6 +104,13 @@ void ProgramManager::BindAssemblyPrograms(std::span<const OGLAssemblyProgram, NU void ProgramManager::RestoreGuestCompute() {} +void ProgramManager::LocalMemoryWarmup() { + if (lmem_warmup_program.handle != 0) { + BindComputeProgram(lmem_warmup_program.handle); + glDispatchCompute(1, 1, 1); + } +} + void ProgramManager::BindPipeline() { if (!is_pipeline_bound) { is_pipeline_bound = true; diff --git a/src/video_core/renderer_opengl/gl_shader_manager.h b/src/video_core/renderer_opengl/gl_shader_manager.h index 07ffab77f..852d8c88e 100644 --- a/src/video_core/renderer_opengl/gl_shader_manager.h +++ b/src/video_core/renderer_opengl/gl_shader_manager.h @@ -30,6 +30,8 @@ public: void RestoreGuestCompute(); + void LocalMemoryWarmup(); + private: void BindPipeline(); @@ -44,6 +46,7 @@ private: u32 current_stage_mask = 0; std::array<GLuint, NUM_STAGES> current_programs{}; GLuint current_assembly_compute_program = 0; + OGLProgram lmem_warmup_program; }; } // namespace OpenGL diff --git a/src/yuzu/configuration/configure_ringcon.ui b/src/yuzu/configuration/configure_ringcon.ui index 514dff372..38ecccc3d 100644 --- a/src/yuzu/configuration/configure_ringcon.ui +++ b/src/yuzu/configuration/configure_ringcon.ui @@ -23,7 +23,7 @@ </size> </property> <property name="text"> - <string>If you want to use this controller configure player 1 as right controller and player 2 as dual joycon before starting the game to allow this controller to be detected properly.</string> + <string>To use Ring-Con, configure player 1 as right Joy-Con (both physical and emulated), and player 2 as left Joy-Con (left physical and dual emulated) before starting the game.</string> </property> <property name="wordWrap"> <bool>true</bool> |