diff options
| -rw-r--r-- | Data/Sys/GameSettings/GNLE82.ini | 41 | ||||
| -rw-r--r-- | Data/Sys/GameSettings/GNLJ82.ini | 41 | ||||
| -rw-r--r-- | Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.java | 286 | ||||
| -rw-r--r-- | Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.kt | 251 | ||||
| -rw-r--r-- | Source/Core/Common/StringUtil.cpp | 252 | ||||
| -rw-r--r-- | Source/Core/Core/HotkeyManager.cpp | 2 | ||||
| -rw-r--r-- | Source/Core/Core/HotkeyManager.h | 4 | ||||
| -rw-r--r-- | Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.cpp | 12 | ||||
| -rw-r--r-- | Source/Core/DolphinQt/HotkeyScheduler.cpp | 12 | ||||
| -rw-r--r-- | Source/Core/InputCommon/GCAdapter.cpp | 27 | ||||
| -rw-r--r-- | Source/Core/VideoCommon/PostProcessing.cpp | 4 | ||||
| -rw-r--r-- | Source/Core/VideoCommon/Present.cpp | 12 | ||||
| -rw-r--r-- | Source/Core/VideoCommon/VideoConfig.h | 4 | ||||
| -rw-r--r-- | Source/UnitTests/Common/StringUtilTest.cpp | 46 |
14 files changed, 664 insertions, 330 deletions
diff --git a/Data/Sys/GameSettings/GNLE82.ini b/Data/Sys/GameSettings/GNLE82.ini new file mode 100644 index 0000000000..e54eb93435 --- /dev/null +++ b/Data/Sys/GameSettings/GNLE82.ini @@ -0,0 +1,41 @@ +# GNLE82 - Mario Kart Arcade GP 2 (NTSC-U) + +[ActionReplay] +$(2007/02/07 02:47:24) Skip Boot Check [mamefan2018] +0402E858 60000000 +$(2007/02/07 02:47:24) Skip Link Check [mamefan2018] +0402EB6C 60000000 +$(2007/02/07 02:47:24) Disable Countdown in Menus [cristian64] +041B7934 60000000 +04231CA0 60000000 +041C1074 60000000 +041C0174 60000000 +041C1A08 60000000 +041C3380 60000000 +041C7A7C 60000000 +041C9ED8 60000000 +0424D35C 60000000 +0415F2F4 60000000 +041D7594 60000000 +041FB9F4 60000000 +041BE248 60000000 +04232818 60000000 +04237B3C 60000000 +$(2007/02/07 02:47:24) All Races: 1 Lap [cristian64] +0409FCD0 38000001 +040A1E94 38000001 +040A3D74 38000001 + +[Gecko] +$(2007/02/07 02:47:24) Widescreen [mamefan2018] +C22C80D4 00000009 +C03F0034 9421FFE0 +93C10004 93E10008 +DBE1000C 7FC802A6 +48000009 3FAAAAAB +7FE802A6 C3FF0000 +EC3F0072 7FC803A6 +83C10004 83E10008 +CBE1000C 38210020 +60000000 00000000 +*Requires **Aspect Ratio** set to `Force 16:9` in Dolphin. diff --git a/Data/Sys/GameSettings/GNLJ82.ini b/Data/Sys/GameSettings/GNLJ82.ini new file mode 100644 index 0000000000..dae1839dc4 --- /dev/null +++ b/Data/Sys/GameSettings/GNLJ82.ini @@ -0,0 +1,41 @@ +# GNLJ82 - Mario Kart Arcade GP 2 (NTSC-J) + +[ActionReplay] +$(2007/02/06 20:29:25) Skip Boot Check [mamefan2018] +0402E8B8 60000000 +$(2007/02/06 20:29:25) Skip Link Check [mamefan2018] +0402EBCC 60000000 +$(2007/02/06 20:29:25) Disable Countdown in Menus [cristian64] +041B7F24 60000000 +042322A8 60000000 +041C1664 60000000 +041C0764 60000000 +041C1FF8 60000000 +041C3970 60000000 +041C806C 60000000 +041CA4C8 60000000 +0424D964 60000000 +0415F90C 60000000 +041D7B84 60000000 +041FBFFC 60000000 +041BE838 60000000 +04232E20 60000000 +04238144 60000000 +$(2007/02/06 20:29:25) All Races: 1 Lap [cristian64] +040A02E8 38000001 +040A24AC 38000001 +040A438C 38000001 + +[Gecko] +$(2007/02/06 20:29:25) Widescreen [mamefan2018] +C22C86EC 00000009 +C03F0034 9421FFE0 +93C10004 93E10008 +DBE1000C 7FC802A6 +48000009 3FAAAAAB +7FE802A6 C3FF0000 +EC3F0072 7FC803A6 +83C10004 83E10008 +CBE1000C 38210020 +60000000 00000000 +*Requires **Aspect Ratio** set to `Force 16:9` in Dolphin. diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.java deleted file mode 100644 index 50d75d8507..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.java +++ /dev/null @@ -1,286 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.services; - -import android.os.Handler; -import android.os.Looper; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; - -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; -import org.dolphinemu.dolphinemu.features.settings.model.ConfigChangedCallback; -import org.dolphinemu.dolphinemu.model.GameFile; -import org.dolphinemu.dolphinemu.model.GameFileCache; -import org.dolphinemu.dolphinemu.ui.platform.Platform; -import org.dolphinemu.dolphinemu.ui.platform.PlatformTab; -import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * Loads game list data on a separate thread. - */ -public final class GameFileCacheManager -{ - private static GameFileCache sGameFileCache = null; - private static final MutableLiveData<GameFile[]> sGameFiles = - new MutableLiveData<>(new GameFile[]{}); - private static boolean sFirstLoadDone = false; - private static boolean sRunRescanAfterLoad = false; - private static boolean sRecursiveScanEnabled; - - private static final ExecutorService sExecutor = Executors.newFixedThreadPool(1); - private static final MutableLiveData<Boolean> sLoadInProgress = new MutableLiveData<>(false); - private static final MutableLiveData<Boolean> sRescanInProgress = new MutableLiveData<>(false); - - private GameFileCacheManager() - { - } - - public static LiveData<GameFile[]> getGameFiles() - { - return sGameFiles; - } - - public static List<GameFile> getGameFilesForPlatformTab(PlatformTab platformTab) - { - GameFile[] allGames = sGameFiles.getValue(); - ArrayList<GameFile> platformTabGames = new ArrayList<>(); - for (GameFile game : allGames) - { - if (Platform.fromInt(game.getPlatform()).toPlatformTab() == platformTab) - { - platformTabGames.add(game); - } - } - return platformTabGames; - } - - public static GameFile getGameFileByGameId(String gameId) - { - GameFile[] allGames = sGameFiles.getValue(); - for (GameFile game : allGames) - { - if (game.getGameId().equals(gameId)) - { - return game; - } - } - return null; - } - - public static GameFile findSecondDisc(GameFile game) - { - GameFile matchWithoutRevision = null; - - GameFile[] allGames = sGameFiles.getValue(); - for (GameFile otherGame : allGames) - { - if (game.getGameId().equals(otherGame.getGameId()) && - game.getDiscNumber() != otherGame.getDiscNumber()) - { - if (game.getRevision() == otherGame.getRevision()) - return otherGame; - else - matchWithoutRevision = otherGame; - } - } - - return matchWithoutRevision; - } - - public static String[] findSecondDiscAndGetPaths(GameFile gameFile) - { - GameFile secondFile = findSecondDisc(gameFile); - if (secondFile == null) - return new String[]{gameFile.getPath()}; - else - return new String[]{gameFile.getPath(), secondFile.getPath()}; - } - - /** - * Returns true if in the process of loading the cache for the first time. - */ - public static LiveData<Boolean> isLoading() - { - return sLoadInProgress; - } - - /** - * Returns true if in the process of rescanning. - */ - public static LiveData<Boolean> isRescanning() - { - return sRescanInProgress; - } - - public static boolean isLoadingOrRescanning() - { - return sLoadInProgress.getValue() || sRescanInProgress.getValue(); - } - - /** - * Asynchronously loads the game file cache from disk, without checking - * if the games are still present in the user's configured folders. - * If this has already been called, calling it again has no effect. - */ - public static void startLoad() - { - createGameFileCacheIfNeeded(); - - if (!sLoadInProgress.getValue()) - { - sLoadInProgress.setValue(true); - new AfterDirectoryInitializationRunner().runWithoutLifecycle( - () -> sExecutor.execute(GameFileCacheManager::load)); - } - } - - /** - * Asynchronously scans for games in the user's configured folders, - * updating the game file cache with the results. - * If loading the game file cache hasn't started or hasn't finished, - * the execution of this will be postponed until it finishes. - */ - public static void startRescan() - { - createGameFileCacheIfNeeded(); - - if (!sRescanInProgress.getValue()) - { - sRescanInProgress.setValue(true); - new AfterDirectoryInitializationRunner().runWithoutLifecycle( - () -> sExecutor.execute(GameFileCacheManager::rescan)); - } - } - - public static GameFile addOrGet(String gamePath) - { - // Common case: The game is in the cache, so just grab it from there. (GameFileCache.addOrGet - // actually already checks for this case, but we want to avoid calling it if possible - // because the executor thread may hold a lock on sGameFileCache for extended periods of time.) - GameFile[] allGames = sGameFiles.getValue(); - for (GameFile game : allGames) - { - if (game.getPath().equals(gamePath)) - { - return game; - } - } - - // Unusual case: The game wasn't found in the cache. - // Scan the game and add it to the cache so that we can return it. - createGameFileCacheIfNeeded(); - return sGameFileCache.addOrGet(gamePath); - } - - /** - * Loads the game file cache from disk, without checking if the - * games are still present in the user's configured folders. - * If this has already been called, calling it again has no effect. - */ - private static void load() - { - if (!sFirstLoadDone) - { - sFirstLoadDone = true; - setUpAutomaticRescan(); - sGameFileCache.load(); - if (sGameFileCache.getSize() != 0) - { - updateGameFileArray(); - } - } - - if (sRunRescanAfterLoad) - { - // Without this, there will be a short blip where the loading indicator in the GUI disappears - // because neither sLoadInProgress nor sRescanInProgress is true - sRescanInProgress.postValue(true); - } - - sLoadInProgress.postValue(false); - - if (sRunRescanAfterLoad) - { - sRunRescanAfterLoad = false; - rescan(); - } - } - - /** - * Scans for games in the user's configured folders, - * updating the game file cache with the results. - * If load hasn't been called before this, the execution of this - * will be postponed until after load runs. - */ - private static void rescan() - { - if (!sFirstLoadDone) - { - sRunRescanAfterLoad = true; - } - else - { - String[] gamePaths = GameFileCache.getAllGamePaths(); - - boolean changed = sGameFileCache.update(gamePaths); - if (changed) - { - updateGameFileArray(); - } - - boolean additionalMetadataChanged = sGameFileCache.updateAdditionalMetadata(); - if (additionalMetadataChanged) - { - updateGameFileArray(); - } - - if (changed || additionalMetadataChanged) - { - sGameFileCache.save(); - } - } - - sRescanInProgress.postValue(false); - } - - private static void updateGameFileArray() - { - GameFile[] gameFilesTemp = sGameFileCache.getAllGames(); - Arrays.sort(gameFilesTemp, (lhs, rhs) -> lhs.getTitle().compareToIgnoreCase(rhs.getTitle())); - sGameFiles.postValue(gameFilesTemp); - } - - private static void createGameFileCacheIfNeeded() - { - // Creating the GameFileCache in the static initializer may be unsafe, because GameFileCache - // relies on native code, and the native library isn't loaded right when the app starts. - // We create it here instead. - - if (sGameFileCache == null) - { - sGameFileCache = new GameFileCache(); - } - } - - private static void setUpAutomaticRescan() - { - sRecursiveScanEnabled = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.getBoolean(); - new ConfigChangedCallback(() -> - new Handler(Looper.getMainLooper()).post(() -> - { - boolean recursiveScanEnabled = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.getBoolean(); - if (sRecursiveScanEnabled != recursiveScanEnabled) - { - sRecursiveScanEnabled = recursiveScanEnabled; - startRescan(); - } - })); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.kt new file mode 100644 index 0000000000..2ba2c246b3 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.kt @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.services + +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.ConfigChangedCallback +import org.dolphinemu.dolphinemu.model.GameFile +import org.dolphinemu.dolphinemu.model.GameFileCache +import org.dolphinemu.dolphinemu.ui.platform.Platform +import org.dolphinemu.dolphinemu.ui.platform.PlatformTab +import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner +import java.util.Arrays +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +/** + * Loads game list data on a separate thread. + */ +object GameFileCacheManager { + private var gameFileCache: GameFileCache? = null + private val gameFiles = MutableLiveData(emptyArray<GameFile>()) + private var firstLoadDone = false + private var runRescanAfterLoad = false + private var recursiveScanEnabled = false + + private val executor: ExecutorService = Executors.newFixedThreadPool(1) + private val loadInProgress = MutableLiveData(false) + private val rescanInProgress = MutableLiveData(false) + + @JvmStatic + fun getGameFiles(): LiveData<Array<GameFile>> { + return gameFiles + } + + @JvmStatic + fun getGameFilesForPlatformTab(platformTab: PlatformTab): List<GameFile> { + val allGames = gameFiles.value!! + val platformTabGames = ArrayList<GameFile>() + for (game in allGames) { + if (Platform.fromInt(game.getPlatform()).toPlatformTab() == platformTab) { + platformTabGames.add(game) + } + } + return platformTabGames + } + + @JvmStatic + fun getGameFileByGameId(gameId: String): GameFile? { + val allGames = gameFiles.value!! + for (game in allGames) { + if (game.getGameId() == gameId) { + return game + } + } + return null + } + + @JvmStatic + fun findSecondDisc(game: GameFile): GameFile? { + var matchWithoutRevision: GameFile? = null + + val allGames = gameFiles.value!! + for (otherGame in allGames) { + if (game.getGameId() == otherGame.getGameId() && game.getDiscNumber() != otherGame.getDiscNumber()) { + if (game.getRevision() == otherGame.getRevision()) { + return otherGame + } else { + matchWithoutRevision = otherGame + } + } + } + + return matchWithoutRevision + } + + @JvmStatic + fun findSecondDiscAndGetPaths(gameFile: GameFile?): Array<String> { + val nonNullGameFile = gameFile!! + val secondFile = findSecondDisc(nonNullGameFile) + return if (secondFile == null) { + arrayOf(nonNullGameFile.getPath()) + } else { + arrayOf(nonNullGameFile.getPath(), secondFile.getPath()) + } + } + + /** + * Returns true if in the process of loading the cache for the first time. + */ + @JvmStatic + fun isLoading(): LiveData<Boolean> { + return loadInProgress + } + + /** + * Returns true if in the process of rescanning. + */ + @JvmStatic + fun isRescanning(): LiveData<Boolean> { + return rescanInProgress + } + + @JvmStatic + fun isLoadingOrRescanning(): Boolean { + return loadInProgress.value!! || rescanInProgress.value!! + } + + /** + * Asynchronously loads the game file cache from disk, without checking + * if the games are still present in the user's configured folders. + * If this has already been called, calling it again has no effect. + */ + @JvmStatic + fun startLoad() { + createGameFileCacheIfNeeded() + + if (!loadInProgress.value!!) { + loadInProgress.value = true + AfterDirectoryInitializationRunner().runWithoutLifecycle { executor.execute(::load) } + } + } + + /** + * Asynchronously scans for games in the user's configured folders, + * updating the game file cache with the results. + * If loading the game file cache hasn't started or hasn't finished, + * the execution of this will be postponed until it finishes. + */ + @JvmStatic + fun startRescan() { + createGameFileCacheIfNeeded() + + if (!rescanInProgress.value!!) { + rescanInProgress.value = true + AfterDirectoryInitializationRunner().runWithoutLifecycle { executor.execute(::rescan) } + } + } + + @JvmStatic + fun addOrGet(gamePath: String?): GameFile { + val nonNullGamePath = gamePath!! + + // Common case: The game is in the cache, so just grab it from there. (GameFileCache.addOrGet + // actually already checks for this case, but we want to avoid calling it if possible + // because the executor thread may hold a lock on gameFileCache for extended periods of time.) + val allGames = gameFiles.value!! + for (game in allGames) { + if (game.getPath() == nonNullGamePath) { + return game + } + } + + // Unusual case: The game wasn't found in the cache. + // Scan the game and add it to the cache so that we can return it. + createGameFileCacheIfNeeded() + return gameFileCache!!.addOrGet(nonNullGamePath)!! + } + + /** + * Loads the game file cache from disk, without checking if the + * games are still present in the user's configured folders. + * If this has already been called, calling it again has no effect. + */ + private fun load() { + if (!firstLoadDone) { + firstLoadDone = true + setUpAutomaticRescan() + gameFileCache!!.load() + if (gameFileCache!!.getSize() != 0) { + updateGameFileArray() + } + } + + if (runRescanAfterLoad) { + // Without this, there will be a short blip where the loading indicator in the GUI disappears + // because neither loadInProgress nor rescanInProgress is true + rescanInProgress.postValue(true) + } + + loadInProgress.postValue(false) + + if (runRescanAfterLoad) { + runRescanAfterLoad = false + rescan() + } + } + + /** + * Scans for games in the user's configured folders, + * updating the game file cache with the results. + * If load hasn't been called before this, the execution of this + * will be postponed until after load runs. + */ + private fun rescan() { + if (!firstLoadDone) { + runRescanAfterLoad = true + } else { + val gamePaths = GameFileCache.getAllGamePaths() + + val changed = gameFileCache!!.update(gamePaths) + if (changed) { + updateGameFileArray() + } + + val additionalMetadataChanged = gameFileCache!!.updateAdditionalMetadata() + if (additionalMetadataChanged) { + updateGameFileArray() + } + + if (changed || additionalMetadataChanged) { + gameFileCache!!.save() + } + } + + rescanInProgress.postValue(false) + } + + private fun updateGameFileArray() { + val gameFilesTemp = gameFileCache!!.getAllGames() + Arrays.sort(gameFilesTemp) { lhs, rhs -> + lhs.getTitle().compareTo(rhs.getTitle(), ignoreCase = true) + } + gameFiles.postValue(gameFilesTemp) + } + + private fun createGameFileCacheIfNeeded() { + // Creating the GameFileCache in the static initializer may be unsafe, because GameFileCache + // relies on native code, and the native library isn't loaded right when the app starts. + // We create it here instead. + if (gameFileCache == null) { + gameFileCache = GameFileCache() + } + } + + private fun setUpAutomaticRescan() { + recursiveScanEnabled = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.boolean + ConfigChangedCallback { + Handler(Looper.getMainLooper()).post { + val recursiveScanEnabled = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.boolean + if (this.recursiveScanEnabled != recursiveScanEnabled) { + this.recursiveScanEnabled = recursiveScanEnabled + startRescan() + } + } + } + } +} diff --git a/Source/Core/Common/StringUtil.cpp b/Source/Core/Common/StringUtil.cpp index 79dc2a4269..ed4837b112 100644 --- a/Source/Core/Common/StringUtil.cpp +++ b/Source/Core/Common/StringUtil.cpp @@ -5,7 +5,7 @@ #include <algorithm> #include <array> -#include <codecvt> +#include <cassert> #include <cstdarg> #include <cstddef> #include <cstdio> @@ -13,7 +13,6 @@ #include <cstring> #include <iomanip> #include <iterator> -#include <locale> #include <sstream> #include <string> #include <vector> @@ -391,6 +390,249 @@ size_t StringUTF8CodePointCount(std::string_view str) return str.size() - std::ranges::count_if(str, [](char c) -> bool { return (c & 0xC0) == 0x80; }); } +constexpr char32_t UNICODE_REPLACEMENT_CHARACTER = 0xfffd; +constexpr char32_t UNICODE_LAST_CODE_POINT = 0x10ffff; + +constexpr u16 UNICODE_HIGH_SURROGATE = 0xd800; +constexpr u16 UNICODE_LOW_SURROGATE = 0xdc00; + +constexpr u16 SURROGATE_VALUE_MASK = 0x3ffu; + +static constexpr bool IsSurrogateCodePoint(char32_t code_point) +{ + return (code_point & 0xf800u) == UNICODE_HIGH_SURROGATE; +} + +template <std::integral CharType> +requires(sizeof(CharType) == 1) +class UTF8Decoder +{ +public: + constexpr explicit UTF8Decoder(std::span<const CharType> chars) + : m_ptr{chars.data()}, m_end_ptr{m_ptr + chars.size()} + { + } + + auto RemainingCodeUnits() const { return m_end_ptr - m_ptr; } + + constexpr char32_t operator()() + { + assert(RemainingCodeUnits() > 0); + + const u8 first_code_unit = *m_ptr; + ++m_ptr; + + switch (std::countl_one(first_code_unit)) + { + case 0: // ASCII. + return first_code_unit; + case 2: + return FinishReadingSequence<2, 0x80>(first_code_unit); + case 3: + { + const u32 code_point = FinishReadingSequence<3, 0x800>(first_code_unit); + if (!IsSurrogateCodePoint(code_point)) + return code_point; + break; + } + case 4: + { + const u32 code_point = FinishReadingSequence<4, 0x10000>(first_code_unit); + if (code_point <= UNICODE_LAST_CODE_POINT) + return code_point; + break; + } + default: + break; + } + + return UNICODE_REPLACEMENT_CHARACTER; + } + +private: + template <u32 ByteCount, u32 FirstValidCodePoint> + constexpr u32 FinishReadingSequence(u8 first_code_unit) + { + // Remove the leading one bits. + u32 code_point = first_code_unit & (0x7fu >> ByteCount); + + for (u32 byte_count = ByteCount - 1; byte_count != 0; --byte_count) + { + if (RemainingCodeUnits() == 0) + return UNICODE_REPLACEMENT_CHARACTER; + + const auto code_unit = u8(*m_ptr); + + if (!IsContinuationByte(code_unit)) + return UNICODE_REPLACEMENT_CHARACTER; + + ++m_ptr; + + code_point = (code_point << 6u) | (code_unit & 0x3fu); + } + + // Overlong encoding. + if (code_point < FirstValidCodePoint) + return UNICODE_REPLACEMENT_CHARACTER; + + return code_point; + } + + static constexpr bool IsContinuationByte(u8 code_unit) { return std::countl_one(code_unit) == 1; } + + const CharType* m_ptr; + const CharType* const m_end_ptr; +}; + +class UTF8Encoder +{ +public: + static constexpr u32 GetMaxUnitsPerCodePoint() { return 4; } + + // `ptr` should point to at least 4 bytes. + // Returns the number of written code units (bytes). + template <std::integral CharType> + requires(sizeof(CharType) == 1) + constexpr u32 operator()(char32_t code_point, CharType* ptr) + { + // ASCII. + if (code_point < 0x80u) + { + *ptr = u8(code_point); + return 1; + } + + if (code_point < 0x800u) + return WriteSequence<2>(code_point, ptr); + + if (code_point < 0x10000u) + return WriteSequence<3>(code_point, ptr); + + if (code_point <= UNICODE_LAST_CODE_POINT) + return WriteSequence<4>(code_point, ptr); + + return (*this)(UNICODE_REPLACEMENT_CHARACTER, ptr); + } + +private: + template <u32 ByteCount> + static constexpr u32 WriteSequence(u32 code_point, auto* ptr) + { + *ptr = u8((0xf0u << (4 - ByteCount)) | (code_point >> (6 * (ByteCount - 1)))); + + for (u32 i = ByteCount - 1; i != 0; --i) + { + ptr[i] = u8(0x80u | (code_point & 0x3fu)); + code_point >>= 6; + } + + return ByteCount; + } +}; + +template <std::integral CharType> +requires(sizeof(CharType) == 2) +class UTF16Decoder +{ +public: + constexpr explicit UTF16Decoder(std::span<const CharType> chars) + : m_ptr{chars.data()}, m_end_ptr{m_ptr + chars.size()} + { + } + + auto RemainingCodeUnits() const { return m_end_ptr - m_ptr; } + + constexpr char32_t operator()() + { + assert(RemainingCodeUnits() > 0); + + const u16 first_code_unit = *m_ptr; + ++m_ptr; + + // Single code unit. + if (!IsSurrogateCodePoint(first_code_unit)) + return first_code_unit; + + // Unexpected low surrogate. + if (first_code_unit >= UNICODE_LOW_SURROGATE) + return UNICODE_REPLACEMENT_CHARACTER; + + // High surrogate at end of data. + if (RemainingCodeUnits() == 0) + return UNICODE_REPLACEMENT_CHARACTER; + + const u16 second_code_unit = *m_ptr; + + // High surrogate not followed by low surrogate. + if ((second_code_unit & u16(~SURROGATE_VALUE_MASK)) != UNICODE_LOW_SURROGATE) + return UNICODE_REPLACEMENT_CHARACTER; + + ++m_ptr; + + // We have a surrogate pair. + return (u32(first_code_unit & SURROGATE_VALUE_MASK) << 10u) + + u32(second_code_unit & SURROGATE_VALUE_MASK) + 0x10000u; + } + +private: + const CharType* m_ptr; + const CharType* const m_end_ptr; +}; + +class UTF16Encoder +{ +public: + static constexpr u32 GetMaxUnitsPerCodePoint() { return 2; } + + // `ptr` should point to at least 2 code units. + // Returns the number of written code units. + template <std::integral CharType> + requires(sizeof(CharType) == 2) + constexpr u32 operator()(char32_t code_point, CharType* ptr) + { + if (code_point < 0x10000u) + { + *ptr = u16(code_point); + return 1; + } + + if (code_point > UNICODE_LAST_CODE_POINT) + return (*this)(UNICODE_REPLACEMENT_CHARACTER, ptr); + + // Create surrogate pair. + const u32 value = code_point - 0x10000; + ptr[0] = u16(((value >> 10u) & SURROGATE_VALUE_MASK) | UNICODE_HIGH_SURROGATE); + ptr[1] = u16((value & SURROGATE_VALUE_MASK) | UNICODE_LOW_SURROGATE); + return 2; + } +}; + +template <typename Decoder, typename Encoder, typename ResultCharType, typename InputCharType> +static constexpr std::basic_string<ResultCharType> +ReEncodeString(std::basic_string_view<InputCharType> input) +{ + Encoder encoder; + + const auto buffer_size = input.size() * encoder.GetMaxUnitsPerCodePoint(); + + // Handle the unlikely case of integer overflow in the buffer size. + const auto input_size = buffer_size / encoder.GetMaxUnitsPerCodePoint(); + + Decoder decoder{std::span{input}.first(input_size)}; + + std::basic_string<ResultCharType> result; + result.resize_and_overwrite(buffer_size, [&](ResultCharType* buf, std::size_t) { + auto* position = buf; + + while (decoder.RemainingCodeUnits() != 0) + position += encoder(decoder(), position); + + return position - buf; + }); + + return result; +} + #ifdef _WIN32 static std::wstring CPToUTF16(u32 code_page, std::string_view input) @@ -567,14 +809,12 @@ std::string UTF16BEToUTF8(const char16_t* str, size_t max_size) std::string UTF16ToUTF8(std::u16string_view input) { - std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> converter; - return converter.to_bytes(input.data(), input.data() + input.size()); + return ReEncodeString<UTF16Decoder<char16_t>, UTF8Encoder, char>(input); } std::u16string UTF8ToUTF16(std::string_view input) { - std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> converter; - return converter.from_bytes(input.data(), input.data() + input.size()); + return ReEncodeString<UTF8Decoder<char>, UTF16Encoder, char16_t>(input); } // This is a replacement for path::u8path, which is deprecated starting with C++20. diff --git a/Source/Core/Core/HotkeyManager.cpp b/Source/Core/Core/HotkeyManager.cpp index 92c6f2b831..47e511e813 100644 --- a/Source/Core/Core/HotkeyManager.cpp +++ b/Source/Core/Core/HotkeyManager.cpp @@ -314,7 +314,7 @@ constexpr std::array<HotkeyGroupInfo, NUM_HOTKEY_GROUPS> s_groups_info = { {_trans("Internal Resolution"), HK_INCREASE_IR, HK_DECREASE_IR}, {_trans("Freelook"), HK_FREELOOK_TOGGLE, HK_FREELOOK_TOGGLE}, // i18n: Stereoscopic 3D - {_trans("3D"), HK_TOGGLE_STEREO_SBS, HK_TOGGLE_STEREO_ANAGLYPH}, + {_trans("3D"), HK_TOGGLE_STEREO_SIDE_BY_SIDE, HK_TOGGLE_STEREO_ANAGLYPH}, // i18n: Stereoscopic 3D {_trans("3D Depth"), HK_DECREASE_DEPTH, HK_INCREASE_CONVERGENCE}, {_trans("Load State"), HK_LOAD_STATE_SLOT_1, HK_LOAD_STATE_SLOT_SELECTED}, diff --git a/Source/Core/Core/HotkeyManager.h b/Source/Core/Core/HotkeyManager.h index 7fb4fb77ac..b367c6b3c1 100644 --- a/Source/Core/Core/HotkeyManager.h +++ b/Source/Core/Core/HotkeyManager.h @@ -107,8 +107,8 @@ enum Hotkey HK_FREELOOK_TOGGLE, - HK_TOGGLE_STEREO_SBS, - HK_TOGGLE_STEREO_TAB, + HK_TOGGLE_STEREO_SIDE_BY_SIDE, + HK_TOGGLE_STEREO_TOP_AND_BOTTOM, HK_TOGGLE_STEREO_ANAGLYPH, HK_DECREASE_DEPTH, diff --git a/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.cpp b/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.cpp index a1864b3b77..228cc348a8 100644 --- a/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.cpp +++ b/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.cpp @@ -241,8 +241,11 @@ void EnhancementsWidget::CreateWidgets() m_3d_convergence_value->setText(QString::asprintf("%.2f", m_3d_convergence->GetValue())); auto current_stereo_mode = Get(m_game_layer, Config::GFX_STEREO_MODE); - if (current_stereo_mode != StereoMode::SBS && current_stereo_mode != StereoMode::TAB) + if (current_stereo_mode != StereoMode::SideBySide && + current_stereo_mode != StereoMode::TopAndBottom) + { m_3d_per_eye_resolution->hide(); + } main_layout->addWidget(enhancements_box); main_layout->addWidget(stereoscopy_box); @@ -257,10 +260,15 @@ void EnhancementsWidget::ConnectWidgets() auto current_stereo_mode = Get(m_game_layer, Config::GFX_STEREO_MODE); LoadPostProcessingShaders(); - if (current_stereo_mode == StereoMode::SBS || current_stereo_mode == StereoMode::TAB) + if (current_stereo_mode == StereoMode::SideBySide || + current_stereo_mode == StereoMode::TopAndBottom) + { m_3d_per_eye_resolution->show(); + } else + { m_3d_per_eye_resolution->hide(); + } }); connect(m_post_processing_effect, &QComboBox::currentIndexChanged, this, diff --git a/Source/Core/DolphinQt/HotkeyScheduler.cpp b/Source/Core/DolphinQt/HotkeyScheduler.cpp index e2722e9cfe..8e4f5261dc 100644 --- a/Source/Core/DolphinQt/HotkeyScheduler.cpp +++ b/Source/Core/DolphinQt/HotkeyScheduler.cpp @@ -551,15 +551,15 @@ void HotkeyScheduler::Run() emit DecrementSelectedStateSlotHotkey(); // Stereoscopy - if (IsHotkey(HK_TOGGLE_STEREO_SBS)) + if (IsHotkey(HK_TOGGLE_STEREO_SIDE_BY_SIDE)) { - if (Config::Get(Config::GFX_STEREO_MODE) != StereoMode::SBS) + if (Config::Get(Config::GFX_STEREO_MODE) != StereoMode::SideBySide) { // Disable post-processing shader, as stereoscopy itself is currently a shader if (Config::Get(Config::GFX_ENHANCE_POST_SHADER) == DUBOIS_ALGORITHM_SHADER) Config::SetCurrent(Config::GFX_ENHANCE_POST_SHADER, ""); - Config::SetCurrent(Config::GFX_STEREO_MODE, StereoMode::SBS); + Config::SetCurrent(Config::GFX_STEREO_MODE, StereoMode::SideBySide); } else { @@ -567,15 +567,15 @@ void HotkeyScheduler::Run() } } - if (IsHotkey(HK_TOGGLE_STEREO_TAB)) + if (IsHotkey(HK_TOGGLE_STEREO_TOP_AND_BOTTOM)) { - if (Config::Get(Config::GFX_STEREO_MODE) != StereoMode::TAB) + if (Config::Get(Config::GFX_STEREO_MODE) != StereoMode::TopAndBottom) { // Disable post-processing shader, as stereoscopy itself is currently a shader if (Config::Get(Config::GFX_ENHANCE_POST_SHADER) == DUBOIS_ALGORITHM_SHADER) Config::SetCurrent(Config::GFX_ENHANCE_POST_SHADER, ""); - Config::SetCurrent(Config::GFX_STEREO_MODE, StereoMode::TAB); + Config::SetCurrent(Config::GFX_STEREO_MODE, StereoMode::TopAndBottom); } else { diff --git a/Source/Core/InputCommon/GCAdapter.cpp b/Source/Core/InputCommon/GCAdapter.cpp index 8438146c34..e11d95d68f 100644 --- a/Source/Core/InputCommon/GCAdapter.cpp +++ b/Source/Core/InputCommon/GCAdapter.cpp @@ -166,8 +166,8 @@ static u64 s_last_init = 0; static std::optional<Config::ConfigChangedCallbackID> s_config_callback_id = std::nullopt; -static bool s_is_adapter_wanted = false; -static std::array<bool, SerialInterface::MAX_SI_CHANNELS> s_config_rumble_enabled{}; +static std::atomic_bool s_is_adapter_wanted = false; +static std::array<std::atomic_bool, SerialInterface::MAX_SI_CHANNELS> s_config_rumble_enabled{}; static std::atomic<double> s_adapter_poll_rate{}; @@ -462,7 +462,7 @@ static void ScanThreadFunc() while (s_adapter_detect_thread_running.IsSet()) { - if (!s_detected && s_is_adapter_wanted && + if (!s_detected && s_is_adapter_wanted.load(std::memory_order_relaxed) && env->CallStaticBooleanMethod(s_adapter_class, is_usb_device_available_func)) { std::lock_guard lk(s_init_mutex); @@ -506,16 +506,19 @@ static void StopScanThread() static void RefreshConfig() { - s_is_adapter_wanted = false; + bool is_adapter_wanted = false; for (int i = 0; i < SerialInterface::MAX_SI_CHANNELS; ++i) { - s_is_adapter_wanted |= Config::Get(Config::GetInfoForSIDevice(i)) == - SerialInterface::SIDevices::SIDEVICE_WIIU_ADAPTER; - s_config_rumble_enabled[i] = Config::Get(Config::GetInfoForAdapterRumble(i)); + is_adapter_wanted |= Config::Get(Config::GetInfoForSIDevice(i)) == + SerialInterface::SIDevices::SIDEVICE_WIIU_ADAPTER; + s_config_rumble_enabled[i].store(Config::Get(Config::GetInfoForAdapterRumble(i)), + std::memory_order_relaxed); } - if (s_is_adapter_wanted) + s_is_adapter_wanted.store(is_adapter_wanted, std::memory_order_relaxed); + + if (is_adapter_wanted) StartScanThread(); else StopScanThread(); @@ -830,7 +833,7 @@ static void Reset() GCPadStatus Input(int chan) { - if (!s_is_adapter_wanted) + if (!s_is_adapter_wanted.load(std::memory_order_relaxed)) return {}; #if GCADAPTER_USE_LIBUSB_IMPLEMENTATION @@ -991,7 +994,8 @@ void ResetRumble() // being called while the libusb state is being reset static void ResetRumbleLockNeeded() { - if (!s_is_adapter_wanted || s_handle == nullptr || s_status != AdapterStatus::Detected) + if (s_handle == nullptr || s_status != AdapterStatus::Detected || + !s_is_adapter_wanted.load(std::memory_order_relaxed)) { return; } @@ -1018,7 +1022,8 @@ static void ResetRumbleLockNeeded() void Output(int chan, u8 rumble_command) { - if (!s_is_adapter_wanted || !s_config_rumble_enabled[chan]) + if (!s_is_adapter_wanted.load(std::memory_order_relaxed) || + !s_config_rumble_enabled[chan].load(std::memory_order_relaxed)) return; #if GCADAPTER_USE_LIBUSB_IMPLEMENTATION diff --git a/Source/Core/VideoCommon/PostProcessing.cpp b/Source/Core/VideoCommon/PostProcessing.cpp index 804c24eef6..92f7c1f99c 100644 --- a/Source/Core/VideoCommon/PostProcessing.cpp +++ b/Source/Core/VideoCommon/PostProcessing.cpp @@ -1017,8 +1017,8 @@ static bool UseGeometryShaderForPostProcess(bool is_intermediary_buffer) case StereoMode::Anaglyph: case StereoMode::Passive: return is_intermediary_buffer; - case StereoMode::SBS: - case StereoMode::TAB: + case StereoMode::SideBySide: + case StereoMode::TopAndBottom: case StereoMode::Off: default: return false; diff --git a/Source/Core/VideoCommon/Present.cpp b/Source/Core/VideoCommon/Present.cpp index 4374782464..0691323c54 100644 --- a/Source/Core/VideoCommon/Present.cpp +++ b/Source/Core/VideoCommon/Present.cpp @@ -392,7 +392,7 @@ Presenter::ConvertStereoRectangle(const MathUtil::Rectangle<int>& rc) const { // Resize target to half its original size auto draw_rc = rc; - if (g_ActiveConfig.stereo_mode == StereoMode::TAB) + if (g_ActiveConfig.stereo_mode == StereoMode::TopAndBottom) { // The height may be negative due to flipped rectangles int height = rc.bottom - rc.top; @@ -409,7 +409,7 @@ Presenter::ConvertStereoRectangle(const MathUtil::Rectangle<int>& rc) const // Create two target rectangle offset to the sides of the backbuffer auto left_rc = draw_rc; auto right_rc = draw_rc; - if (g_ActiveConfig.stereo_mode == StereoMode::TAB) + if (g_ActiveConfig.stereo_mode == StereoMode::TopAndBottom) { left_rc.top -= m_backbuffer_height / 4; left_rc.bottom -= m_backbuffer_height / 4; @@ -477,13 +477,13 @@ float Presenter::CalculateDrawAspectRatio(bool allow_stretch) const if (g_ActiveConfig.stereo_per_eye_resolution_full) { - if (g_ActiveConfig.stereo_mode == StereoMode::SBS) + if (g_ActiveConfig.stereo_mode == StereoMode::SideBySide) { // Render twice as wide if using side-by-side 3D, since the 3D will halve the horizontal // resolution resulting_aspect_ratio *= 2.0; } - else if (g_ActiveConfig.stereo_mode == StereoMode::TAB) + else if (g_ActiveConfig.stereo_mode == StereoMode::TopAndBottom) { // Render twice as tall if using top-and-bottom 3D, since the 3D will halve the vertical // resolution @@ -830,8 +830,8 @@ void Presenter::RenderXFBToScreen(const MathUtil::Rectangle<int>& target_rc, g_gfx->SelectMainBuffer(); } - else if (g_ActiveConfig.stereo_mode == StereoMode::SBS || - g_ActiveConfig.stereo_mode == StereoMode::TAB) + else if (g_ActiveConfig.stereo_mode == StereoMode::SideBySide || + g_ActiveConfig.stereo_mode == StereoMode::TopAndBottom) { const auto [left_rc, right_rc] = ConvertStereoRectangle(target_rc); diff --git a/Source/Core/VideoCommon/VideoConfig.h b/Source/Core/VideoCommon/VideoConfig.h index 2d0ab116a1..eed6a3420e 100644 --- a/Source/Core/VideoCommon/VideoConfig.h +++ b/Source/Core/VideoCommon/VideoConfig.h @@ -33,8 +33,8 @@ enum class AspectMode : int enum class StereoMode : int { Off, - SBS, - TAB, + SideBySide, + TopAndBottom, Anaglyph, QuadBuffer, Passive diff --git a/Source/UnitTests/Common/StringUtilTest.cpp b/Source/UnitTests/Common/StringUtilTest.cpp index f7c3b9169b..7b6f1828bc 100644 --- a/Source/UnitTests/Common/StringUtilTest.cpp +++ b/Source/UnitTests/Common/StringUtilTest.cpp @@ -411,18 +411,52 @@ TEST(StringUtil, IPv4PortRangeToString) TEST(StringUtil, CharacterEncodingConversion) { + const std::string utf8_variety = "🎮 hello ¥ᚼᛒ﹏🐬"; + const std::u16string utf16_variety = u"🎮 hello ¥ᚼᛒ﹏🐬"; + + // UTF-16 -> UTF-8 + const std::string utf8_replacement_char = "\xef\xbf\xbd"; + + // Unmatched high surrogate. + EXPECT_EQ(UTF16ToUTF8(u"\xd800" + utf16_variety), utf8_replacement_char + utf8_variety); + EXPECT_EQ(UTF16ToUTF8(utf16_variety + u"\xdbff"), utf8_variety + utf8_replacement_char); + + // Unmatched low surrogate. + EXPECT_EQ(UTF16ToUTF8(u"\xdc00" + utf16_variety), utf8_replacement_char + utf8_variety); + EXPECT_EQ(UTF16ToUTF8(utf16_variety + u"\xdfff"), utf8_variety + utf8_replacement_char); + + // UTF-8 -> UTF-16 + const std::u16string utf16_replacement_char = u"\xfffd"; + + // Unexpected bytes. + EXPECT_EQ(UTF8ToUTF16("\x80" + utf8_variety), utf16_replacement_char + utf16_variety); + EXPECT_EQ(UTF8ToUTF16("\xf8" + utf8_variety), utf16_replacement_char + utf16_variety); + + // Overlong encodings. + EXPECT_EQ(UTF8ToUTF16("\xc0\x8a" + utf8_variety), utf16_replacement_char + utf16_variety); + EXPECT_EQ(UTF8ToUTF16("\xe0\x81\x8a" + utf8_variety), utf16_replacement_char + utf16_variety); + EXPECT_EQ(UTF8ToUTF16("\xf0\x81\x81\x8a" + utf8_variety), utf16_replacement_char + utf16_variety); + + // Non-terminated character sequences. + EXPECT_EQ(UTF8ToUTF16("\xa0" + utf8_variety), utf16_replacement_char + utf16_variety); + EXPECT_EQ(UTF8ToUTF16("\xc0\xf0"), utf16_replacement_char + utf16_replacement_char); + EXPECT_EQ(UTF8ToUTF16(utf8_variety + "\xf0\x9f"), utf16_variety + utf16_replacement_char); + EXPECT_EQ(UTF8ToUTF16("\xf0\x9fZ"), utf16_replacement_char + u"Z"); + + // Code point greater than U+10FFFF. + EXPECT_EQ(UTF8ToUTF16("\xf7\x80\x80\x80" + utf8_variety), utf16_replacement_char + utf16_variety); + + // Decoded surrogate code points are rejected. + EXPECT_EQ(UTF8ToUTF16("\xed\xb6\x81" + utf8_variety), utf16_replacement_char + utf16_variety); + // wstring EXPECT_EQ(WStringToUTF8(L"hello 🐬"), "hello 🐬"); - // UTF-16 - EXPECT_EQ(UTF16ToUTF8(u"hello 🐬"), "hello 🐬"); - EXPECT_EQ(UTF8ToUTF16("hello 🐬"), u"hello 🐬"); - // UTF-16BE - char16_t utf16be_str[] = u"hello 🐬"; + auto utf16be_str = utf16_variety; for (auto& c : utf16be_str) c = Common::swap16(c); - EXPECT_EQ(UTF16BEToUTF8(utf16be_str, 99), "hello 🐬"); + EXPECT_EQ(UTF16BEToUTF8(utf16be_str.c_str(), 99), utf8_variety); // Shift JIS EXPECT_EQ(SHIFTJISToUTF8("\x83\x43\x83\x8b\x83\x4a"), "イルカ"); |
