36 Commits

Author SHA1 Message Date
patchzyy 6d836dab3e Bump version to 0.2.24 2026-08-29 15:22:41 +02:00
patchzyy 6bffedf028 Merge pull request #67 from patchzyy/fix-accented-paths
fix accented paths
2026-08-29 15:02:06 +02:00
patchzyy 09ca4e98f4 Update system_bridge.cpp 2026-08-29 15:01:51 +02:00
patchzyy 6dc4e59052 Coderabbit fixes 2026-08-29 11:56:35 +02:00
patchzyy 82b295b20c Merge pull request #77 from patchzyy/Fix-unsupported-locale
Fix paranthesees
2026-08-28 22:19:24 +02:00
patchzyy eb1721fd72 Update NativeBuildFlags.ps1 2026-08-28 20:45:46 +02:00
patchzyy 129c714f02 Update disk space 2026-08-28 20:42:20 +02:00
patchzyy 2794024d3a Fix paranthesees 2026-08-28 20:38:27 +02:00
patchzyy b5e5858e1d Use UTF-8-safe filesystem paths end-to-end 2026-08-28 19:01:14 +02:00
patchzyy 4897e7e27d Use safe UTF-8 path construction 2026-08-28 10:52:01 +02:00
patchzyy 7ebda6bf7f mangle path fix 2026-08-28 10:30:07 +02:00
patchzyy 0403f176bd fix accented paths 2026-08-28 01:25:13 +02:00
patchzyy 1912292c80 Merge pull request #42 from GalaxisBeast/main
add gamecube controller support for all 4 ports
2026-08-27 23:58:32 +02:00
patchzyy e498e62622 Merge pull request #63 from s5bug/fix-translator-readme-formatting
fix formatting of translator/README.md
2026-08-27 23:53:52 +02:00
Aly f73739f248 fix formatting of translator/README.md 2026-08-27 14:56:15 -06:00
patchzyy 3389fe35fc Merge pull request #57 from patchzyy/fix-ghosts
Implement missing ISFS_ReadDir
2026-08-27 19:45:42 +02:00
Cristian Boehm e11062a1ba Merge branch 'patchzyy:main' into main 2026-08-27 13:29:49 -04:00
patchzyy 1d1d064d37 Implement missing ISFS_ReadDir 2026-08-27 19:15:21 +02:00
patchzyy 9d1f3db1d5 Merge pull request #48 from patchzyy/fix-choking-logging
Update os_report.cpp
2026-08-27 14:43:06 +02:00
GalaxisBeast 251529d66e fix issues
fix timeout loop, fixed drift issues, fixed infinite rumble, fixed gamecube controller adapter taking over all ports, require assigning a virtual controller port specifically to the gamecube controller adapter, and block inputs from gamecube controller while settings menu (f10 menu) is open
2026-08-26 20:12:15 -04:00
patchzyy 987b666296 Update os_report.cpp 2026-08-27 00:59:40 +02:00
patchzyy 260022b4ba version update 2026-08-26 23:39:04 +02:00
GalaxisBeast f1c2b60df1 fix mistake where i literally deleted the code 2026-08-26 17:37:14 -04:00
Cristian Boehm 5193dd9749 Merge branch 'main' into main 2026-08-26 17:22:47 -04:00
patchzyy ec9bd92415 Merge pull request #43 from patchzyy/Unknown-Controller-support
Unknown controller support
2026-08-26 23:04:55 +02:00
patchzyy dbf6d05625 SDL cache + output stream 2026-08-26 22:50:20 +02:00
GalaxisBeast 8ba44162fa Update wup028_adapter.cpp 2026-08-26 15:55:57 -04:00
GalaxisBeast 9ee14e582d Update wup028_adapter.cpp 2026-08-26 15:41:00 -04:00
GalaxisBeast 0c349a9296 Update pad.cpp 2026-08-26 15:35:32 -04:00
patchzyy 21b57f08a4 Unknown controller support 2026-08-26 21:32:22 +02:00
GalaxisBeast e146b10af8 add gamecube controller support for all 4 ports 2026-08-26 15:17:28 -04:00
patchzyy edd03f8991 Merge pull request #23 from patchzyy/Premature-success-fix
fix underflow and crash on malformed stream
2026-08-24 21:50:19 +02:00
patchzyy 9c4f4b738a fix underflow and crash on malformed stream 2026-08-24 19:20:20 +02:00
patchzyy 8d5d93f02d version update 2026-08-24 13:40:44 +02:00
patchzyy c75b482224 Merge pull request #17 from patchzyy/decompress-overlow
Fix szs memory corruption
2026-08-24 12:03:50 +02:00
patchzyy e5ae29b27e Create egg_decomp.cpp 2026-08-24 11:30:11 +02:00
46 changed files with 1764 additions and 390 deletions
+3
View File
@@ -25,6 +25,9 @@ Code.pul
# Build output # Build output
/build/ /build/
/build-*/ /build-*/
/.build-output/
/.build-test/
/native-build/
/dist/ /dist/
/out/ /out/
[Bb]in/ [Bb]in/
+1 -1
View File
@@ -253,7 +253,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
$manifest = [ordered]@{ $manifest = [ordered]@{
SchemaVersion = 2 SchemaVersion = 2
ProductVersion = '0.2.21' ProductVersion = '0.2.24'
ExpectedGameId = $pins.GameId ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256 ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256 ExpectedRelSha256 = $pins.RelSha256
+5 -4
View File
@@ -151,9 +151,10 @@ if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirect
throw '-BaseOutputDirectory is valid only with -Profile both.' throw '-BaseOutputDirectory is valid only with -Profile both.'
} }
$translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe' $translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe'
$cmake = Join-Path $Toolkit 'CMake\bin\cmake.exe' $toolchain = Get-MkwShellSafeToolchainRoot $Toolkit
$ninja = Join-Path $Toolkit 'Ninja\ninja.exe' $cmake = Join-Path $toolchain 'CMake\bin\cmake.exe'
$toolchainBin = Join-Path $Toolkit 'llvm-mingw\bin' $ninja = Join-Path $toolchain 'Ninja\ninja.exe'
$toolchainBin = Join-Path $toolchain 'llvm-mingw\bin'
$cc = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang.exe' $cc = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang.exe'
$cxx = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang++.exe' $cxx = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang++.exe'
$windres = Join-Path $toolchainBin 'x86_64-w64-mingw32-windres.exe' $windres = Join-Path $toolchainBin 'x86_64-w64-mingw32-windres.exe'
@@ -203,7 +204,7 @@ if ($Parallel -gt 0) {
$oldPath = $env:PATH $oldPath = $env:PATH
$oldDotnet = $env:DOTNET_ROOT $oldDotnet = $env:DOTNET_ROOT
try { try {
$env:PATH = Get-MkwToolchainPath $Toolkit $env:PATH = Get-MkwToolchainPath $toolchain
Remove-Item Env:DOTNET_ROOT -ErrorAction SilentlyContinue Remove-Item Env:DOTNET_ROOT -ErrorAction SilentlyContinue
Push-Location $Workspace Push-Location $Workspace
try { try {
+37
View File
@@ -40,6 +40,43 @@ function Get-MkwToolchainPath([string]$ToolchainRoot) {
) -join ';') ) -join ';')
} }
function Get-MkwShellSafeToolchainRoot([string]$ToolchainRoot) {
if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' }
$full = [IO.Path]::GetFullPath($ToolchainRoot)
# A drive root keeps its separator: "C:" is relative to the current directory on that drive.
if ($full -ne [IO.Path]::GetPathRoot($full)) { $full = $full.TrimEnd('\') }
if ($full -notmatch '[()&^%!]') { return $full }
$sha = [Security.Cryptography.SHA256]::Create()
try {
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant()))
} finally { $sha.Dispose() }
$linkName = 'toolchain-' + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
$failures = @()
foreach ($base in @($env:ProgramData, $env:PUBLIC)) {
if ([string]::IsNullOrWhiteSpace($base) -or $base -match '[()&^%! ]') { continue }
$link = Join-Path (Join-Path $base 'WiiCompiled') $linkName
try {
[IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null
# The name already identifies the target, so an existing junction that still resolves is
# this one; only a broken leftover is replaced. Directory.Delete removes the reparse
# point itself, where Remove-Item -Recurse would delete the toolchain it points at.
if (-not (Test-Path -LiteralPath (Join-Path $link 'CMake\bin\cmake.exe') -PathType Leaf)) {
if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) }
New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null
}
Write-Host "MKWCBUILD: Building through $link, because $full contains characters cmd.exe cannot parse"
return $link
} catch {
$failures += "$link ($($_.Exception.Message))"
}
}
throw ("The toolchain path $full contains a character (one of ( ) & ^ % !) that the compiler " +
'cannot be invoked through, and no junction to it could be created: ' + ($failures -join '; ') +
'. Install to a path without those characters.')
}
function Get-MkwProjectPins([string]$ProjectFile) { function Get-MkwProjectPins([string]$ProjectFile) {
<# <#
The Mario Kart Wii facts pinned by projects/mkwii/recomp.yml (game identity, clean input The Mario Kart Wii facts pinned by projects/mkwii/recomp.yml (game identity, clean input
+1 -1
View File
@@ -121,7 +121,7 @@ internal static class PlatformChecks
internal static class ProductInfo internal static class ProductInfo
{ {
public const string Name = "WiiCompiled"; public const string Name = "WiiCompiled";
public const string Version = "0.2.21"; public const string Version = "0.2.24";
/// <summary> /// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and /// The setup executable is copied into the installation under this name. It is the launcher and
@@ -7,7 +7,7 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName> <AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup</RootNamespace> <RootNamespace>WiiCompiled.Setup</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.21</Version> <Version>0.2.24</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description> <Description>Command-line installer and launcher for WiiCompiled</Description>
+1 -1
View File
@@ -63,7 +63,7 @@ inputs like paddles, touchpads and share buttons show up when the hardware repor
- Windows 10 or 11, 64-bit - Windows 10 or 11, 64-bit
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher - GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher - CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation - About 20 GB of free disk space during installation (Final game size ~5 GB)
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM, - A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted. GCZ, CISO, WBFS, WIA and RVZ are accepted.
+1
View File
@@ -229,6 +229,7 @@ s32 PADGetNativeButtonPressed(u32 port);
PADSignedNativeAxis PADGetNativeAxisPulled(u32 port); PADSignedNativeAxis PADGetNativeAxisPulled(u32 port);
void PADRestoreDefaultMapping(u32 port); void PADRestoreDefaultMapping(u32 port);
void PADBlockInput(bool block); void PADBlockInput(bool block);
bool PADIsInputBlocked(void);
/** /**
* Set the default controller mapping used. * Set the default controller mapping used.
+3 -1
View File
@@ -111,7 +111,9 @@ ECardResult CardGciFolder::createFile(const char* filename, size_t size, FileHan
} }
gciFileHeader->swapEndian(); gciFileHeader->swapEndian();
m_files.push_back({*gciFileHeader, fileSize, reinterpret_cast<const char8_t*>(gciFilename.c_str()), false}); // push non-endian swapped header first // push non-endian swapped header first
m_files.push_back({*gciFileHeader, fileSize,
std::u8string(gciFilename.begin(), gciFilename.end()), false});
handleOut = FileHandle(m_files.size() - 1, 0); handleOut = FileHandle(m_files.size() - 1, 0);
return ECardResult::READY; return ECardResult::READY;
+1 -1
View File
@@ -175,7 +175,7 @@ void CARDInit(const char* game, const char* maker) {
std::filesystem::path cardWorkingDir; std::filesystem::path cardWorkingDir;
if (aurora::g_config.userPath != nullptr) if (aurora::g_config.userPath != nullptr)
cardWorkingDir = reinterpret_cast<const char8_t*>(aurora::g_config.userPath); cardWorkingDir = fs_path_from_string(aurora::g_config.userPath);
else else
cardWorkingDir = std::filesystem::current_path(); cardWorkingDir = std::filesystem::current_path();
+22 -16
View File
@@ -1,3 +1,4 @@
#include "../../fs_helper.hpp"
#include "../../input.hpp" #include "../../input.hpp"
#include "../../internal.hpp" #include "../../internal.hpp"
#include <dolphin/pad.h> #include <dolphin/pad.h>
@@ -5,6 +6,7 @@
#include <SDL3/SDL_mouse.h> #include <SDL3/SDL_mouse.h>
#include <array> #include <array>
#include <atomic>
#include <sys/stat.h> #include <sys/stat.h>
#include <ranges> #include <ranges>
@@ -283,7 +285,7 @@ constexpr PADCLampRegion ClampRegion{
bool g_initialized; bool g_initialized;
bool g_keyboardBindingsLoaded = false; bool g_keyboardBindingsLoaded = false;
bool g_blockPAD = false; std::atomic_bool g_blockPAD{false};
bool g_suppressHeldOnRead = false; bool g_suppressHeldOnRead = false;
std::array<PADButton, PAD_CHANMAX> g_suppressedButtons{}; std::array<PADButton, PAD_CHANMAX> g_suppressedButtons{};
std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{}; std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
@@ -491,7 +493,7 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re
return; return;
} }
std::string basePath{aurora::g_config.userPath}; const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
if (!controller->m_mappingLoaded) { if (!controller->m_mappingLoaded) {
__PADSetDefaultMapping(controller); __PADSetDefaultMapping(controller);
controller->m_axisMapping = g_defaultAxes; controller->m_axisMapping = g_defaultAxes;
@@ -499,8 +501,9 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re
controller->m_mappingLoaded = true; controller->m_mappingLoaded = true;
const auto path = fmt::format("{}/{}_{:04X}_{:04X}.controller", basePath, PADGetName(playerIndex), controller->m_vid, const auto path = fs_path_to_string(
controller->m_pid); basePath / fmt::format("{}_{:04X}_{:04X}.controller", PADGetName(playerIndex), controller->m_vid,
controller->m_pid));
SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb"); SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb");
if (file == nullptr) { if (file == nullptr) {
return; return;
@@ -659,7 +662,8 @@ u32 PADRead(PADStatus* status) {
int numKeys = 0; int numKeys = 0;
const bool* kbState = SDL_GetKeyboardState(&numKeys); const bool* kbState = SDL_GetKeyboardState(&numKeys);
const bool captureHeldInput = g_suppressHeldOnRead && !g_blockPAD; const bool inputBlocked = g_blockPAD.load(std::memory_order_acquire);
const bool captureHeldInput = g_suppressHeldOnRead && !inputBlocked;
g_suppressHeldOnRead = false; g_suppressHeldOnRead = false;
uint32_t rumbleSupport = 0; uint32_t rumbleSupport = 0;
@@ -882,7 +886,7 @@ u32 PADRead(PADStatus* status) {
} }
} }
if (g_blockPAD) { if (inputBlocked) {
neutralize_status(status[i]); neutralize_status(status[i]);
} else { } else {
apply_unblock_suppression(status[i], i, captureHeldInput); apply_unblock_suppression(status[i], i, captureHeldInput);
@@ -1247,8 +1251,8 @@ constexpr uint32_t k_keyboardMagic = SBIG('KBND');
constexpr int32_t k_keyboardVersion = 3; constexpr int32_t k_keyboardVersion = 3;
static void load_keyboard_bindings() { static void load_keyboard_bindings() {
const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat"; const auto filePath = fs_path_from_string(aurora::g_config.userPath) / "keyboard_bindings.dat";
SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "rb"); SDL_IOStream* file = SDL_IOFromFile(fs_path_to_string(filePath).c_str(), "rb");
if (file == nullptr) { if (file == nullptr) {
return; return;
} }
@@ -1317,10 +1321,11 @@ static void load_keyboard_bindings() {
} }
static void save_keyboard_bindings() { static void save_keyboard_bindings() {
const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat"; const auto filePath = fs_path_from_string(aurora::g_config.userPath) / "keyboard_bindings.dat";
SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "wb"); const auto filePathStr = fs_path_to_string(filePath);
SDL_IOStream* file = SDL_IOFromFile(filePathStr.c_str(), "wb");
if (file == nullptr) { if (file == nullptr) {
aurora::input::Log.warn("save_keyboard_bindings: failed to open {} for writing", filePath.string()); aurora::input::Log.warn("save_keyboard_bindings: failed to open {} for writing", filePathStr);
return; return;
} }
@@ -1344,14 +1349,14 @@ void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier)
} }
void PADSerializeMappings() { void PADSerializeMappings() {
const std::filesystem::path basePath{aurora::g_config.userPath}; const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
for (auto& controller : aurora::input::g_GameControllers | std::views::values) { for (auto& controller : aurora::input::g_GameControllers | std::views::values) {
EnsureMappingLoaded(&controller); EnsureMappingLoaded(&controller);
const auto filePath = const auto filePath =
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index), basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
controller.m_vid, controller.m_pid); controller.m_vid, controller.m_pid);
std::string filePathStr = filePath.string(); std::string filePathStr = fs_path_to_string(filePath);
// don't truncate the file if it already exists // don't truncate the file if it already exists
const char* openMode = std::filesystem::exists(filePath) ? "r+b" : "wb"; const char* openMode = std::filesystem::exists(filePath) ? "r+b" : "wb";
@@ -1370,7 +1375,7 @@ void PADSerializeMappings() {
// start writing data at next 32-byte aligned offset // start writing data at next 32-byte aligned offset
const int64_t dataStart = SDL_TellIO(file) + 31 & ~31; const int64_t dataStart = SDL_TellIO(file) + 31 & ~31;
if (dataStart == -1) { if (dataStart == -1) {
aurora::input::Log.warn("Unable to seek in controller bindings! Path: \"{}\"", filePath.string()); aurora::input::Log.warn("Unable to seek in controller bindings! Path: \"{}\"", filePathStr);
return; return;
} }
SDL_SeekIO(file, dataStart, SDL_IO_SEEK_SET); SDL_SeekIO(file, dataStart, SDL_IO_SEEK_SET);
@@ -1530,12 +1535,13 @@ void PADRestoreDefaultMapping(const u32 port) {
} }
void PADBlockInput(const bool block) { void PADBlockInput(const bool block) {
if (g_blockPAD && !block) { if (g_blockPAD.exchange(block, std::memory_order_acq_rel) && !block) {
g_suppressHeldOnRead = true; g_suppressHeldOnRead = true;
} }
g_blockPAD = block;
} }
bool PADIsInputBlocked() { return g_blockPAD.load(std::memory_order_acquire); }
SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) { SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) {
const auto* ctrl = __PADGetControllerForIndex(index); const auto* ctrl = __PADGetControllerForIndex(index);
if (ctrl == nullptr) { if (ctrl == nullptr) {
+10 -2
View File
@@ -1,11 +1,19 @@
#pragma once #pragma once
#include <filesystem> #include <filesystem>
#include <string>
#include <string_view>
/** /**
* Converts a std::filesystem::path to a std::string, UTF-8, without exploding on Windows. * Narrow path strings crossing the aurora boundary are UTF-8. path::string() and the
* char path constructor go through the ANSI codepage on Windows, so they must not be
* used for anything the host handed us or hands back to SDL, sqlite or ImGui.
*/ */
inline std::string fs_path_to_string(const std::filesystem::path& path) { inline std::string fs_path_to_string(const std::filesystem::path& path) {
const auto u8str = path.u8string(); const auto u8str = path.u8string();
return { reinterpret_cast<const char*>(u8str.c_str()) }; return { reinterpret_cast<const char*>(u8str.c_str()), u8str.size() };
}
inline std::filesystem::path fs_path_from_string(std::string_view utf8) {
return std::filesystem::path(std::u8string(utf8.begin(), utf8.end()));
} }
+2 -1
View File
@@ -2,6 +2,7 @@
#include "clear.hpp" #include "clear.hpp"
#include "../gx/pipeline.hpp" #include "../gx/pipeline.hpp"
#include "../fs_helper.hpp"
#include "../sqlite_utils.hpp" #include "../sqlite_utils.hpp"
#include "../webgpu/gpu.hpp" #include "../webgpu/gpu.hpp"
@@ -715,7 +716,7 @@ static bool prepare_pipeline_cache_db() {
return true; return true;
} }
const auto path = (std::filesystem::path{g_config.pipelineCachePath} / "pipeline_cache.db").string(); const auto path = fs_path_to_string(fs_path_from_string(g_config.pipelineCachePath) / "pipeline_cache.db");
auto ret = sqlite3_open(path.c_str(), &g_pipelineCacheDb); auto ret = sqlite3_open(path.c_str(), &g_pipelineCacheDb);
if (ret != SQLITE_OK) { if (ret != SQLITE_OK) {
Log.error("Failed to open pipeline cache database: {}", sqlite3_errmsg(g_pipelineCacheDb)); Log.error("Failed to open pipeline cache database: {}", sqlite3_errmsg(g_pipelineCacheDb));
+2 -2
View File
@@ -506,8 +506,8 @@ void build_index() noexcept {
return; return;
} }
auto userPath = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.userPath)}; auto userPath = fs_path_from_string(g_config.userPath);
auto cachePath = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.cachePath)}; auto cachePath = fs_path_from_string(g_config.cachePath);
s_replacementRoot = userPath / "texture_replacements"; s_replacementRoot = userPath / "texture_replacements";
s_dumpRoot = cachePath / "texture_dumps"; s_dumpRoot = cachePath / "texture_dumps";
+2 -1
View File
@@ -10,6 +10,7 @@
#include <SDL3/SDL_events.h> #include <SDL3/SDL_events.h>
#include <SDL3/SDL_render.h> #include <SDL3/SDL_render.h>
#include "fs_helper.hpp"
#include "internal.hpp" #include "internal.hpp"
#include "webgpu/gpu.hpp" #include "webgpu/gpu.hpp"
#include "window.hpp" #include "window.hpp"
@@ -37,7 +38,7 @@ void remove_legacy_ini_file(const char* basePath) noexcept {
} }
std::error_code ec; std::error_code ec;
std::filesystem::remove(std::filesystem::path{basePath} / "imgui.ini", ec); std::filesystem::remove(fs_path_from_string(basePath) / "imgui.ini", ec);
} }
void create_context() noexcept { void create_context() noexcept {
+37 -4
View File
@@ -255,6 +255,18 @@ IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerId
: IdentityMatch::None; : IdentityMatch::None;
} }
void assign_player_index(GameController& controller, int32_t port) {
SDL_SetGamepadPlayerIndex(controller.m_controller, port);
controller.m_playerIndex = port;
}
// SDL forgets the index for devices mapped after connect, so player_index() falls
// back to the cached copy; both have to move together or a port looks doubly taken.
int32_t effective_player_index(const GameController& controller) {
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
return player >= 0 ? player : controller.m_playerIndex;
}
bool is_instance_claimed(const std::array<Uint32, PAD_MAX_CONTROLLERS>& claimedControllers, size_t claimedCount, bool is_instance_claimed(const std::array<Uint32, PAD_MAX_CONTROLLERS>& claimedControllers, size_t claimedCount,
Uint32 instance) { Uint32 instance) {
return std::find(claimedControllers.begin(), claimedControllers.begin() + claimedCount, instance) != return std::find(claimedControllers.begin(), claimedControllers.begin() + claimedCount, instance) !=
@@ -269,10 +281,10 @@ void apply_port_preferences() noexcept {
} }
for (auto& [instance, controller] : g_GameControllers) { for (auto& [instance, controller] : g_GameControllers) {
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller); const int32_t player = effective_player_index(controller);
if (player >= 0 && player < PAD_MAX_CONTROLLERS && g_portPreferences[player].state != PortPreferenceState::Unset) { if (player >= 0 && player < PAD_MAX_CONTROLLERS && g_portPreferences[player].state != PortPreferenceState::Unset) {
// Keep SDL's default player assignment from taking explicitly configured ports // Keep SDL's default player assignment from taking explicitly configured ports
SDL_SetGamepadPlayerIndex(controller.m_controller, -1); assign_player_index(controller, -1);
} }
} }
@@ -293,7 +305,7 @@ void apply_port_preferences() noexcept {
switch (identity_match(preference.identity, controller_identity(controller))) { switch (identity_match(preference.identity, controller_identity(controller))) {
case IdentityMatch::Exact: case IdentityMatch::Exact:
SDL_SetGamepadPlayerIndex(controller.m_controller, static_cast<int32_t>(port)); assign_player_index(controller, static_cast<int32_t>(port));
claimedControllers[claimedCount++] = instance; claimedControllers[claimedCount++] = instance;
fallbackController = nullptr; fallbackController = nullptr;
break; break;
@@ -311,11 +323,18 @@ void apply_port_preferences() noexcept {
} }
if (fallbackController != nullptr) { if (fallbackController != nullptr) {
SDL_SetGamepadPlayerIndex(fallbackController->m_controller, static_cast<int32_t>(port)); assign_player_index(*fallbackController, static_cast<int32_t>(port));
claimedControllers[claimedCount++] = fallbackInstance; claimedControllers[claimedCount++] = fallbackInstance;
} }
} }
} }
// Ports are explicit assignments. SDL may choose a player index at connection
// time, but accepting it would make a newly connected controller silently take
// over a game port before the user assigns it in the controller menu.
void ensure_player_index(GameController& controller) noexcept {
assign_player_index(controller, -1);
}
} // namespace } // namespace
GameController* get_controller_for_player(uint32_t player) noexcept { GameController* get_controller_for_player(uint32_t player) noexcept {
@@ -364,6 +383,7 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
controller.m_hasRgbLed = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false); controller.m_hasRgbLed = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false);
SDL_JoystickID instance = SDL_GetJoystickID(SDL_GetGamepadJoystick(ctrl)); SDL_JoystickID instance = SDL_GetJoystickID(SDL_GetGamepadJoystick(ctrl));
g_GameControllers[instance] = controller; g_GameControllers[instance] = controller;
ensure_player_index(g_GameControllers[instance]);
apply_port_preferences(); apply_port_preferences();
return instance; return instance;
} }
@@ -371,6 +391,19 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
return -1; return -1;
} }
bool refresh_controller(SDL_JoystickID instance) noexcept {
const auto it = g_GameControllers.find(instance);
if (it == g_GameControllers.end()) {
return false;
}
// The SDL mapping changed underneath us; drop the cached PAD bindings so they
// are rebuilt from the new one.
it->second.m_mappingLoaded = false;
ensure_player_index(it->second);
apply_port_preferences();
return true;
}
void remove_controller(Uint32 instance) noexcept { void remove_controller(Uint32 instance) noexcept {
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) { if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
SDL_CloseGamepad(it->second.m_controller); SDL_CloseGamepad(it->second.m_controller);
+1
View File
@@ -51,6 +51,7 @@ struct GameController {
GameController* get_controller_for_player(uint32_t player) noexcept; GameController* get_controller_for_player(uint32_t player) noexcept;
Sint32 get_instance_for_player(uint32_t player) noexcept; Sint32 get_instance_for_player(uint32_t player) noexcept;
SDL_JoystickID add_controller(SDL_JoystickID which) noexcept; SDL_JoystickID add_controller(SDL_JoystickID which) noexcept;
bool refresh_controller(SDL_JoystickID instance) noexcept;
void remove_controller(Uint32 instance) noexcept; void remove_controller(Uint32 instance) noexcept;
Sint32 player_index(Uint32 instance) noexcept; Sint32 player_index(Uint32 instance) noexcept;
void set_player_index(Uint32 instance, Sint32 index) noexcept; void set_player_index(Uint32 instance, Sint32 index) noexcept;
+7 -3
View File
@@ -137,7 +137,7 @@ static void prune_stale_rows() {
static bool cache_init_core() { static bool cache_init_core() {
Log.debug("SQLite version {}", sqlite3_libversion()); Log.debug("SQLite version {}", sqlite3_libversion());
const auto path = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.cachePath)} / "dawn_cache.db"; const auto path = fs_path_from_string(g_config.cachePath) / "dawn_cache.db";
std::string file = fs_path_to_string(path); std::string file = fs_path_to_string(path);
Log.debug("Using dawn cache at {}", file); Log.debug("Using dawn cache at {}", file);
auto ret = sqlite3_open(file.c_str(), &db); auto ret = sqlite3_open(file.c_str(), &db);
@@ -165,8 +165,12 @@ static bool cache_init_core() {
db = nullptr; db = nullptr;
std::error_code ec; std::error_code ec;
std::filesystem::remove(path, ec); std::filesystem::remove(path, ec);
std::filesystem::remove(std::filesystem::path{file + "-wal"}, ec); auto wal = path;
std::filesystem::remove(std::filesystem::path{file + "-shm"}, ec); wal += "-wal";
std::filesystem::remove(wal, ec);
auto shm = path;
shm += "-shm";
std::filesystem::remove(shm, ec);
ret = sqlite3_open(file.c_str(), &db); ret = sqlite3_open(file.c_str(), &db);
if (ret != SQLITE_OK) { if (ret != SQLITE_OK) {
Log.error("Failed to recreate database: {}", sqlite3_errmsg(db)); Log.error("Failed to recreate database: {}", sqlite3_errmsg(db));
+9
View File
@@ -294,6 +294,15 @@ void process_event(SDL_Event& event) {
}); });
break; break;
} }
case SDL_EVENT_GAMEPAD_REMAPPED: {
if (input::refresh_controller(event.gdevice.which)) {
g_events.push_back(AuroraEvent{
.type = AURORA_CONTROLLER_ADDED,
.controller = event.gdevice.which,
});
}
break;
}
case SDL_EVENT_GAMEPAD_REMOVED: { case SDL_EVENT_GAMEPAD_REMOVED: {
input::remove_controller(event.gdevice.which); input::remove_controller(event.gdevice.which);
g_events.push_back(AuroraEvent{ g_events.push_back(AuroraEvent{
+1 -1
View File
@@ -205,7 +205,7 @@ function(mkw_configure_product target)
endif() endif()
target_link_libraries(${target} PRIVATE target_link_libraries(${target} PRIVATE
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp) dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp setupapi winusb)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE) set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
foreach(runtime_dll libc++.dll libunwind.dll) foreach(runtime_dll libc++.dll libunwind.dll)
+2 -1
View File
@@ -81,7 +81,8 @@ inline bool WriteSerial(const std::filesystem::path& path, const std::string& se
return false; return false;
} }
const std::filesystem::path temporary = path.string() + ".tmp"; std::filesystem::path temporary = path;
temporary += ".tmp";
{ {
std::ofstream output(temporary, std::ios::trunc); std::ofstream output(temporary, std::ios::trunc);
if (!output) { if (!output) {
@@ -0,0 +1,21 @@
#pragma once
#include <SDL3/SDL_events.h>
// Press-to-bind setup for joysticks SDL either doesn't recognize as gamepads or
// recognizes with a mapping that lacks the analog stick (e.g. raphnet adapters).
// The wizard produces a standard SDL gamepad mapping, applies it live, and
// persists it to gamecontrollerdb.txt in the user data directory.
namespace controller_mapping_wizard {
void LoadPersistedMappings();
void HandleSdlEvent(const SDL_Event& event);
// Lists devices that need setup inside the controller settings menu.
void DrawSetupList();
// Draws the wizard window when active; call once per overlay frame.
void Draw();
bool IsActive();
} // namespace controller_mapping_wizard
+2 -2
View File
@@ -20,14 +20,14 @@
namespace DvdFstContract { namespace DvdFstContract {
struct RegisteredFile { struct RegisteredFile {
std::string hostPath; std::filesystem::path hostPath;
std::string dvdPath; std::string dvdPath;
uint32_t size = 0; uint32_t size = 0;
uint32_t discOffsetWords = 0; uint32_t discOffsetWords = 0;
}; };
struct IndexedEntry { struct IndexedEntry {
std::string hostPath; std::filesystem::path hostPath;
std::string dvdPath; std::string dvdPath;
uint32_t size = 0; uint32_t size = 0;
uint32_t discOffsetWords = 0; uint32_t discOffsetWords = 0;
+8 -20
View File
@@ -27,13 +27,14 @@ inline std::optional<std::filesystem::path> ExistingDirectory(const std::filesys
if (path.empty()) { if (path.empty()) {
RT_LOGF(RT_TAG_NAND, "ERROR: %s\n", message); RT_LOGF(RT_TAG_NAND, "ERROR: %s\n", message);
} else { } else {
RT_LOGF(RT_TAG_NAND, "ERROR: %s: %s\n", message, path.string().c_str()); RT_LOGF(RT_TAG_NAND, "ERROR: %s: %s\n", message,
RuntimeConfigFile::PathToUtf8(path).c_str());
} }
RT_LOGF(RT_TAG_NAND, "Set [paths] nand_root in Config.toml.\n"); RT_LOGF(RT_TAG_NAND, "Set [paths] nand_root in Config.toml.\n");
std::string details = message ? message : "The configured NAND could not be initialized."; std::string details = message ? message : "The configured NAND could not be initialized.";
if (!path.empty()) { if (!path.empty()) {
details += "\n\nPath: "; details += "\n\nPath: ";
details += path.string(); details += RuntimeConfigFile::PathToUtf8(path);
} }
details += "\n\nSet [paths] nand_root in Config.toml and try again."; details += "\n\nSet [paths] nand_root in Config.toml and try again.";
// Same fatal idiom as the DVD and OS paths: crash artifacts first so the run // Same fatal idiom as the DVD and OS paths: crash artifacts first so the run
@@ -51,18 +52,6 @@ inline std::filesystem::path ResolveConfiguredPath(const std::string& value) {
return RuntimeConfigFile::ResolveRelativeToConfig(value); return RuntimeConfigFile::ResolveRelativeToConfig(value);
} }
inline std::string PathStringWithoutTrailingSeparators(std::filesystem::path path) {
std::string text = path.string();
while (!text.empty()) {
const char tail = text.back();
if (tail != '\\' && tail != '/') {
break;
}
text.pop_back();
}
return text;
}
inline std::filesystem::path ManagedNandRootPath() { inline std::filesystem::path ManagedNandRootPath() {
return RuntimeConfigFile::ApplicationDataDirectory() / "NAND"; return RuntimeConfigFile::ApplicationDataDirectory() / "NAND";
} }
@@ -134,7 +123,9 @@ inline bool SeedMissingBootstrapFiles(const std::filesystem::path& root) {
const std::filesystem::path relativePath{std::string(file)}; const std::filesystem::path relativePath{std::string(file)};
ec.clear(); ec.clear();
if (!CopyBootstrapFile(*payload, root, relativePath, ec)) { if (!CopyBootstrapFile(*payload, root, relativePath, ec)) {
RT_LOG(RT_TAG_NAND) << "could not create " << (root / relativePath).string() << std::endl; RT_LOG(RT_TAG_NAND) << "could not create "
<< RuntimeConfigFile::PathToUtf8(root / relativePath)
<< std::endl;
return false; return false;
} }
} }
@@ -167,7 +158,8 @@ inline std::filesystem::path CreateManagedNandRoot() {
} }
} }
RT_LOG(RT_TAG_NAND) << "using managed NAND root: " << root.string() << std::endl; RT_LOG(RT_TAG_NAND) << "using managed NAND root: " << RuntimeConfigFile::PathToUtf8(root)
<< std::endl;
return root; return root;
} }
@@ -187,8 +179,4 @@ inline std::filesystem::path DiscoverNandRootPath() {
return CreateManagedNandRoot(); return CreateManagedNandRoot();
} }
inline std::string DiscoverNandRootString() {
return PathStringWithoutTrailingSeparators(DiscoverNandRootPath());
}
} // namespace RuntimeNandPath } // namespace RuntimeNandPath
+4 -1
View File
@@ -3,6 +3,7 @@
#include <atomic> #include <atomic>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <filesystem>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <vector> #include <vector>
@@ -67,8 +68,10 @@ void RunMemoryInitializers();
void RegisterPostRelInitializer(InitializerFn fn); void RegisterPostRelInitializer(InitializerFn fn);
void RunPostRelInitializers(); void RunPostRelInitializers();
// The generated call site passes a UTF-8 literal; it is decoded once here and
// stays a path from then on.
void RegisterDvdOverlayRoot(std::string root); void RegisterDvdOverlayRoot(std::string root);
const std::vector<std::string>& DvdOverlayRoots(); const std::vector<std::filesystem::path>& DvdOverlayRoots();
// Riivolution settings pinned by the distribution's recomp.yml. The XML path is // Riivolution settings pinned by the distribution's recomp.yml. The XML path is
// relative to the pack/overlay root; option selections use Riivolution's 1-based // relative to the pack/overlay root; option selections use Riivolution's 1-based
+38 -4
View File
@@ -60,10 +60,25 @@ struct RuntimeUserConfig {
// comma-separated SDL-style physical button names ("south", or // comma-separated SDL-style physical button names ("south", or
// "dpad_up,left_shoulder") as values; pressing either bound button counts. // "dpad_up,left_shoulder") as values; pressing either bound button counts.
std::array<std::optional<std::string>, 12> controllerButtons; std::array<std::optional<std::string>, 12> controllerButtons;
// One-based physical WUP-028 adapter port assigned to each game port.
// Zero or a missing value means the adapter does not own that game port.
std::array<uint32_t, 4> gameCubeAdapterPorts{};
}; };
namespace RuntimeConfigFile { namespace RuntimeConfigFile {
// Narrow path strings are UTF-8 everywhere in the runtime; string() and the
// char path constructor would use the ANSI codepage on Windows, which drops
// characters the codepage cannot represent.
inline std::string PathToUtf8(const std::filesystem::path& path) {
const std::u8string text = path.u8string();
return std::string(text.begin(), text.end());
}
inline std::filesystem::path PathFromUtf8(std::string_view text) {
return std::filesystem::path(std::u8string(text.begin(), text.end()));
}
inline constexpr const char* kConfigFileName = "Config.toml"; inline constexpr const char* kConfigFileName = "Config.toml";
inline constexpr const char* kApplicationDirectoryName = "WiiCompiled"; inline constexpr const char* kApplicationDirectoryName = "WiiCompiled";
@@ -323,6 +338,12 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.controllerButtons[index] = config.controllerButtons[index] =
FindConfigValue<std::string>(document, "controller", buttonKeys[index]); FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
} }
for (size_t index = 0; index < config.gameCubeAdapterPorts.size(); ++index) {
const std::string key = "adapter_port_" + std::to_string(index + 1);
if (auto value = FindConfigUint(document, "controller", key); value && *value <= 4) {
config.gameCubeAdapterPorts[index] = *value;
}
}
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen"); config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
config.windowPosX = FindConfigInt(document, "video", "window_x"); config.windowPosX = FindConfigInt(document, "video", "window_x");
@@ -410,7 +431,7 @@ inline RuntimeUserConfig ParseConfig(std::istream& input, std::string sourceName
inline RuntimeUserConfig LoadConfigFile() { inline RuntimeUserConfig LoadConfigFile() {
EnsureConfigFile(); EnsureConfigFile();
std::ifstream file(ResolveConfigPath(), std::ios::binary); std::ifstream file(ResolveConfigPath(), std::ios::binary);
return file ? ParseConfig(file, ResolveConfigPath().string()) : RuntimeUserConfig{}; return file ? ParseConfig(file, PathToUtf8(ResolveConfigPath())) : RuntimeUserConfig{};
} }
inline const RuntimeUserConfig& Get() { inline const RuntimeUserConfig& Get() {
@@ -501,7 +522,7 @@ inline bool WriteSetting(std::string_view section, std::string_view key, std::st
} }
std::ofstream output(path, std::ios::trunc); std::ofstream output(path, std::ios::trunc);
if (!output) { if (!output) {
std::cerr << "[runtime-config] Unable to write " << path.string() << std::endl; std::cerr << "[runtime-config] Unable to write " << PathToUtf8(path) << std::endl;
return false; return false;
} }
for (const auto& outputLine : lines) { for (const auto& outputLine : lines) {
@@ -586,6 +607,19 @@ inline bool SetControllerButton(size_t index, std::string value) {
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value)); return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
} }
inline int GameCubeAdapterPort(size_t gamePort) {
if (gamePort >= Get().gameCubeAdapterPorts.size()) return -1;
const uint32_t physicalPort = Get().gameCubeAdapterPorts[gamePort];
return physicalPort >= 1 && physicalPort <= 4 ? static_cast<int>(physicalPort - 1) : -1;
}
inline bool SetGameCubeAdapterPort(size_t gamePort, int physicalPort) {
if (gamePort >= Mutable().gameCubeAdapterPorts.size() || physicalPort < -1 || physicalPort >= 4) return false;
const uint32_t storedPort = physicalPort < 0 ? 0u : static_cast<uint32_t>(physicalPort + 1);
Mutable().gameCubeAdapterPorts[gamePort] = storedPort;
return WriteSetting("controller", "adapter_port_" + std::to_string(gamePort + 1), std::to_string(storedPort));
}
inline bool SetAudioVolume(float value) { inline bool SetAudioVolume(float value) {
value = std::clamp(value, 0.0f, 1.0f); value = std::clamp(value, 0.0f, 1.0f);
Mutable().audioVolume = value; Mutable().audioVolume = value;
@@ -754,7 +788,7 @@ inline std::string DvdRoot(std::string fallback = "") {
// never to the process working directory (docs/WHEELWIZARD_CONTRACT.md). // never to the process working directory (docs/WHEELWIZARD_CONTRACT.md).
inline std::filesystem::path ResolveRelativeTo(const std::filesystem::path& base, inline std::filesystem::path ResolveRelativeTo(const std::filesystem::path& base,
const std::string& value) { const std::string& value) {
std::filesystem::path path(value); std::filesystem::path path = PathFromUtf8(value);
if (path.is_relative()) { if (path.is_relative()) {
path = base / path; path = base / path;
} }
@@ -784,7 +818,7 @@ inline void LogLoadedConfig() {
static const bool logged = [] { static const bool logged = [] {
const auto& config = Get(); const auto& config = Get();
const auto configPath = ResolveConfigPath(); const auto configPath = ResolveConfigPath();
std::cout << "[runtime-config] " << configPath.string(); std::cout << "[runtime-config] " << PathToUtf8(configPath);
if (!std::filesystem::exists(configPath)) { if (!std::filesystem::exists(configPath)) {
std::cout << " not found; using built-in defaults"; std::cout << " not found; using built-in defaults";
} else { } else {
+2 -1
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <filesystem>
#include <optional> #include <optional>
#include <ostream> #include <ostream>
#include <string> #include <string>
@@ -72,5 +73,5 @@ public:
// `mem1Path` and MEM2 to `mem1Path + ".mem2"`, logging outcomes to `os`. // `mem1Path` and MEM2 to `mem1Path + ".mem2"`, logging outcomes to `os`.
static void DumpCrashHeuristics(std::ostream& os, const struct CpuContext* cpu, static void DumpCrashHeuristics(std::ostream& os, const struct CpuContext* cpu,
const uint32_t* missingGuestTarget); const uint32_t* missingGuestTarget);
static void WriteGuestMemorySnapshot(std::ostream& os, const char* mem1Path); static void WriteGuestMemorySnapshot(std::ostream& os, const std::filesystem::path& mem1Path);
}; };
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <array>
#include <cstdint>
#include <string>
struct PADStatus;
namespace Wup028Adapter {
enum class ConnectionState : uint8_t {
Searching,
DriverError,
Connected,
};
struct AdapterInfo {
ConnectionState state = ConnectionState::Searching;
std::string deviceName;
std::string detail;
float pollRateHz = 0.0f;
uint8_t inputEndpoint = 0;
uint8_t outputEndpoint = 0;
std::array<bool, 4> ports{};
// Raw adapter type/status byte. High nibble 1 is wired, 2 is wireless.
std::array<uint8_t, 4> portStatus{};
std::array<uint64_t, 4> portChangeSequence{};
};
// Starts the project-owned WUP-028 worker. Safe to call more than once.
void Initialize();
void Shutdown();
// Returns true while an official adapter is open. Each entry is a native
// GameCube port; an empty port is represented by PAD_ERR_NO_CONTROLLER.
bool Read(std::array<PADStatus, 4>& statuses);
// Assigns a physical adapter port to a game port. Pass -1 to leave the game
// port under Aurora's normal controller assignment.
void SetPortAssignment(uint32_t gamePort, int physicalPort);
int GetPortAssignment(uint32_t gamePort);
// Returns true when the command belongs to an active WUP-028, allowing the
// caller to avoid also sending it to Aurora's unrelated controller backend.
bool SetRumble(uint32_t port, bool enabled);
AdapterInfo GetInfo();
} // namespace Wup028Adapter
+477
View File
@@ -0,0 +1,477 @@
#include "controller_mapping_wizard.h"
#include "runtime_config.h"
#include "runtime_log.h"
#include <imgui.h>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_joystick.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <optional>
#include <string>
#include <vector>
namespace controller_mapping_wizard {
namespace {
using Clock = std::chrono::steady_clock;
constexpr int16_t kStickThreshold = 16000;
constexpr int16_t kTriggerThreshold = 10000;
constexpr auto kCaptureDebounce = std::chrono::milliseconds(350);
enum class StepKind {
Button, // button or single-direction hat press
Trigger, // button press or axis pull
Stick, // axis motion in the prompted direction
};
struct Step {
const char* mappingKey;
const char* prompt;
StepKind kind;
};
// Prompts describe what the control does in-game; the SDL fields land on the
// right GC controls through pad.cpp's "standard" defaults (Z lives on
// rightshoulder, L/R on the trigger axes).
constexpr std::array<Step, 16> kSteps = {{
{"a", "Press the button for A (accelerate / select)", StepKind::Button},
{"b", "Press the button for B (brake / back)", StepKind::Button},
{"x", "Press the button for X", StepKind::Button},
{"y", "Press the button for Y", StepKind::Button},
{"start", "Press the button for pause (Start)", StepKind::Button},
{"rightshoulder", "Press the button for rear view (Z)", StepKind::Button},
{"lefttrigger", "Press or pull the control for using items (L)", StepKind::Trigger},
{"righttrigger", "Press or pull the control for hop / drift (R)", StepKind::Trigger},
{"dpup", "Press D-pad Up", StepKind::Button},
{"dpdown", "Press D-pad Down", StepKind::Button},
{"dpleft", "Press D-pad Left", StepKind::Button},
{"dpright", "Press D-pad Right", StepKind::Button},
{"leftx", "Move the Control Stick LEFT", StepKind::Stick},
{"lefty", "Move the Control Stick UP", StepKind::Stick},
{"rightx", "Move the C-Stick LEFT (or Skip)", StepKind::Stick},
{"righty", "Move the C-Stick UP (or Skip)", StepKind::Stick},
}};
struct WizardState {
bool active = false;
SDL_JoystickID instance = 0;
SDL_Joystick* joystick = nullptr;
bool ownsJoystick = false;
std::string deviceName;
size_t stepIndex = 0;
std::array<std::optional<std::string>, kSteps.size()> bindings{};
std::vector<int16_t> axisBaseline;
Clock::time_point acceptAfter{};
std::string status;
};
WizardState g_wizard;
std::filesystem::path MappingDbPath() {
return RuntimeConfigFile::ApplicationDataDirectory() / "gamecontrollerdb.txt";
}
std::string GuidString(SDL_JoystickID instance) {
char buf[33] = {};
SDL_GUIDToString(SDL_GetJoystickGUIDForID(instance), buf, sizeof(buf));
return buf;
}
bool BindingUsed(const std::string& value) {
return std::any_of(g_wizard.bindings.begin(), g_wizard.bindings.end(),
[&](const std::optional<std::string>& b) { return b && *b == value; });
}
void SnapshotAxes() {
g_wizard.axisBaseline.clear();
const int axes = SDL_GetNumJoystickAxes(g_wizard.joystick);
for (int i = 0; i < axes; ++i) {
g_wizard.axisBaseline.push_back(SDL_GetJoystickAxis(g_wizard.joystick, i));
}
}
void AdvanceStep(std::optional<std::string> value) {
g_wizard.bindings[g_wizard.stepIndex] = std::move(value);
++g_wizard.stepIndex;
g_wizard.acceptAfter = Clock::now() + kCaptureDebounce;
SnapshotAxes();
}
void StopWizard() {
if (g_wizard.ownsJoystick && g_wizard.joystick != nullptr) {
SDL_CloseJoystick(g_wizard.joystick);
}
g_wizard = WizardState{};
}
void StartWizard(SDL_JoystickID instance) {
StopWizard();
SDL_Joystick* joystick = nullptr;
bool owns = false;
if (SDL_Gamepad* gamepad = SDL_GetGamepadFromID(instance)) {
joystick = SDL_GetGamepadJoystick(gamepad);
} else {
joystick = SDL_OpenJoystick(instance);
owns = true;
}
if (joystick == nullptr) {
RT_LOG(RT_TAG_CONFIG) << "controller wizard: failed to open joystick " << instance << ": "
<< SDL_GetError() << std::endl;
return;
}
g_wizard.active = true;
g_wizard.instance = instance;
g_wizard.joystick = joystick;
g_wizard.ownsJoystick = owns;
const char* name = SDL_GetJoystickNameForID(instance);
g_wizard.deviceName = name != nullptr ? name : "Controller";
g_wizard.acceptAfter = Clock::now() + kCaptureDebounce;
SnapshotAxes();
}
std::string BuildMappingString() {
std::string name = g_wizard.deviceName;
std::replace(name.begin(), name.end(), ',', ' ');
std::string mapping = GuidString(g_wizard.instance) + "," + name + ",";
for (size_t i = 0; i < kSteps.size(); ++i) {
if (g_wizard.bindings[i]) {
mapping += std::string(kSteps[i].mappingKey) + ":" + *g_wizard.bindings[i] + ",";
}
}
mapping += "platform:Windows,";
return mapping;
}
bool PersistMapping(const std::string& guid, const std::string& mapping) {
const std::filesystem::path path = MappingDbPath();
std::vector<std::string> lines;
{
std::ifstream in(path);
std::string line;
while (std::getline(in, line)) {
if (line.rfind(guid + ",", 0) != 0) {
lines.push_back(line);
}
}
}
lines.push_back(mapping);
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
std::ofstream out(path, std::ios::trunc);
if (!out) {
return false;
}
for (const auto& line : lines) {
out << line << '\n';
}
out.close();
return static_cast<bool>(out);
}
void FinishWizard() {
const std::string guid = GuidString(g_wizard.instance);
const std::string mapping = BuildMappingString();
if (SDL_AddGamepadMapping(mapping.c_str()) < 0) {
g_wizard.status = std::string("Failed to apply mapping: ") + SDL_GetError();
RT_LOG(RT_TAG_CONFIG) << "controller wizard: " << g_wizard.status << " (" << mapping << ")"
<< std::endl;
return;
}
if (!PersistMapping(guid, mapping)) {
g_wizard.status =
"Failed to save mapping to " + RuntimeConfigFile::PathToUtf8(MappingDbPath());
RT_LOG(RT_TAG_CONFIG) << "controller wizard: " << g_wizard.status << std::endl;
return;
}
RT_LOG(RT_TAG_CONFIG) << "controller wizard: applied mapping " << mapping << std::endl;
StopWizard();
}
struct SetupCandidate {
SDL_JoystickID id;
std::string name;
bool incompleteMapping;
};
// A device needs setup when SDL has no gamepad mapping for it at all, or when
// the mapping it matched has no analog stick even though the hardware reports
// axes (SDL's built-in raphnet WUSBMote entry is button-only).
std::vector<SetupCandidate> CollectCandidates() {
std::vector<SetupCandidate> candidates;
int count = 0;
SDL_JoystickID* ids = SDL_GetJoysticks(&count);
if (ids == nullptr) {
return candidates;
}
for (int i = 0; i < count; ++i) {
const SDL_JoystickID id = ids[i];
const char* rawName = SDL_GetJoystickNameForID(id);
const std::string name = rawName != nullptr ? rawName : "Unknown controller";
if (!SDL_IsGamepad(id)) {
candidates.push_back({id, name, false});
continue;
}
SDL_Gamepad* gamepad = SDL_GetGamepadFromID(id);
if (gamepad == nullptr) {
continue;
}
char* mapping = SDL_GetGamepadMappingForID(id);
if (mapping == nullptr) {
continue;
}
const std::string mappingStr = mapping;
SDL_free(mapping);
const bool hasStick = mappingStr.find("leftx:") != std::string::npos &&
mappingStr.find("lefty:") != std::string::npos;
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
if (!hasStick && joystick != nullptr && SDL_GetNumJoystickAxes(joystick) >= 2) {
candidates.push_back({id, name, true});
}
}
SDL_free(ids);
return candidates;
}
void HandleButtonDown(const SDL_JoyButtonEvent& event) {
const Step& step = kSteps[g_wizard.stepIndex];
if (step.kind == StepKind::Stick) {
return;
}
const std::string value = "b" + std::to_string(event.button);
if (BindingUsed(value)) {
g_wizard.status = "That button is already bound";
return;
}
g_wizard.status.clear();
AdvanceStep(value);
}
void HandleHatMotion(const SDL_JoyHatEvent& event) {
const Step& step = kSteps[g_wizard.stepIndex];
if (step.kind == StepKind::Stick) {
return;
}
// Only single-direction presses bind cleanly; diagonals are ignored.
if (event.value != SDL_HAT_UP && event.value != SDL_HAT_RIGHT && event.value != SDL_HAT_DOWN &&
event.value != SDL_HAT_LEFT) {
return;
}
const std::string value =
"h" + std::to_string(event.hat) + "." + std::to_string(static_cast<int>(event.value));
if (BindingUsed(value)) {
g_wizard.status = "That direction is already bound";
return;
}
g_wizard.status.clear();
AdvanceStep(value);
}
void HandleAxisMotion(const SDL_JoyAxisEvent& event) {
const Step& step = kSteps[g_wizard.stepIndex];
if (step.kind == StepKind::Button) {
return;
}
if (event.axis >= g_wizard.axisBaseline.size()) {
return;
}
const int32_t delta =
static_cast<int32_t>(event.value) - static_cast<int32_t>(g_wizard.axisBaseline[event.axis]);
const int16_t threshold = step.kind == StepKind::Stick ? kStickThreshold : kTriggerThreshold;
if (std::abs(delta) < threshold) {
return;
}
// Stick prompts ask for LEFT/UP, which SDL expects to be negative; triggers
// are expected to increase when pulled. A wrong-way delta means the raw
// axis is inverted, which the mapping expresses with a '~' suffix.
const bool expectNegative = step.kind == StepKind::Stick;
const bool inverted = expectNegative ? delta > 0 : delta < 0;
std::string value = "a" + std::to_string(event.axis);
// Reject reusing an axis already bound (with or without inversion).
if (BindingUsed(value) || BindingUsed(value + "~")) {
g_wizard.status = "That axis is already bound";
return;
}
if (inverted) {
value += "~";
}
g_wizard.status.clear();
AdvanceStep(value);
}
} // namespace
void LoadPersistedMappings() {
std::ifstream in(MappingDbPath());
if (!in) {
return;
}
std::string line;
int added = 0;
while (std::getline(in, line)) {
const std::string trimmed = RuntimeConfigFile::Trim(line);
if (trimmed.empty() || trimmed[0] == '#') {
continue;
}
if (SDL_AddGamepadMapping(trimmed.c_str()) >= 0) {
++added;
} else {
RT_LOG(RT_TAG_CONFIG) << "gamecontrollerdb.txt: rejected mapping: " << trimmed
<< " (" << SDL_GetError() << ")" << std::endl;
}
}
if (added > 0) {
RT_LOG(RT_TAG_CONFIG) << "gamecontrollerdb.txt: applied " << added << " custom mapping"
<< (added == 1 ? "" : "s") << std::endl;
}
}
void HandleSdlEvent(const SDL_Event& event) {
if (!g_wizard.active) {
return;
}
if (event.type == SDL_EVENT_JOYSTICK_REMOVED && event.jdevice.which == g_wizard.instance) {
StopWizard();
return;
}
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE) {
StopWizard();
return;
}
if (g_wizard.stepIndex >= kSteps.size() || Clock::now() < g_wizard.acceptAfter) {
return;
}
switch (event.type) {
case SDL_EVENT_JOYSTICK_BUTTON_DOWN:
if (event.jbutton.which == g_wizard.instance) {
HandleButtonDown(event.jbutton);
}
break;
case SDL_EVENT_JOYSTICK_HAT_MOTION:
if (event.jhat.which == g_wizard.instance) {
HandleHatMotion(event.jhat);
}
break;
case SDL_EVENT_JOYSTICK_AXIS_MOTION:
if (event.jaxis.which == g_wizard.instance) {
HandleAxisMotion(event.jaxis);
}
break;
default:
break;
}
}
void DrawSetupList() {
const std::vector<SetupCandidate> candidates = CollectCandidates();
if (candidates.empty()) {
return;
}
ImGui::SeparatorText("Unrecognized controllers");
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::TextDisabled(
"These devices have no usable gamepad mapping. Set one up by pressing "
"each control when asked.");
ImGui::PopTextWrapPos();
for (const auto& candidate : candidates) {
ImGui::PushID(static_cast<int>(candidate.id));
ImGui::TextUnformatted(candidate.name.c_str());
ImGui::SameLine();
if (ImGui::SmallButton(candidate.incompleteMapping ? "Fix mapping" : "Set up")) {
StartWizard(candidate.id);
}
if (candidate.incompleteMapping && ImGui::IsItemHovered()) {
ImGui::SetTooltip("SDL matched a mapping without an analog stick for this device");
}
ImGui::PopID();
}
}
void Draw() {
if (!g_wizard.active) {
return;
}
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + viewport->Size.x * 0.5f,
viewport->Pos.y + viewport->Size.y * 0.5f),
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(420.0f, 0.0f), ImGuiCond_Appearing);
bool open = true;
if (ImGui::Begin("Controller setup", &open,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings)) {
ImGui::TextUnformatted(g_wizard.deviceName.c_str());
ImGui::Separator();
if (g_wizard.stepIndex < kSteps.size()) {
ImGui::Text("Step %d of %d", static_cast<int>(g_wizard.stepIndex + 1),
static_cast<int>(kSteps.size()));
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 400.0f);
ImGui::TextUnformatted(kSteps[g_wizard.stepIndex].prompt);
ImGui::PopTextWrapPos();
ImGui::Spacing();
if (ImGui::Button("Skip")) {
g_wizard.status.clear();
AdvanceStep(std::nullopt);
}
ImGui::SameLine();
ImGui::BeginDisabled(g_wizard.stepIndex == 0);
if (ImGui::Button("Back")) {
--g_wizard.stepIndex;
g_wizard.bindings[g_wizard.stepIndex].reset();
g_wizard.status.clear();
g_wizard.acceptAfter = Clock::now() + kCaptureDebounce;
SnapshotAxes();
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
open = false;
}
} else {
const size_t boundCount =
std::count_if(g_wizard.bindings.begin(), g_wizard.bindings.end(),
[](const std::optional<std::string>& b) { return b.has_value(); });
ImGui::Text("Captured %d of %d controls.", static_cast<int>(boundCount),
static_cast<int>(kSteps.size()));
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 400.0f);
ImGui::TextDisabled("Save applies the mapping now and remembers it for future launches.");
ImGui::PopTextWrapPos();
ImGui::Spacing();
ImGui::BeginDisabled(boundCount == 0);
if (ImGui::Button("Save")) {
FinishWizard();
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Back")) {
--g_wizard.stepIndex;
g_wizard.bindings[g_wizard.stepIndex].reset();
g_wizard.acceptAfter = Clock::now() + kCaptureDebounce;
SnapshotAxes();
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
open = false;
}
}
if (!g_wizard.status.empty()) {
ImGui::Spacing();
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.3f, 1.0f), "%s", g_wizard.status.c_str());
}
}
ImGui::End();
if (!open && g_wizard.active) {
StopWizard();
}
}
bool IsActive() { return g_wizard.active; }
} // namespace controller_mapping_wizard
+4 -2
View File
@@ -695,12 +695,14 @@ private:
const auto path = FindDspCoefficientRom(); const auto path = FindDspCoefficientRom();
std::ifstream stream(path, std::ios::binary | std::ios::ate); std::ifstream stream(path, std::ios::binary | std::ios::ate);
if (!stream || stream.tellg() != static_cast<std::streamoff>(m_coeffs.size() * 2)) { if (!stream || stream.tellg() != static_cast<std::streamoff>(m_coeffs.size() * 2)) {
throw std::runtime_error("Bundled Wii DSP coefficient ROM has an invalid size: " + path.string()); throw std::runtime_error("Bundled Wii DSP coefficient ROM has an invalid size: " +
RuntimeConfigFile::PathToUtf8(path));
} }
stream.seekg(0); stream.seekg(0);
std::array<uint8_t, kResamplingCoefficientCount * 2> bytes{}; std::array<uint8_t, kResamplingCoefficientCount * 2> bytes{};
if (!stream.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) { if (!stream.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
throw std::runtime_error("Failed to read bundled Wii DSP coefficient ROM: " + path.string()); throw std::runtime_error("Failed to read bundled Wii DSP coefficient ROM: " +
RuntimeConfigFile::PathToUtf8(path));
} }
for (size_t i = 0; i < m_coeffs.size(); ++i) { for (size_t i = 0; i < m_coeffs.size(); ++i) {
const uint16_t word = static_cast<uint16_t>(bytes[i * 2]) << 8 | const uint16_t word = static_cast<uint16_t>(bytes[i * 2]) << 8 |
+73
View File
@@ -0,0 +1,73 @@
#include "hle_stubs.h"
#include <cstdint>
#include "memory.h"
#include "runtime_log.h"
// Native because a crafted Yaz0 run writes past the caller's buffer
// (github.com/vabold/szsHaxx)
// https://github.com/vabold/Kinoko/blob/main/source/egg/core/Decomp.cc
extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
{
const uint32_t expandSize = (static_cast<uint32_t>(MemoryInline::FlatRead8(src + 4)) << 24) |
(static_cast<uint32_t>(MemoryInline::FlatRead8(src + 5)) << 16) |
(static_cast<uint32_t>(MemoryInline::FlatRead8(src + 6)) << 8) |
static_cast<uint32_t>(MemoryInline::FlatRead8(src + 7));
uint32_t srcIdx = 16;
uint32_t dstIdx = 0;
uint32_t mask = 0;
uint32_t flags = 0;
while (static_cast<int32_t>(dstIdx) < static_cast<int32_t>(expandSize)) {
if (mask == 0) {
flags = MemoryInline::FlatRead8(src + srcIdx++);
mask = 0x80;
}
if ((flags & mask) != 0) {
MemoryInline::FlatWrite8(dst + dstIdx++, MemoryInline::FlatRead8(src + srcIdx++));
} else {
const uint32_t high = MemoryInline::FlatRead8(src + srcIdx);
const uint32_t low = MemoryInline::FlatRead8(src + srcIdx + 1);
srcIdx += 2;
const uint32_t rep = (high << 8) | low;
// Without this check dstIdx - distance underflows and the
// copy leaks guest memory from before the destination buffer.
const uint32_t distance = (rep & 0xFFF) + 1;
if (distance > dstIdx) {
RT_LOG(RT_TAG_HLE) << "decodeSZS: malformed stream from 0x" << std::hex << src
<< std::dec << ", back-reference before output" << std::endl;
ShowRuntimeFatalPopup("corrupt compressed file",
"The game stopped decoding a malformed Yaz0 file.");
std::abort();
}
uint32_t copyIdx = dstIdx - distance;
uint32_t count = rep >> 12;
count = count != 0
? count + 2
: static_cast<uint32_t>(MemoryInline::FlatRead8(src + srcIdx++)) + 18;
for (uint32_t i = 0; i < count; ++i) {
if (dstIdx >= expandSize) {
RT_LOG(RT_TAG_HLE) << "decodeSZS: malformed stream from 0x" << std::hex << src
<< std::dec << ", output overran " << expandSize << " bytes"
<< std::endl;
ShowRuntimeFatalPopup("corrupt compressed file",
"The game stopped decoding a malformed Yaz0 file.");
std::abort();
}
MemoryInline::FlatWrite8(dst + dstIdx++, MemoryInline::FlatRead8(dst + copyIdx++));
}
}
mask >>= 1;
}
return expandSize;
}
PPC_NATIVE_OVERRIDE(80218C2C, EGG_Decomp_decodeSZS_80218c2c, uint32_t,
(uint32_t src, uint32_t dst), (src, dst));
+15 -2
View File
@@ -1,6 +1,7 @@
#include "hle_stubs.h" #include "hle_stubs.h"
#include "memory.h" #include "memory.h"
#include "hle/controller_status_contract.h" #include "hle/controller_status_contract.h"
#include "wup028_adapter.h"
#include <algorithm> #include <algorithm>
#include <cstdio> #include <cstdio>
@@ -33,6 +34,7 @@ void WritePadStatus(uint32_t base, const PADStatus& status) {
extern "C" uint32_t PAD__Init_HLE() extern "C" uint32_t PAD__Init_HLE()
{ {
Wup028Adapter::Initialize();
return PADInit() ? 1u : 0u; return PADInit() ? 1u : 0u;
} }
PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ()); PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ());
@@ -44,7 +46,16 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
} }
PADStatus statuses[PAD_CHANMAX]{}; PADStatus statuses[PAD_CHANMAX]{};
const uint32_t rumbleMask = PADRead(statuses); std::array<PADStatus, PAD_CHANMAX> adapterStatuses{};
uint32_t rumbleMask = PADRead(statuses);
if (Wup028Adapter::Read(adapterStatuses) && !PADIsInputBlocked()) {
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
if (adapterStatuses[port].err == PAD_ERR_NONE) {
statuses[port] = adapterStatuses[port];
rumbleMask |= PAD_CHAN0_BIT >> port;
}
}
}
try { try {
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) { for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
@@ -73,6 +84,8 @@ PPC_NATIVE_OVERRIDE(801AF1E4, PAD__Recalibrate_HLE, uint32_t, (uint32_t mask), (
extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command) extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command)
{ {
PADControlMotor(chan, command); if (!Wup028Adapter::SetRumble(static_cast<uint32_t>(chan), command == PAD_MOTOR_RUMBLE)) {
PADControlMotor(chan, command);
}
} }
PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command)); PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command));
+31 -7
View File
@@ -2,6 +2,7 @@
#include <cstdint> #include <cstdint>
#include <iostream> #include <iostream>
#include <set>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -103,17 +104,26 @@ static void HLE_LogOSReport(CpuContext* cpu, const char* fmt)
[&state]() { return NextOsReportDouble(state); }, [&state]() { return NextOsReportDouble(state); },
[](uint32_t address) { return ReadGuestStringForReport(address); }); [](uint32_t address) { return ReadGuestStringForReport(address); });
// nw4r warnings arrive as "<file>:<line> Warning:" plus a bare newline, so
// consecutive identical messages never land back to back. Blank lines are
// transparent to the repeat tracker so the pair still collapses.
static thread_local std::string lastBuffer; static thread_local std::string lastBuffer;
static thread_local size_t repeated = 0; static thread_local size_t repeated = 0;
if (buffer == lastBuffer) { const bool blank = buffer.find_first_not_of(" \t\r\n") == std::string::npos;
if (blank) {
if (repeated != 0) {
return;
}
} else if (buffer == lastBuffer) {
++repeated; ++repeated;
return; return;
} else {
if (repeated != 0) {
std::cout << "[OSReport] previous message repeated " << repeated << " time(s)" << std::endl;
repeated = 0;
}
lastBuffer = buffer;
} }
if (repeated != 0) {
std::cout << "[OSReport] previous message repeated " << repeated << " time(s)" << std::endl;
repeated = 0;
}
lastBuffer = buffer;
std::cout << "[OSReport] " << buffer; std::cout << "[OSReport] " << buffer;
@@ -128,9 +138,23 @@ static void HLE_LogOSReport(CpuContext* cpu, const char* fmt)
// the guest caller because OS__Report is an HLE boundary. The context is // the guest caller because OS__Report is an HLE boundary. The context is
// synchronized at this boundary, so capture the guest backchain at the // synchronized at this boundary, so capture the guest backchain at the
// first warning/panic instead of attributing the later PPCHalt unwind. // first warning/panic instead of attributing the later PPCHalt unwind.
//
// The dump is expensive and stdio is an unbuffered pipe, so a guest that
// warns every frame would stall the game thread on backpressure. One dump
// per distinct site, with an overall cap.
if (buffer.find(" Warning:") != std::string::npos || if (buffer.find(" Warning:") != std::string::npos ||
buffer.find(" Panic:") != std::string::npos) { buffer.find(" Panic:") != std::string::npos) {
SystemBridge::DumpCpuState(cpu); constexpr size_t kMaxWarningDumps = 8;
static thread_local std::set<std::string> dumpedSites;
static thread_local size_t dumpsEmitted = 0;
if (dumpsEmitted < kMaxWarningDumps && dumpedSites.insert(buffer).second) {
++dumpsEmitted;
SystemBridge::DumpCpuState(cpu);
if (dumpsEmitted == kMaxWarningDumps) {
std::cerr << "[runtime] guest warning context dumps capped at " << kMaxWarningDumps
<< "; further warnings log the message only." << std::endl;
}
}
} }
} }
+35 -37
View File
@@ -40,7 +40,7 @@ namespace fs = std::filesystem;
// The extracted ISO "DATA" folder. We map its "files" subfolder to the DVD // The extracted ISO "DATA" folder. We map its "files" subfolder to the DVD
// root "/" and its "sys" subfolder to "/sys/". The root is user-owned input: // root "/" and its "sys" subfolder to "/sys/". The root is user-owned input:
// it is never embedded into or copied by the public runtime. // it is never embedded into or copied by the public runtime.
static std::string g_dvdRoot; static fs::path g_dvdRoot;
static std::once_flag g_dvdRootOnce; static std::once_flag g_dvdRootOnce;
static uint32_t CurrentDiscGameCode() { static uint32_t CurrentDiscGameCode() {
@@ -66,7 +66,7 @@ static uint32_t CurrentDiscGameCode() {
#define DVD_FILEINFO_OFFSET_LEN 0x34 #define DVD_FILEINFO_OFFSET_LEN 0x34
struct DVDFileEntry { struct DVDFileEntry {
std::string hostPath; // Full Windows path fs::path hostPath;
std::string dvdPath; // Virtual Wii path (e.g., "/Race/Course.szs") std::string dvdPath; // Virtual Wii path (e.g., "/Race/Course.szs")
uint32_t size; uint32_t size;
uint32_t discOffsetWords = 0; uint32_t discOffsetWords = 0;
@@ -78,7 +78,7 @@ struct FstFileEntry {
uint32_t end; uint32_t end;
uint32_t size; uint32_t size;
std::string dvdPath; std::string dvdPath;
std::string hostPath; fs::path hostPath;
}; };
// Global State // Global State
@@ -111,11 +111,9 @@ static void CopyToGuestAsDma(uint32_t dest, const uint8_t* data, size_t size) {
GxNotifyGuestRamDmaWrite(dest, static_cast<uint32_t>(size)); GxNotifyGuestRamDmaWrite(dest, static_cast<uint32_t>(size));
} }
static std::string NormalizeDvdHostPath(std::string path) { // Host path strings only ever leave this module as UTF-8 display text.
while (!path.empty() && (path.back() == '\\' || path.back() == '/')) { static std::string HostPathText(const fs::path& path) {
path.pop_back(); return RuntimeConfigFile::PathToUtf8(path);
}
return path;
} }
static bool IsDvdDataRoot(const fs::path& path) { static bool IsDvdDataRoot(const fs::path& path) {
@@ -140,7 +138,7 @@ static bool IsDvdDataRoot(const fs::path& path) {
[[noreturn]] static void FailDvdRoot(const char* source, const fs::path& path = {}) { [[noreturn]] static void FailDvdRoot(const char* source, const fs::path& path = {}) {
RT_LOGF(RT_TAG_DVD, "ERROR: %s", source); RT_LOGF(RT_TAG_DVD, "ERROR: %s", source);
if (!path.empty()) { if (!path.empty()) {
std::fprintf(stderr, ": %s", path.string().c_str()); std::fprintf(stderr, ": %s", HostPathText(path).c_str());
} }
std::fprintf(stderr, std::fprintf(stderr,
"\n[dvd] Set [paths] dvd_root in Config.toml " "\n[dvd] Set [paths] dvd_root in Config.toml "
@@ -148,14 +146,14 @@ static bool IsDvdDataRoot(const fs::path& path) {
std::string details = source ? source : "The configured DVD root could not be opened."; std::string details = source ? source : "The configured DVD root could not be opened.";
if (!path.empty()) { if (!path.empty()) {
details += "\n\nPath: "; details += "\n\nPath: ";
details += path.string(); details += HostPathText(path);
} }
details += "\n\nSet [paths] dvd_root in Config.toml to the extracted " details += "\n\nSet [paths] dvd_root in Config.toml to the extracted "
"Mario Kart Wii DATA directory."; "Mario Kart Wii DATA directory.";
FailDvd("dvd_root", "DVD data is unavailable", details); FailDvd("dvd_root", "DVD data is unavailable", details);
} }
static const std::string& GetDvdRoot() { static const fs::path& GetDvdRoot() {
std::call_once(g_dvdRootOnce, []() { std::call_once(g_dvdRootOnce, []() {
const fs::path path = RuntimeConfigFile::ResolvedDvdRoot(); const fs::path path = RuntimeConfigFile::ResolvedDvdRoot();
if (path.empty()) { if (path.empty()) {
@@ -164,14 +162,14 @@ static const std::string& GetDvdRoot() {
if (!IsDvdDataRoot(path)) { if (!IsDvdDataRoot(path)) {
FailDvdRoot("Configured DVD root is not an extracted DATA directory", path); FailDvdRoot("Configured DVD root is not an extracted DATA directory", path);
} }
g_dvdRoot = NormalizeDvdHostPath(path.string()); g_dvdRoot = path;
}); });
return g_dvdRoot; return g_dvdRoot;
} }
static std::string NormalizePath(const std::string& path); static std::string NormalizePath(const std::string& path);
static std::string ResolveDvdMappedHostPath(const std::string& dvdPath, const std::string& fallbackHostPath); static fs::path ResolveDvdMappedHostPath(const std::string& dvdPath, const fs::path& fallbackHostPath);
static void InvokeDvdCallback(uint32_t callbackPtr, int32_t result, uint32_t fileInfoPtr) { static void InvokeDvdCallback(uint32_t callbackPtr, int32_t result, uint32_t fileInfoPtr) {
if (callbackPtr == 0) { if (callbackPtr == 0) {
@@ -240,7 +238,7 @@ static void LoadFstIndex() {
} }
g_fstLoaded = true; g_fstLoaded = true;
fs::path fstPath = fs::path(GetDvdRoot()) / "sys" / "fst.bin"; const fs::path fstPath = GetDvdRoot() / "sys" / "fst.bin";
std::ifstream fstFile(fstPath, std::ios::binary); std::ifstream fstFile(fstPath, std::ios::binary);
if (!fstFile.is_open()) { if (!fstFile.is_open()) {
return; return;
@@ -333,7 +331,7 @@ static void LoadFstIndex() {
entry.end = endBytes; entry.end = endBytes;
entry.size = fileSize; entry.size = fileSize;
entry.dvdPath = "/" + relPath; entry.dvdPath = "/" + relPath;
const std::string baseHostPath = (fs::path(GetDvdRoot()) / "files" / fs::path(relPath)).string(); const fs::path baseHostPath = GetDvdRoot() / "files" / fs::path(relPath);
entry.hostPath = ResolveDvdMappedHostPath(entry.dvdPath, baseHostPath); entry.hostPath = ResolveDvdMappedHostPath(entry.dvdPath, baseHostPath);
g_fstFiles.push_back(std::move(entry)); g_fstFiles.push_back(std::move(entry));
} }
@@ -408,7 +406,7 @@ static void RegisterFileEntry(std::string dvdPath, const fs::path& hostPath, uin
dvdPath = DvdFstContract::CanonicalizePath(dvdPath); dvdPath = DvdFstContract::CanonicalizePath(dvdPath);
DVDFileEntry fileEntry; DVDFileEntry fileEntry;
fileEntry.hostPath = hostPath.string(); fileEntry.hostPath = hostPath;
fileEntry.dvdPath = dvdPath; fileEntry.dvdPath = dvdPath;
fileEntry.size = size; fileEntry.size = size;
@@ -445,7 +443,7 @@ static void WalkDirectory(const fs::path& root, bool recursive, bool announceErr
fs::recursive_directory_iterator it(root, fs::directory_options::skip_permission_denied, ec); fs::recursive_directory_iterator it(root, fs::directory_options::skip_permission_denied, ec);
if (ec) { if (ec) {
if (announceErrors) { if (announceErrors) {
RT_LOG(RT_TAG_DVD) << "WARNING: cannot enumerate " << root.string() << ": " RT_LOG(RT_TAG_DVD) << "WARNING: cannot enumerate " << HostPathText(root) << ": "
<< ec.message() << std::endl; << ec.message() << std::endl;
} }
return; return;
@@ -458,7 +456,7 @@ static void WalkDirectory(const fs::path& root, bool recursive, bool announceErr
it.increment(ec); it.increment(ec);
if (ec) { if (ec) {
if (announceErrors) { if (announceErrors) {
RT_LOG(RT_TAG_DVD) << "WARNING: stopped enumerating " << root.string() << ": " RT_LOG(RT_TAG_DVD) << "WARNING: stopped enumerating " << HostPathText(root) << ": "
<< ec.message() << std::endl; << ec.message() << std::endl;
return; return;
} }
@@ -488,7 +486,7 @@ static void ScanDirectory(const fs::path& root, const std::string& virtualPrefix
const std::uintmax_t size = fs::file_size(entry.path(), entryEc); const std::uintmax_t size = fs::file_size(entry.path(), entryEc);
if (entryEc) { if (entryEc) {
RT_LOG(RT_TAG_DVD) << "WARNING: skipping " << entry.path().string() << ": " RT_LOG(RT_TAG_DVD) << "WARNING: skipping " << HostPathText(entry.path()) << ": "
<< entryEc.message() << std::endl; << entryEc.message() << std::endl;
return; return;
} }
@@ -502,7 +500,7 @@ static void ScanDirectory(const fs::path& root, const std::string& virtualPrefix
if (prefix.back() != '/' && prefix.back() != '\\') { if (prefix.back() != '/' && prefix.back() != '\\') {
prefix += "/"; prefix += "/";
} }
std::string dvdPath = prefix + relative.string(); std::string dvdPath = prefix + HostPathText(relative);
if (!addNewFiles && !DvdEntryExists(dvdPath)) { if (!addNewFiles && !DvdEntryExists(dvdPath)) {
return; return;
} }
@@ -536,7 +534,7 @@ static void ApplyFolderByNameMapping(const RuntimeRiivolution::Mapping& mapping)
if (!entry.is_regular_file(entryEc) || entryEc) { if (!entry.is_regular_file(entryEc) || entryEc) {
return; return;
} }
std::string name = entry.path().filename().string(); std::string name = HostPathText(entry.path().filename());
RuntimeHle::LowerInPlace(name); RuntimeHle::LowerInPlace(name);
const auto matches = discPathsByName.find(name); const auto matches = discPathsByName.find(name);
if (matches == discPathsByName.end()) { if (matches == discPathsByName.end()) {
@@ -554,7 +552,7 @@ static void ApplyFolderByNameMapping(const RuntimeRiivolution::Mapping& mapping)
WalkDirectory(mapping.hostPath, mapping.recursive, /*announceErrors=*/false, applyEntry); WalkDirectory(mapping.hostPath, mapping.recursive, /*announceErrors=*/false, applyEntry);
RT_LOG(RT_TAG_DVD) << mapping.hostPath.string() << ": replaced " << replaced RT_LOG(RT_TAG_DVD) << HostPathText(mapping.hostPath) << ": replaced " << replaced
<< " disc file(s) by filename" << std::endl; << " disc file(s) by filename" << std::endl;
} }
@@ -562,7 +560,7 @@ static void ScanOverlayRoot(const RuntimeRiivolution::Overlay& overlay) {
if (!overlay.patches) { if (!overlay.patches) {
// Fallback for mod roots that mirror the disc filesystem directly (not a // Fallback for mod roots that mirror the disc filesystem directly (not a
// Riivolution pack, which wouldn't map anything useful this way). // Riivolution pack, which wouldn't map anything useful this way).
RT_LOG(RT_TAG_DVD) << overlay.root.string() RT_LOG(RT_TAG_DVD) << HostPathText(overlay.root)
<< ": no Riivolution XML found, treating the root as a disc-shaped overlay" << ": no Riivolution XML found, treating the root as a disc-shaped overlay"
<< std::endl; << std::endl;
ScanDirectory(overlay.root, "/"); ScanDirectory(overlay.root, "/");
@@ -592,7 +590,7 @@ static void ScanOverlayRoot(const RuntimeRiivolution::Overlay& overlay) {
} }
} }
static std::string ResolveDvdMappedHostPath(const std::string& dvdPath, const std::string& fallbackHostPath) { static fs::path ResolveDvdMappedHostPath(const std::string& dvdPath, const fs::path& fallbackHostPath) {
const std::string normalized = NormalizePath(dvdPath); const std::string normalized = NormalizePath(dvdPath);
const auto it = g_pathToEntry.find(normalized); const auto it = g_pathToEntry.find(normalized);
if (it != g_pathToEntry.end() && it->second >= 0 && if (it != g_pathToEntry.end() && it->second >= 0 &&
@@ -604,7 +602,7 @@ static std::string ResolveDvdMappedHostPath(const std::string& dvdPath, const st
const fs::path candidate = overlay.root / fs::path(normalized.substr(1)); const fs::path candidate = overlay.root / fs::path(normalized.substr(1));
std::error_code ec; std::error_code ec;
if (fs::is_regular_file(candidate, ec)) { if (fs::is_regular_file(candidate, ec)) {
return candidate.string(); return candidate;
} }
} }
@@ -740,7 +738,7 @@ extern "C" const char* DVDResolveHostPathForTest(const char* dvdPath)
return nullptr; return nullptr;
} }
resolved = g_fileEntries[it->second].hostPath; resolved = HostPathText(g_fileEntries[it->second].hostPath);
return resolved.c_str(); return resolved.c_str();
} }
@@ -808,7 +806,7 @@ extern "C" void DVDInit_8015EA1C()
Memory::Write16(diskHeader + 0x04, 0x3031); // '01' (Maker) Memory::Write16(diskHeader + 0x04, 0x3031); // '01' (Maker)
Memory::Write8(diskHeader + 0x06, 0x01); // Disk #1 Memory::Write8(diskHeader + 0x06, 0x01); // Disk #1
// 4. Scan Files // 4. Scan Files
fs::path rootPath(GetDvdRoot()); const fs::path& rootPath = GetDvdRoot();
// Map "<dvd_root>/files" -> "/" // Map "<dvd_root>/files" -> "/"
ScanDirectory(rootPath / "files", "/"); ScanDirectory(rootPath / "files", "/");
@@ -879,7 +877,7 @@ extern "C" int32_t DVDReadPrio_8015E834(uint32_t fileInfoPtr, uint32_t bufferPtr
const DVDFileEntry& entry = g_fileEntries[extent->entryIndex]; const DVDFileEntry& entry = g_fileEntries[extent->entryIndex];
if (offset < 0 || length < 0) { if (offset < 0 || length < 0) {
return DvdReadFatal(fileInfoPtr, entry.hostPath, offset, return DvdReadFatal(fileInfoPtr, HostPathText(entry.hostPath), offset,
length > 0 ? static_cast<uint32_t>(length) : 0, length > 0 ? static_cast<uint32_t>(length) : 0,
"negative DVD read offset or length"); "negative DVD read offset or length");
} }
@@ -889,7 +887,7 @@ extern "C" int32_t DVDReadPrio_8015E834(uint32_t fileInfoPtr, uint32_t bufferPtr
uint32_t uLength = (uint32_t)length; uint32_t uLength = (uint32_t)length;
if (requestedOffset >= entry.size) { if (requestedOffset >= entry.size) {
return DvdReadFatal(fileInfoPtr, entry.hostPath, offset, uLength, return DvdReadFatal(fileInfoPtr, HostPathText(entry.hostPath), offset, uLength,
"read offset is outside the indexed DVD file"); "read offset is outside the indexed DVD file");
} }
const uint32_t uOffset = static_cast<uint32_t>(requestedOffset); const uint32_t uOffset = static_cast<uint32_t>(requestedOffset);
@@ -899,14 +897,14 @@ extern "C" int32_t DVDReadPrio_8015E834(uint32_t fileInfoPtr, uint32_t bufferPtr
} }
if (uLength != 0 && !Memory::Contains(bufferPtr, uLength)) { if (uLength != 0 && !Memory::Contains(bufferPtr, uLength)) {
return DvdReadFatal(fileInfoPtr, entry.hostPath, offset, uLength, return DvdReadFatal(fileInfoPtr, HostPathText(entry.hostPath), offset, uLength,
"DVD read destination is outside guest memory"); "DVD read destination is outside guest memory");
} }
std::vector<uint8_t> tempBuf; std::vector<uint8_t> tempBuf;
DvdReadContract::HostReadFailure failure; DvdReadContract::HostReadFailure failure;
if (!DvdReadContract::ReadExact(entry.hostPath, uOffset, uLength, tempBuf, failure)) { if (!DvdReadContract::ReadExact(entry.hostPath, uOffset, uLength, tempBuf, failure)) {
return DvdReadFatal(fileInfoPtr, entry.hostPath, offset, uLength, return DvdReadFatal(fileInfoPtr, HostPathText(entry.hostPath), offset, uLength,
DvdReadContract::Describe(failure)); DvdReadContract::Describe(failure));
} }
@@ -962,11 +960,11 @@ extern "C" int32_t DVD__ReadAbsAsyncPrio_HLE_801628cc(uint32_t cmdBlockPtr,
requestedLength, requestedLength,
"absolute DVD read offset is not mapped to a host file"); "absolute DVD read offset is not mapped to a host file");
} else if (requestedLength != 0 && readInfo.readLength != requestedLength) { } else if (requestedLength != 0 && readInfo.readLength != requestedLength) {
bytesRead = DvdReadFatal(cmdBlockPtr, readInfo.entry->hostPath, bytesRead = DvdReadFatal(cmdBlockPtr, HostPathText(readInfo.entry->hostPath),
readInfo.fileOffset, requestedLength, readInfo.fileOffset, requestedLength,
"requested range extends beyond the indexed DVD file"); "requested range extends beyond the indexed DVD file");
} else if (requestedLength != 0 && !Memory::Contains(bufferPtr, requestedLength)) { } else if (requestedLength != 0 && !Memory::Contains(bufferPtr, requestedLength)) {
bytesRead = DvdReadFatal(cmdBlockPtr, readInfo.entry->hostPath, bytesRead = DvdReadFatal(cmdBlockPtr, HostPathText(readInfo.entry->hostPath),
readInfo.fileOffset, requestedLength, readInfo.fileOffset, requestedLength,
"DVD read destination is outside guest memory"); "DVD read destination is outside guest memory");
} else { } else {
@@ -977,7 +975,7 @@ extern "C" int32_t DVD__ReadAbsAsyncPrio_HLE_801628cc(uint32_t cmdBlockPtr,
readInfo.readLength, readInfo.readLength,
tempBuf, tempBuf,
failure)) { failure)) {
bytesRead = DvdReadFatal(cmdBlockPtr, readInfo.entry->hostPath, bytesRead = DvdReadFatal(cmdBlockPtr, HostPathText(readInfo.entry->hostPath),
readInfo.fileOffset, readInfo.readLength, readInfo.fileOffset, readInfo.readLength,
DvdReadContract::Describe(failure)); DvdReadContract::Describe(failure));
} else { } else {
@@ -1076,12 +1074,12 @@ extern "C" int32_t DVDLowRead_80166330(uint32_t buffer, uint32_t length, uint32_
return finish(false); return finish(false);
} }
if (readInfo.readLength != length) { if (readInfo.readLength != length) {
ReportDvdReadError(readInfo.entry->hostPath, readInfo.fileOffset, length, ReportDvdReadError(HostPathText(readInfo.entry->hostPath), readInfo.fileOffset, length,
"requested range extends beyond the indexed DVD file"); "requested range extends beyond the indexed DVD file");
return finish(false); return finish(false);
} }
if (!Memory::Contains(buffer, length)) { if (!Memory::Contains(buffer, length)) {
ReportDvdReadError(readInfo.entry->hostPath, readInfo.fileOffset, length, ReportDvdReadError(HostPathText(readInfo.entry->hostPath), readInfo.fileOffset, length,
"DVD read destination is outside guest memory"); "DVD read destination is outside guest memory");
return finish(false); return finish(false);
} }
@@ -1093,7 +1091,7 @@ extern "C" int32_t DVDLowRead_80166330(uint32_t buffer, uint32_t length, uint32_
readInfo.readLength, readInfo.readLength,
tempBuf, tempBuf,
failure)) { failure)) {
ReportDvdReadError(readInfo.entry->hostPath, readInfo.fileOffset, ReportDvdReadError(HostPathText(readInfo.entry->hostPath), readInfo.fileOffset,
readInfo.readLength, DvdReadContract::Describe(failure)); readInfo.readLength, DvdReadContract::Describe(failure));
return finish(false); return finish(false);
} }
+22 -27
View File
@@ -91,7 +91,7 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
return NAND_RESULT_INVALID; return NAND_RESULT_INVALID;
} }
std::string hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
// Existing-file write opens go through a shadow copy seeded from the original, so a // Existing-file write opens go through a shadow copy seeded from the original, so a
// crash between NANDWrite and NANDClose cannot leave a torn file (the game patches // crash between NANDWrite and NANDClose cannot leave a torn file (the game patches
@@ -101,23 +101,23 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
// Another live handle already refers to this file. A shadow would hide the // Another live handle already refers to this file. A shadow would hide the
// writes from that handle, so stay in place for this open. // writes from that handle, so stay in place for this open.
LogNandWarning("NANDOpen", "WARNING: '%s' already has a live handle, writing in place", LogNandWarning("NANDOpen", "WARNING: '%s' already has a live handle, writing in place",
hostPath.c_str()); HostPathText(hostPath).c_str());
} else { } else {
const std::string tempPath = SafeTempPathFor(hostPath); const std::filesystem::path tempPath = SafeTempPathFor(hostPath);
if (DiscardStaleSafeTemp(tempPath)) { if (DiscardStaleSafeTemp(tempPath)) {
std::error_code ec; std::error_code ec;
std::filesystem::copy_file(hostPath, tempPath, std::filesystem::copy_file(hostPath, tempPath,
std::filesystem::copy_options::overwrite_existing, ec); std::filesystem::copy_options::overwrite_existing, ec);
if (ec) { if (ec) {
LogNandWarning("NANDOpen", "WARNING: could not seed shadow '%s' (%s), writing in place", LogNandWarning("NANDOpen", "WARNING: could not seed shadow '%s' (%s), writing in place",
tempPath.c_str(), ec.message().c_str()); HostPathText(tempPath).c_str(), ec.message().c_str());
std::remove(tempPath.c_str()); NandRemove(tempPath);
} else { } else {
FILE* shadow = std::fopen(tempPath.c_str(), "r+b"); FILE* shadow = NandFopen(tempPath, "r+b");
if (!shadow) { if (!shadow) {
LogNandWarning("NANDOpen", "WARNING: could not open shadow '%s', writing in place", LogNandWarning("NANDOpen", "WARNING: could not open shadow '%s', writing in place",
tempPath.c_str()); HostPathText(tempPath).c_str());
std::remove(tempPath.c_str()); NandRemove(tempPath);
} else { } else {
const int32_t shadowFd = AllocateFd(tempPath, shadow, static_cast<int32_t>(mode)); const int32_t shadowFd = AllocateFd(tempPath, shadow, static_cast<int32_t>(mode));
{ {
@@ -141,20 +141,20 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
else if (mode == 2) fopenMode = "r+b"; else if (mode == 2) fopenMode = "r+b";
else if (mode == 3) fopenMode = "r+b"; else if (mode == 3) fopenMode = "r+b";
FILE* file = std::fopen(hostPath.c_str(), fopenMode); FILE* file = NandFopen(hostPath, fopenMode);
if (!file && mode >= 2) { if (!file && mode >= 2) {
// Try creating for write modes // Try creating for write modes
file = std::fopen(hostPath.c_str(), "w+b"); file = NandFopen(hostPath, "w+b");
} }
// Create parent directories and retry // Create parent directories and retry
if (!file && CreateParentDirectories(hostPath)) { if (!file && CreateParentDirectories(hostPath)) {
file = std::fopen(hostPath.c_str(), mode >= 2 ? "w+b" : "rb"); file = NandFopen(hostPath, mode >= 2 ? "w+b" : "rb");
} }
if (!file) { if (!file) {
if (IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) { if (IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
file = std::fopen(hostPath.c_str(), fopenMode); file = NandFopen(hostPath, fopenMode);
} }
if (!file) { if (!file) {
LogNandError("NANDOpen", "FAILED to open"); LogNandError("NANDOpen", "FAILED to open");
@@ -282,7 +282,7 @@ extern "C" int32_t NANDCreate_HLE(uint32_t pathPtr, uint32_t perm, uint32_t attr
return NAND_RESULT_INVALID; return NAND_RESULT_INVALID;
} }
std::string hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
CreateParentDirectories(hostPath); CreateParentDirectories(hostPath);
// Check if file already exists // Check if file already exists
@@ -291,7 +291,7 @@ extern "C" int32_t NANDCreate_HLE(uint32_t pathPtr, uint32_t perm, uint32_t attr
} }
// Create empty file // Create empty file
FILE* f = std::fopen(hostPath.c_str(), "wb"); FILE* f = NandFopen(hostPath, "wb");
if (!f) { if (!f) {
return NAND_RESULT_UNKNOWN; return NAND_RESULT_UNKNOWN;
} }
@@ -307,13 +307,13 @@ extern "C" int32_t NANDDelete_HLE(uint32_t pathPtr) {
return NAND_RESULT_INVALID; return NAND_RESULT_INVALID;
} }
std::string hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
if (!PathExists(hostPath)) { if (!PathExists(hostPath)) {
return NAND_RESULT_NOEXISTS; return NAND_RESULT_NOEXISTS;
} }
if (std::remove(hostPath.c_str()) == 0) { if (NandRemove(hostPath)) {
return NAND_RESULT_OK; return NAND_RESULT_OK;
} }
@@ -327,7 +327,7 @@ extern "C" int32_t NANDCreateDir_HLE(uint32_t pathPtr, uint32_t perm, uint32_t a
return NAND_RESULT_INVALID; return NAND_RESULT_INVALID;
} }
std::string hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
if (PathExists(hostPath)) { if (PathExists(hostPath)) {
if (IsDirectory(hostPath)) { if (IsDirectory(hostPath)) {
@@ -337,11 +337,6 @@ extern "C" int32_t NANDCreateDir_HLE(uint32_t pathPtr, uint32_t perm, uint32_t a
} }
if (CreateDirectoryPath(hostPath)) { if (CreateDirectoryPath(hostPath)) {
#ifdef _WIN32
_mkdir(hostPath.c_str());
#else
mkdir(hostPath.c_str(), 0755);
#endif
return NAND_RESULT_OK; return NAND_RESULT_OK;
} }
@@ -369,13 +364,13 @@ extern "C" int32_t NANDMove_HLE(uint32_t srcPathPtr, uint32_t dstPathPtr) {
// filename (for example /tmp/banner.bin -> <title home>/banner.bin). // filename (for example /tmp/banner.bin -> <title home>/banner.bin).
const std::filesystem::path dstHost = dstDirectoryHost / srcName; const std::filesystem::path dstHost = dstDirectoryHost / srcName;
if (!PathExists(srcHost.string())) { if (!PathExists(srcHost)) {
return NAND_RESULT_NOEXISTS; return NAND_RESULT_NOEXISTS;
} }
if (!IsDirectory(dstDirectoryHost.string())) { if (!IsDirectory(dstDirectoryHost)) {
return NAND_RESULT_NOEXISTS; return NAND_RESULT_NOEXISTS;
} }
if (PathExists(dstHost.string())) { if (PathExists(dstHost)) {
return NAND_RESULT_EXISTS; return NAND_RESULT_EXISTS;
} }
@@ -396,7 +391,7 @@ extern "C" int32_t NANDGetStatus_HLE(uint32_t pathPtr, uint32_t outStatusPtr) {
return NAND_RESULT_INVALID; return NAND_RESULT_INVALID;
} }
std::string hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
if (!PathExists(hostPath)) { if (!PathExists(hostPath)) {
return NAND_RESULT_NOEXISTS; return NAND_RESULT_NOEXISTS;
@@ -417,7 +412,7 @@ extern "C" int32_t NANDGetType_HLE(uint32_t pathPtr, uint32_t outTypePtr) {
return NAND_RESULT_INVALID; return NAND_RESULT_INVALID;
} }
std::string hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
if (!PathExists(hostPath)) { if (!PathExists(hostPath)) {
return NAND_RESULT_NOEXISTS; return NAND_RESULT_NOEXISTS;
+40 -32
View File
@@ -234,8 +234,10 @@ PPC_NATIVE_OVERRIDE(8019E7B4, NANDPrivateGetTypeAsync_HLE, int32_t,
static const char kNandSafeTempSuffix[] = ".nandsafe.tmp"; static const char kNandSafeTempSuffix[] = ".nandsafe.tmp";
std::string SafeTempPathFor(const std::string& hostPath) { std::filesystem::path SafeTempPathFor(const std::filesystem::path& hostPath) {
return hostPath + kNandSafeTempSuffix; std::filesystem::path tempPath = hostPath;
tempPath += kNandSafeTempSuffix;
return tempPath;
} }
// Push the CRT buffer out and then force the OS to put it on the platter, so the data is // Push the CRT buffer out and then force the OS to put it on the platter, so the data is
@@ -264,24 +266,25 @@ static bool FlushFileToDisk(FILE* file) {
// Replace `targetPath` with `tempPath` in one step. Either the old or the new contents // Replace `targetPath` with `tempPath` in one step. Either the old or the new contents
// survive a crash; there is no window where the target is truncated or partial. // survive a crash; there is no window where the target is truncated or partial.
static bool AtomicReplaceHostFile(const char* who, const std::string& tempPath, static bool AtomicReplaceHostFile(const char* who, const std::filesystem::path& tempPath,
const std::string& targetPath) { const std::filesystem::path& targetPath) {
#ifdef _WIN32 #ifdef _WIN32
if (MoveFileExA(tempPath.c_str(), targetPath.c_str(), if (MoveFileExW(tempPath.c_str(), targetPath.c_str(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
return true; return true;
} }
LogNandError(who, "ERROR: MoveFileEx('%s' -> '%s') failed (err=%lu)", LogNandError(who, "ERROR: MoveFileEx('%s' -> '%s') failed (err=%lu)",
tempPath.c_str(), targetPath.c_str(), static_cast<unsigned long>(GetLastError())); HostPathText(tempPath).c_str(), HostPathText(targetPath).c_str(),
static_cast<unsigned long>(GetLastError()));
return false; return false;
#else #else
if (std::rename(tempPath.c_str(), targetPath.c_str()) != 0) { if (!NandRename(tempPath, targetPath)) {
LogNandError(who, "ERROR: rename('%s' -> '%s') failed", LogNandError(who, "ERROR: rename('%s' -> '%s') failed",
tempPath.c_str(), targetPath.c_str()); HostPathText(tempPath).c_str(), HostPathText(targetPath).c_str());
return false; return false;
} }
// Durably record the directory entry so the rename itself survives a crash. // Durably record the directory entry so the rename itself survives a crash.
const std::string directory = std::filesystem::path(targetPath).parent_path().string(); const std::string directory = targetPath.parent_path().string();
const int dirFd = open(directory.c_str(), O_RDONLY); const int dirFd = open(directory.c_str(), O_RDONLY);
if (dirFd >= 0) { if (dirFd >= 0) {
fsync(dirFd); fsync(dirFd);
@@ -293,23 +296,24 @@ static bool AtomicReplaceHostFile(const char* who, const std::string& tempPath,
// Remove a scratch file left behind by a previous run that died between safe open and // Remove a scratch file left behind by a previous run that died between safe open and
// safe close. Its contents are worthless: the original was never replaced. // safe close. Its contents are worthless: the original was never replaced.
bool DiscardStaleSafeTemp(const std::string& tempPath) { bool DiscardStaleSafeTemp(const std::filesystem::path& tempPath) {
if (!PathExists(tempPath)) { if (!PathExists(tempPath)) {
return true; return true;
} }
LogNandWarning("nand-shadow", "WARNING: discarding stale scratch file '%s' from a previous run", LogNandWarning("nand-shadow", "WARNING: discarding stale scratch file '%s' from a previous run",
tempPath.c_str()); HostPathText(tempPath).c_str());
if (std::remove(tempPath.c_str()) == 0) { if (NandRemove(tempPath)) {
return true; return true;
} }
LogNandError("nand-shadow", "FAILED to remove stale scratch file '%s'", tempPath.c_str()); LogNandError("nand-shadow", "FAILED to remove stale scratch file '%s'",
HostPathText(tempPath).c_str());
return false; return false;
} }
// True when any live handle already refers to `hostPath`, either directly or as the // True when any live handle already refers to `hostPath`, either directly or as the
// commit target of a shadow. Used to keep shadow writes from hiding data behind a second // commit target of a shadow. Used to keep shadow writes from hiding data behind a second
// handle on the same file. // handle on the same file.
bool IsHostPathOpen(const std::string& hostPath) { bool IsHostPathOpen(const std::filesystem::path& hostPath) {
std::lock_guard<std::mutex> lock(g_fdMutex); std::lock_guard<std::mutex> lock(g_fdMutex);
for (const auto& entry : g_fileHandles) { for (const auto& entry : g_fileHandles) {
if (entry.second.path == hostPath || entry.second.safeCommitPath == hostPath) { if (entry.second.path == hostPath || entry.second.safeCommitPath == hostPath) {
@@ -324,8 +328,8 @@ bool IsHostPathOpen(const std::string& hostPath) {
// dropped and the original is left exactly as it was, and the error is returned so the // dropped and the original is left exactly as it was, and the error is returned so the
// guest's close call fails instead of silently reporting success. // guest's close call fails instead of silently reporting success.
int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) { int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) {
std::string tempPath; std::filesystem::path tempPath;
std::string commitPath; std::filesystem::path commitPath;
FILE* file = nullptr; FILE* file = nullptr;
int32_t mode = 0; int32_t mode = 0;
@@ -358,7 +362,7 @@ int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) {
if (!needsCommit) { if (!needsCommit) {
if (!flushed) { if (!flushed) {
LogNandError(who, "ERROR: flush of '%s' failed", tempPath.c_str()); LogNandError(who, "ERROR: flush of '%s' failed", HostPathText(tempPath).c_str());
return NAND_RESULT_UNKNOWN; return NAND_RESULT_UNKNOWN;
} }
return NAND_RESULT_OK; return NAND_RESULT_OK;
@@ -366,13 +370,13 @@ int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) {
if (!flushed) { if (!flushed) {
LogNandError(who, "ERROR: flush of '%s' failed, discarding it and leaving '%s' untouched", LogNandError(who, "ERROR: flush of '%s' failed, discarding it and leaving '%s' untouched",
tempPath.c_str(), commitPath.c_str()); HostPathText(tempPath).c_str(), HostPathText(commitPath).c_str());
std::remove(tempPath.c_str()); NandRemove(tempPath);
return NAND_RESULT_UNKNOWN; return NAND_RESULT_UNKNOWN;
} }
if (!AtomicReplaceHostFile(who, tempPath, commitPath)) { if (!AtomicReplaceHostFile(who, tempPath, commitPath)) {
std::remove(tempPath.c_str()); NandRemove(tempPath);
return NAND_RESULT_UNKNOWN; return NAND_RESULT_UNKNOWN;
} }
@@ -394,7 +398,7 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
return NAND_RESULT_INVALID; return NAND_RESULT_INVALID;
} }
const std::string hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
if (hostPath.empty()) { if (hostPath.empty()) {
LogNandError("NANDSafeOpen", "FAILED to translate path '%s'", path); LogNandError("NANDSafeOpen", "FAILED to translate path '%s'", path);
return NAND_RESULT_INVALID; return NAND_RESULT_INVALID;
@@ -407,12 +411,13 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
if (mode == 1) { if (mode == 1) {
// Read-only safe open reads the original in place; the library builds no scratch // Read-only safe open reads the original in place; the library builds no scratch
// copy for this case. // copy for this case.
FILE* file = std::fopen(hostPath.c_str(), "rb"); FILE* file = NandFopen(hostPath, "rb");
if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) { if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
file = std::fopen(hostPath.c_str(), "rb"); file = NandFopen(hostPath, "rb");
} }
if (!file) { if (!file) {
LogNandError("NANDSafeOpen", "FAILED to open '%s' for reading", hostPath.c_str()); LogNandError("NANDSafeOpen", "FAILED to open '%s' for reading",
HostPathText(hostPath).c_str());
return NAND_RESULT_NOEXISTS; return NAND_RESULT_NOEXISTS;
} }
@@ -425,15 +430,16 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
// Write modes. The library queries the attributes of the original first, so a safe // Write modes. The library queries the attributes of the original first, so a safe
// open of a file that does not exist fails instead of creating one. // open of a file that does not exist fails instead of creating one.
if (!PathExists(hostPath)) { if (!PathExists(hostPath)) {
LogNandError("NANDSafeOpen", "FAILED: '%s' does not exist, safe open never creates it", hostPath.c_str()); LogNandError("NANDSafeOpen", "FAILED: '%s' does not exist, safe open never creates it",
HostPathText(hostPath).c_str());
return NAND_RESULT_NOEXISTS; return NAND_RESULT_NOEXISTS;
} }
if (IsDirectory(hostPath)) { if (IsDirectory(hostPath)) {
LogNandError("NANDSafeOpen", "FAILED: '%s' is a directory", hostPath.c_str()); LogNandError("NANDSafeOpen", "FAILED: '%s' is a directory", HostPathText(hostPath).c_str());
return NAND_RESULT_INVALID; return NAND_RESULT_INVALID;
} }
const std::string tempPath = SafeTempPathFor(hostPath); const std::filesystem::path tempPath = SafeTempPathFor(hostPath);
if (!DiscardStaleSafeTemp(tempPath)) { if (!DiscardStaleSafeTemp(tempPath)) {
return NAND_RESULT_ACCESS; return NAND_RESULT_ACCESS;
} }
@@ -445,15 +451,17 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
std::filesystem::copy_options::overwrite_existing, ec); std::filesystem::copy_options::overwrite_existing, ec);
if (ec) { if (ec) {
LogNandError("NANDSafeOpen", "FAILED to seed scratch file '%s' from '%s': %s", LogNandError("NANDSafeOpen", "FAILED to seed scratch file '%s' from '%s': %s",
tempPath.c_str(), hostPath.c_str(), ec.message().c_str()); HostPathText(tempPath).c_str(), HostPathText(hostPath).c_str(),
std::remove(tempPath.c_str()); ec.message().c_str());
NandRemove(tempPath);
return NAND_RESULT_UNKNOWN; return NAND_RESULT_UNKNOWN;
} }
FILE* file = std::fopen(tempPath.c_str(), "r+b"); FILE* file = NandFopen(tempPath, "r+b");
if (!file) { if (!file) {
LogNandError("NANDSafeOpen", "FAILED to open scratch file '%s'", tempPath.c_str()); LogNandError("NANDSafeOpen", "FAILED to open scratch file '%s'",
std::remove(tempPath.c_str()); HostPathText(tempPath).c_str());
NandRemove(tempPath);
return NAND_RESULT_UNKNOWN; return NAND_RESULT_UNKNOWN;
} }
+69 -57
View File
@@ -12,7 +12,7 @@
// ============================================================================ // ============================================================================
// Base path for the host Wii NAND directory (resolved at runtime). // Base path for the host Wii NAND directory (resolved at runtime).
static std::string g_dolphinWiiBase; static std::filesystem::path g_dolphinWiiBase;
static std::once_flag g_dolphinWiiBaseOnce; static std::once_flag g_dolphinWiiBaseOnce;
// ============================================================================ // ============================================================================
@@ -47,7 +47,7 @@ std::map<int32_t, FileHandle> g_fileHandles;
static int32_t g_nextFd = 100; // Start at 100 to avoid confusion with stdio fds static int32_t g_nextFd = 100; // Start at 100 to avoid confusion with stdio fds
std::mutex g_fdMutex; std::mutex g_fdMutex;
int32_t AllocateFd(const std::string& path, FILE* file, int32_t mode) { int32_t AllocateFd(const std::filesystem::path& path, FILE* file, int32_t mode) {
std::lock_guard<std::mutex> lock(g_fdMutex); std::lock_guard<std::mutex> lock(g_fdMutex);
int32_t fd = g_nextFd++; int32_t fd = g_nextFd++;
g_fileHandles[fd] = {file, path, mode, 0}; g_fileHandles[fd] = {file, path, mode, 0};
@@ -88,29 +88,50 @@ std::string CurrentNandDataDir() {
return path; return path;
} }
const std::string& GetNandBasePath() { const std::filesystem::path& GetNandBasePath() {
std::call_once(g_dolphinWiiBaseOnce, []() { std::call_once(g_dolphinWiiBaseOnce, []() {
g_dolphinWiiBase = RuntimeNandPath::DiscoverNandRootString(); g_dolphinWiiBase = RuntimeNandPath::DiscoverNandRootPath();
}); });
return g_dolphinWiiBase; return g_dolphinWiiBase;
} }
static std::string BuildHostNandPath(std::string wiiPathStr) { std::string HostPathText(const std::filesystem::path& path) {
std::string hostPath = GetNandBasePath(); return RuntimeConfigFile::PathToUtf8(path);
for (char& c : wiiPathStr) { }
if (c == '/') {
#ifdef _WIN32
c = '\\';
#endif
}
}
if (!wiiPathStr.empty() && (wiiPathStr[0] == '\\' || wiiPathStr[0] == '/')) { FILE* NandFopen(const std::filesystem::path& path, const char* mode) {
hostPath += wiiPathStr; #ifdef _WIN32
} else { const std::wstring wideMode(mode, mode + std::strlen(mode));
hostPath += "\\"; return _wfopen(path.c_str(), wideMode.c_str());
hostPath += wiiPathStr; #else
return std::fopen(path.c_str(), mode);
#endif
}
bool NandRemove(const std::filesystem::path& path) {
std::error_code ec;
return std::filesystem::remove(path, ec) && !ec;
}
bool NandRename(const std::filesystem::path& from, const std::filesystem::path& to) {
std::error_code ec;
std::filesystem::rename(from, to, ec);
return !ec;
}
// Guest paths are absolute and already lexically resolved against the NAND root, so
// they are appended as relative components instead of replacing the root.
static std::filesystem::path BuildHostNandPath(const std::string& wiiPathStr) {
std::filesystem::path hostPath = GetNandBasePath();
size_t cursor = 0;
while (cursor < wiiPathStr.size()) {
const size_t slash = wiiPathStr.find('/', cursor);
const size_t end = slash == std::string::npos ? wiiPathStr.size() : slash;
if (end != cursor) {
hostPath /= wiiPathStr.substr(cursor, end - cursor);
}
cursor = end + 1;
} }
return hostPath; return hostPath;
} }
@@ -169,7 +190,7 @@ static std::string NormalizeAbsoluteWiiPath(const char* wiiPath) {
struct RiivolutionSaveRedirect { struct RiivolutionSaveRedirect {
bool enabled = false; bool enabled = false;
bool clone = false; bool clone = false;
std::string hostDirectory; std::filesystem::path hostDirectory;
}; };
static std::once_flag g_riivolutionSaveRedirectOnce; static std::once_flag g_riivolutionSaveRedirectOnce;
@@ -187,7 +208,7 @@ static const RiivolutionSaveRedirect& GetRiivolutionSaveRedirect() {
} }
g_riivolutionSaveRedirect.enabled = true; g_riivolutionSaveRedirect.enabled = true;
g_riivolutionSaveRedirect.clone = redirect->clone; g_riivolutionSaveRedirect.clone = redirect->clone;
g_riivolutionSaveRedirect.hostDirectory = redirect->hostDirectory.string(); g_riivolutionSaveRedirect.hostDirectory = redirect->hostDirectory;
// Riivolution creates the redirect folder if it does not exist. // Riivolution creates the redirect folder if it does not exist.
CreateDirectoryPath(g_riivolutionSaveRedirect.hostDirectory); CreateDirectoryPath(g_riivolutionSaveRedirect.hostDirectory);
}); });
@@ -195,8 +216,8 @@ static const RiivolutionSaveRedirect& GetRiivolutionSaveRedirect() {
return g_riivolutionSaveRedirect; return g_riivolutionSaveRedirect;
} }
static void CloneRiivolutionSaveIfNeeded(const std::string& sourceHostPath, static void CloneRiivolutionSaveIfNeeded(const std::filesystem::path& sourceHostPath,
const std::string& redirectedHostPath, const std::filesystem::path& redirectedHostPath,
const RiivolutionSaveRedirect& redirect) { const RiivolutionSaveRedirect& redirect) {
if (!redirect.clone || PathExists(redirectedHostPath) || !PathExists(sourceHostPath)) { if (!redirect.clone || PathExists(redirectedHostPath) || !PathExists(sourceHostPath)) {
return; return;
@@ -209,11 +230,13 @@ static void CloneRiivolutionSaveIfNeeded(const std::string& sourceHostPath,
std::filesystem::copy_options::skip_existing, ec); std::filesystem::copy_options::skip_existing, ec);
if (ec) { if (ec) {
LogNandWarning("RiivolutionSave", "WARNING: failed to clone '%s' -> '%s': %s", LogNandWarning("RiivolutionSave", "WARNING: failed to clone '%s' -> '%s': %s",
sourceHostPath.c_str(), redirectedHostPath.c_str(), ec.message().c_str()); HostPathText(sourceHostPath).c_str(),
HostPathText(redirectedHostPath).c_str(), ec.message().c_str());
} }
} }
static bool ResolveRiivolutionSaveHostPath(const std::string& absoluteWiiPath, std::string& outHostPath) { static bool ResolveRiivolutionSaveHostPath(const std::string& absoluteWiiPath,
std::filesystem::path& outHostPath) {
const RiivolutionSaveRedirect& redirect = GetRiivolutionSaveRedirect(); const RiivolutionSaveRedirect& redirect = GetRiivolutionSaveRedirect();
if (!redirect.enabled) { if (!redirect.enabled) {
return false; return false;
@@ -232,23 +255,23 @@ static bool ResolveRiivolutionSaveHostPath(const std::string& absoluteWiiPath, s
relative = absoluteWiiPath.substr(dataDir.size() + 1); relative = absoluteWiiPath.substr(dataDir.size() + 1);
} }
std::filesystem::path redirected(redirect.hostDirectory); std::filesystem::path redirected = redirect.hostDirectory;
if (!relative.empty()) { if (!relative.empty()) {
redirected /= std::filesystem::path(relative); redirected /= std::filesystem::path(relative);
} }
outHostPath = redirected.string(); outHostPath = redirected;
CloneRiivolutionSaveIfNeeded(BuildHostNandPath(absoluteWiiPath), outHostPath, redirect); CloneRiivolutionSaveIfNeeded(BuildHostNandPath(absoluteWiiPath), outHostPath, redirect);
return true; return true;
} }
std::string TranslateNandPath(const char* wiiPath) { std::filesystem::path TranslateNandPath(const char* wiiPath) {
std::string wiiPathStr = NormalizeAbsoluteWiiPath(wiiPath); std::string wiiPathStr = NormalizeAbsoluteWiiPath(wiiPath);
if (wiiPathStr.empty()) { if (wiiPathStr.empty()) {
return ""; return {};
} }
std::string redirectedHostPath; std::filesystem::path redirectedHostPath;
if (ResolveRiivolutionSaveHostPath(wiiPathStr, redirectedHostPath)) { if (ResolveRiivolutionSaveHostPath(wiiPathStr, redirectedHostPath)) {
return redirectedHostPath; return redirectedHostPath;
} }
@@ -266,7 +289,8 @@ struct U8Node {
uint32_t size; uint32_t size;
}; };
static bool ExtractFromU8(const std::string& archivePath, const char* targetName, std::vector<uint8_t>& outData) { static bool ExtractFromU8(const std::filesystem::path& archivePath, const char* targetName,
std::vector<uint8_t>& outData) {
std::ifstream file(archivePath, std::ios::binary); std::ifstream file(archivePath, std::ios::binary);
if (!file) { if (!file) {
return false; return false;
@@ -352,13 +376,13 @@ static bool ExtractFromU8(const std::string& archivePath, const char* targetName
// as an argument and the path predicate below hard-codes the same name. // as an argument and the path predicate below hard-codes the same name.
static constexpr char kFaceLibResourceName[] = "RFL_Res.dat"; static constexpr char kFaceLibResourceName[] = "RFL_Res.dat";
bool SeedFaceLibResource(const std::string& hostPath) { bool SeedFaceLibResource(const std::filesystem::path& hostPath) {
std::vector<uint8_t> payload; std::vector<uint8_t> payload;
if (const auto dvdRoot = RuntimeConfigFile::ResolvedDvdRoot(); !dvdRoot.empty()) { if (const auto dvdRoot = RuntimeConfigFile::ResolvedDvdRoot(); !dvdRoot.empty()) {
const auto arcPath = dvdRoot / "files" / "contents" / "RFLRes01.arc"; const auto arcPath = dvdRoot / "files" / "contents" / "RFLRes01.arc";
std::error_code ec; std::error_code ec;
if (std::filesystem::exists(arcPath, ec)) { if (std::filesystem::exists(arcPath, ec)) {
ExtractFromU8(arcPath.string(), kFaceLibResourceName, payload); ExtractFromU8(arcPath, kFaceLibResourceName, payload);
} }
} }
@@ -371,12 +395,12 @@ bool SeedFaceLibResource(const std::string& hostPath) {
std::ofstream out(hostPath, std::ios::binary); std::ofstream out(hostPath, std::ios::binary);
if (!out) { if (!out) {
LogNandError("FaceLibSeed", "Failed to create %s", hostPath.c_str()); LogNandError("FaceLibSeed", "Failed to create %s", HostPathText(hostPath).c_str());
return false; return false;
} }
out.write(reinterpret_cast<const char*>(payload.data()), static_cast<std::streamsize>(payload.size())); out.write(reinterpret_cast<const char*>(payload.data()), static_cast<std::streamsize>(payload.size()));
if (!out) { if (!out) {
LogNandError("FaceLibSeed", "Failed to write %s", hostPath.c_str()); LogNandError("FaceLibSeed", "Failed to write %s", HostPathText(hostPath).c_str());
return false; return false;
} }
@@ -388,45 +412,33 @@ bool IsFaceLibResourcePath(const char* path) {
} }
// Create directories recursively // Create directories recursively
bool CreateDirectoryPath(const std::string& path) { bool CreateDirectoryPath(const std::filesystem::path& path) {
if (path.empty()) { if (path.empty()) {
return true; return true;
} }
std::error_code ec; std::error_code ec;
std::filesystem::create_directories(path, ec); std::filesystem::create_directories(path, ec);
return !ec || std::filesystem::is_directory(path); return !ec || std::filesystem::is_directory(path, ec);
} }
// Check if a path exists // Check if a path exists
bool PathExists(const std::string& path) { bool PathExists(const std::filesystem::path& path) {
#ifdef _WIN32 std::error_code ec;
return GetFileAttributesA(path.c_str()) != INVALID_FILE_ATTRIBUTES; return std::filesystem::exists(path, ec) && !ec;
#else
return access(path.c_str(), F_OK) == 0;
#endif
} }
// Check if path is a directory // Check if path is a directory
bool IsDirectory(const std::string& path) { bool IsDirectory(const std::filesystem::path& path) {
#ifdef _WIN32 std::error_code ec;
DWORD attrs = GetFileAttributesA(path.c_str()); return std::filesystem::is_directory(path, ec) && !ec;
return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY);
#else
struct stat st;
return stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode);
#endif
} }
bool CreateParentDirectories(const std::string& path) { bool CreateParentDirectories(const std::filesystem::path& path) {
size_t lastSlash = path.rfind('\\'); if (!path.has_parent_path()) {
if (lastSlash == std::string::npos) {
lastSlash = path.rfind('/');
}
if (lastSlash == std::string::npos) {
return false; return false;
} }
CreateDirectoryPath(path.substr(0, lastSlash)); CreateDirectoryPath(path.parent_path());
return true; return true;
} }
+22 -18
View File
@@ -29,15 +29,10 @@
#include <vector> #include <vector>
#include <filesystem> #include <filesystem>
#include <string> #include <string>
#include <sys/stat.h>
#ifdef _WIN32 #ifdef _WIN32
#include <direct.h>
#include <io.h> #include <io.h>
#include <windows.h> #include <windows.h>
#define mkdir(path, mode) _mkdir(path)
#define access _access
#define F_OK 0
#else #else
#include <fcntl.h> #include <fcntl.h>
#include <sys/types.h> #include <sys/types.h>
@@ -67,19 +62,19 @@ void LogNandWarning(const char* func, const char* fmt, ...);
struct FileHandle { struct FileHandle {
FILE* file = nullptr; FILE* file = nullptr;
std::string path; std::filesystem::path path;
int32_t mode = 0; // 1=read, 2=write, 3=read/write int32_t mode = 0; // 1=read, 2=write, 3=read/write
uint32_t position = 0; uint32_t position = 0;
// Non-empty only for write-mode NANDSafeOpen handles. `path` then points at the // Non-empty only for write-mode NANDSafeOpen handles. `path` then points at the
// sibling scratch file the guest is writing into, and this is the original file it // sibling scratch file the guest is writing into, and this is the original file it
// atomically replaces on NANDSafeClose. // atomically replaces on NANDSafeClose.
std::string safeCommitPath; std::filesystem::path safeCommitPath;
}; };
extern std::map<int32_t, FileHandle> g_fileHandles; extern std::map<int32_t, FileHandle> g_fileHandles;
extern std::mutex g_fdMutex; extern std::mutex g_fdMutex;
int32_t AllocateFd(const std::string& path, FILE* file, int32_t mode); int32_t AllocateFd(const std::filesystem::path& path, FILE* file, int32_t mode);
FileHandle* GetHandle(int32_t fd); FileHandle* GetHandle(int32_t fd);
void CloseFd(int32_t fd); void CloseFd(int32_t fd);
@@ -89,18 +84,27 @@ void CloseFd(int32_t fd);
uint32_t CurrentMkwTitleIdLo(); uint32_t CurrentMkwTitleIdLo();
std::string CurrentNandDataDir(); std::string CurrentNandDataDir();
const std::string& GetNandBasePath(); const std::filesystem::path& GetNandBasePath();
std::string TranslateNandPath(const char* wiiPath); std::filesystem::path TranslateNandPath(const char* wiiPath);
bool CreateDirectoryPath(const std::string& path); bool CreateDirectoryPath(const std::filesystem::path& path);
bool PathExists(const std::string& path); bool PathExists(const std::filesystem::path& path);
bool IsDirectory(const std::string& path); bool IsDirectory(const std::filesystem::path& path);
// Create the directory that contains `path`. False when `path` has no directory // Create the directory that contains `path`. False when `path` has no directory
// component, i.e. there was nothing to create. // component, i.e. there was nothing to create.
bool CreateParentDirectories(const std::string& path); bool CreateParentDirectories(const std::filesystem::path& path);
bool SeedFaceLibResource(const std::string& hostPath); // Host paths keep their native encoding end to end; these are the only places a NAND
// path is narrowed, and they narrow to UTF-8 for display.
std::string HostPathText(const std::filesystem::path& path);
// fopen takes an ANSI-codepage name on Windows, which cannot express every path.
FILE* NandFopen(const std::filesystem::path& path, const char* mode);
bool NandRemove(const std::filesystem::path& path);
bool NandRename(const std::filesystem::path& from, const std::filesystem::path& to);
bool SeedFaceLibResource(const std::filesystem::path& hostPath);
bool IsFaceLibResourcePath(const char* path); bool IsFaceLibResourcePath(const char* path);
// ============================================================================ // ============================================================================
@@ -178,9 +182,9 @@ enum ISFSResult {
int32_t ISFS_OpenLib_Initialize(CpuContext* ctx); int32_t ISFS_OpenLib_Initialize(CpuContext* ctx);
// Shadow-write machinery, defined with the NANDSafeOpen/NANDSafeClose section below. // Shadow-write machinery, defined with the NANDSafeOpen/NANDSafeClose section below.
std::string SafeTempPathFor(const std::string& hostPath); std::filesystem::path SafeTempPathFor(const std::filesystem::path& hostPath);
bool DiscardStaleSafeTemp(const std::string& tempPath); bool DiscardStaleSafeTemp(const std::filesystem::path& tempPath);
bool IsHostPathOpen(const std::string& hostPath); bool IsHostPathOpen(const std::filesystem::path& hostPath);
int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError); int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError);
// Synchronous NAND library entry points (defined in nand_api.cpp); the async // Synchronous NAND library entry points (defined in nand_api.cpp); the async
+98 -30
View File
@@ -329,7 +329,7 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
} }
// It's a NAND file path // It's a NAND file path
std::string hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
// Seed FaceLib resources before the existence check so every open mode can // Seed FaceLib resources before the existence check so every open mode can
// still find them on a fresh managed NAND. // still find them on a fresh managed NAND.
@@ -346,16 +346,16 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
if (mode == 2 || mode == 3) { if (mode == 2 || mode == 3) {
if (!PathExists(hostPath)) { if (!PathExists(hostPath)) {
LogNandWarning("IOS_Open", "'%s' does not exist; open mode %u never creates it", LogNandWarning("IOS_Open", "'%s' does not exist; open mode %u never creates it",
hostPath.c_str(), mode); HostPathText(hostPath).c_str(), mode);
return ISFS_ENOENT; return ISFS_ENOENT;
} }
fopenMode = "r+b"; // Write-only opens still need read for seeks fopenMode = "r+b"; // Write-only opens still need read for seeks
} }
FILE* file = std::fopen(hostPath.c_str(), fopenMode); FILE* file = NandFopen(hostPath, fopenMode);
if (!file) { if (!file) {
LogNandError("IOS_Open", "FAILED to open '%s'", hostPath.c_str()); LogNandError("IOS_Open", "FAILED to open '%s'", HostPathText(hostPath).c_str());
return ISFS_ENOENT; return ISFS_ENOENT;
} }
@@ -516,14 +516,9 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
return ISFS_EINVAL; return ISFS_EINVAL;
} }
const char* path = (const char*)Memory::GetPointer(inBufPtr + 6); const char* path = (const char*)Memory::GetPointer(inBufPtr + 6);
std::string hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
if (CreateDirectoryPath(hostPath)) { if (CreateDirectoryPath(hostPath)) {
#ifdef _WIN32
_mkdir(hostPath.c_str());
#else
mkdir(hostPath.c_str(), 0755);
#endif
return ISFS_OK; return ISFS_OK;
} }
return ISFS_EIO; return ISFS_EIO;
@@ -534,16 +529,11 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
return ISFS_EINVAL; return ISFS_EINVAL;
} }
const char* path = (const char*)Memory::GetPointer(inBufPtr); const char* path = (const char*)Memory::GetPointer(inBufPtr);
std::string hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
if (IsDirectory(hostPath)) { // fs::remove refuses a non-empty directory, matching rmdir.
#ifdef _WIN32 if (NandRemove(hostPath)) {
if (RemoveDirectoryA(hostPath.c_str())) return ISFS_OK; return ISFS_OK;
#else
if (rmdir(hostPath.c_str()) == 0) return ISFS_OK;
#endif
} else {
if (std::remove(hostPath.c_str()) == 0) return ISFS_OK;
} }
return ISFS_ENOENT; return ISFS_ENOENT;
} }
@@ -553,7 +543,7 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
return ISFS_EINVAL; return ISFS_EINVAL;
} }
const char* path = (const char*)Memory::GetPointer(inBufPtr); const char* path = (const char*)Memory::GetPointer(inBufPtr);
std::string hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
if (!PathExists(hostPath)) { if (!PathExists(hostPath)) {
return ISFS_ENOENT; return ISFS_ENOENT;
@@ -582,11 +572,11 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
return ISFS_EINVAL; return ISFS_EINVAL;
} }
const char* path = (const char*)Memory::GetPointer(inBufPtr + 6); const char* path = (const char*)Memory::GetPointer(inBufPtr + 6);
std::string hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
CreateParentDirectories(hostPath); CreateParentDirectories(hostPath);
// Create empty file // Create empty file
FILE* f = std::fopen(hostPath.c_str(), "wb"); FILE* f = NandFopen(hostPath, "wb");
if (f) { if (f) {
std::fclose(f); std::fclose(f);
return ISFS_OK; return ISFS_OK;
@@ -606,10 +596,10 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
} }
const char* srcPath = (const char*)Memory::GetPointer(inBufPtr); const char* srcPath = (const char*)Memory::GetPointer(inBufPtr);
const char* dstPath = (const char*)Memory::GetPointer(inBufPtr + 0x40); const char* dstPath = (const char*)Memory::GetPointer(inBufPtr + 0x40);
std::string srcHost = TranslateNandPath(srcPath); const std::filesystem::path srcHost = TranslateNandPath(srcPath);
std::string dstHost = TranslateNandPath(dstPath); const std::filesystem::path dstHost = TranslateNandPath(dstPath);
if (std::rename(srcHost.c_str(), dstHost.c_str()) == 0) { if (NandRename(srcHost, dstHost)) {
return ISFS_OK; return ISFS_OK;
} }
return ISFS_EIO; return ISFS_EIO;
@@ -805,11 +795,11 @@ int32_t ISFS_OpenLib_Initialize(CpuContext* ctx) {
g_isfsInitialized = true; g_isfsInitialized = true;
// Create the title data directory if it doesn't exist // Create the title data directory if it doesn't exist
char titlePath[256]; char titleId[32];
const std::string& base = GetNandBasePath(); std::snprintf(titleId, sizeof(titleId), "%08x", kNandTitleIdHi);
std::snprintf(titlePath, sizeof(titlePath), "%s\\title\\%08x\\%08x\\data", char gameId[32];
base.c_str(), kNandTitleIdHi, CurrentMkwTitleIdLo()); std::snprintf(gameId, sizeof(gameId), "%08x", CurrentMkwTitleIdLo());
CreateDirectoryPath(titlePath); CreateDirectoryPath(GetNandBasePath() / "title" / titleId / gameId / "data");
if (!ctx) { if (!ctx) {
return ISFS_OK; return ISFS_OK;
@@ -899,6 +889,74 @@ REGISTER_NATIVE_FUNCTION_AS(0x80169BCC, ISFS_OpenLib_HLE_80169BCC, "ISFS_OpenLib
// IOS_Ioctlv HLE - Vector Ioctl for complex ISFS operations // IOS_Ioctlv HLE - Vector Ioctl for complex ISFS operations
// ============================================================================ // ============================================================================
static int32_t HandleIsfsReadDir(uint32_t numIn, uint32_t numOut, uint32_t vectorPtr) {
const bool countOnly = (numIn == 1 && numOut == 1);
if (!countOnly && !(numIn == 2 && numOut == 2)) {
LogNandWarning("IOS_Ioctlv", "READDIR unsupported vector shape numIn=%u numOut=%u",
numIn, numOut);
return ISFS_EINVAL;
}
const IosVector pathVec = ReadIosVector(vectorPtr, 0);
const std::string wiiPath = ReadGuestCString(pathVec.address, 64);
if (wiiPath.empty()) {
return ISFS_EINVAL;
}
const std::filesystem::path hostPath = TranslateNandPath(wiiPath.c_str());
if (!IsDirectory(hostPath)) {
return ISFS_ENOENT;
}
// NAND names are at most 12 characters; longer host names cannot exist on
// a real NAND (this also hides *.nandsafe.tmp write shadows).
constexpr size_t kMaxNandNameLength = 12;
std::vector<std::string> names;
std::error_code ec;
for (const auto& entry : std::filesystem::directory_iterator(hostPath, ec)) {
std::string name = HostPathText(entry.path().filename());
if (name.empty() || name.size() > kMaxNandNameLength) {
continue;
}
names.push_back(std::move(name));
}
std::sort(names.begin(), names.end());
if (countOnly) {
const IosVector countOut = ReadIosVector(vectorPtr, 1);
if (countOut.size < 4 || !Memory::Contains(countOut.address, 4)) {
return ISFS_EINVAL;
}
Memory::Write32(countOut.address, static_cast<uint32_t>(names.size()));
return ISFS_OK;
}
const IosVector maxVec = ReadIosVector(vectorPtr, 1);
const IosVector namesOut = ReadIosVector(vectorPtr, 2);
const IosVector countOut = ReadIosVector(vectorPtr, 3);
if (maxVec.size < 4 || !Memory::Contains(maxVec.address, 4) ||
countOut.size < 4 || !Memory::Contains(countOut.address, 4) ||
!IsValidGuestRange(namesOut.address, namesOut.size)) {
return ISFS_EINVAL;
}
const uint32_t maxCount = Memory::Read32(maxVec.address);
constexpr uint32_t kEntryWindow = 13; // 12 chars + terminator
uint32_t cursor = 0;
uint32_t written = 0;
for (const std::string& name : names) {
if (written >= maxCount || cursor + kEntryWindow > namesOut.size) {
break;
}
uint8_t* out = Memory::GetPointer(namesOut.address + cursor, kEntryWindow);
std::memset(out, 0, kEntryWindow);
std::memcpy(out, name.data(), name.size());
cursor += static_cast<uint32_t>(name.size()) + 1;
++written;
}
Memory::Write32(countOut.address, written);
return ISFS_OK;
}
extern "C" int32_t NAND_IOS_Ioctlv_HLE( extern "C" int32_t NAND_IOS_Ioctlv_HLE(
uint32_t fd, uint32_t fd,
uint32_t cmd, uint32_t cmd,
@@ -919,6 +977,16 @@ extern "C" int32_t NAND_IOS_Ioctlv_HLE(
return HandleDolphinIoctlv(cmd, numIn, numOut, vectorPtr); return HandleDolphinIoctlv(cmd, numIn, numOut, vectorPtr);
} }
if (fd == ISFS_DEV_FD) {
if (!vectorPtr || !Memory::Contains(vectorPtr, static_cast<size_t>(numIn + numOut) * 8u)) {
return ISFS_EINVAL;
}
if (cmd == ISFS_IOCTL_READDIR) {
return HandleIsfsReadDir(numIn, numOut, vectorPtr);
}
return ISFS_OK;
}
if (fd == ES_DEV_FD) { if (fd == ES_DEV_FD) {
if (!vectorPtr || !Memory::Contains(vectorPtr, static_cast<size_t>(numIn + numOut) * 8u)) { if (!vectorPtr || !Memory::Contains(vectorPtr, static_cast<size_t>(numIn + numOut) * 8u)) {
return ISFS_EINVAL; return ISFS_EINVAL;
+30 -16
View File
@@ -74,8 +74,18 @@ std::string RiivoGameId() {
return id; return id;
} }
// Every narrow path string here is UTF-8, including the ones the XML halves of
// resolved paths are concatenated with.
using RuntimeConfigFile::PathFromUtf8;
using RuntimeConfigFile::PathToUtf8;
std::string RiivoGenericText(const fs::path& path) {
const std::u8string text = path.generic_u8string();
return std::string(text.begin(), text.end());
}
std::string RiivoComparablePath(const fs::path& path) { std::string RiivoComparablePath(const fs::path& path) {
std::string text = path.lexically_normal().generic_string(); std::string text = RiivoGenericText(path.lexically_normal());
#ifdef _WIN32 #ifdef _WIN32
RuntimeHle::LowerInPlace(text); RuntimeHle::LowerInPlace(text);
#endif #endif
@@ -86,6 +96,8 @@ void RiivoAddRoot(std::vector<RuntimeRiivolution::Overlay>& overlays, fs::path r
const char* source) { const char* source) {
std::error_code ec; std::error_code ec;
if (!fs::is_directory(root, ec)) { if (!fs::is_directory(root, ec)) {
RT_LOG(RT_TAG_RIIVOLUTION) << "rejected overlay root (" << (source ? source : "unknown")
<< "): " << PathToUtf8(root) << " is not a reachable directory" << std::endl;
return; return;
} }
@@ -104,7 +116,7 @@ void RiivoAddRoot(std::vector<RuntimeRiivolution::Overlay>& overlays, fs::path r
} }
RT_LOG(RT_TAG_RIIVOLUTION) << "overlay root (" << (source ? source : "unknown") RT_LOG(RT_TAG_RIIVOLUTION) << "overlay root (" << (source ? source : "unknown")
<< "): " << normalized.string() << std::endl; << "): " << PathToUtf8(normalized) << std::endl;
overlays.push_back({std::move(normalized), std::nullopt}); overlays.push_back({std::move(normalized), std::nullopt});
} }
@@ -133,7 +145,7 @@ std::vector<RuntimeRiivolution::Overlay> RiivoDiscoverRoots() {
} }
for (const auto& root : RecompMod::DvdOverlayRoots()) { for (const auto& root : RecompMod::DvdOverlayRoots()) {
RiivoAddRoot(overlays, fs::path(root), "recomp mod manifest"); RiivoAddRoot(overlays, root, "recomp mod manifest");
} }
return overlays; return overlays;
@@ -154,7 +166,7 @@ std::optional<RiivoXmlSet> RiivoFindXmls(const fs::path& overlayRoot) {
// <sd>/RetroRewind6), so externals resolve against the root's parent. // <sd>/RetroRewind6), so externals resolve against the root's parent.
const std::string& configured = RecompMod::RiivolutionXml(); const std::string& configured = RecompMod::RiivolutionXml();
if (!configured.empty()) { if (!configured.empty()) {
const fs::path configuredXml = overlayRoot / fs::path(configured); const fs::path configuredXml = overlayRoot / PathFromUtf8(configured);
if (fs::is_regular_file(configuredXml, ec)) { if (fs::is_regular_file(configuredXml, ec)) {
return RiivoXmlSet{overlayRoot.parent_path(), {configuredXml}}; return RiivoXmlSet{overlayRoot.parent_path(), {configuredXml}};
} }
@@ -211,7 +223,7 @@ void RiivoCollectMappings(const RiivolutionContract::Patch& patch, const std::st
++set.skippedExternals; ++set.skippedExternals;
continue; continue;
} }
const fs::path hostFile(*resolved); const fs::path hostFile = PathFromUtf8(*resolved);
if (!fs::is_regular_file(hostFile, ec)) { if (!fs::is_regular_file(hostFile, ec)) {
++set.skippedExternals; ++set.skippedExternals;
continue; continue;
@@ -227,7 +239,7 @@ void RiivoCollectMappings(const RiivolutionContract::Patch& patch, const std::st
++set.skippedExternals; ++set.skippedExternals;
continue; continue;
} }
const fs::path hostFolder(*resolved); const fs::path hostFolder = PathFromUtf8(*resolved);
if (!fs::is_directory(hostFolder, ec)) { if (!fs::is_directory(hostFolder, ec)) {
++set.skippedExternals; ++set.skippedExternals;
continue; continue;
@@ -246,7 +258,7 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
} }
const std::string gameId = RiivoGameId(); const std::string gameId = RiivoGameId();
const std::string sdRootGeneric = xmlSet->sdRoot.generic_string(); const std::string sdRootGeneric = RiivoGenericText(xmlSet->sdRoot);
const auto manifestSelections = RiivoManifestSelections(); const auto manifestSelections = RiivoManifestSelections();
// Dolphin-compatible remembered choices; a recomp.yml pin overrides them. // Dolphin-compatible remembered choices; a recomp.yml pin overrides them.
@@ -261,19 +273,20 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
for (const fs::path& xmlFile : xmlSet->xmlFiles) { for (const fs::path& xmlFile : xmlSet->xmlFiles) {
const auto text = RiivoReadFile(xmlFile); const auto text = RiivoReadFile(xmlFile);
if (!text) { if (!text) {
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: cannot read " << xmlFile.string() << std::endl; RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: cannot read " << PathToUtf8(xmlFile)
<< std::endl;
continue; continue;
} }
auto disc = RiivolutionContract::ParseString(*text); auto disc = RiivolutionContract::ParseString(*text);
if (!disc) { if (!disc) {
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << xmlFile.string() RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << PathToUtf8(xmlFile)
<< " is not a valid Riivolution XML (version 1 wiidisc); ignoring it" << " is not a valid Riivolution XML (version 1 wiidisc); ignoring it"
<< std::endl; << std::endl;
continue; continue;
} }
if (!disc->IsValidForGame(gameId, std::nullopt, std::nullopt)) { if (!disc->IsValidForGame(gameId, std::nullopt, std::nullopt)) {
RT_LOG(RT_TAG_RIIVOLUTION) << xmlFile.string() << ": not valid for " << gameId RT_LOG(RT_TAG_RIIVOLUTION) << PathToUtf8(xmlFile) << ": not valid for " << gameId
<< ", skipped" << std::endl; << ", skipped" << std::endl;
continue; continue;
} }
@@ -284,7 +297,7 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
RiivolutionContract::ApplySelections(*disc, manifestSelections); RiivolutionContract::ApplySelections(*disc, manifestSelections);
const auto activePatches = disc->GeneratePatches(gameId); const auto activePatches = disc->GeneratePatches(gameId);
const std::string xmlDirGeneric = xmlFile.parent_path().generic_string(); const std::string xmlDirGeneric = RiivoGenericText(xmlFile.parent_path());
const size_t before = set.mappings.size(); const size_t before = set.mappings.size();
for (const auto& patch : activePatches) { for (const auto& patch : activePatches) {
@@ -297,9 +310,10 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
if (const auto resolvedSave = RiivolutionContract::MakeAbsoluteFromRelative( if (const auto resolvedSave = RiivolutionContract::MakeAbsoluteFromRelative(
sdRootGeneric, xmlDirGeneric, savegame->external)) { sdRootGeneric, xmlDirGeneric, savegame->external)) {
state.saveRedirect = state.saveRedirect =
RuntimeRiivolution::SaveRedirect{fs::path(*resolvedSave), savegame->clone}; RuntimeRiivolution::SaveRedirect{PathFromUtf8(*resolvedSave),
savegame->clone};
RT_LOG(RT_TAG_RIIVOLUTION) << "savegame redirect: " RT_LOG(RT_TAG_RIIVOLUTION) << "savegame redirect: "
<< state.saveRedirect->hostDirectory.string() << PathToUtf8(state.saveRedirect->hostDirectory)
<< (savegame->clone ? " (clone)" : "") << std::endl; << (savegame->clone ? " (clone)" : "") << std::endl;
} }
} }
@@ -308,11 +322,11 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
// A pack whose XML parses but activates nothing is the most confusing // A pack whose XML parses but activates nothing is the most confusing
// failure this layer has: the game boots, plays, and quietly shows // failure this layer has: the game boots, plays, and quietly shows
// vanilla content. Always say what happened. // vanilla content. Always say what happened.
RT_LOG(RT_TAG_RIIVOLUTION) << xmlFile.string() << ": " << activePatches.size() RT_LOG(RT_TAG_RIIVOLUTION) << PathToUtf8(xmlFile) << ": " << activePatches.size()
<< " active patch(es), " << (set.mappings.size() - before) << " mapping(s)" << " active patch(es), " << (set.mappings.size() - before) << " mapping(s)"
<< std::endl; << std::endl;
if (activePatches.empty()) { if (activePatches.empty()) {
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << xmlFile.string() RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << PathToUtf8(xmlFile)
<< " has no enabled options for " << gameId << " has no enabled options for " << gameId
<< "; check the riivolution option selections (recomp.yml) or " << "; check the riivolution option selections (recomp.yml) or "
<< sdRootGeneric << "/riivolution/config/" << gameId.substr(0, 4) << ".xml" << sdRootGeneric << "/riivolution/config/" << gameId.substr(0, 4) << ".xml"
@@ -321,7 +335,7 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
} }
if (set.skippedExternals != 0) { if (set.skippedExternals != 0) {
RT_LOG(RT_TAG_RIIVOLUTION) << overlayRoot.string() << ": skipped " RT_LOG(RT_TAG_RIIVOLUTION) << PathToUtf8(overlayRoot) << ": skipped "
<< set.skippedExternals << " mapping(s) whose external path does not exist" << set.skippedExternals << " mapping(s) whose external path does not exist"
<< std::endl; << std::endl;
} }
+35 -11
View File
@@ -47,6 +47,7 @@
#include "system_bridge.h" #include "system_bridge.h"
#include "ppc_runtime.h" #include "ppc_runtime.h"
#include "aurora_events.h" #include "aurora_events.h"
#include "wup028_adapter.h"
#include "fiber_manager.h" #include "fiber_manager.h"
#include "hle_stubs.h" #include "hle_stubs.h"
#include "runtime_config.h" #include "runtime_config.h"
@@ -394,7 +395,7 @@ void InitializeProcessTranscript(int argc, char** argv) {
} }
const std::filesystem::path path = GetRunLogDirectory() / "console.log"; const std::filesystem::path path = GetRunLogDirectory() / "console.log";
state.file.open(path.string(), std::ios::out | std::ios::trunc | std::ios::binary); state.file.open(path, std::ios::out | std::ios::trunc | std::ios::binary);
if (!state.file) { if (!state.file) {
return; return;
} }
@@ -570,14 +571,31 @@ std::string FormatHostStackTrace(unsigned framesToSkip) {
for (USHORT i = 0; i < captured; ++i) { for (USHORT i = 0; i < captured; ++i) {
const DWORD64 addr = reinterpret_cast<DWORD64>(frames[i]); const DWORD64 addr = reinterpret_cast<DWORD64>(frames[i]);
HMODULE module = nullptr; HMODULE module = nullptr;
char modulePath[MAX_PATH] = "?"; std::string modulePath = "?";
DWORD64 moduleBase = 0; DWORD64 moduleBase = 0;
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCSTR>(frames[i]), reinterpret_cast<LPCWSTR>(frames[i]),
&module) != 0 && &module) != 0 &&
module != nullptr) { module != nullptr) {
moduleBase = reinterpret_cast<DWORD64>(module); moduleBase = reinterpret_cast<DWORD64>(module);
(void)GetModuleFileNameA(module, modulePath, MAX_PATH); std::wstring modulePathBuffer(MAX_PATH, L'\0');
for (;;) {
const DWORD length =
GetModuleFileNameW(module, modulePathBuffer.data(), static_cast<DWORD>(modulePathBuffer.size()));
if (length == 0) {
break;
}
if (length < modulePathBuffer.size()) {
modulePathBuffer.resize(length);
modulePath = RuntimeConfigFile::PathToUtf8(std::filesystem::path(modulePathBuffer));
break;
}
// Truncated; retry with a larger buffer up to the extended path limit.
if (modulePathBuffer.size() >= 32768) {
break;
}
modulePathBuffer.resize(modulePathBuffer.size() * 2);
}
} }
const char* symbolName = "?"; const char* symbolName = "?";
@@ -641,7 +659,7 @@ void WriteFatalLogImpl(std::string_view reason, std::string_view extraDetails =
std::string fileName = "crash_"; std::string fileName = "crash_";
fileName.append(reason); fileName.append(reason);
fileName.append(".txt"); fileName.append(".txt");
std::ofstream out((runDirectory / fileName).string(), std::ios::out | std::ios::trunc); std::ofstream out(runDirectory / fileName, std::ios::out | std::ios::trunc);
if (!out) { if (!out) {
return; return;
} }
@@ -680,11 +698,12 @@ void WriteFatalLogImpl(std::string_view reason, std::string_view extraDetails =
// are large. // are large.
static std::atomic_bool s_memorySnapshotWritten{false}; static std::atomic_bool s_memorySnapshotWritten{false};
if (!s_memorySnapshotWritten.exchange(true, std::memory_order_acq_rel)) { if (!s_memorySnapshotWritten.exchange(true, std::memory_order_acq_rel)) {
SystemBridge::WriteGuestMemorySnapshot(out, (runDirectory / "mem1.bin").string().c_str()); SystemBridge::WriteGuestMemorySnapshot(out, runDirectory / "mem1.bin");
} }
out.flush(); out.flush();
RT_LOG(RT_TAG_RUNTIME) << "crash artifacts written to " << runDirectory.string() << std::endl; RT_LOG(RT_TAG_RUNTIME) << "crash artifacts written to "
<< RuntimeConfigFile::PathToUtf8(runDirectory) << std::endl;
} }
void SetRuntimeExitCodeImpl(int code) { void SetRuntimeExitCodeImpl(int code) {
@@ -1183,10 +1202,11 @@ int RuntimeMain(int argc, char** argv) {
std::filesystem::create_directories(rendererCacheDirectory, rendererPathError); std::filesystem::create_directories(rendererCacheDirectory, rendererPathError);
if (rendererPathError) { if (rendererPathError) {
RT_LOG(RT_TAG_RUNTIME) << "Unable to create renderer cache directory " RT_LOG(RT_TAG_RUNTIME) << "Unable to create renderer cache directory "
<< rendererCacheDirectory << ": " << rendererPathError.message() << std::endl; << RuntimeConfigFile::PathToUtf8(rendererCacheDirectory) << ": "
<< rendererPathError.message() << std::endl;
} }
const std::string auroraUserPath = applicationDataDirectory.string(); const std::string auroraUserPath = RuntimeConfigFile::PathToUtf8(applicationDataDirectory);
const std::string auroraCachePath = rendererCacheDirectory.string(); const std::string auroraCachePath = RuntimeConfigFile::PathToUtf8(rendererCacheDirectory);
auroraConfig.userPath = auroraUserPath.c_str(); auroraConfig.userPath = auroraUserPath.c_str();
auroraConfig.cachePath = auroraCachePath.c_str(); auroraConfig.cachePath = auroraCachePath.c_str();
auroraConfig.logCallback = &RuntimeAuroraLogCallback; auroraConfig.logCallback = &RuntimeAuroraLogCallback;
@@ -1252,6 +1272,7 @@ int RuntimeMain(int argc, char** argv) {
} }
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait); aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
GxGuestWrite::InstallAuroraHooks(); GxGuestWrite::InstallAuroraHooks();
Wup028Adapter::Initialize();
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width, UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
auroraInfo.windowSize.native_fb_height); auroraInfo.windowSize.native_fb_height);
settings_overlay::InitializeRuntimeSettings(); settings_overlay::InitializeRuntimeSettings();
@@ -1293,6 +1314,7 @@ int RuntimeMain(int argc, char** argv) {
// Shutdown fiber system // Shutdown fiber system
Fiber::GuestFiberManager::Shutdown(); Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true); WindowPlacementPersistence::Flush(true);
Wup028Adapter::Shutdown();
aurora_shutdown(); aurora_shutdown();
SetRuntimeExitCodeImpl(0); SetRuntimeExitCodeImpl(0);
ShutdownProcessTranscript(); ShutdownProcessTranscript();
@@ -1310,6 +1332,7 @@ int RuntimeMain(int argc, char** argv) {
SetRuntimeExitCodeImpl(1); SetRuntimeExitCodeImpl(1);
Fiber::GuestFiberManager::Shutdown(); Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true); WindowPlacementPersistence::Flush(true);
Wup028Adapter::Shutdown();
aurora_shutdown(); aurora_shutdown();
ShutdownProcessTranscript(); ShutdownProcessTranscript();
return 1; return 1;
@@ -1321,6 +1344,7 @@ int RuntimeMain(int argc, char** argv) {
SetRuntimeExitCodeImpl(1); SetRuntimeExitCodeImpl(1);
Fiber::GuestFiberManager::Shutdown(); Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true); WindowPlacementPersistence::Flush(true);
Wup028Adapter::Shutdown();
aurora_shutdown(); aurora_shutdown();
ShutdownProcessTranscript(); ShutdownProcessTranscript();
return 1; return 1;
+6 -6
View File
@@ -33,8 +33,8 @@ std::vector<RecompMod::InitializerFn>& PostRelInitializers() {
return initializers; return initializers;
} }
std::vector<std::string>& OverlayRoots() { std::vector<std::filesystem::path>& OverlayRoots() {
static std::vector<std::string> roots; static std::vector<std::filesystem::path> roots;
return roots; return roots;
} }
@@ -279,17 +279,17 @@ void RegisterDvdOverlayRoot(std::string root) {
// against the executable directory instead. // against the executable directory instead.
const std::filesystem::path base = const std::filesystem::path base =
RuntimeConfigFile::ExecutableDirectory().value_or(std::filesystem::current_path()); RuntimeConfigFile::ExecutableDirectory().value_or(std::filesystem::current_path());
root = RuntimeConfigFile::ResolveRelativeTo(base, root).string(); std::filesystem::path resolved = RuntimeConfigFile::ResolveRelativeTo(base, root);
std::lock_guard<std::mutex> lock(ModMutex()); std::lock_guard<std::mutex> lock(ModMutex());
auto& roots = OverlayRoots(); auto& roots = OverlayRoots();
const auto it = std::find(roots.begin(), roots.end(), root); const auto it = std::find(roots.begin(), roots.end(), resolved);
if (it == roots.end()) { if (it == roots.end()) {
roots.push_back(std::move(root)); roots.push_back(std::move(resolved));
} }
} }
const std::vector<std::string>& DvdOverlayRoots() { const std::vector<std::filesystem::path>& DvdOverlayRoots() {
return OverlayRoots(); return OverlayRoots();
} }
+81 -4
View File
@@ -1,5 +1,7 @@
#include "settings_overlay.h" #include "settings_overlay.h"
#include "wup028_adapter.h"
#include "audio_backend.h" #include "audio_backend.h"
#include "controller_mapping_wizard.h"
#include "game_graphics_options.h" #include "game_graphics_options.h"
#include "music_attenuation.h" #include "music_attenuation.h"
#include "runtime_config.h" #include "runtime_config.h"
@@ -310,6 +312,34 @@ void ApplyConfiguredMappings() {
} }
} }
void DrawGameCubeAdapterInfo() {
ImGui::Separator();
if (!ImGui::BeginMenu("GameCube adapter info")) return;
const auto adapter = Wup028Adapter::GetInfo();
const char* state = adapter.state == Wup028Adapter::ConnectionState::Connected
? "Connected"
: adapter.state == Wup028Adapter::ConnectionState::DriverError ? "Driver error"
: "Searching";
ImGui::Text("Status: %s", state);
if (!adapter.deviceName.empty()) {
ImGui::Text("Device: %s", adapter.deviceName.c_str());
}
ImGui::TextWrapped("%s", adapter.detail.c_str());
if (adapter.state == Wup028Adapter::ConnectionState::Connected) {
ImGui::Text("Poll rate: %.1f reports/s", adapter.pollRateHz);
ImGui::Text("Endpoints: IN 0x%02X, OUT 0x%02X", adapter.inputEndpoint, adapter.outputEndpoint);
for (size_t port = 0; port < adapter.ports.size(); ++port) {
const uint8_t type = adapter.portStatus[port] & 0x30;
const char* typeName = type == 0x10 ? "wired" : type == 0x20 ? "wireless" : "none";
ImGui::Text("Adapter port %u: %s (type %s, raw 0x%02X)", static_cast<unsigned>(port + 1),
adapter.ports[port] ? "Controller connected" : "Empty", typeName,
adapter.portStatus[port]);
}
}
ImGui::EndMenu();
}
void DrawControllerSettings() { void DrawControllerSettings() {
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) { for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
const std::string label = "Port " + std::to_string(port + 1); const std::string label = "Port " + std::to_string(port + 1);
@@ -320,20 +350,60 @@ void DrawControllerSettings() {
} }
ImGui::Separator(); ImGui::Separator();
const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort);
const int adapterAssignment = Wup028Adapter::GetPortAssignment(selectedGamePort);
if (adapterAssignment >= 0) {
ImGui::Text("Assigned: GameCube adapter port %d", adapterAssignment + 1);
} else {
const char* currentName = PADGetName(selectedGamePort);
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
}
if (ImGui::BeginMenu("Assign GameCube adapter port")) {
if (ImGui::MenuItem("None", nullptr, adapterAssignment < 0)) {
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
}
const auto adapter = Wup028Adapter::GetInfo();
for (int physicalPort = 0; physicalPort < PAD_CHANMAX; ++physicalPort) {
const std::string label = "Adapter port " + std::to_string(physicalPort + 1) +
(adapter.ports[static_cast<size_t>(physicalPort)] ? " (connected)" : " (empty)");
if (ImGui::MenuItem(label.c_str(), nullptr, adapterAssignment == physicalPort)) {
for (uint32_t gamePort = 0; gamePort < PAD_CHANMAX; ++gamePort) {
if (gamePort != selectedGamePort && Wup028Adapter::GetPortAssignment(gamePort) == physicalPort) {
RuntimeConfigFile::SetGameCubeAdapterPort(gamePort, -1);
}
}
PADClearPort(selectedGamePort);
Wup028Adapter::SetPortAssignment(selectedGamePort, physicalPort);
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, physicalPort);
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
}
}
ImGui::EndMenu();
}
if (ImGui::MenuItem("Unassign controller")) {
PADClearPort(selectedGamePort);
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
}
ImGui::Separator();
controller_mapping_wizard::DrawSetupList();
const uint32_t controllerCount = PADCount(); const uint32_t controllerCount = PADCount();
if (controllerCount == 0) { if (controllerCount == 0) {
ImGui::TextDisabled("No controller connected"); ImGui::TextDisabled("No controller connected");
DrawGameCubeAdapterInfo();
return; return;
} }
const char* currentName = PADGetName(static_cast<uint32_t>(g_controllerPort));
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
if (ImGui::BeginMenu("Assign connected controller")) { if (ImGui::BeginMenu("Assign connected controller")) {
for (uint32_t index = 0; index < controllerCount; ++index) { for (uint32_t index = 0; index < controllerCount; ++index) {
const char* name = PADGetNameForControllerIndex(index); const char* name = PADGetNameForControllerIndex(index);
ImGui::PushID(static_cast<int>(index)); ImGui::PushID(static_cast<int>(index));
if (ImGui::MenuItem(name != nullptr ? name : "Unknown controller")) { if (ImGui::MenuItem(name != nullptr ? name : "Unknown controller")) {
PADSetPortForIndex(index, static_cast<uint32_t>(g_controllerPort)); Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
PADSetPortForIndex(index, selectedGamePort);
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min()); g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
ApplyConfiguredMappings(); ApplyConfiguredMappings();
} }
@@ -346,6 +416,7 @@ void DrawControllerSettings() {
PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount); PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount);
if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) { if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) {
ImGui::TextDisabled("Assign a controller to edit its buttons"); ImGui::TextDisabled("Assign a controller to edit its buttons");
DrawGameCubeAdapterInfo();
return; return;
} }
@@ -485,7 +556,7 @@ void DrawControllerSettings() {
ImGui::TextUnformatted(kControllerButtons[i].label); ImGui::TextUnformatted(kControllerButtons[i].label);
ImGui::PopID(); ImGui::PopID();
} }
DrawGameCubeAdapterInfo();
} }
void DrawAudioSettings() { void DrawAudioSettings() {
@@ -840,6 +911,7 @@ void PersistDisplayModeIfChanged() {
} // namespace } // namespace
void InitializeRuntimeSettings() noexcept { void InitializeRuntimeSettings() noexcept {
controller_mapping_wizard::LoadPersistedMappings();
ApplyConfiguredMappings(); ApplyConfiguredMappings();
AudioBackend::Instance().SetMasterVolume(static_cast<float>(g_audioVolumePercent) / 100.0f); AudioBackend::Instance().SetMasterVolume(static_cast<float>(g_audioVolumePercent) / 100.0f);
AudioBackend::Instance().SetMuted(g_audioMuted); AudioBackend::Instance().SetMuted(g_audioMuted);
@@ -872,6 +944,7 @@ void HandleEvents(const AuroraEvent* events) noexcept {
if (ev->type != AURORA_SDL_EVENT) { if (ev->type != AURORA_SDL_EVENT) {
continue; continue;
} }
controller_mapping_wizard::HandleSdlEvent(ev->sdl);
if (IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) { if (IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) {
SetTopBarVisible(!g_topBarVisible); SetTopBarVisible(!g_topBarVisible);
} }
@@ -893,6 +966,10 @@ void Draw() noexcept {
} }
DrawFpsOverlay(); DrawFpsOverlay();
DrawTopBar(); DrawTopBar();
controller_mapping_wizard::Draw();
// The wizard captures raw presses; keep them out of the game even when the
// top bar is hidden mid-setup.
PADBlockInput(g_topBarVisible || controller_mapping_wizard::IsActive());
DrawStartupScreen(); DrawStartupScreen();
} }
+6 -4
View File
@@ -19,6 +19,7 @@
#include "memory.h" #include "memory.h"
#include "ppc_runtime.h" #include "ppc_runtime.h"
#include "recomp_mod_loader.h" #include "recomp_mod_loader.h"
#include "runtime_config.h"
#include "runtime_log.h" #include "runtime_log.h"
#include "runtime_product.h" #include "runtime_product.h"
#include "timebase_contract.h" #include "timebase_contract.h"
@@ -409,14 +410,14 @@ void SystemBridge::Initialize() {
RT_LOG(RT_TAG_RUNTIME) << "Executed " << count << " static constructors." << std::endl; RT_LOG(RT_TAG_RUNTIME) << "Executed " << count << " static constructors." << std::endl;
} }
void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const char* mem1Path) { void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const std::filesystem::path& mem1Path) {
constexpr uint32_t kMem1Base = 0x80000000u; constexpr uint32_t kMem1Base = 0x80000000u;
constexpr uint32_t kMem1Size = 0x01800000u; constexpr uint32_t kMem1Size = 0x01800000u;
if (Memory::Contains(kMem1Base, kMem1Size)) { if (Memory::Contains(kMem1Base, kMem1Size)) {
std::ofstream dump(mem1Path, std::ios::binary | std::ios::trunc); std::ofstream dump(mem1Path, std::ios::binary | std::ios::trunc);
dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem1Base, kMem1Size)), dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem1Base, kMem1Size)),
kMem1Size); kMem1Size);
os << "[runtime] MEM1 snapshot written to " << mem1Path os << "[runtime] MEM1 snapshot written to " << RuntimeConfigFile::PathToUtf8(mem1Path)
<< (dump.good() ? "" : " (write failed)") << std::endl; << (dump.good() ? "" : " (write failed)") << std::endl;
} }
constexpr uint32_t kMem2Base = 0x90000000u; constexpr uint32_t kMem2Base = 0x90000000u;
@@ -424,11 +425,12 @@ void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const char* mem1Pa
if (!Memory::Contains(kMem2Base, mem2Size)) { if (!Memory::Contains(kMem2Base, mem2Size)) {
continue; continue;
} }
const std::string mem2Path = std::string(mem1Path) + ".mem2"; std::filesystem::path mem2Path = mem1Path;
mem2Path += ".mem2";
std::ofstream dump(mem2Path, std::ios::binary | std::ios::trunc); std::ofstream dump(mem2Path, std::ios::binary | std::ios::trunc);
dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem2Base, mem2Size)), dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem2Base, mem2Size)),
mem2Size); mem2Size);
os << "[runtime] MEM2 snapshot written to " << mem2Path os << "[runtime] MEM2 snapshot written to " << RuntimeConfigFile::PathToUtf8(mem2Path)
<< (dump.good() ? "" : " (write failed)") << std::endl; << (dump.good() ? "" : " (write failed)") << std::endl;
break; break;
} }
+450
View File
@@ -0,0 +1,450 @@
#include "wup028_adapter.h"
#include "runtime_log.h"
#include "runtime_config.h"
#include <dolphin/pad.h>
#include <windows.h>
#include <initguid.h>
#include <setupapi.h>
#include <usb.h>
#include <usbiodef.h>
#include <winusb.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cwctype>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
namespace Wup028Adapter {
namespace {
constexpr uint16_t kNintendoVendor = 0x057e;
constexpr uint16_t kAdapterProduct = 0x0337;
constexpr size_t kReportSize = 37;
constexpr auto kInputReportTimeout = std::chrono::milliseconds(500);
std::mutex g_mutex;
std::array<PADStatus, PAD_CHANMAX> g_statuses{};
std::array<uint8_t, PAD_CHANMAX> g_rumble{};
std::array<int8_t, PAD_CHANMAX> g_portAssignments{{-1, -1, -1, -1}};
std::thread g_worker;
std::atomic_bool g_stop{false};
std::atomic_bool g_running{false};
std::atomic_bool g_connected{false};
AdapterInfo g_info;
struct Device {
HANDLE file = INVALID_HANDLE_VALUE;
HANDLE event = nullptr;
WINUSB_INTERFACE_HANDLE usb = nullptr;
UCHAR inputPipe = 0;
UCHAR outputPipe = 0;
~Device() { Close(); }
void Close() {
if (usb != nullptr) WinUsb_Free(usb);
if (event != nullptr) CloseHandle(event);
if (file != INVALID_HANDLE_VALUE) CloseHandle(file);
usb = nullptr;
event = nullptr;
file = INVALID_HANDLE_VALUE;
}
};
bool Transfer(Device& device, bool input, UCHAR pipe, UCHAR* data, ULONG size, ULONG& transferred,
DWORD timeoutMs, bool* timedOut = nullptr) {
if (timedOut != nullptr) *timedOut = false;
ResetEvent(device.event);
OVERLAPPED operation{};
operation.hEvent = device.event;
const BOOL started = input ? WinUsb_ReadPipe(device.usb, pipe, data, size, &transferred, &operation)
: WinUsb_WritePipe(device.usb, pipe, data, size, &transferred, &operation);
if (started) return true;
if (GetLastError() != ERROR_IO_PENDING) return false;
const DWORD wait = WaitForSingleObject(device.event, timeoutMs);
if (wait == WAIT_OBJECT_0) {
return WinUsb_GetOverlappedResult(device.usb, &operation, &transferred, FALSE);
}
CancelIoEx(device.file, &operation);
WaitForSingleObject(device.event, INFINITE);
WinUsb_GetOverlappedResult(device.usb, &operation, &transferred, FALSE);
if (timedOut != nullptr && wait == WAIT_TIMEOUT) *timedOut = true;
return false;
}
struct DeviceMatch {
std::wstring path;
std::string name;
};
std::string WideToUtf8(const wchar_t* value) {
if (value == nullptr || *value == L'\0') return {};
const int size = WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr);
if (size <= 1) return {};
std::string result(static_cast<size_t>(size), '\0');
WideCharToMultiByte(CP_UTF8, 0, value, -1, result.data(), size, nullptr, nullptr);
result.resize(static_cast<size_t>(size - 1));
return result;
}
DeviceMatch FindAdapter() {
HDEVINFO devices = SetupDiGetClassDevsW(&GUID_DEVINTERFACE_USB_DEVICE, nullptr, nullptr,
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (devices == INVALID_HANDLE_VALUE) return {};
DeviceMatch result;
for (DWORD index = 0;; ++index) {
SP_DEVICE_INTERFACE_DATA iface{sizeof(iface)};
if (!SetupDiEnumDeviceInterfaces(devices, nullptr, &GUID_DEVINTERFACE_USB_DEVICE, index, &iface)) break;
DWORD required = 0;
SetupDiGetDeviceInterfaceDetailW(devices, &iface, nullptr, 0, &required, nullptr);
if (required < sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W)) continue;
std::vector<uint8_t> storage(required);
auto* detail = reinterpret_cast<SP_DEVICE_INTERFACE_DETAIL_DATA_W*>(storage.data());
detail->cbSize = sizeof(*detail);
SP_DEVINFO_DATA deviceInfo{sizeof(deviceInfo)};
if (!SetupDiGetDeviceInterfaceDetailW(devices, &iface, detail, required, nullptr, &deviceInfo)) continue;
std::wstring lower(detail->DevicePath);
std::transform(lower.begin(), lower.end(), lower.begin(),
[](wchar_t c) { return static_cast<wchar_t>(std::towlower(c)); });
if (lower.find(L"vid_057e") != std::wstring::npos && lower.find(L"pid_0337") != std::wstring::npos) {
result.path = detail->DevicePath;
std::array<wchar_t, 256> description{};
if (SetupDiGetDeviceRegistryPropertyW(devices, &deviceInfo, SPDRP_FRIENDLYNAME, nullptr,
reinterpret_cast<BYTE*>(description.data()),
static_cast<DWORD>(description.size() * sizeof(wchar_t)), nullptr) ||
SetupDiGetDeviceRegistryPropertyW(devices, &deviceInfo, SPDRP_DEVICEDESC, nullptr,
reinterpret_cast<BYTE*>(description.data()),
static_cast<DWORD>(description.size() * sizeof(wchar_t)), nullptr)) {
result.name = WideToUtf8(description.data());
}
break;
}
}
SetupDiDestroyDeviceInfoList(devices);
return result;
}
bool Open(Device& device, std::string& name, std::string& error) {
const auto match = FindAdapter();
if (match.path.empty()) {
error = "No VID 057E / PID 0337 adapter is present";
return false;
}
name = match.name.empty() ? "WUP-028-compatible adapter" : match.name;
device.file = CreateFileW(match.path.c_str(), GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, nullptr);
if (device.file == INVALID_HANDLE_VALUE || !WinUsb_Initialize(device.file, &device.usb)) {
error = "WinUSB could not open the adapter (Windows error " + std::to_string(GetLastError()) + ")";
device.Close();
return false;
}
device.event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (device.event == nullptr) {
error = "Could not create the WinUSB transfer event";
device.Close();
return false;
}
USB_INTERFACE_DESCRIPTOR descriptor{};
if (!WinUsb_QueryInterfaceSettings(device.usb, 0, &descriptor)) {
error = "WinUSB could not query the adapter interface";
device.Close();
return false;
}
for (UCHAR index = 0; index < descriptor.bNumEndpoints; ++index) {
WINUSB_PIPE_INFORMATION pipe{};
if (!WinUsb_QueryPipe(device.usb, 0, index, &pipe)) continue;
if ((pipe.PipeType == UsbdPipeTypeInterrupt || pipe.PipeType == UsbdPipeTypeBulk) &&
USB_ENDPOINT_DIRECTION_IN(pipe.PipeId) && device.inputPipe == 0) {
device.inputPipe = pipe.PipeId;
} else if ((pipe.PipeType == UsbdPipeTypeInterrupt || pipe.PipeType == UsbdPipeTypeBulk) &&
USB_ENDPOINT_DIRECTION_OUT(pipe.PipeId) && device.outputPipe == 0) {
device.outputPipe = pipe.PipeId;
}
}
if (device.inputPipe == 0 || device.outputPipe == 0) {
error = "The adapter has no usable input/output endpoint";
device.Close();
return false;
}
UCHAR command = 0x13; // Enable the adapter's 37-byte input stream.
ULONG written = 0;
if (!Transfer(device, false, device.outputPipe, &command, 1, written, 1000) || written != 1) {
error = "The adapter rejected input initialization on endpoint 0x";
const char hex[] = "0123456789ABCDEF";
error += hex[device.outputPipe >> 4];
error += hex[device.outputPipe & 15];
device.Close();
return false;
}
return true;
}
int8_t Axis(uint8_t raw) {
constexpr int kCenter = 128;
constexpr int kCenterTolerance = 10;
if (raw >= kCenter - kCenterTolerance && raw <= kCenter + kCenterTolerance) return 0;
return static_cast<int8_t>(std::clamp(static_cast<int>(raw) - kCenter, -128, 127));
}
PADStatus DecodePort(const uint8_t* p) {
PADStatus out{};
// The WUP-028 protocol uses type 1 for wired pads and type 2 for
// WaveBird/wireless receivers. Testing only bit 0x10 drops every type-2
// controller and makes otherwise valid ports appear empty.
if ((p[0] & 0x30) == 0) {
out.err = PAD_ERR_NO_CONTROLLER;
return out;
}
if (p[1] & 0x01) out.button |= PAD_BUTTON_A;
if (p[1] & 0x02) out.button |= PAD_BUTTON_B;
if (p[1] & 0x04) out.button |= PAD_BUTTON_X;
if (p[1] & 0x08) out.button |= PAD_BUTTON_Y;
if (p[1] & 0x10) out.button |= PAD_BUTTON_LEFT;
if (p[1] & 0x20) out.button |= PAD_BUTTON_RIGHT;
if (p[1] & 0x40) out.button |= PAD_BUTTON_DOWN;
if (p[1] & 0x80) out.button |= PAD_BUTTON_UP;
if (p[2] & 0x01) out.button |= PAD_BUTTON_START;
if (p[2] & 0x02) out.button |= PAD_TRIGGER_Z;
if (p[2] & 0x04) out.button |= PAD_TRIGGER_R;
if (p[2] & 0x08) out.button |= PAD_TRIGGER_L;
out.stickX = Axis(p[3]);
out.stickY = Axis(p[4]);
out.substickX = Axis(p[5]);
out.substickY = Axis(p[6]);
out.triggerL = p[7];
out.triggerR = p[8];
out.err = PAD_ERR_NONE;
return out;
}
bool SendRumble(Device& device, const std::array<uint8_t, PAD_CHANMAX>& motors) {
std::array<UCHAR, 5> report{{0x11, motors[0], motors[1], motors[2], motors[3]}};
ULONG written = 0;
return Transfer(device, false, device.outputPipe, report.data(), report.size(), written, 1000) &&
written == report.size();
}
bool RefreshInputStream(Device& device) {
UCHAR command = 0x13;
ULONG written = 0;
return Transfer(device, false, device.outputPipe, &command, 1, written, 1000) && written == 1;
}
void ClearConnectedPorts(const char* reason) {
std::lock_guard lock(g_mutex);
g_rumble.fill(0);
for (size_t port = 0; port < g_info.ports.size(); ++port) {
if (g_info.ports[port]) {
g_info.ports[port] = false;
++g_info.portChangeSequence[port];
RT_LOG(RT_TAG_RUNTIME) << "GameCube adapter port " << (port + 1)
<< " controller disconnected (" << reason << ")" << std::endl;
}
g_statuses[port] = {};
g_statuses[port].err = PAD_ERR_NO_CONTROLLER;
g_info.portStatus[port] = 0;
}
}
void Worker() {
std::string lastError;
while (!g_stop.load(std::memory_order_acquire)) {
Device device;
std::string name;
std::string error;
if (!Open(device, name, error)) {
g_connected.store(false, std::memory_order_release);
{
std::lock_guard lock(g_mutex);
g_info.state = error.starts_with("No VID") ? ConnectionState::Searching : ConnectionState::DriverError;
g_info.deviceName = name;
g_info.detail = error;
g_info.pollRateHz = 0.0f;
g_info.inputEndpoint = 0;
g_info.outputEndpoint = 0;
g_info.ports.fill(false);
g_info.portStatus.fill(0);
}
if (error != lastError && !error.starts_with("No VID")) {
RT_LOG(RT_TAG_RUNTIME) << "GameCube adapter: " << error << std::endl;
}
lastError = error;
std::this_thread::sleep_for(std::chrono::seconds(1));
continue;
}
RT_LOG(RT_TAG_RUNTIME) << name << " connected (input endpoint 0x" << std::hex
<< static_cast<unsigned>(device.inputPipe) << ", output endpoint 0x"
<< static_cast<unsigned>(device.outputPipe) << std::dec << ")" << std::endl;
lastError.clear();
g_connected.store(true, std::memory_order_release);
{
std::lock_guard lock(g_mutex);
g_info.state = ConnectionState::Connected;
g_info.deviceName = name;
g_info.detail = "Receiving native GameCube reports";
g_info.inputEndpoint = device.inputPipe;
g_info.outputEndpoint = device.outputPipe;
}
std::array<uint8_t, PAD_CHANMAX> sentRumble{};
auto rateStart = std::chrono::steady_clock::now();
uint32_t rateReports = 0;
std::array<bool, PAD_CHANMAX> reportedPorts{};
auto lastReport = std::chrono::steady_clock::now();
while (!g_stop.load(std::memory_order_acquire)) {
std::array<UCHAR, kReportSize> report{};
ULONG read = 0;
bool timedOut = false;
if (!Transfer(device, true, device.inputPipe, report.data(), report.size(), read, 100, &timedOut)) {
if (timedOut && std::chrono::steady_clock::now() - lastReport < kInputReportTimeout) continue;
break;
}
if (read != report.size() || report[0] != 0x21) {
if (std::chrono::steady_clock::now() - lastReport >= kInputReportTimeout) break;
continue;
}
lastReport = std::chrono::steady_clock::now();
++rateReports;
std::array<PADStatus, PAD_CHANMAX> decoded{};
for (size_t port = 0; port < decoded.size(); ++port) decoded[port] = DecodePort(report.data() + 1 + port * 9);
std::array<uint8_t, PAD_CHANMAX> desired{};
std::array<int8_t, PAD_CHANMAX> transitions{};
bool refreshStream = false;
{
std::lock_guard lock(g_mutex);
g_statuses = decoded;
desired = g_rumble;
for (size_t port = 0; port < decoded.size(); ++port) {
g_info.portStatus[port] = report[1 + port * 9];
const bool present = decoded[port].err == PAD_ERR_NONE;
if (present != reportedPorts[port]) {
reportedPorts[port] = present;
g_info.ports[port] = present;
++g_info.portChangeSequence[port];
transitions[port] = present ? 1 : -1;
}
}
const auto now = std::chrono::steady_clock::now();
const float seconds = std::chrono::duration<float>(now - rateStart).count();
if (seconds >= 1.0f) {
g_info.pollRateHz = static_cast<float>(rateReports) / seconds;
rateReports = 0;
rateStart = now;
refreshStream = true;
}
}
for (size_t port = 0; port < transitions.size(); ++port) {
if (transitions[port] != 0) {
RT_LOG(RT_TAG_RUNTIME) << "GameCube adapter port " << (port + 1) << " controller "
<< (transitions[port] > 0 ? "connected" : "disconnected") << std::endl;
}
}
if (desired != sentRumble) {
if (!SendRumble(device, desired)) break;
sentRumble = desired;
}
if (refreshStream && !RefreshInputStream(device)) break;
}
// Do not leave a motor latched on when stopping or abandoning this handle.
SendRumble(device, {});
g_connected.store(false, std::memory_order_release);
ClearConnectedPorts("adapter unavailable");
{
std::lock_guard lock(g_mutex);
g_info.state = ConnectionState::Searching;
g_info.detail = "Adapter disconnected; waiting for reconnect";
g_info.pollRateHz = 0.0f;
}
if (!g_stop.load(std::memory_order_acquire)) {
RT_LOG(RT_TAG_RUNTIME) << "WUP-028 GameCube adapter disconnected; waiting for reconnect" << std::endl;
}
}
}
} // namespace
void Initialize() {
bool expected = false;
if (!g_running.compare_exchange_strong(expected, true)) return;
for (auto& status : g_statuses) status.err = PAD_ERR_NO_CONTROLLER;
for (size_t gamePort = 0; gamePort < g_portAssignments.size(); ++gamePort) {
g_portAssignments[gamePort] = static_cast<int8_t>(RuntimeConfigFile::GameCubeAdapterPort(gamePort));
}
g_stop.store(false, std::memory_order_release);
g_worker = std::thread(Worker);
}
void Shutdown() {
if (!g_running.exchange(false)) return;
g_stop.store(true, std::memory_order_release);
if (g_worker.joinable()) g_worker.join();
g_connected.store(false, std::memory_order_release);
}
bool Read(std::array<PADStatus, 4>& statuses) {
if (!g_connected.load(std::memory_order_acquire)) return false;
std::lock_guard lock(g_mutex);
for (auto& status : statuses) status.err = PAD_ERR_NO_CONTROLLER;
for (size_t gamePort = 0; gamePort < statuses.size(); ++gamePort) {
const int physicalPort = g_portAssignments[gamePort];
if (physicalPort >= 0) statuses[gamePort] = g_statuses[static_cast<size_t>(physicalPort)];
}
return true;
}
void SetPortAssignment(uint32_t gamePort, int physicalPort) {
if (gamePort >= g_portAssignments.size() || physicalPort < -1 || physicalPort >= PAD_CHANMAX) return;
std::lock_guard lock(g_mutex);
const int oldPhysicalPort = g_portAssignments[gamePort];
if (oldPhysicalPort >= 0) g_rumble[static_cast<size_t>(oldPhysicalPort)] = 0;
if (physicalPort >= 0) {
for (auto& assignment : g_portAssignments) {
if (assignment == physicalPort) {
assignment = -1;
g_rumble[static_cast<size_t>(physicalPort)] = 0;
}
}
}
g_portAssignments[gamePort] = static_cast<int8_t>(physicalPort);
}
int GetPortAssignment(uint32_t gamePort) {
if (gamePort >= g_portAssignments.size()) return -1;
std::lock_guard lock(g_mutex);
return g_portAssignments[gamePort];
}
bool SetRumble(uint32_t port, bool enabled) {
if (port >= g_rumble.size() || !g_connected.load(std::memory_order_acquire)) return false;
std::lock_guard lock(g_mutex);
if (!g_connected.load(std::memory_order_acquire)) return false;
const int physicalPort = g_portAssignments[port];
if (physicalPort < 0) return false;
const size_t adapterPort = static_cast<size_t>(physicalPort);
if (g_statuses[adapterPort].err != PAD_ERR_NONE) {
g_rumble[adapterPort] = 0;
return false;
}
g_rumble[adapterPort] = enabled ? 1 : 0;
return true;
}
AdapterInfo GetInfo() {
std::lock_guard lock(g_mutex);
return g_info;
}
} // namespace Wup028Adapter
-68
View File
@@ -1,145 +1,77 @@
# Wiicompiled Static Recompiler # Wiicompiled Static Recompiler
The translator parses GameCube/Wii DOL files, decodes PowerPC instructions, lifts them through The translator parses GameCube/Wii DOL files, decodes PowerPC instructions, lifts them through
IR/SSA and type inference, and emits C++ that is compiled into a native executable together with IR/SSA and type inference, and emits C++ that is compiled into a native executable together with
a runtime in `runtime/`. Project-specific paths and addresses are supplied through a versioned a runtime in `runtime/`. Project-specific paths and addresses are supplied through a versioned
YAML manifest, so the translator itself contains no game-specific data. YAML manifest, so the translator itself contains no game-specific data.
> Translating a DOL does not exempt you from owning the game it came from. > Translating a DOL does not exempt you from owning the game it came from.
## How translating a DOL works ## How translating a DOL works
A project manifest (YAML) names the input DOL and pins its layout; everything else is derived. A project manifest (YAML) names the input DOL and pins its layout; everything else is derived.
Translation is four commands: Translation is four commands:
1. **`translate-recursive <entry-point> --project <manifest>`** - walks the call graph from the 1. **`translate-recursive <entry-point> --project <manifest>`** - walks the call graph from the
entry point, decodes every reachable function, and emits C++ (plus JSON metadata describing entry point, decodes every reachable function, and emits C++ (plus JSON metadata describing
what was emitted). what was emitted).
2. **`generate-data-init --project <manifest>`** - writes the embedded `.data`/`.rodata`/`.sdata` 2. **`generate-data-init --project <manifest>`** - writes the embedded `.data`/`.rodata`/`.sdata`
section initializer and `RuntimeConfig.h`. section initializer and `RuntimeConfig.h`.
3. **`emit-build-shards --project <manifest>`** - emits the CMake build graph (`shards.cmake`) 3. **`emit-build-shards --project <manifest>`** - emits the CMake build graph (`shards.cmake`)
covering both generated sources and `runtime/src`. covering both generated sources and `runtime/src`.
4. **CMake + Ninja with Clang** compiles `runtime/` plus the generated output into one executable. 4. **CMake + Ninja with Clang** compiles `runtime/` plus the generated output into one executable.
Discovery is purely recursive from the entry point unless the manifest provides an optional Discovery is purely recursive from the entry point unless the manifest provides an optional
`function_map` (one `hexaddr name` per line) that seeds additional function boundaries. Unsupported `function_map` (one `hexaddr name` per line) that seeds additional function boundaries. Unsupported
instructions fail translation by default. instructions fail translation by default.
See `projects/examples/generic-dol.yml` for a minimal manifest driven by `RECOMP_GENERIC_DOL`. See `projects/examples/generic-dol.yml` for a minimal manifest driven by `RECOMP_GENERIC_DOL`.
## Prerequisites ## Prerequisites
| Tool | Notes | | Tool | Notes |
| --- | --- | | --- | --- |
| .NET 8 SDK | Builds and runs the translator. | | .NET 8 SDK | Builds and runs the translator. |
| CMake ≥ 3.16 and Ninja | Configures and drives the native build. | | CMake ≥ 3.16 and Ninja | Configures and drives the native build. |
| Clang / LLVM | The shipped build uses LLVM-MinGW targeting `x86-64-v3`. MSVC is not the tested path. | | Clang / LLVM | The shipped build uses LLVM-MinGW targeting `x86-64-v3`. MSVC is not the tested path. |
Build the CLI once and invoke the assembly directly: Build the CLI once and invoke the assembly directly:
```powershell ```powershell
dotnet build translator/src/Translator.Cli/Translator.Cli.csproj -c Release dotnet build translator/src/Translator.Cli/Translator.Cli.csproj -c Release
$translator = 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll' $translator = 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll'
``` ```
## Manifest essentials ## Manifest essentials
- `inputs.dol.path` - the DOL to translate; optional SHA-256 pinning rejects wrong revisions. - `inputs.dol.path` - the DOL to translate; optional SHA-256 pinning rejects wrong revisions.
- `memory.base` / `size` - guest address space. - `memory.base` / `size` - guest address space.
- `memory.sda_base` / `sda2_base` - the r13/r2 Small Data Area bases your DOL's boot code installs - `memory.sda_base` / `sda2_base` - the r13/r2 Small Data Area bases your DOL's boot code installs
(`lis`/`ori` pairs in `__init_registers`). Required by any command that writes `RuntimeConfig.h`; (`lis`/`ori` pairs in `__init_registers`). Required by any command that writes `RuntimeConfig.h`;
the translator does not guess them. the translator does not guess them.
- `translation.function_map.path` - optional symbol map used as the discovery oracle. - `translation.function_map.path` - optional symbol map used as the discovery oracle.
- `translation.allow_unsupported_instructions` - off by default; enabling it emits runtime traps - `translation.allow_unsupported_instructions` - off by default; enabling it emits runtime traps
instead of failing, and such a build can never ship. instead of failing, and such a build can never ship.
Relative paths resolve from `workspace_root`, which itself resolves from the manifest directory. Relative paths resolve from `workspace_root`, which itself resolves from the manifest directory.
## Commands ## Commands
- `info [--project path]` - `info [--project path]`
- `translate-recursive <address> --project path` - `translate-recursive <address> --project path`
- `generate-data-init --project path` - `generate-data-init --project path`
- `emit-base-manifest --project path` - `emit-base-manifest --project path`
- `emit-build-shards --project path` - `emit-build-shards --project path`
- `translate-mod --project path [--profile name] ...` - static Kamek/Pulsar module translation - `translate-mod --project path [--profile name] ...` - static Kamek/Pulsar module translation
Any command prints its own option list with `--help`. Any command prints its own option list with `--help`.
## Test ## Test
```powershell ```powershell
dotnet test translator/Translator.sln -c Release dotnet test translator/Translator.sln -c Release
``` ```