summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Data/Sys/GameSettings/GNLE82.ini41
-rw-r--r--Data/Sys/GameSettings/GNLJ82.ini41
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.java286
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.kt251
-rw-r--r--Source/Core/Common/StringUtil.cpp252
-rw-r--r--Source/Core/Core/HotkeyManager.cpp2
-rw-r--r--Source/Core/Core/HotkeyManager.h4
-rw-r--r--Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.cpp12
-rw-r--r--Source/Core/DolphinQt/HotkeyScheduler.cpp12
-rw-r--r--Source/Core/InputCommon/GCAdapter.cpp27
-rw-r--r--Source/Core/VideoCommon/PostProcessing.cpp4
-rw-r--r--Source/Core/VideoCommon/Present.cpp12
-rw-r--r--Source/Core/VideoCommon/VideoConfig.h4
-rw-r--r--Source/UnitTests/Common/StringUtilTest.cpp46
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"), "イルカ");