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/
/build-*/
/.build-output/
/.build-test/
/native-build/
/dist/
/out/
[Bb]in/
+1 -1
View File
@@ -253,7 +253,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
$manifest = [ordered]@{
SchemaVersion = 2
ProductVersion = '0.2.21'
ProductVersion = '0.2.24'
ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256
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.'
}
$translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe'
$cmake = Join-Path $Toolkit 'CMake\bin\cmake.exe'
$ninja = Join-Path $Toolkit 'Ninja\ninja.exe'
$toolchainBin = Join-Path $Toolkit 'llvm-mingw\bin'
$toolchain = Get-MkwShellSafeToolchainRoot $Toolkit
$cmake = Join-Path $toolchain 'CMake\bin\cmake.exe'
$ninja = Join-Path $toolchain 'Ninja\ninja.exe'
$toolchainBin = Join-Path $toolchain 'llvm-mingw\bin'
$cc = 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'
@@ -203,7 +204,7 @@ if ($Parallel -gt 0) {
$oldPath = $env:PATH
$oldDotnet = $env:DOTNET_ROOT
try {
$env:PATH = Get-MkwToolchainPath $Toolkit
$env:PATH = Get-MkwToolchainPath $toolchain
Remove-Item Env:DOTNET_ROOT -ErrorAction SilentlyContinue
Push-Location $Workspace
try {
+37
View File
@@ -40,6 +40,43 @@ function Get-MkwToolchainPath([string]$ToolchainRoot) {
) -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) {
<#
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
{
public const string Name = "WiiCompiled";
public const string Version = "0.2.21";
public const string Version = "0.2.24";
/// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and
@@ -7,7 +7,7 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.21</Version>
<Version>0.2.24</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<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
- GPU: GTX 1650 / RX 6400 / Arc A310 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,
GCZ, CISO, WBFS, WIA and RVZ are accepted.
+1
View File
@@ -229,6 +229,7 @@ s32 PADGetNativeButtonPressed(u32 port);
PADSignedNativeAxis PADGetNativeAxisPulled(u32 port);
void PADRestoreDefaultMapping(u32 port);
void PADBlockInput(bool block);
bool PADIsInputBlocked(void);
/**
* 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();
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);
return ECardResult::READY;
+1 -1
View File
@@ -175,7 +175,7 @@ void CARDInit(const char* game, const char* maker) {
std::filesystem::path cardWorkingDir;
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
cardWorkingDir = std::filesystem::current_path();
+22 -16
View File
@@ -1,3 +1,4 @@
#include "../../fs_helper.hpp"
#include "../../input.hpp"
#include "../../internal.hpp"
#include <dolphin/pad.h>
@@ -5,6 +6,7 @@
#include <SDL3/SDL_mouse.h>
#include <array>
#include <atomic>
#include <sys/stat.h>
#include <ranges>
@@ -283,7 +285,7 @@ constexpr PADCLampRegion ClampRegion{
bool g_initialized;
bool g_keyboardBindingsLoaded = false;
bool g_blockPAD = false;
std::atomic_bool g_blockPAD{false};
bool g_suppressHeldOnRead = false;
std::array<PADButton, PAD_CHANMAX> g_suppressedButtons{};
std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
@@ -491,7 +493,7 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re
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) {
__PADSetDefaultMapping(controller);
controller->m_axisMapping = g_defaultAxes;
@@ -499,8 +501,9 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re
controller->m_mappingLoaded = true;
const auto path = fmt::format("{}/{}_{:04X}_{:04X}.controller", basePath, PADGetName(playerIndex), controller->m_vid,
controller->m_pid);
const auto path = fs_path_to_string(
basePath / fmt::format("{}_{:04X}_{:04X}.controller", PADGetName(playerIndex), controller->m_vid,
controller->m_pid));
SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb");
if (file == nullptr) {
return;
@@ -659,7 +662,8 @@ u32 PADRead(PADStatus* status) {
int numKeys = 0;
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;
uint32_t rumbleSupport = 0;
@@ -882,7 +886,7 @@ u32 PADRead(PADStatus* status) {
}
}
if (g_blockPAD) {
if (inputBlocked) {
neutralize_status(status[i]);
} else {
apply_unblock_suppression(status[i], i, captureHeldInput);
@@ -1247,8 +1251,8 @@ constexpr uint32_t k_keyboardMagic = SBIG('KBND');
constexpr int32_t k_keyboardVersion = 3;
static void load_keyboard_bindings() {
const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat";
SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "rb");
const auto filePath = fs_path_from_string(aurora::g_config.userPath) / "keyboard_bindings.dat";
SDL_IOStream* file = SDL_IOFromFile(fs_path_to_string(filePath).c_str(), "rb");
if (file == nullptr) {
return;
}
@@ -1317,10 +1321,11 @@ static void load_keyboard_bindings() {
}
static void save_keyboard_bindings() {
const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat";
SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "wb");
const auto filePath = fs_path_from_string(aurora::g_config.userPath) / "keyboard_bindings.dat";
const auto filePathStr = fs_path_to_string(filePath);
SDL_IOStream* file = SDL_IOFromFile(filePathStr.c_str(), "wb");
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;
}
@@ -1344,14 +1349,14 @@ void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier)
}
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) {
EnsureMappingLoaded(&controller);
const auto filePath =
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
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
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
const int64_t dataStart = SDL_TellIO(file) + 31 & ~31;
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;
}
SDL_SeekIO(file, dataStart, SDL_IO_SEEK_SET);
@@ -1530,12 +1535,13 @@ void PADRestoreDefaultMapping(const u32 port) {
}
void PADBlockInput(const bool block) {
if (g_blockPAD && !block) {
if (g_blockPAD.exchange(block, std::memory_order_acq_rel) && !block) {
g_suppressHeldOnRead = true;
}
g_blockPAD = block;
}
bool PADIsInputBlocked() { return g_blockPAD.load(std::memory_order_acquire); }
SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) {
const auto* ctrl = __PADGetControllerForIndex(index);
if (ctrl == nullptr) {
+10 -2
View File
@@ -1,11 +1,19 @@
#pragma once
#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) {
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 "../gx/pipeline.hpp"
#include "../fs_helper.hpp"
#include "../sqlite_utils.hpp"
#include "../webgpu/gpu.hpp"
@@ -715,7 +716,7 @@ static bool prepare_pipeline_cache_db() {
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);
if (ret != SQLITE_OK) {
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;
}
auto userPath = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.userPath)};
auto cachePath = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.cachePath)};
auto userPath = fs_path_from_string(g_config.userPath);
auto cachePath = fs_path_from_string(g_config.cachePath);
s_replacementRoot = userPath / "texture_replacements";
s_dumpRoot = cachePath / "texture_dumps";
+2 -1
View File
@@ -10,6 +10,7 @@
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_render.h>
#include "fs_helper.hpp"
#include "internal.hpp"
#include "webgpu/gpu.hpp"
#include "window.hpp"
@@ -37,7 +38,7 @@ void remove_legacy_ini_file(const char* basePath) noexcept {
}
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 {
+37 -4
View File
@@ -255,6 +255,18 @@ IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerId
: 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,
Uint32 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) {
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) {
// 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))) {
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;
fallbackController = nullptr;
break;
@@ -311,11 +323,18 @@ void apply_port_preferences() noexcept {
}
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;
}
}
}
// 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
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);
SDL_JoystickID instance = SDL_GetJoystickID(SDL_GetGamepadJoystick(ctrl));
g_GameControllers[instance] = controller;
ensure_player_index(g_GameControllers[instance]);
apply_port_preferences();
return instance;
}
@@ -371,6 +391,19 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
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 {
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
SDL_CloseGamepad(it->second.m_controller);
+1
View File
@@ -51,6 +51,7 @@ struct GameController {
GameController* get_controller_for_player(uint32_t player) noexcept;
Sint32 get_instance_for_player(uint32_t player) noexcept;
SDL_JoystickID add_controller(SDL_JoystickID which) noexcept;
bool refresh_controller(SDL_JoystickID instance) noexcept;
void remove_controller(Uint32 instance) noexcept;
Sint32 player_index(Uint32 instance) 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() {
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);
Log.debug("Using dawn cache at {}", file);
auto ret = sqlite3_open(file.c_str(), &db);
@@ -165,8 +165,12 @@ static bool cache_init_core() {
db = nullptr;
std::error_code ec;
std::filesystem::remove(path, ec);
std::filesystem::remove(std::filesystem::path{file + "-wal"}, ec);
std::filesystem::remove(std::filesystem::path{file + "-shm"}, ec);
auto wal = path;
wal += "-wal";
std::filesystem::remove(wal, ec);
auto shm = path;
shm += "-shm";
std::filesystem::remove(shm, ec);
ret = sqlite3_open(file.c_str(), &db);
if (ret != SQLITE_OK) {
Log.error("Failed to recreate database: {}", sqlite3_errmsg(db));
+9
View File
@@ -294,6 +294,15 @@ void process_event(SDL_Event& event) {
});
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: {
input::remove_controller(event.gdevice.which);
g_events.push_back(AuroraEvent{
+1 -1
View File
@@ -205,7 +205,7 @@ function(mkw_configure_product target)
endif()
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)
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;
}
const std::filesystem::path temporary = path.string() + ".tmp";
std::filesystem::path temporary = path;
temporary += ".tmp";
{
std::ofstream output(temporary, std::ios::trunc);
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 {
struct RegisteredFile {
std::string hostPath;
std::filesystem::path hostPath;
std::string dvdPath;
uint32_t size = 0;
uint32_t discOffsetWords = 0;
};
struct IndexedEntry {
std::string hostPath;
std::filesystem::path hostPath;
std::string dvdPath;
uint32_t size = 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()) {
RT_LOGF(RT_TAG_NAND, "ERROR: %s\n", message);
} 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");
std::string details = message ? message : "The configured NAND could not be initialized.";
if (!path.empty()) {
details += "\n\nPath: ";
details += path.string();
details += RuntimeConfigFile::PathToUtf8(path);
}
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
@@ -51,18 +52,6 @@ inline std::filesystem::path ResolveConfiguredPath(const std::string& 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() {
return RuntimeConfigFile::ApplicationDataDirectory() / "NAND";
}
@@ -134,7 +123,9 @@ inline bool SeedMissingBootstrapFiles(const std::filesystem::path& root) {
const std::filesystem::path relativePath{std::string(file)};
ec.clear();
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;
}
}
@@ -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;
}
@@ -187,8 +179,4 @@ inline std::filesystem::path DiscoverNandRootPath() {
return CreateManagedNandRoot();
}
inline std::string DiscoverNandRootString() {
return PathStringWithoutTrailingSeparators(DiscoverNandRootPath());
}
} // namespace RuntimeNandPath
+4 -1
View File
@@ -3,6 +3,7 @@
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
@@ -67,8 +68,10 @@ void RunMemoryInitializers();
void RegisterPostRelInitializer(InitializerFn fn);
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);
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
// 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
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
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 {
// 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* kApplicationDirectoryName = "WiiCompiled";
@@ -323,6 +338,12 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.controllerButtons[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.windowPosX = FindConfigInt(document, "video", "window_x");
@@ -410,7 +431,7 @@ inline RuntimeUserConfig ParseConfig(std::istream& input, std::string sourceName
inline RuntimeUserConfig LoadConfigFile() {
EnsureConfigFile();
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() {
@@ -501,7 +522,7 @@ inline bool WriteSetting(std::string_view section, std::string_view key, std::st
}
std::ofstream output(path, std::ios::trunc);
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;
}
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));
}
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) {
value = std::clamp(value, 0.0f, 1.0f);
Mutable().audioVolume = value;
@@ -754,7 +788,7 @@ inline std::string DvdRoot(std::string fallback = "") {
// never to the process working directory (docs/WHEELWIZARD_CONTRACT.md).
inline std::filesystem::path ResolveRelativeTo(const std::filesystem::path& base,
const std::string& value) {
std::filesystem::path path(value);
std::filesystem::path path = PathFromUtf8(value);
if (path.is_relative()) {
path = base / path;
}
@@ -784,7 +818,7 @@ inline void LogLoadedConfig() {
static const bool logged = [] {
const auto& config = Get();
const auto configPath = ResolveConfigPath();
std::cout << "[runtime-config] " << configPath.string();
std::cout << "[runtime-config] " << PathToUtf8(configPath);
if (!std::filesystem::exists(configPath)) {
std::cout << " not found; using built-in defaults";
} else {
+2 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <optional>
#include <ostream>
#include <string>
@@ -72,5 +73,5 @@ public:
// `mem1Path` and MEM2 to `mem1Path + ".mem2"`, logging outcomes to `os`.
static void DumpCrashHeuristics(std::ostream& os, const struct CpuContext* cpu,
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();
std::ifstream stream(path, std::ios::binary | std::ios::ate);
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);
std::array<uint8_t, kResamplingCoefficientCount * 2> bytes{};
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) {
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 "memory.h"
#include "hle/controller_status_contract.h"
#include "wup028_adapter.h"
#include <algorithm>
#include <cstdio>
@@ -33,6 +34,7 @@ void WritePadStatus(uint32_t base, const PADStatus& status) {
extern "C" uint32_t PAD__Init_HLE()
{
Wup028Adapter::Initialize();
return PADInit() ? 1u : 0u;
}
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]{};
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 {
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)
{
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));
+31 -7
View File
@@ -2,6 +2,7 @@
#include <cstdint>
#include <iostream>
#include <set>
#include <string>
#include <vector>
@@ -103,17 +104,26 @@ static void HLE_LogOSReport(CpuContext* cpu, const char* fmt)
[&state]() { return NextOsReportDouble(state); },
[](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 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;
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;
@@ -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
// synchronized at this boundary, so capture the guest backchain at the
// 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 ||
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
// root "/" and its "sys" subfolder to "/sys/". The root is user-owned input:
// 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 uint32_t CurrentDiscGameCode() {
@@ -66,7 +66,7 @@ static uint32_t CurrentDiscGameCode() {
#define DVD_FILEINFO_OFFSET_LEN 0x34
struct DVDFileEntry {
std::string hostPath; // Full Windows path
fs::path hostPath;
std::string dvdPath; // Virtual Wii path (e.g., "/Race/Course.szs")
uint32_t size;
uint32_t discOffsetWords = 0;
@@ -78,7 +78,7 @@ struct FstFileEntry {
uint32_t end;
uint32_t size;
std::string dvdPath;
std::string hostPath;
fs::path hostPath;
};
// 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));
}
static std::string NormalizeDvdHostPath(std::string path) {
while (!path.empty() && (path.back() == '\\' || path.back() == '/')) {
path.pop_back();
}
return path;
// Host path strings only ever leave this module as UTF-8 display text.
static std::string HostPathText(const fs::path& path) {
return RuntimeConfigFile::PathToUtf8(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 = {}) {
RT_LOGF(RT_TAG_DVD, "ERROR: %s", source);
if (!path.empty()) {
std::fprintf(stderr, ": %s", path.string().c_str());
std::fprintf(stderr, ": %s", HostPathText(path).c_str());
}
std::fprintf(stderr,
"\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.";
if (!path.empty()) {
details += "\n\nPath: ";
details += path.string();
details += HostPathText(path);
}
details += "\n\nSet [paths] dvd_root in Config.toml to the extracted "
"Mario Kart Wii DATA directory.";
FailDvd("dvd_root", "DVD data is unavailable", details);
}
static const std::string& GetDvdRoot() {
static const fs::path& GetDvdRoot() {
std::call_once(g_dvdRootOnce, []() {
const fs::path path = RuntimeConfigFile::ResolvedDvdRoot();
if (path.empty()) {
@@ -164,14 +162,14 @@ static const std::string& GetDvdRoot() {
if (!IsDvdDataRoot(path)) {
FailDvdRoot("Configured DVD root is not an extracted DATA directory", path);
}
g_dvdRoot = NormalizeDvdHostPath(path.string());
g_dvdRoot = path;
});
return g_dvdRoot;
}
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) {
if (callbackPtr == 0) {
@@ -240,7 +238,7 @@ static void LoadFstIndex() {
}
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);
if (!fstFile.is_open()) {
return;
@@ -333,7 +331,7 @@ static void LoadFstIndex() {
entry.end = endBytes;
entry.size = fileSize;
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);
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);
DVDFileEntry fileEntry;
fileEntry.hostPath = hostPath.string();
fileEntry.hostPath = hostPath;
fileEntry.dvdPath = dvdPath;
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);
if (ec) {
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;
}
return;
@@ -458,7 +456,7 @@ static void WalkDirectory(const fs::path& root, bool recursive, bool announceErr
it.increment(ec);
if (ec) {
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;
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);
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;
return;
}
@@ -502,7 +500,7 @@ static void ScanDirectory(const fs::path& root, const std::string& virtualPrefix
if (prefix.back() != '/' && prefix.back() != '\\') {
prefix += "/";
}
std::string dvdPath = prefix + relative.string();
std::string dvdPath = prefix + HostPathText(relative);
if (!addNewFiles && !DvdEntryExists(dvdPath)) {
return;
}
@@ -536,7 +534,7 @@ static void ApplyFolderByNameMapping(const RuntimeRiivolution::Mapping& mapping)
if (!entry.is_regular_file(entryEc) || entryEc) {
return;
}
std::string name = entry.path().filename().string();
std::string name = HostPathText(entry.path().filename());
RuntimeHle::LowerInPlace(name);
const auto matches = discPathsByName.find(name);
if (matches == discPathsByName.end()) {
@@ -554,7 +552,7 @@ static void ApplyFolderByNameMapping(const RuntimeRiivolution::Mapping& mapping)
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;
}
@@ -562,7 +560,7 @@ static void ScanOverlayRoot(const RuntimeRiivolution::Overlay& overlay) {
if (!overlay.patches) {
// Fallback for mod roots that mirror the disc filesystem directly (not a
// 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"
<< std::endl;
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 auto it = g_pathToEntry.find(normalized);
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));
std::error_code 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;
}
resolved = g_fileEntries[it->second].hostPath;
resolved = HostPathText(g_fileEntries[it->second].hostPath);
return resolved.c_str();
}
@@ -808,7 +806,7 @@ extern "C" void DVDInit_8015EA1C()
Memory::Write16(diskHeader + 0x04, 0x3031); // '01' (Maker)
Memory::Write8(diskHeader + 0x06, 0x01); // Disk #1
// 4. Scan Files
fs::path rootPath(GetDvdRoot());
const fs::path& rootPath = GetDvdRoot();
// Map "<dvd_root>/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];
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,
"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;
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");
}
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)) {
return DvdReadFatal(fileInfoPtr, entry.hostPath, offset, uLength,
return DvdReadFatal(fileInfoPtr, HostPathText(entry.hostPath), offset, uLength,
"DVD read destination is outside guest memory");
}
std::vector<uint8_t> tempBuf;
DvdReadContract::HostReadFailure 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));
}
@@ -962,11 +960,11 @@ extern "C" int32_t DVD__ReadAbsAsyncPrio_HLE_801628cc(uint32_t cmdBlockPtr,
requestedLength,
"absolute DVD read offset is not mapped to a host file");
} else if (requestedLength != 0 && readInfo.readLength != requestedLength) {
bytesRead = DvdReadFatal(cmdBlockPtr, readInfo.entry->hostPath,
bytesRead = DvdReadFatal(cmdBlockPtr, HostPathText(readInfo.entry->hostPath),
readInfo.fileOffset, requestedLength,
"requested range extends beyond the indexed DVD file");
} else if (requestedLength != 0 && !Memory::Contains(bufferPtr, requestedLength)) {
bytesRead = DvdReadFatal(cmdBlockPtr, readInfo.entry->hostPath,
bytesRead = DvdReadFatal(cmdBlockPtr, HostPathText(readInfo.entry->hostPath),
readInfo.fileOffset, requestedLength,
"DVD read destination is outside guest memory");
} else {
@@ -977,7 +975,7 @@ extern "C" int32_t DVD__ReadAbsAsyncPrio_HLE_801628cc(uint32_t cmdBlockPtr,
readInfo.readLength,
tempBuf,
failure)) {
bytesRead = DvdReadFatal(cmdBlockPtr, readInfo.entry->hostPath,
bytesRead = DvdReadFatal(cmdBlockPtr, HostPathText(readInfo.entry->hostPath),
readInfo.fileOffset, readInfo.readLength,
DvdReadContract::Describe(failure));
} else {
@@ -1076,12 +1074,12 @@ extern "C" int32_t DVDLowRead_80166330(uint32_t buffer, uint32_t length, uint32_
return finish(false);
}
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");
return finish(false);
}
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");
return finish(false);
}
@@ -1093,7 +1091,7 @@ extern "C" int32_t DVDLowRead_80166330(uint32_t buffer, uint32_t length, uint32_
readInfo.readLength,
tempBuf,
failure)) {
ReportDvdReadError(readInfo.entry->hostPath, readInfo.fileOffset,
ReportDvdReadError(HostPathText(readInfo.entry->hostPath), readInfo.fileOffset,
readInfo.readLength, DvdReadContract::Describe(failure));
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;
}
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
// 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
// writes from that handle, so stay in place for this open.
LogNandWarning("NANDOpen", "WARNING: '%s' already has a live handle, writing in place",
hostPath.c_str());
HostPathText(hostPath).c_str());
} else {
const std::string tempPath = SafeTempPathFor(hostPath);
const std::filesystem::path tempPath = SafeTempPathFor(hostPath);
if (DiscardStaleSafeTemp(tempPath)) {
std::error_code ec;
std::filesystem::copy_file(hostPath, tempPath,
std::filesystem::copy_options::overwrite_existing, ec);
if (ec) {
LogNandWarning("NANDOpen", "WARNING: could not seed shadow '%s' (%s), writing in place",
tempPath.c_str(), ec.message().c_str());
std::remove(tempPath.c_str());
HostPathText(tempPath).c_str(), ec.message().c_str());
NandRemove(tempPath);
} else {
FILE* shadow = std::fopen(tempPath.c_str(), "r+b");
FILE* shadow = NandFopen(tempPath, "r+b");
if (!shadow) {
LogNandWarning("NANDOpen", "WARNING: could not open shadow '%s', writing in place",
tempPath.c_str());
std::remove(tempPath.c_str());
HostPathText(tempPath).c_str());
NandRemove(tempPath);
} else {
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 == 3) fopenMode = "r+b";
FILE* file = std::fopen(hostPath.c_str(), fopenMode);
FILE* file = NandFopen(hostPath, fopenMode);
if (!file && mode >= 2) {
// Try creating for write modes
file = std::fopen(hostPath.c_str(), "w+b");
file = NandFopen(hostPath, "w+b");
}
// Create parent directories and retry
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 (IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
file = std::fopen(hostPath.c_str(), fopenMode);
file = NandFopen(hostPath, fopenMode);
}
if (!file) {
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;
}
std::string hostPath = TranslateNandPath(path);
const std::filesystem::path hostPath = TranslateNandPath(path);
CreateParentDirectories(hostPath);
// 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
FILE* f = std::fopen(hostPath.c_str(), "wb");
FILE* f = NandFopen(hostPath, "wb");
if (!f) {
return NAND_RESULT_UNKNOWN;
}
@@ -307,13 +307,13 @@ extern "C" int32_t NANDDelete_HLE(uint32_t pathPtr) {
return NAND_RESULT_INVALID;
}
std::string hostPath = TranslateNandPath(path);
const std::filesystem::path hostPath = TranslateNandPath(path);
if (!PathExists(hostPath)) {
return NAND_RESULT_NOEXISTS;
}
if (std::remove(hostPath.c_str()) == 0) {
if (NandRemove(hostPath)) {
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;
}
std::string hostPath = TranslateNandPath(path);
const std::filesystem::path hostPath = TranslateNandPath(path);
if (PathExists(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)) {
#ifdef _WIN32
_mkdir(hostPath.c_str());
#else
mkdir(hostPath.c_str(), 0755);
#endif
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).
const std::filesystem::path dstHost = dstDirectoryHost / srcName;
if (!PathExists(srcHost.string())) {
if (!PathExists(srcHost)) {
return NAND_RESULT_NOEXISTS;
}
if (!IsDirectory(dstDirectoryHost.string())) {
if (!IsDirectory(dstDirectoryHost)) {
return NAND_RESULT_NOEXISTS;
}
if (PathExists(dstHost.string())) {
if (PathExists(dstHost)) {
return NAND_RESULT_EXISTS;
}
@@ -396,7 +391,7 @@ extern "C" int32_t NANDGetStatus_HLE(uint32_t pathPtr, uint32_t outStatusPtr) {
return NAND_RESULT_INVALID;
}
std::string hostPath = TranslateNandPath(path);
const std::filesystem::path hostPath = TranslateNandPath(path);
if (!PathExists(hostPath)) {
return NAND_RESULT_NOEXISTS;
@@ -417,7 +412,7 @@ extern "C" int32_t NANDGetType_HLE(uint32_t pathPtr, uint32_t outTypePtr) {
return NAND_RESULT_INVALID;
}
std::string hostPath = TranslateNandPath(path);
const std::filesystem::path hostPath = TranslateNandPath(path);
if (!PathExists(hostPath)) {
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";
std::string SafeTempPathFor(const std::string& hostPath) {
return hostPath + kNandSafeTempSuffix;
std::filesystem::path SafeTempPathFor(const std::filesystem::path& hostPath) {
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
@@ -264,24 +266,25 @@ static bool FlushFileToDisk(FILE* file) {
// 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.
static bool AtomicReplaceHostFile(const char* who, const std::string& tempPath,
const std::string& targetPath) {
static bool AtomicReplaceHostFile(const char* who, const std::filesystem::path& tempPath,
const std::filesystem::path& targetPath) {
#ifdef _WIN32
if (MoveFileExA(tempPath.c_str(), targetPath.c_str(),
if (MoveFileExW(tempPath.c_str(), targetPath.c_str(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
return true;
}
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;
#else
if (std::rename(tempPath.c_str(), targetPath.c_str()) != 0) {
if (!NandRename(tempPath, targetPath)) {
LogNandError(who, "ERROR: rename('%s' -> '%s') failed",
tempPath.c_str(), targetPath.c_str());
HostPathText(tempPath).c_str(), HostPathText(targetPath).c_str());
return false;
}
// 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);
if (dirFd >= 0) {
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
// 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)) {
return true;
}
LogNandWarning("nand-shadow", "WARNING: discarding stale scratch file '%s' from a previous run",
tempPath.c_str());
if (std::remove(tempPath.c_str()) == 0) {
HostPathText(tempPath).c_str());
if (NandRemove(tempPath)) {
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;
}
// 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
// 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);
for (const auto& entry : g_fileHandles) {
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
// guest's close call fails instead of silently reporting success.
int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) {
std::string tempPath;
std::string commitPath;
std::filesystem::path tempPath;
std::filesystem::path commitPath;
FILE* file = nullptr;
int32_t mode = 0;
@@ -358,7 +362,7 @@ int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) {
if (!needsCommit) {
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_OK;
@@ -366,13 +370,13 @@ int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) {
if (!flushed) {
LogNandError(who, "ERROR: flush of '%s' failed, discarding it and leaving '%s' untouched",
tempPath.c_str(), commitPath.c_str());
std::remove(tempPath.c_str());
HostPathText(tempPath).c_str(), HostPathText(commitPath).c_str());
NandRemove(tempPath);
return NAND_RESULT_UNKNOWN;
}
if (!AtomicReplaceHostFile(who, tempPath, commitPath)) {
std::remove(tempPath.c_str());
NandRemove(tempPath);
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;
}
const std::string hostPath = TranslateNandPath(path);
const std::filesystem::path hostPath = TranslateNandPath(path);
if (hostPath.empty()) {
LogNandError("NANDSafeOpen", "FAILED to translate path '%s'", path);
return NAND_RESULT_INVALID;
@@ -407,12 +411,13 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
if (mode == 1) {
// Read-only safe open reads the original in place; the library builds no scratch
// copy for this case.
FILE* file = std::fopen(hostPath.c_str(), "rb");
FILE* file = NandFopen(hostPath, "rb");
if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
file = std::fopen(hostPath.c_str(), "rb");
file = NandFopen(hostPath, "rb");
}
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;
}
@@ -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
// open of a file that does not exist fails instead of creating one.
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;
}
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;
}
const std::string tempPath = SafeTempPathFor(hostPath);
const std::filesystem::path tempPath = SafeTempPathFor(hostPath);
if (!DiscardStaleSafeTemp(tempPath)) {
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);
if (ec) {
LogNandError("NANDSafeOpen", "FAILED to seed scratch file '%s' from '%s': %s",
tempPath.c_str(), hostPath.c_str(), ec.message().c_str());
std::remove(tempPath.c_str());
HostPathText(tempPath).c_str(), HostPathText(hostPath).c_str(),
ec.message().c_str());
NandRemove(tempPath);
return NAND_RESULT_UNKNOWN;
}
FILE* file = std::fopen(tempPath.c_str(), "r+b");
FILE* file = NandFopen(tempPath, "r+b");
if (!file) {
LogNandError("NANDSafeOpen", "FAILED to open scratch file '%s'", tempPath.c_str());
std::remove(tempPath.c_str());
LogNandError("NANDSafeOpen", "FAILED to open scratch file '%s'",
HostPathText(tempPath).c_str());
NandRemove(tempPath);
return NAND_RESULT_UNKNOWN;
}
+69 -57
View File
@@ -12,7 +12,7 @@
// ============================================================================
// 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;
// ============================================================================
@@ -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
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);
int32_t fd = g_nextFd++;
g_fileHandles[fd] = {file, path, mode, 0};
@@ -88,29 +88,50 @@ std::string CurrentNandDataDir() {
return path;
}
const std::string& GetNandBasePath() {
const std::filesystem::path& GetNandBasePath() {
std::call_once(g_dolphinWiiBaseOnce, []() {
g_dolphinWiiBase = RuntimeNandPath::DiscoverNandRootString();
g_dolphinWiiBase = RuntimeNandPath::DiscoverNandRootPath();
});
return g_dolphinWiiBase;
}
static std::string BuildHostNandPath(std::string wiiPathStr) {
std::string hostPath = GetNandBasePath();
for (char& c : wiiPathStr) {
if (c == '/') {
#ifdef _WIN32
c = '\\';
#endif
}
}
std::string HostPathText(const std::filesystem::path& path) {
return RuntimeConfigFile::PathToUtf8(path);
}
if (!wiiPathStr.empty() && (wiiPathStr[0] == '\\' || wiiPathStr[0] == '/')) {
hostPath += wiiPathStr;
} else {
hostPath += "\\";
hostPath += wiiPathStr;
FILE* NandFopen(const std::filesystem::path& path, const char* mode) {
#ifdef _WIN32
const std::wstring wideMode(mode, mode + std::strlen(mode));
return _wfopen(path.c_str(), wideMode.c_str());
#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;
}
@@ -169,7 +190,7 @@ static std::string NormalizeAbsoluteWiiPath(const char* wiiPath) {
struct RiivolutionSaveRedirect {
bool enabled = false;
bool clone = false;
std::string hostDirectory;
std::filesystem::path hostDirectory;
};
static std::once_flag g_riivolutionSaveRedirectOnce;
@@ -187,7 +208,7 @@ static const RiivolutionSaveRedirect& GetRiivolutionSaveRedirect() {
}
g_riivolutionSaveRedirect.enabled = true;
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.
CreateDirectoryPath(g_riivolutionSaveRedirect.hostDirectory);
});
@@ -195,8 +216,8 @@ static const RiivolutionSaveRedirect& GetRiivolutionSaveRedirect() {
return g_riivolutionSaveRedirect;
}
static void CloneRiivolutionSaveIfNeeded(const std::string& sourceHostPath,
const std::string& redirectedHostPath,
static void CloneRiivolutionSaveIfNeeded(const std::filesystem::path& sourceHostPath,
const std::filesystem::path& redirectedHostPath,
const RiivolutionSaveRedirect& redirect) {
if (!redirect.clone || PathExists(redirectedHostPath) || !PathExists(sourceHostPath)) {
return;
@@ -209,11 +230,13 @@ static void CloneRiivolutionSaveIfNeeded(const std::string& sourceHostPath,
std::filesystem::copy_options::skip_existing, ec);
if (ec) {
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();
if (!redirect.enabled) {
return false;
@@ -232,23 +255,23 @@ static bool ResolveRiivolutionSaveHostPath(const std::string& absoluteWiiPath, s
relative = absoluteWiiPath.substr(dataDir.size() + 1);
}
std::filesystem::path redirected(redirect.hostDirectory);
std::filesystem::path redirected = redirect.hostDirectory;
if (!relative.empty()) {
redirected /= std::filesystem::path(relative);
}
outHostPath = redirected.string();
outHostPath = redirected;
CloneRiivolutionSaveIfNeeded(BuildHostNandPath(absoluteWiiPath), outHostPath, redirect);
return true;
}
std::string TranslateNandPath(const char* wiiPath) {
std::filesystem::path TranslateNandPath(const char* wiiPath) {
std::string wiiPathStr = NormalizeAbsoluteWiiPath(wiiPath);
if (wiiPathStr.empty()) {
return "";
return {};
}
std::string redirectedHostPath;
std::filesystem::path redirectedHostPath;
if (ResolveRiivolutionSaveHostPath(wiiPathStr, redirectedHostPath)) {
return redirectedHostPath;
}
@@ -266,7 +289,8 @@ struct U8Node {
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);
if (!file) {
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.
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;
if (const auto dvdRoot = RuntimeConfigFile::ResolvedDvdRoot(); !dvdRoot.empty()) {
const auto arcPath = dvdRoot / "files" / "contents" / "RFLRes01.arc";
std::error_code 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);
if (!out) {
LogNandError("FaceLibSeed", "Failed to create %s", hostPath.c_str());
LogNandError("FaceLibSeed", "Failed to create %s", HostPathText(hostPath).c_str());
return false;
}
out.write(reinterpret_cast<const char*>(payload.data()), static_cast<std::streamsize>(payload.size()));
if (!out) {
LogNandError("FaceLibSeed", "Failed to write %s", hostPath.c_str());
LogNandError("FaceLibSeed", "Failed to write %s", HostPathText(hostPath).c_str());
return false;
}
@@ -388,45 +412,33 @@ bool IsFaceLibResourcePath(const char* path) {
}
// Create directories recursively
bool CreateDirectoryPath(const std::string& path) {
bool CreateDirectoryPath(const std::filesystem::path& path) {
if (path.empty()) {
return true;
}
std::error_code 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
bool PathExists(const std::string& path) {
#ifdef _WIN32
return GetFileAttributesA(path.c_str()) != INVALID_FILE_ATTRIBUTES;
#else
return access(path.c_str(), F_OK) == 0;
#endif
bool PathExists(const std::filesystem::path& path) {
std::error_code ec;
return std::filesystem::exists(path, ec) && !ec;
}
// Check if path is a directory
bool IsDirectory(const std::string& path) {
#ifdef _WIN32
DWORD attrs = GetFileAttributesA(path.c_str());
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 IsDirectory(const std::filesystem::path& path) {
std::error_code ec;
return std::filesystem::is_directory(path, ec) && !ec;
}
bool CreateParentDirectories(const std::string& path) {
size_t lastSlash = path.rfind('\\');
if (lastSlash == std::string::npos) {
lastSlash = path.rfind('/');
}
if (lastSlash == std::string::npos) {
bool CreateParentDirectories(const std::filesystem::path& path) {
if (!path.has_parent_path()) {
return false;
}
CreateDirectoryPath(path.substr(0, lastSlash));
CreateDirectoryPath(path.parent_path());
return true;
}
+22 -18
View File
@@ -29,15 +29,10 @@
#include <vector>
#include <filesystem>
#include <string>
#include <sys/stat.h>
#ifdef _WIN32
#include <direct.h>
#include <io.h>
#include <windows.h>
#define mkdir(path, mode) _mkdir(path)
#define access _access
#define F_OK 0
#else
#include <fcntl.h>
#include <sys/types.h>
@@ -67,19 +62,19 @@ void LogNandWarning(const char* func, const char* fmt, ...);
struct FileHandle {
FILE* file = nullptr;
std::string path;
std::filesystem::path path;
int32_t mode = 0; // 1=read, 2=write, 3=read/write
uint32_t position = 0;
// 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
// atomically replaces on NANDSafeClose.
std::string safeCommitPath;
std::filesystem::path safeCommitPath;
};
extern std::map<int32_t, FileHandle> g_fileHandles;
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);
void CloseFd(int32_t fd);
@@ -89,18 +84,27 @@ void CloseFd(int32_t fd);
uint32_t CurrentMkwTitleIdLo();
std::string CurrentNandDataDir();
const std::string& GetNandBasePath();
std::string TranslateNandPath(const char* wiiPath);
const std::filesystem::path& GetNandBasePath();
std::filesystem::path TranslateNandPath(const char* wiiPath);
bool CreateDirectoryPath(const std::string& path);
bool PathExists(const std::string& path);
bool IsDirectory(const std::string& path);
bool CreateDirectoryPath(const std::filesystem::path& path);
bool PathExists(const std::filesystem::path& path);
bool IsDirectory(const std::filesystem::path& path);
// Create the directory that contains `path`. False when `path` has no directory
// 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);
// ============================================================================
@@ -178,9 +182,9 @@ enum ISFSResult {
int32_t ISFS_OpenLib_Initialize(CpuContext* ctx);
// Shadow-write machinery, defined with the NANDSafeOpen/NANDSafeClose section below.
std::string SafeTempPathFor(const std::string& hostPath);
bool DiscardStaleSafeTemp(const std::string& tempPath);
bool IsHostPathOpen(const std::string& hostPath);
std::filesystem::path SafeTempPathFor(const std::filesystem::path& hostPath);
bool DiscardStaleSafeTemp(const std::filesystem::path& tempPath);
bool IsHostPathOpen(const std::filesystem::path& hostPath);
int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError);
// 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
std::string hostPath = TranslateNandPath(path);
const std::filesystem::path hostPath = TranslateNandPath(path);
// Seed FaceLib resources before the existence check so every open mode can
// 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 (!PathExists(hostPath)) {
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;
}
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) {
LogNandError("IOS_Open", "FAILED to open '%s'", hostPath.c_str());
LogNandError("IOS_Open", "FAILED to open '%s'", HostPathText(hostPath).c_str());
return ISFS_ENOENT;
}
@@ -516,14 +516,9 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
return ISFS_EINVAL;
}
const char* path = (const char*)Memory::GetPointer(inBufPtr + 6);
std::string hostPath = TranslateNandPath(path);
const std::filesystem::path hostPath = TranslateNandPath(path);
if (CreateDirectoryPath(hostPath)) {
#ifdef _WIN32
_mkdir(hostPath.c_str());
#else
mkdir(hostPath.c_str(), 0755);
#endif
return ISFS_OK;
}
return ISFS_EIO;
@@ -534,16 +529,11 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
return ISFS_EINVAL;
}
const char* path = (const char*)Memory::GetPointer(inBufPtr);
std::string hostPath = TranslateNandPath(path);
const std::filesystem::path hostPath = TranslateNandPath(path);
if (IsDirectory(hostPath)) {
#ifdef _WIN32
if (RemoveDirectoryA(hostPath.c_str())) 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;
// fs::remove refuses a non-empty directory, matching rmdir.
if (NandRemove(hostPath)) {
return ISFS_OK;
}
return ISFS_ENOENT;
}
@@ -553,7 +543,7 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
return ISFS_EINVAL;
}
const char* path = (const char*)Memory::GetPointer(inBufPtr);
std::string hostPath = TranslateNandPath(path);
const std::filesystem::path hostPath = TranslateNandPath(path);
if (!PathExists(hostPath)) {
return ISFS_ENOENT;
@@ -582,11 +572,11 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
return ISFS_EINVAL;
}
const char* path = (const char*)Memory::GetPointer(inBufPtr + 6);
std::string hostPath = TranslateNandPath(path);
const std::filesystem::path hostPath = TranslateNandPath(path);
CreateParentDirectories(hostPath);
// Create empty file
FILE* f = std::fopen(hostPath.c_str(), "wb");
FILE* f = NandFopen(hostPath, "wb");
if (f) {
std::fclose(f);
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* dstPath = (const char*)Memory::GetPointer(inBufPtr + 0x40);
std::string srcHost = TranslateNandPath(srcPath);
std::string dstHost = TranslateNandPath(dstPath);
const std::filesystem::path srcHost = TranslateNandPath(srcPath);
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_EIO;
@@ -805,11 +795,11 @@ int32_t ISFS_OpenLib_Initialize(CpuContext* ctx) {
g_isfsInitialized = true;
// Create the title data directory if it doesn't exist
char titlePath[256];
const std::string& base = GetNandBasePath();
std::snprintf(titlePath, sizeof(titlePath), "%s\\title\\%08x\\%08x\\data",
base.c_str(), kNandTitleIdHi, CurrentMkwTitleIdLo());
CreateDirectoryPath(titlePath);
char titleId[32];
std::snprintf(titleId, sizeof(titleId), "%08x", kNandTitleIdHi);
char gameId[32];
std::snprintf(gameId, sizeof(gameId), "%08x", CurrentMkwTitleIdLo());
CreateDirectoryPath(GetNandBasePath() / "title" / titleId / gameId / "data");
if (!ctx) {
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
// ============================================================================
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(
uint32_t fd,
uint32_t cmd,
@@ -919,6 +977,16 @@ extern "C" int32_t NAND_IOS_Ioctlv_HLE(
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 (!vectorPtr || !Memory::Contains(vectorPtr, static_cast<size_t>(numIn + numOut) * 8u)) {
return ISFS_EINVAL;
+30 -16
View File
@@ -74,8 +74,18 @@ std::string RiivoGameId() {
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 text = path.lexically_normal().generic_string();
std::string text = RiivoGenericText(path.lexically_normal());
#ifdef _WIN32
RuntimeHle::LowerInPlace(text);
#endif
@@ -86,6 +96,8 @@ void RiivoAddRoot(std::vector<RuntimeRiivolution::Overlay>& overlays, fs::path r
const char* source) {
std::error_code 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;
}
@@ -104,7 +116,7 @@ void RiivoAddRoot(std::vector<RuntimeRiivolution::Overlay>& overlays, fs::path r
}
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});
}
@@ -133,7 +145,7 @@ std::vector<RuntimeRiivolution::Overlay> RiivoDiscoverRoots() {
}
for (const auto& root : RecompMod::DvdOverlayRoots()) {
RiivoAddRoot(overlays, fs::path(root), "recomp mod manifest");
RiivoAddRoot(overlays, root, "recomp mod manifest");
}
return overlays;
@@ -154,7 +166,7 @@ std::optional<RiivoXmlSet> RiivoFindXmls(const fs::path& overlayRoot) {
// <sd>/RetroRewind6), so externals resolve against the root's parent.
const std::string& configured = RecompMod::RiivolutionXml();
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)) {
return RiivoXmlSet{overlayRoot.parent_path(), {configuredXml}};
}
@@ -211,7 +223,7 @@ void RiivoCollectMappings(const RiivolutionContract::Patch& patch, const std::st
++set.skippedExternals;
continue;
}
const fs::path hostFile(*resolved);
const fs::path hostFile = PathFromUtf8(*resolved);
if (!fs::is_regular_file(hostFile, ec)) {
++set.skippedExternals;
continue;
@@ -227,7 +239,7 @@ void RiivoCollectMappings(const RiivolutionContract::Patch& patch, const std::st
++set.skippedExternals;
continue;
}
const fs::path hostFolder(*resolved);
const fs::path hostFolder = PathFromUtf8(*resolved);
if (!fs::is_directory(hostFolder, ec)) {
++set.skippedExternals;
continue;
@@ -246,7 +258,7 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
}
const std::string gameId = RiivoGameId();
const std::string sdRootGeneric = xmlSet->sdRoot.generic_string();
const std::string sdRootGeneric = RiivoGenericText(xmlSet->sdRoot);
const auto manifestSelections = RiivoManifestSelections();
// 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) {
const auto text = RiivoReadFile(xmlFile);
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;
}
auto disc = RiivolutionContract::ParseString(*text);
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"
<< std::endl;
continue;
}
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;
continue;
}
@@ -284,7 +297,7 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
RiivolutionContract::ApplySelections(*disc, manifestSelections);
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();
for (const auto& patch : activePatches) {
@@ -297,9 +310,10 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
if (const auto resolvedSave = RiivolutionContract::MakeAbsoluteFromRelative(
sdRootGeneric, xmlDirGeneric, savegame->external)) {
state.saveRedirect =
RuntimeRiivolution::SaveRedirect{fs::path(*resolvedSave), savegame->clone};
RuntimeRiivolution::SaveRedirect{PathFromUtf8(*resolvedSave),
savegame->clone};
RT_LOG(RT_TAG_RIIVOLUTION) << "savegame redirect: "
<< state.saveRedirect->hostDirectory.string()
<< PathToUtf8(state.saveRedirect->hostDirectory)
<< (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
// failure this layer has: the game boots, plays, and quietly shows
// 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)"
<< std::endl;
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
<< "; check the riivolution option selections (recomp.yml) or "
<< 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) {
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"
<< std::endl;
}
+35 -11
View File
@@ -47,6 +47,7 @@
#include "system_bridge.h"
#include "ppc_runtime.h"
#include "aurora_events.h"
#include "wup028_adapter.h"
#include "fiber_manager.h"
#include "hle_stubs.h"
#include "runtime_config.h"
@@ -394,7 +395,7 @@ void InitializeProcessTranscript(int argc, char** argv) {
}
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) {
return;
}
@@ -570,14 +571,31 @@ std::string FormatHostStackTrace(unsigned framesToSkip) {
for (USHORT i = 0; i < captured; ++i) {
const DWORD64 addr = reinterpret_cast<DWORD64>(frames[i]);
HMODULE module = nullptr;
char modulePath[MAX_PATH] = "?";
std::string modulePath = "?";
DWORD64 moduleBase = 0;
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCSTR>(frames[i]),
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(frames[i]),
&module) != 0 &&
module != nullptr) {
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 = "?";
@@ -641,7 +659,7 @@ void WriteFatalLogImpl(std::string_view reason, std::string_view extraDetails =
std::string fileName = "crash_";
fileName.append(reason);
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) {
return;
}
@@ -680,11 +698,12 @@ void WriteFatalLogImpl(std::string_view reason, std::string_view extraDetails =
// are large.
static std::atomic_bool s_memorySnapshotWritten{false};
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();
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) {
@@ -1183,10 +1202,11 @@ int RuntimeMain(int argc, char** argv) {
std::filesystem::create_directories(rendererCacheDirectory, rendererPathError);
if (rendererPathError) {
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 auroraCachePath = rendererCacheDirectory.string();
const std::string auroraUserPath = RuntimeConfigFile::PathToUtf8(applicationDataDirectory);
const std::string auroraCachePath = RuntimeConfigFile::PathToUtf8(rendererCacheDirectory);
auroraConfig.userPath = auroraUserPath.c_str();
auroraConfig.cachePath = auroraCachePath.c_str();
auroraConfig.logCallback = &RuntimeAuroraLogCallback;
@@ -1252,6 +1272,7 @@ int RuntimeMain(int argc, char** argv) {
}
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
GxGuestWrite::InstallAuroraHooks();
Wup028Adapter::Initialize();
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
auroraInfo.windowSize.native_fb_height);
settings_overlay::InitializeRuntimeSettings();
@@ -1293,6 +1314,7 @@ int RuntimeMain(int argc, char** argv) {
// Shutdown fiber system
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
Wup028Adapter::Shutdown();
aurora_shutdown();
SetRuntimeExitCodeImpl(0);
ShutdownProcessTranscript();
@@ -1310,6 +1332,7 @@ int RuntimeMain(int argc, char** argv) {
SetRuntimeExitCodeImpl(1);
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
Wup028Adapter::Shutdown();
aurora_shutdown();
ShutdownProcessTranscript();
return 1;
@@ -1321,6 +1344,7 @@ int RuntimeMain(int argc, char** argv) {
SetRuntimeExitCodeImpl(1);
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
Wup028Adapter::Shutdown();
aurora_shutdown();
ShutdownProcessTranscript();
return 1;
+6 -6
View File
@@ -33,8 +33,8 @@ std::vector<RecompMod::InitializerFn>& PostRelInitializers() {
return initializers;
}
std::vector<std::string>& OverlayRoots() {
static std::vector<std::string> roots;
std::vector<std::filesystem::path>& OverlayRoots() {
static std::vector<std::filesystem::path> roots;
return roots;
}
@@ -279,17 +279,17 @@ void RegisterDvdOverlayRoot(std::string root) {
// against the executable directory instead.
const std::filesystem::path base =
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());
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()) {
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();
}
+81 -4
View File
@@ -1,5 +1,7 @@
#include "settings_overlay.h"
#include "wup028_adapter.h"
#include "audio_backend.h"
#include "controller_mapping_wizard.h"
#include "game_graphics_options.h"
#include "music_attenuation.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() {
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
const std::string label = "Port " + std::to_string(port + 1);
@@ -320,20 +350,60 @@ void DrawControllerSettings() {
}
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();
if (controllerCount == 0) {
ImGui::TextDisabled("No controller connected");
DrawGameCubeAdapterInfo();
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")) {
for (uint32_t index = 0; index < controllerCount; ++index) {
const char* name = PADGetNameForControllerIndex(index);
ImGui::PushID(static_cast<int>(index));
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());
ApplyConfiguredMappings();
}
@@ -346,6 +416,7 @@ void DrawControllerSettings() {
PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount);
if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) {
ImGui::TextDisabled("Assign a controller to edit its buttons");
DrawGameCubeAdapterInfo();
return;
}
@@ -485,7 +556,7 @@ void DrawControllerSettings() {
ImGui::TextUnformatted(kControllerButtons[i].label);
ImGui::PopID();
}
DrawGameCubeAdapterInfo();
}
void DrawAudioSettings() {
@@ -840,6 +911,7 @@ void PersistDisplayModeIfChanged() {
} // namespace
void InitializeRuntimeSettings() noexcept {
controller_mapping_wizard::LoadPersistedMappings();
ApplyConfiguredMappings();
AudioBackend::Instance().SetMasterVolume(static_cast<float>(g_audioVolumePercent) / 100.0f);
AudioBackend::Instance().SetMuted(g_audioMuted);
@@ -872,6 +944,7 @@ void HandleEvents(const AuroraEvent* events) noexcept {
if (ev->type != AURORA_SDL_EVENT) {
continue;
}
controller_mapping_wizard::HandleSdlEvent(ev->sdl);
if (IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) {
SetTopBarVisible(!g_topBarVisible);
}
@@ -893,6 +966,10 @@ void Draw() noexcept {
}
DrawFpsOverlay();
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();
}
+6 -4
View File
@@ -19,6 +19,7 @@
#include "memory.h"
#include "ppc_runtime.h"
#include "recomp_mod_loader.h"
#include "runtime_config.h"
#include "runtime_log.h"
#include "runtime_product.h"
#include "timebase_contract.h"
@@ -409,14 +410,14 @@ void SystemBridge::Initialize() {
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 kMem1Size = 0x01800000u;
if (Memory::Contains(kMem1Base, kMem1Size)) {
std::ofstream dump(mem1Path, std::ios::binary | std::ios::trunc);
dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem1Base, kMem1Size)),
kMem1Size);
os << "[runtime] MEM1 snapshot written to " << mem1Path
os << "[runtime] MEM1 snapshot written to " << RuntimeConfigFile::PathToUtf8(mem1Path)
<< (dump.good() ? "" : " (write failed)") << std::endl;
}
constexpr uint32_t kMem2Base = 0x90000000u;
@@ -424,11 +425,12 @@ void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const char* mem1Pa
if (!Memory::Contains(kMem2Base, mem2Size)) {
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);
dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem2Base, mem2Size)),
mem2Size);
os << "[runtime] MEM2 snapshot written to " << mem2Path
os << "[runtime] MEM2 snapshot written to " << RuntimeConfigFile::PathToUtf8(mem2Path)
<< (dump.good() ? "" : " (write failed)") << std::endl;
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
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
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.
> Translating a DOL does not exempt you from owning the game it came from.
## How translating a DOL works
A project manifest (YAML) names the input DOL and pins its layout; everything else is derived.
Translation is four commands:
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
what was emitted).
2. **`generate-data-init --project <manifest>`** - writes the embedded `.data`/`.rodata`/`.sdata`
section initializer and `RuntimeConfig.h`.
3. **`emit-build-shards --project <manifest>`** - emits the CMake build graph (`shards.cmake`)
covering both generated sources and `runtime/src`.
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
`function_map` (one `hexaddr name` per line) that seeds additional function boundaries. Unsupported
instructions fail translation by default.
See `projects/examples/generic-dol.yml` for a minimal manifest driven by `RECOMP_GENERIC_DOL`.
## Prerequisites
| Tool | Notes |
| --- | --- |
| .NET 8 SDK | Builds and runs the translator. |
| 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. |
Build the CLI once and invoke the assembly directly:
```powershell
dotnet build translator/src/Translator.Cli/Translator.Cli.csproj -c Release
$translator = 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll'
```
## Manifest essentials
- `inputs.dol.path` - the DOL to translate; optional SHA-256 pinning rejects wrong revisions.
- `memory.base` / `size` - guest address space.
- `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`;
the translator does not guess them.
- `translation.function_map.path` - optional symbol map used as the discovery oracle.
- `translation.allow_unsupported_instructions` - off by default; enabling it emits runtime traps
instead of failing, and such a build can never ship.
Relative paths resolve from `workspace_root`, which itself resolves from the manifest directory.
## Commands
- `info [--project path]`
- `translate-recursive <address> --project path`
- `generate-data-init --project path`
- `emit-base-manifest --project path`
- `emit-build-shards --project path`
- `translate-mod --project path [--profile name] ...` - static Kamek/Pulsar module translation
Any command prints its own option list with `--help`.
## Test
```powershell
dotnet test translator/Translator.sln -c Release
```