mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-13 09:59:47 -04:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d836dab3e | |||
| 6bffedf028 | |||
| 09ca4e98f4 | |||
| 6dc4e59052 | |||
| 82b295b20c | |||
| eb1721fd72 | |||
| 129c714f02 | |||
| 2794024d3a | |||
| b5e5858e1d | |||
| 4897e7e27d | |||
| 7ebda6bf7f | |||
| 0403f176bd | |||
| 1912292c80 | |||
| e498e62622 | |||
| f73739f248 | |||
| 3389fe35fc | |||
| e11062a1ba | |||
| 1d1d064d37 | |||
| 9d1f3db1d5 | |||
| 251529d66e | |||
| 987b666296 | |||
| 260022b4ba | |||
| f1c2b60df1 | |||
| 5193dd9749 | |||
| ec9bd92415 | |||
| dbf6d05625 | |||
| 8ba44162fa | |||
| 9ee14e582d | |||
| 0c349a9296 | |||
| 21b57f08a4 | |||
| e146b10af8 | |||
| edd03f8991 | |||
| 9c4f4b738a | |||
| 8d5d93f02d | |||
| c75b482224 | |||
| e5ae29b27e | |||
| 384623a4c7 | |||
| 6c2712307e | |||
| c5a53ce0b3 |
@@ -25,6 +25,9 @@ Code.pul
|
|||||||
# Build output
|
# Build output
|
||||||
/build/
|
/build/
|
||||||
/build-*/
|
/build-*/
|
||||||
|
/.build-output/
|
||||||
|
/.build-test/
|
||||||
|
/native-build/
|
||||||
/dist/
|
/dist/
|
||||||
/out/
|
/out/
|
||||||
[Bb]in/
|
[Bb]in/
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
|
|||||||
|
|
||||||
$manifest = [ordered]@{
|
$manifest = [ordered]@{
|
||||||
SchemaVersion = 2
|
SchemaVersion = 2
|
||||||
ProductVersion = '0.2.20'
|
ProductVersion = '0.2.24'
|
||||||
ExpectedGameId = $pins.GameId
|
ExpectedGameId = $pins.GameId
|
||||||
ExpectedDolSha256 = $pins.DolSha256
|
ExpectedDolSha256 = $pins.DolSha256
|
||||||
ExpectedRelSha256 = $pins.RelSha256
|
ExpectedRelSha256 = $pins.RelSha256
|
||||||
|
|||||||
@@ -151,9 +151,10 @@ if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirect
|
|||||||
throw '-BaseOutputDirectory is valid only with -Profile both.'
|
throw '-BaseOutputDirectory is valid only with -Profile both.'
|
||||||
}
|
}
|
||||||
$translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe'
|
$translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe'
|
||||||
$cmake = Join-Path $Toolkit 'CMake\bin\cmake.exe'
|
$toolchain = Get-MkwShellSafeToolchainRoot $Toolkit
|
||||||
$ninja = Join-Path $Toolkit 'Ninja\ninja.exe'
|
$cmake = Join-Path $toolchain 'CMake\bin\cmake.exe'
|
||||||
$toolchainBin = Join-Path $Toolkit 'llvm-mingw\bin'
|
$ninja = Join-Path $toolchain 'Ninja\ninja.exe'
|
||||||
|
$toolchainBin = Join-Path $toolchain 'llvm-mingw\bin'
|
||||||
$cc = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang.exe'
|
$cc = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang.exe'
|
||||||
$cxx = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang++.exe'
|
$cxx = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang++.exe'
|
||||||
$windres = Join-Path $toolchainBin 'x86_64-w64-mingw32-windres.exe'
|
$windres = Join-Path $toolchainBin 'x86_64-w64-mingw32-windres.exe'
|
||||||
@@ -203,7 +204,7 @@ if ($Parallel -gt 0) {
|
|||||||
$oldPath = $env:PATH
|
$oldPath = $env:PATH
|
||||||
$oldDotnet = $env:DOTNET_ROOT
|
$oldDotnet = $env:DOTNET_ROOT
|
||||||
try {
|
try {
|
||||||
$env:PATH = Get-MkwToolchainPath $Toolkit
|
$env:PATH = Get-MkwToolchainPath $toolchain
|
||||||
Remove-Item Env:DOTNET_ROOT -ErrorAction SilentlyContinue
|
Remove-Item Env:DOTNET_ROOT -ErrorAction SilentlyContinue
|
||||||
Push-Location $Workspace
|
Push-Location $Workspace
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -40,6 +40,43 @@ function Get-MkwToolchainPath([string]$ToolchainRoot) {
|
|||||||
) -join ';')
|
) -join ';')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-MkwShellSafeToolchainRoot([string]$ToolchainRoot) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' }
|
||||||
|
$full = [IO.Path]::GetFullPath($ToolchainRoot)
|
||||||
|
# A drive root keeps its separator: "C:" is relative to the current directory on that drive.
|
||||||
|
if ($full -ne [IO.Path]::GetPathRoot($full)) { $full = $full.TrimEnd('\') }
|
||||||
|
if ($full -notmatch '[()&^%!]') { return $full }
|
||||||
|
|
||||||
|
$sha = [Security.Cryptography.SHA256]::Create()
|
||||||
|
try {
|
||||||
|
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant()))
|
||||||
|
} finally { $sha.Dispose() }
|
||||||
|
$linkName = 'toolchain-' + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
|
||||||
|
|
||||||
|
$failures = @()
|
||||||
|
foreach ($base in @($env:ProgramData, $env:PUBLIC)) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($base) -or $base -match '[()&^%! ]') { continue }
|
||||||
|
$link = Join-Path (Join-Path $base 'WiiCompiled') $linkName
|
||||||
|
try {
|
||||||
|
[IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null
|
||||||
|
# The name already identifies the target, so an existing junction that still resolves is
|
||||||
|
# this one; only a broken leftover is replaced. Directory.Delete removes the reparse
|
||||||
|
# point itself, where Remove-Item -Recurse would delete the toolchain it points at.
|
||||||
|
if (-not (Test-Path -LiteralPath (Join-Path $link 'CMake\bin\cmake.exe') -PathType Leaf)) {
|
||||||
|
if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) }
|
||||||
|
New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null
|
||||||
|
}
|
||||||
|
Write-Host "MKWCBUILD: Building through $link, because $full contains characters cmd.exe cannot parse"
|
||||||
|
return $link
|
||||||
|
} catch {
|
||||||
|
$failures += "$link ($($_.Exception.Message))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw ("The toolchain path $full contains a character (one of ( ) & ^ % !) that the compiler " +
|
||||||
|
'cannot be invoked through, and no junction to it could be created: ' + ($failures -join '; ') +
|
||||||
|
'. Install to a path without those characters.')
|
||||||
|
}
|
||||||
|
|
||||||
function Get-MkwProjectPins([string]$ProjectFile) {
|
function Get-MkwProjectPins([string]$ProjectFile) {
|
||||||
<#
|
<#
|
||||||
The Mario Kart Wii facts pinned by projects/mkwii/recomp.yml (game identity, clean input
|
The Mario Kart Wii facts pinned by projects/mkwii/recomp.yml (game identity, clean input
|
||||||
|
|||||||
@@ -305,27 +305,27 @@ internal sealed class InstallerEngine
|
|||||||
RuntimeConfiguration.SetRetroRewindRoot(configPath, canonicalRetroRoot);
|
RuntimeConfiguration.SetRetroRewindRoot(configPath, canonicalRetroRoot);
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
|
|
||||||
// A portable installation is owned by the folder it lives in, not by this machine. Adding a
|
try
|
||||||
// machine-wide uninstall entry for it would outlive the folder and, worse, would overwrite
|
|
||||||
// the entry of a normal installation on the same account.
|
|
||||||
if (PortableRoot.TryFind(installDirectory) is not null)
|
|
||||||
{
|
{
|
||||||
_reporter.Diagnostic(
|
// A portable installation is owned by the folder it lives in, not by this machine. Adding
|
||||||
"Portable installation: no Windows uninstall entry was registered. Remove the folder, " +
|
// a machine-wide uninstall entry for it would outlive the folder and, worse, would
|
||||||
"or run the copied setup with --uninstall, to uninstall it.");
|
// overwrite the entry of a normal installation on the same account.
|
||||||
}
|
if (PortableRoot.TryFind(installDirectory) is not null)
|
||||||
else
|
{
|
||||||
{
|
_reporter.Diagnostic(
|
||||||
try
|
"Portable installation: no Windows uninstall entry was registered. Remove the folder, " +
|
||||||
|
"or run the copied setup with --uninstall, to uninstall it.");
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
ShellIntegration.RegisterUninstaller(installDirectory, state.RetroRewindInstalled);
|
ShellIntegration.RegisterUninstaller(installDirectory, state.RetroRewindInstalled);
|
||||||
ShellIntegration.CreateShortcuts(installDirectory);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_reporter.Diagnostic("The installation succeeded, but Windows shell integration failed: " +
|
|
||||||
ex.Message);
|
|
||||||
}
|
}
|
||||||
|
ShellIntegration.CreateShortcuts(installDirectory);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_reporter.Diagnostic("The installation succeeded, but Windows shell integration failed: " +
|
||||||
|
ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
_reporter.Progress(InstallStages.Publish, "Installation complete.", completionPercent);
|
_reporter.Progress(InstallStages.Publish, "Installation complete.", completionPercent);
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ internal static class PlatformChecks
|
|||||||
internal static class ProductInfo
|
internal static class ProductInfo
|
||||||
{
|
{
|
||||||
public const string Name = "WiiCompiled";
|
public const string Name = "WiiCompiled";
|
||||||
public const string Version = "0.2.20";
|
public const string Version = "0.2.24";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The setup executable is copied into the installation under this name. It is the launcher and
|
/// The setup executable is copied into the installation under this name. It is the launcher and
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<AssemblyName>WiiCompiled.Setup</AssemblyName>
|
<AssemblyName>WiiCompiled.Setup</AssemblyName>
|
||||||
<RootNamespace>WiiCompiled.Setup</RootNamespace>
|
<RootNamespace>WiiCompiled.Setup</RootNamespace>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<Version>0.2.20</Version>
|
<Version>0.2.24</Version>
|
||||||
<Authors>patchzy</Authors>
|
<Authors>patchzy</Authors>
|
||||||
<Product>WiiCompiled</Product>
|
<Product>WiiCompiled</Product>
|
||||||
<Description>Command-line installer and launcher for WiiCompiled</Description>
|
<Description>Command-line installer and launcher for WiiCompiled</Description>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ inputs like paddles, touchpads and share buttons show up when the hardware repor
|
|||||||
- Windows 10 or 11, 64-bit
|
- Windows 10 or 11, 64-bit
|
||||||
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
|
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
|
||||||
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
|
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
|
||||||
- About 20 GB of free disk space during installation
|
- About 20 GB of free disk space during installation (Final game size ~5 GB)
|
||||||
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
|
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
|
||||||
GCZ, CISO, WBFS, WIA and RVZ are accepted.
|
GCZ, CISO, WBFS, WIA and RVZ are accepted.
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ s32 PADGetNativeButtonPressed(u32 port);
|
|||||||
PADSignedNativeAxis PADGetNativeAxisPulled(u32 port);
|
PADSignedNativeAxis PADGetNativeAxisPulled(u32 port);
|
||||||
void PADRestoreDefaultMapping(u32 port);
|
void PADRestoreDefaultMapping(u32 port);
|
||||||
void PADBlockInput(bool block);
|
void PADBlockInput(bool block);
|
||||||
|
bool PADIsInputBlocked(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the default controller mapping used.
|
* Set the default controller mapping used.
|
||||||
|
|||||||
@@ -111,7 +111,9 @@ ECardResult CardGciFolder::createFile(const char* filename, size_t size, FileHan
|
|||||||
}
|
}
|
||||||
|
|
||||||
gciFileHeader->swapEndian();
|
gciFileHeader->swapEndian();
|
||||||
m_files.push_back({*gciFileHeader, fileSize, reinterpret_cast<const char8_t*>(gciFilename.c_str()), false}); // push non-endian swapped header first
|
// push non-endian swapped header first
|
||||||
|
m_files.push_back({*gciFileHeader, fileSize,
|
||||||
|
std::u8string(gciFilename.begin(), gciFilename.end()), false});
|
||||||
handleOut = FileHandle(m_files.size() - 1, 0);
|
handleOut = FileHandle(m_files.size() - 1, 0);
|
||||||
|
|
||||||
return ECardResult::READY;
|
return ECardResult::READY;
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ void CARDInit(const char* game, const char* maker) {
|
|||||||
|
|
||||||
std::filesystem::path cardWorkingDir;
|
std::filesystem::path cardWorkingDir;
|
||||||
if (aurora::g_config.userPath != nullptr)
|
if (aurora::g_config.userPath != nullptr)
|
||||||
cardWorkingDir = reinterpret_cast<const char8_t*>(aurora::g_config.userPath);
|
cardWorkingDir = fs_path_from_string(aurora::g_config.userPath);
|
||||||
else
|
else
|
||||||
cardWorkingDir = std::filesystem::current_path();
|
cardWorkingDir = std::filesystem::current_path();
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include "../../fs_helper.hpp"
|
||||||
#include "../../input.hpp"
|
#include "../../input.hpp"
|
||||||
#include "../../internal.hpp"
|
#include "../../internal.hpp"
|
||||||
#include <dolphin/pad.h>
|
#include <dolphin/pad.h>
|
||||||
@@ -5,6 +6,7 @@
|
|||||||
#include <SDL3/SDL_mouse.h>
|
#include <SDL3/SDL_mouse.h>
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
|
|
||||||
@@ -283,7 +285,7 @@ constexpr PADCLampRegion ClampRegion{
|
|||||||
|
|
||||||
bool g_initialized;
|
bool g_initialized;
|
||||||
bool g_keyboardBindingsLoaded = false;
|
bool g_keyboardBindingsLoaded = false;
|
||||||
bool g_blockPAD = false;
|
std::atomic_bool g_blockPAD{false};
|
||||||
bool g_suppressHeldOnRead = false;
|
bool g_suppressHeldOnRead = false;
|
||||||
std::array<PADButton, PAD_CHANMAX> g_suppressedButtons{};
|
std::array<PADButton, PAD_CHANMAX> g_suppressedButtons{};
|
||||||
std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
|
std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
|
||||||
@@ -491,7 +493,7 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string basePath{aurora::g_config.userPath};
|
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
|
||||||
if (!controller->m_mappingLoaded) {
|
if (!controller->m_mappingLoaded) {
|
||||||
__PADSetDefaultMapping(controller);
|
__PADSetDefaultMapping(controller);
|
||||||
controller->m_axisMapping = g_defaultAxes;
|
controller->m_axisMapping = g_defaultAxes;
|
||||||
@@ -499,8 +501,9 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re
|
|||||||
|
|
||||||
controller->m_mappingLoaded = true;
|
controller->m_mappingLoaded = true;
|
||||||
|
|
||||||
const auto path = fmt::format("{}/{}_{:04X}_{:04X}.controller", basePath, PADGetName(playerIndex), controller->m_vid,
|
const auto path = fs_path_to_string(
|
||||||
controller->m_pid);
|
basePath / fmt::format("{}_{:04X}_{:04X}.controller", PADGetName(playerIndex), controller->m_vid,
|
||||||
|
controller->m_pid));
|
||||||
SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb");
|
SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb");
|
||||||
if (file == nullptr) {
|
if (file == nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -659,7 +662,8 @@ u32 PADRead(PADStatus* status) {
|
|||||||
|
|
||||||
int numKeys = 0;
|
int numKeys = 0;
|
||||||
const bool* kbState = SDL_GetKeyboardState(&numKeys);
|
const bool* kbState = SDL_GetKeyboardState(&numKeys);
|
||||||
const bool captureHeldInput = g_suppressHeldOnRead && !g_blockPAD;
|
const bool inputBlocked = g_blockPAD.load(std::memory_order_acquire);
|
||||||
|
const bool captureHeldInput = g_suppressHeldOnRead && !inputBlocked;
|
||||||
g_suppressHeldOnRead = false;
|
g_suppressHeldOnRead = false;
|
||||||
|
|
||||||
uint32_t rumbleSupport = 0;
|
uint32_t rumbleSupport = 0;
|
||||||
@@ -882,7 +886,7 @@ u32 PADRead(PADStatus* status) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_blockPAD) {
|
if (inputBlocked) {
|
||||||
neutralize_status(status[i]);
|
neutralize_status(status[i]);
|
||||||
} else {
|
} else {
|
||||||
apply_unblock_suppression(status[i], i, captureHeldInput);
|
apply_unblock_suppression(status[i], i, captureHeldInput);
|
||||||
@@ -1247,8 +1251,8 @@ constexpr uint32_t k_keyboardMagic = SBIG('KBND');
|
|||||||
constexpr int32_t k_keyboardVersion = 3;
|
constexpr int32_t k_keyboardVersion = 3;
|
||||||
|
|
||||||
static void load_keyboard_bindings() {
|
static void load_keyboard_bindings() {
|
||||||
const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat";
|
const auto filePath = fs_path_from_string(aurora::g_config.userPath) / "keyboard_bindings.dat";
|
||||||
SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "rb");
|
SDL_IOStream* file = SDL_IOFromFile(fs_path_to_string(filePath).c_str(), "rb");
|
||||||
if (file == nullptr) {
|
if (file == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1317,10 +1321,11 @@ static void load_keyboard_bindings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void save_keyboard_bindings() {
|
static void save_keyboard_bindings() {
|
||||||
const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat";
|
const auto filePath = fs_path_from_string(aurora::g_config.userPath) / "keyboard_bindings.dat";
|
||||||
SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "wb");
|
const auto filePathStr = fs_path_to_string(filePath);
|
||||||
|
SDL_IOStream* file = SDL_IOFromFile(filePathStr.c_str(), "wb");
|
||||||
if (file == nullptr) {
|
if (file == nullptr) {
|
||||||
aurora::input::Log.warn("save_keyboard_bindings: failed to open {} for writing", filePath.string());
|
aurora::input::Log.warn("save_keyboard_bindings: failed to open {} for writing", filePathStr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1344,14 +1349,14 @@ void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PADSerializeMappings() {
|
void PADSerializeMappings() {
|
||||||
const std::filesystem::path basePath{aurora::g_config.userPath};
|
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
|
||||||
|
|
||||||
for (auto& controller : aurora::input::g_GameControllers | std::views::values) {
|
for (auto& controller : aurora::input::g_GameControllers | std::views::values) {
|
||||||
EnsureMappingLoaded(&controller);
|
EnsureMappingLoaded(&controller);
|
||||||
const auto filePath =
|
const auto filePath =
|
||||||
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
|
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
|
||||||
controller.m_vid, controller.m_pid);
|
controller.m_vid, controller.m_pid);
|
||||||
std::string filePathStr = filePath.string();
|
std::string filePathStr = fs_path_to_string(filePath);
|
||||||
|
|
||||||
// don't truncate the file if it already exists
|
// don't truncate the file if it already exists
|
||||||
const char* openMode = std::filesystem::exists(filePath) ? "r+b" : "wb";
|
const char* openMode = std::filesystem::exists(filePath) ? "r+b" : "wb";
|
||||||
@@ -1370,7 +1375,7 @@ void PADSerializeMappings() {
|
|||||||
// start writing data at next 32-byte aligned offset
|
// start writing data at next 32-byte aligned offset
|
||||||
const int64_t dataStart = SDL_TellIO(file) + 31 & ~31;
|
const int64_t dataStart = SDL_TellIO(file) + 31 & ~31;
|
||||||
if (dataStart == -1) {
|
if (dataStart == -1) {
|
||||||
aurora::input::Log.warn("Unable to seek in controller bindings! Path: \"{}\"", filePath.string());
|
aurora::input::Log.warn("Unable to seek in controller bindings! Path: \"{}\"", filePathStr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SDL_SeekIO(file, dataStart, SDL_IO_SEEK_SET);
|
SDL_SeekIO(file, dataStart, SDL_IO_SEEK_SET);
|
||||||
@@ -1530,12 +1535,13 @@ void PADRestoreDefaultMapping(const u32 port) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PADBlockInput(const bool block) {
|
void PADBlockInput(const bool block) {
|
||||||
if (g_blockPAD && !block) {
|
if (g_blockPAD.exchange(block, std::memory_order_acq_rel) && !block) {
|
||||||
g_suppressHeldOnRead = true;
|
g_suppressHeldOnRead = true;
|
||||||
}
|
}
|
||||||
g_blockPAD = block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PADIsInputBlocked() { return g_blockPAD.load(std::memory_order_acquire); }
|
||||||
|
|
||||||
SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) {
|
SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) {
|
||||||
const auto* ctrl = __PADGetControllerForIndex(index);
|
const auto* ctrl = __PADGetControllerForIndex(index);
|
||||||
if (ctrl == nullptr) {
|
if (ctrl == nullptr) {
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a std::filesystem::path to a std::string, UTF-8, without exploding on Windows.
|
* Narrow path strings crossing the aurora boundary are UTF-8. path::string() and the
|
||||||
|
* char path constructor go through the ANSI codepage on Windows, so they must not be
|
||||||
|
* used for anything the host handed us or hands back to SDL, sqlite or ImGui.
|
||||||
*/
|
*/
|
||||||
inline std::string fs_path_to_string(const std::filesystem::path& path) {
|
inline std::string fs_path_to_string(const std::filesystem::path& path) {
|
||||||
const auto u8str = path.u8string();
|
const auto u8str = path.u8string();
|
||||||
return { reinterpret_cast<const char*>(u8str.c_str()) };
|
return { reinterpret_cast<const char*>(u8str.c_str()), u8str.size() };
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::filesystem::path fs_path_from_string(std::string_view utf8) {
|
||||||
|
return std::filesystem::path(std::u8string(utf8.begin(), utf8.end()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "clear.hpp"
|
#include "clear.hpp"
|
||||||
#include "../gx/pipeline.hpp"
|
#include "../gx/pipeline.hpp"
|
||||||
|
#include "../fs_helper.hpp"
|
||||||
#include "../sqlite_utils.hpp"
|
#include "../sqlite_utils.hpp"
|
||||||
#include "../webgpu/gpu.hpp"
|
#include "../webgpu/gpu.hpp"
|
||||||
|
|
||||||
@@ -715,7 +716,7 @@ static bool prepare_pipeline_cache_db() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto path = (std::filesystem::path{g_config.pipelineCachePath} / "pipeline_cache.db").string();
|
const auto path = fs_path_to_string(fs_path_from_string(g_config.pipelineCachePath) / "pipeline_cache.db");
|
||||||
auto ret = sqlite3_open(path.c_str(), &g_pipelineCacheDb);
|
auto ret = sqlite3_open(path.c_str(), &g_pipelineCacheDb);
|
||||||
if (ret != SQLITE_OK) {
|
if (ret != SQLITE_OK) {
|
||||||
Log.error("Failed to open pipeline cache database: {}", sqlite3_errmsg(g_pipelineCacheDb));
|
Log.error("Failed to open pipeline cache database: {}", sqlite3_errmsg(g_pipelineCacheDb));
|
||||||
|
|||||||
@@ -506,8 +506,8 @@ void build_index() noexcept {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto userPath = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.userPath)};
|
auto userPath = fs_path_from_string(g_config.userPath);
|
||||||
auto cachePath = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.cachePath)};
|
auto cachePath = fs_path_from_string(g_config.cachePath);
|
||||||
|
|
||||||
s_replacementRoot = userPath / "texture_replacements";
|
s_replacementRoot = userPath / "texture_replacements";
|
||||||
s_dumpRoot = cachePath / "texture_dumps";
|
s_dumpRoot = cachePath / "texture_dumps";
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <SDL3/SDL_events.h>
|
#include <SDL3/SDL_events.h>
|
||||||
#include <SDL3/SDL_render.h>
|
#include <SDL3/SDL_render.h>
|
||||||
|
|
||||||
|
#include "fs_helper.hpp"
|
||||||
#include "internal.hpp"
|
#include "internal.hpp"
|
||||||
#include "webgpu/gpu.hpp"
|
#include "webgpu/gpu.hpp"
|
||||||
#include "window.hpp"
|
#include "window.hpp"
|
||||||
@@ -37,7 +38,7 @@ void remove_legacy_ini_file(const char* basePath) noexcept {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::remove(std::filesystem::path{basePath} / "imgui.ini", ec);
|
std::filesystem::remove(fs_path_from_string(basePath) / "imgui.ini", ec);
|
||||||
}
|
}
|
||||||
|
|
||||||
void create_context() noexcept {
|
void create_context() noexcept {
|
||||||
|
|||||||
@@ -255,6 +255,18 @@ IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerId
|
|||||||
: IdentityMatch::None;
|
: IdentityMatch::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void assign_player_index(GameController& controller, int32_t port) {
|
||||||
|
SDL_SetGamepadPlayerIndex(controller.m_controller, port);
|
||||||
|
controller.m_playerIndex = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SDL forgets the index for devices mapped after connect, so player_index() falls
|
||||||
|
// back to the cached copy; both have to move together or a port looks doubly taken.
|
||||||
|
int32_t effective_player_index(const GameController& controller) {
|
||||||
|
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
|
||||||
|
return player >= 0 ? player : controller.m_playerIndex;
|
||||||
|
}
|
||||||
|
|
||||||
bool is_instance_claimed(const std::array<Uint32, PAD_MAX_CONTROLLERS>& claimedControllers, size_t claimedCount,
|
bool is_instance_claimed(const std::array<Uint32, PAD_MAX_CONTROLLERS>& claimedControllers, size_t claimedCount,
|
||||||
Uint32 instance) {
|
Uint32 instance) {
|
||||||
return std::find(claimedControllers.begin(), claimedControllers.begin() + claimedCount, instance) !=
|
return std::find(claimedControllers.begin(), claimedControllers.begin() + claimedCount, instance) !=
|
||||||
@@ -269,10 +281,10 @@ void apply_port_preferences() noexcept {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (auto& [instance, controller] : g_GameControllers) {
|
for (auto& [instance, controller] : g_GameControllers) {
|
||||||
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
|
const int32_t player = effective_player_index(controller);
|
||||||
if (player >= 0 && player < PAD_MAX_CONTROLLERS && g_portPreferences[player].state != PortPreferenceState::Unset) {
|
if (player >= 0 && player < PAD_MAX_CONTROLLERS && g_portPreferences[player].state != PortPreferenceState::Unset) {
|
||||||
// Keep SDL's default player assignment from taking explicitly configured ports
|
// Keep SDL's default player assignment from taking explicitly configured ports
|
||||||
SDL_SetGamepadPlayerIndex(controller.m_controller, -1);
|
assign_player_index(controller, -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,7 +305,7 @@ void apply_port_preferences() noexcept {
|
|||||||
|
|
||||||
switch (identity_match(preference.identity, controller_identity(controller))) {
|
switch (identity_match(preference.identity, controller_identity(controller))) {
|
||||||
case IdentityMatch::Exact:
|
case IdentityMatch::Exact:
|
||||||
SDL_SetGamepadPlayerIndex(controller.m_controller, static_cast<int32_t>(port));
|
assign_player_index(controller, static_cast<int32_t>(port));
|
||||||
claimedControllers[claimedCount++] = instance;
|
claimedControllers[claimedCount++] = instance;
|
||||||
fallbackController = nullptr;
|
fallbackController = nullptr;
|
||||||
break;
|
break;
|
||||||
@@ -311,11 +323,18 @@ void apply_port_preferences() noexcept {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fallbackController != nullptr) {
|
if (fallbackController != nullptr) {
|
||||||
SDL_SetGamepadPlayerIndex(fallbackController->m_controller, static_cast<int32_t>(port));
|
assign_player_index(*fallbackController, static_cast<int32_t>(port));
|
||||||
claimedControllers[claimedCount++] = fallbackInstance;
|
claimedControllers[claimedCount++] = fallbackInstance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ports are explicit assignments. SDL may choose a player index at connection
|
||||||
|
// time, but accepting it would make a newly connected controller silently take
|
||||||
|
// over a game port before the user assigns it in the controller menu.
|
||||||
|
void ensure_player_index(GameController& controller) noexcept {
|
||||||
|
assign_player_index(controller, -1);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
GameController* get_controller_for_player(uint32_t player) noexcept {
|
GameController* get_controller_for_player(uint32_t player) noexcept {
|
||||||
@@ -364,6 +383,7 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
|||||||
controller.m_hasRgbLed = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false);
|
controller.m_hasRgbLed = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false);
|
||||||
SDL_JoystickID instance = SDL_GetJoystickID(SDL_GetGamepadJoystick(ctrl));
|
SDL_JoystickID instance = SDL_GetJoystickID(SDL_GetGamepadJoystick(ctrl));
|
||||||
g_GameControllers[instance] = controller;
|
g_GameControllers[instance] = controller;
|
||||||
|
ensure_player_index(g_GameControllers[instance]);
|
||||||
apply_port_preferences();
|
apply_port_preferences();
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
@@ -371,6 +391,19 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool refresh_controller(SDL_JoystickID instance) noexcept {
|
||||||
|
const auto it = g_GameControllers.find(instance);
|
||||||
|
if (it == g_GameControllers.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// The SDL mapping changed underneath us; drop the cached PAD bindings so they
|
||||||
|
// are rebuilt from the new one.
|
||||||
|
it->second.m_mappingLoaded = false;
|
||||||
|
ensure_player_index(it->second);
|
||||||
|
apply_port_preferences();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void remove_controller(Uint32 instance) noexcept {
|
void remove_controller(Uint32 instance) noexcept {
|
||||||
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
||||||
SDL_CloseGamepad(it->second.m_controller);
|
SDL_CloseGamepad(it->second.m_controller);
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ struct GameController {
|
|||||||
GameController* get_controller_for_player(uint32_t player) noexcept;
|
GameController* get_controller_for_player(uint32_t player) noexcept;
|
||||||
Sint32 get_instance_for_player(uint32_t player) noexcept;
|
Sint32 get_instance_for_player(uint32_t player) noexcept;
|
||||||
SDL_JoystickID add_controller(SDL_JoystickID which) noexcept;
|
SDL_JoystickID add_controller(SDL_JoystickID which) noexcept;
|
||||||
|
bool refresh_controller(SDL_JoystickID instance) noexcept;
|
||||||
void remove_controller(Uint32 instance) noexcept;
|
void remove_controller(Uint32 instance) noexcept;
|
||||||
Sint32 player_index(Uint32 instance) noexcept;
|
Sint32 player_index(Uint32 instance) noexcept;
|
||||||
void set_player_index(Uint32 instance, Sint32 index) noexcept;
|
void set_player_index(Uint32 instance, Sint32 index) noexcept;
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ static void prune_stale_rows() {
|
|||||||
static bool cache_init_core() {
|
static bool cache_init_core() {
|
||||||
Log.debug("SQLite version {}", sqlite3_libversion());
|
Log.debug("SQLite version {}", sqlite3_libversion());
|
||||||
|
|
||||||
const auto path = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.cachePath)} / "dawn_cache.db";
|
const auto path = fs_path_from_string(g_config.cachePath) / "dawn_cache.db";
|
||||||
std::string file = fs_path_to_string(path);
|
std::string file = fs_path_to_string(path);
|
||||||
Log.debug("Using dawn cache at {}", file);
|
Log.debug("Using dawn cache at {}", file);
|
||||||
auto ret = sqlite3_open(file.c_str(), &db);
|
auto ret = sqlite3_open(file.c_str(), &db);
|
||||||
@@ -165,8 +165,12 @@ static bool cache_init_core() {
|
|||||||
db = nullptr;
|
db = nullptr;
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::remove(path, ec);
|
std::filesystem::remove(path, ec);
|
||||||
std::filesystem::remove(std::filesystem::path{file + "-wal"}, ec);
|
auto wal = path;
|
||||||
std::filesystem::remove(std::filesystem::path{file + "-shm"}, ec);
|
wal += "-wal";
|
||||||
|
std::filesystem::remove(wal, ec);
|
||||||
|
auto shm = path;
|
||||||
|
shm += "-shm";
|
||||||
|
std::filesystem::remove(shm, ec);
|
||||||
ret = sqlite3_open(file.c_str(), &db);
|
ret = sqlite3_open(file.c_str(), &db);
|
||||||
if (ret != SQLITE_OK) {
|
if (ret != SQLITE_OK) {
|
||||||
Log.error("Failed to recreate database: {}", sqlite3_errmsg(db));
|
Log.error("Failed to recreate database: {}", sqlite3_errmsg(db));
|
||||||
|
|||||||
@@ -294,6 +294,15 @@ void process_event(SDL_Event& event) {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case SDL_EVENT_GAMEPAD_REMAPPED: {
|
||||||
|
if (input::refresh_controller(event.gdevice.which)) {
|
||||||
|
g_events.push_back(AuroraEvent{
|
||||||
|
.type = AURORA_CONTROLLER_ADDED,
|
||||||
|
.controller = event.gdevice.which,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case SDL_EVENT_GAMEPAD_REMOVED: {
|
case SDL_EVENT_GAMEPAD_REMOVED: {
|
||||||
input::remove_controller(event.gdevice.which);
|
input::remove_controller(event.gdevice.which);
|
||||||
g_events.push_back(AuroraEvent{
|
g_events.push_back(AuroraEvent{
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ function(mkw_configure_product target)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_link_libraries(${target} PRIVATE
|
target_link_libraries(${target} PRIVATE
|
||||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
|
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp setupapi winusb)
|
||||||
|
|
||||||
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
|
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||||
foreach(runtime_dll libc++.dll libunwind.dll)
|
foreach(runtime_dll libc++.dll libunwind.dll)
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ inline bool WriteSerial(const std::filesystem::path& path, const std::string& se
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::filesystem::path temporary = path.string() + ".tmp";
|
std::filesystem::path temporary = path;
|
||||||
|
temporary += ".tmp";
|
||||||
{
|
{
|
||||||
std::ofstream output(temporary, std::ios::trunc);
|
std::ofstream output(temporary, std::ios::trunc);
|
||||||
if (!output) {
|
if (!output) {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_events.h>
|
||||||
|
|
||||||
|
// Press-to-bind setup for joysticks SDL either doesn't recognize as gamepads or
|
||||||
|
// recognizes with a mapping that lacks the analog stick (e.g. raphnet adapters).
|
||||||
|
// The wizard produces a standard SDL gamepad mapping, applies it live, and
|
||||||
|
// persists it to gamecontrollerdb.txt in the user data directory.
|
||||||
|
namespace controller_mapping_wizard {
|
||||||
|
|
||||||
|
void LoadPersistedMappings();
|
||||||
|
void HandleSdlEvent(const SDL_Event& event);
|
||||||
|
|
||||||
|
// Lists devices that need setup inside the controller settings menu.
|
||||||
|
void DrawSetupList();
|
||||||
|
// Draws the wizard window when active; call once per overlay frame.
|
||||||
|
void Draw();
|
||||||
|
|
||||||
|
bool IsActive();
|
||||||
|
|
||||||
|
} // namespace controller_mapping_wizard
|
||||||
@@ -20,14 +20,14 @@
|
|||||||
namespace DvdFstContract {
|
namespace DvdFstContract {
|
||||||
|
|
||||||
struct RegisteredFile {
|
struct RegisteredFile {
|
||||||
std::string hostPath;
|
std::filesystem::path hostPath;
|
||||||
std::string dvdPath;
|
std::string dvdPath;
|
||||||
uint32_t size = 0;
|
uint32_t size = 0;
|
||||||
uint32_t discOffsetWords = 0;
|
uint32_t discOffsetWords = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct IndexedEntry {
|
struct IndexedEntry {
|
||||||
std::string hostPath;
|
std::filesystem::path hostPath;
|
||||||
std::string dvdPath;
|
std::string dvdPath;
|
||||||
uint32_t size = 0;
|
uint32_t size = 0;
|
||||||
uint32_t discOffsetWords = 0;
|
uint32_t discOffsetWords = 0;
|
||||||
|
|||||||
@@ -27,13 +27,14 @@ inline std::optional<std::filesystem::path> ExistingDirectory(const std::filesys
|
|||||||
if (path.empty()) {
|
if (path.empty()) {
|
||||||
RT_LOGF(RT_TAG_NAND, "ERROR: %s\n", message);
|
RT_LOGF(RT_TAG_NAND, "ERROR: %s\n", message);
|
||||||
} else {
|
} else {
|
||||||
RT_LOGF(RT_TAG_NAND, "ERROR: %s: %s\n", message, path.string().c_str());
|
RT_LOGF(RT_TAG_NAND, "ERROR: %s: %s\n", message,
|
||||||
|
RuntimeConfigFile::PathToUtf8(path).c_str());
|
||||||
}
|
}
|
||||||
RT_LOGF(RT_TAG_NAND, "Set [paths] nand_root in Config.toml.\n");
|
RT_LOGF(RT_TAG_NAND, "Set [paths] nand_root in Config.toml.\n");
|
||||||
std::string details = message ? message : "The configured NAND could not be initialized.";
|
std::string details = message ? message : "The configured NAND could not be initialized.";
|
||||||
if (!path.empty()) {
|
if (!path.empty()) {
|
||||||
details += "\n\nPath: ";
|
details += "\n\nPath: ";
|
||||||
details += path.string();
|
details += RuntimeConfigFile::PathToUtf8(path);
|
||||||
}
|
}
|
||||||
details += "\n\nSet [paths] nand_root in Config.toml and try again.";
|
details += "\n\nSet [paths] nand_root in Config.toml and try again.";
|
||||||
// Same fatal idiom as the DVD and OS paths: crash artifacts first so the run
|
// Same fatal idiom as the DVD and OS paths: crash artifacts first so the run
|
||||||
@@ -51,18 +52,6 @@ inline std::filesystem::path ResolveConfiguredPath(const std::string& value) {
|
|||||||
return RuntimeConfigFile::ResolveRelativeToConfig(value);
|
return RuntimeConfigFile::ResolveRelativeToConfig(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline std::string PathStringWithoutTrailingSeparators(std::filesystem::path path) {
|
|
||||||
std::string text = path.string();
|
|
||||||
while (!text.empty()) {
|
|
||||||
const char tail = text.back();
|
|
||||||
if (tail != '\\' && tail != '/') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
text.pop_back();
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::filesystem::path ManagedNandRootPath() {
|
inline std::filesystem::path ManagedNandRootPath() {
|
||||||
return RuntimeConfigFile::ApplicationDataDirectory() / "NAND";
|
return RuntimeConfigFile::ApplicationDataDirectory() / "NAND";
|
||||||
}
|
}
|
||||||
@@ -134,7 +123,9 @@ inline bool SeedMissingBootstrapFiles(const std::filesystem::path& root) {
|
|||||||
const std::filesystem::path relativePath{std::string(file)};
|
const std::filesystem::path relativePath{std::string(file)};
|
||||||
ec.clear();
|
ec.clear();
|
||||||
if (!CopyBootstrapFile(*payload, root, relativePath, ec)) {
|
if (!CopyBootstrapFile(*payload, root, relativePath, ec)) {
|
||||||
RT_LOG(RT_TAG_NAND) << "could not create " << (root / relativePath).string() << std::endl;
|
RT_LOG(RT_TAG_NAND) << "could not create "
|
||||||
|
<< RuntimeConfigFile::PathToUtf8(root / relativePath)
|
||||||
|
<< std::endl;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +158,8 @@ inline std::filesystem::path CreateManagedNandRoot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RT_LOG(RT_TAG_NAND) << "using managed NAND root: " << root.string() << std::endl;
|
RT_LOG(RT_TAG_NAND) << "using managed NAND root: " << RuntimeConfigFile::PathToUtf8(root)
|
||||||
|
<< std::endl;
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,8 +179,4 @@ inline std::filesystem::path DiscoverNandRootPath() {
|
|||||||
return CreateManagedNandRoot();
|
return CreateManagedNandRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
inline std::string DiscoverNandRootString() {
|
|
||||||
return PathStringWithoutTrailingSeparators(DiscoverNandRootPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace RuntimeNandPath
|
} // namespace RuntimeNandPath
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -67,8 +68,10 @@ void RunMemoryInitializers();
|
|||||||
void RegisterPostRelInitializer(InitializerFn fn);
|
void RegisterPostRelInitializer(InitializerFn fn);
|
||||||
void RunPostRelInitializers();
|
void RunPostRelInitializers();
|
||||||
|
|
||||||
|
// The generated call site passes a UTF-8 literal; it is decoded once here and
|
||||||
|
// stays a path from then on.
|
||||||
void RegisterDvdOverlayRoot(std::string root);
|
void RegisterDvdOverlayRoot(std::string root);
|
||||||
const std::vector<std::string>& DvdOverlayRoots();
|
const std::vector<std::filesystem::path>& DvdOverlayRoots();
|
||||||
|
|
||||||
// Riivolution settings pinned by the distribution's recomp.yml. The XML path is
|
// Riivolution settings pinned by the distribution's recomp.yml. The XML path is
|
||||||
// relative to the pack/overlay root; option selections use Riivolution's 1-based
|
// relative to the pack/overlay root; option selections use Riivolution's 1-based
|
||||||
|
|||||||
@@ -60,10 +60,25 @@ struct RuntimeUserConfig {
|
|||||||
// comma-separated SDL-style physical button names ("south", or
|
// comma-separated SDL-style physical button names ("south", or
|
||||||
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
|
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
|
||||||
std::array<std::optional<std::string>, 12> controllerButtons;
|
std::array<std::optional<std::string>, 12> controllerButtons;
|
||||||
|
// One-based physical WUP-028 adapter port assigned to each game port.
|
||||||
|
// Zero or a missing value means the adapter does not own that game port.
|
||||||
|
std::array<uint32_t, 4> gameCubeAdapterPorts{};
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace RuntimeConfigFile {
|
namespace RuntimeConfigFile {
|
||||||
|
|
||||||
|
// Narrow path strings are UTF-8 everywhere in the runtime; string() and the
|
||||||
|
// char path constructor would use the ANSI codepage on Windows, which drops
|
||||||
|
// characters the codepage cannot represent.
|
||||||
|
inline std::string PathToUtf8(const std::filesystem::path& path) {
|
||||||
|
const std::u8string text = path.u8string();
|
||||||
|
return std::string(text.begin(), text.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::filesystem::path PathFromUtf8(std::string_view text) {
|
||||||
|
return std::filesystem::path(std::u8string(text.begin(), text.end()));
|
||||||
|
}
|
||||||
|
|
||||||
inline constexpr const char* kConfigFileName = "Config.toml";
|
inline constexpr const char* kConfigFileName = "Config.toml";
|
||||||
inline constexpr const char* kApplicationDirectoryName = "WiiCompiled";
|
inline constexpr const char* kApplicationDirectoryName = "WiiCompiled";
|
||||||
|
|
||||||
@@ -323,6 +338,12 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
|||||||
config.controllerButtons[index] =
|
config.controllerButtons[index] =
|
||||||
FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
|
FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
|
||||||
}
|
}
|
||||||
|
for (size_t index = 0; index < config.gameCubeAdapterPorts.size(); ++index) {
|
||||||
|
const std::string key = "adapter_port_" + std::to_string(index + 1);
|
||||||
|
if (auto value = FindConfigUint(document, "controller", key); value && *value <= 4) {
|
||||||
|
config.gameCubeAdapterPorts[index] = *value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
|
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
|
||||||
config.windowPosX = FindConfigInt(document, "video", "window_x");
|
config.windowPosX = FindConfigInt(document, "video", "window_x");
|
||||||
@@ -410,7 +431,7 @@ inline RuntimeUserConfig ParseConfig(std::istream& input, std::string sourceName
|
|||||||
inline RuntimeUserConfig LoadConfigFile() {
|
inline RuntimeUserConfig LoadConfigFile() {
|
||||||
EnsureConfigFile();
|
EnsureConfigFile();
|
||||||
std::ifstream file(ResolveConfigPath(), std::ios::binary);
|
std::ifstream file(ResolveConfigPath(), std::ios::binary);
|
||||||
return file ? ParseConfig(file, ResolveConfigPath().string()) : RuntimeUserConfig{};
|
return file ? ParseConfig(file, PathToUtf8(ResolveConfigPath())) : RuntimeUserConfig{};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline const RuntimeUserConfig& Get() {
|
inline const RuntimeUserConfig& Get() {
|
||||||
@@ -501,7 +522,7 @@ inline bool WriteSetting(std::string_view section, std::string_view key, std::st
|
|||||||
}
|
}
|
||||||
std::ofstream output(path, std::ios::trunc);
|
std::ofstream output(path, std::ios::trunc);
|
||||||
if (!output) {
|
if (!output) {
|
||||||
std::cerr << "[runtime-config] Unable to write " << path.string() << std::endl;
|
std::cerr << "[runtime-config] Unable to write " << PathToUtf8(path) << std::endl;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (const auto& outputLine : lines) {
|
for (const auto& outputLine : lines) {
|
||||||
@@ -586,6 +607,19 @@ inline bool SetControllerButton(size_t index, std::string value) {
|
|||||||
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
|
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline int GameCubeAdapterPort(size_t gamePort) {
|
||||||
|
if (gamePort >= Get().gameCubeAdapterPorts.size()) return -1;
|
||||||
|
const uint32_t physicalPort = Get().gameCubeAdapterPorts[gamePort];
|
||||||
|
return physicalPort >= 1 && physicalPort <= 4 ? static_cast<int>(physicalPort - 1) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool SetGameCubeAdapterPort(size_t gamePort, int physicalPort) {
|
||||||
|
if (gamePort >= Mutable().gameCubeAdapterPorts.size() || physicalPort < -1 || physicalPort >= 4) return false;
|
||||||
|
const uint32_t storedPort = physicalPort < 0 ? 0u : static_cast<uint32_t>(physicalPort + 1);
|
||||||
|
Mutable().gameCubeAdapterPorts[gamePort] = storedPort;
|
||||||
|
return WriteSetting("controller", "adapter_port_" + std::to_string(gamePort + 1), std::to_string(storedPort));
|
||||||
|
}
|
||||||
|
|
||||||
inline bool SetAudioVolume(float value) {
|
inline bool SetAudioVolume(float value) {
|
||||||
value = std::clamp(value, 0.0f, 1.0f);
|
value = std::clamp(value, 0.0f, 1.0f);
|
||||||
Mutable().audioVolume = value;
|
Mutable().audioVolume = value;
|
||||||
@@ -754,7 +788,7 @@ inline std::string DvdRoot(std::string fallback = "") {
|
|||||||
// never to the process working directory (docs/WHEELWIZARD_CONTRACT.md).
|
// never to the process working directory (docs/WHEELWIZARD_CONTRACT.md).
|
||||||
inline std::filesystem::path ResolveRelativeTo(const std::filesystem::path& base,
|
inline std::filesystem::path ResolveRelativeTo(const std::filesystem::path& base,
|
||||||
const std::string& value) {
|
const std::string& value) {
|
||||||
std::filesystem::path path(value);
|
std::filesystem::path path = PathFromUtf8(value);
|
||||||
if (path.is_relative()) {
|
if (path.is_relative()) {
|
||||||
path = base / path;
|
path = base / path;
|
||||||
}
|
}
|
||||||
@@ -784,7 +818,7 @@ inline void LogLoadedConfig() {
|
|||||||
static const bool logged = [] {
|
static const bool logged = [] {
|
||||||
const auto& config = Get();
|
const auto& config = Get();
|
||||||
const auto configPath = ResolveConfigPath();
|
const auto configPath = ResolveConfigPath();
|
||||||
std::cout << "[runtime-config] " << configPath.string();
|
std::cout << "[runtime-config] " << PathToUtf8(configPath);
|
||||||
if (!std::filesystem::exists(configPath)) {
|
if (!std::filesystem::exists(configPath)) {
|
||||||
std::cout << " not found; using built-in defaults";
|
std::cout << " not found; using built-in defaults";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <ostream>
|
#include <ostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -72,5 +73,5 @@ public:
|
|||||||
// `mem1Path` and MEM2 to `mem1Path + ".mem2"`, logging outcomes to `os`.
|
// `mem1Path` and MEM2 to `mem1Path + ".mem2"`, logging outcomes to `os`.
|
||||||
static void DumpCrashHeuristics(std::ostream& os, const struct CpuContext* cpu,
|
static void DumpCrashHeuristics(std::ostream& os, const struct CpuContext* cpu,
|
||||||
const uint32_t* missingGuestTarget);
|
const uint32_t* missingGuestTarget);
|
||||||
static void WriteGuestMemorySnapshot(std::ostream& os, const char* mem1Path);
|
static void WriteGuestMemorySnapshot(std::ostream& os, const std::filesystem::path& mem1Path);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -695,12 +695,14 @@ private:
|
|||||||
const auto path = FindDspCoefficientRom();
|
const auto path = FindDspCoefficientRom();
|
||||||
std::ifstream stream(path, std::ios::binary | std::ios::ate);
|
std::ifstream stream(path, std::ios::binary | std::ios::ate);
|
||||||
if (!stream || stream.tellg() != static_cast<std::streamoff>(m_coeffs.size() * 2)) {
|
if (!stream || stream.tellg() != static_cast<std::streamoff>(m_coeffs.size() * 2)) {
|
||||||
throw std::runtime_error("Bundled Wii DSP coefficient ROM has an invalid size: " + path.string());
|
throw std::runtime_error("Bundled Wii DSP coefficient ROM has an invalid size: " +
|
||||||
|
RuntimeConfigFile::PathToUtf8(path));
|
||||||
}
|
}
|
||||||
stream.seekg(0);
|
stream.seekg(0);
|
||||||
std::array<uint8_t, kResamplingCoefficientCount * 2> bytes{};
|
std::array<uint8_t, kResamplingCoefficientCount * 2> bytes{};
|
||||||
if (!stream.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
|
if (!stream.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
|
||||||
throw std::runtime_error("Failed to read bundled Wii DSP coefficient ROM: " + path.string());
|
throw std::runtime_error("Failed to read bundled Wii DSP coefficient ROM: " +
|
||||||
|
RuntimeConfigFile::PathToUtf8(path));
|
||||||
}
|
}
|
||||||
for (size_t i = 0; i < m_coeffs.size(); ++i) {
|
for (size_t i = 0; i < m_coeffs.size(); ++i) {
|
||||||
const uint16_t word = static_cast<uint16_t>(bytes[i * 2]) << 8 |
|
const uint16_t word = static_cast<uint16_t>(bytes[i * 2]) << 8 |
|
||||||
|
|||||||
@@ -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));
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "hle_stubs.h"
|
#include "hle_stubs.h"
|
||||||
#include "memory.h"
|
#include "memory.h"
|
||||||
#include "hle/controller_status_contract.h"
|
#include "hle/controller_status_contract.h"
|
||||||
|
#include "wup028_adapter.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -33,6 +34,7 @@ void WritePadStatus(uint32_t base, const PADStatus& status) {
|
|||||||
|
|
||||||
extern "C" uint32_t PAD__Init_HLE()
|
extern "C" uint32_t PAD__Init_HLE()
|
||||||
{
|
{
|
||||||
|
Wup028Adapter::Initialize();
|
||||||
return PADInit() ? 1u : 0u;
|
return PADInit() ? 1u : 0u;
|
||||||
}
|
}
|
||||||
PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ());
|
PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ());
|
||||||
@@ -44,7 +46,16 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
|
|||||||
}
|
}
|
||||||
|
|
||||||
PADStatus statuses[PAD_CHANMAX]{};
|
PADStatus statuses[PAD_CHANMAX]{};
|
||||||
const uint32_t rumbleMask = PADRead(statuses);
|
std::array<PADStatus, PAD_CHANMAX> adapterStatuses{};
|
||||||
|
uint32_t rumbleMask = PADRead(statuses);
|
||||||
|
if (Wup028Adapter::Read(adapterStatuses) && !PADIsInputBlocked()) {
|
||||||
|
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
|
||||||
|
if (adapterStatuses[port].err == PAD_ERR_NONE) {
|
||||||
|
statuses[port] = adapterStatuses[port];
|
||||||
|
rumbleMask |= PAD_CHAN0_BIT >> port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
|
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
|
||||||
@@ -73,6 +84,8 @@ PPC_NATIVE_OVERRIDE(801AF1E4, PAD__Recalibrate_HLE, uint32_t, (uint32_t mask), (
|
|||||||
|
|
||||||
extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command)
|
extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command)
|
||||||
{
|
{
|
||||||
PADControlMotor(chan, command);
|
if (!Wup028Adapter::SetRumble(static_cast<uint32_t>(chan), command == PAD_MOTOR_RUMBLE)) {
|
||||||
|
PADControlMotor(chan, command);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command));
|
PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -103,17 +104,26 @@ static void HLE_LogOSReport(CpuContext* cpu, const char* fmt)
|
|||||||
[&state]() { return NextOsReportDouble(state); },
|
[&state]() { return NextOsReportDouble(state); },
|
||||||
[](uint32_t address) { return ReadGuestStringForReport(address); });
|
[](uint32_t address) { return ReadGuestStringForReport(address); });
|
||||||
|
|
||||||
|
// nw4r warnings arrive as "<file>:<line> Warning:" plus a bare newline, so
|
||||||
|
// consecutive identical messages never land back to back. Blank lines are
|
||||||
|
// transparent to the repeat tracker so the pair still collapses.
|
||||||
static thread_local std::string lastBuffer;
|
static thread_local std::string lastBuffer;
|
||||||
static thread_local size_t repeated = 0;
|
static thread_local size_t repeated = 0;
|
||||||
if (buffer == lastBuffer) {
|
const bool blank = buffer.find_first_not_of(" \t\r\n") == std::string::npos;
|
||||||
|
if (blank) {
|
||||||
|
if (repeated != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (buffer == lastBuffer) {
|
||||||
++repeated;
|
++repeated;
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
if (repeated != 0) {
|
||||||
|
std::cout << "[OSReport] previous message repeated " << repeated << " time(s)" << std::endl;
|
||||||
|
repeated = 0;
|
||||||
|
}
|
||||||
|
lastBuffer = buffer;
|
||||||
}
|
}
|
||||||
if (repeated != 0) {
|
|
||||||
std::cout << "[OSReport] previous message repeated " << repeated << " time(s)" << std::endl;
|
|
||||||
repeated = 0;
|
|
||||||
}
|
|
||||||
lastBuffer = buffer;
|
|
||||||
|
|
||||||
std::cout << "[OSReport] " << buffer;
|
std::cout << "[OSReport] " << buffer;
|
||||||
|
|
||||||
@@ -128,9 +138,23 @@ static void HLE_LogOSReport(CpuContext* cpu, const char* fmt)
|
|||||||
// the guest caller because OS__Report is an HLE boundary. The context is
|
// the guest caller because OS__Report is an HLE boundary. The context is
|
||||||
// synchronized at this boundary, so capture the guest backchain at the
|
// synchronized at this boundary, so capture the guest backchain at the
|
||||||
// first warning/panic instead of attributing the later PPCHalt unwind.
|
// first warning/panic instead of attributing the later PPCHalt unwind.
|
||||||
|
//
|
||||||
|
// The dump is expensive and stdio is an unbuffered pipe, so a guest that
|
||||||
|
// warns every frame would stall the game thread on backpressure. One dump
|
||||||
|
// per distinct site, with an overall cap.
|
||||||
if (buffer.find(" Warning:") != std::string::npos ||
|
if (buffer.find(" Warning:") != std::string::npos ||
|
||||||
buffer.find(" Panic:") != std::string::npos) {
|
buffer.find(" Panic:") != std::string::npos) {
|
||||||
SystemBridge::DumpCpuState(cpu);
|
constexpr size_t kMaxWarningDumps = 8;
|
||||||
|
static thread_local std::set<std::string> dumpedSites;
|
||||||
|
static thread_local size_t dumpsEmitted = 0;
|
||||||
|
if (dumpsEmitted < kMaxWarningDumps && dumpedSites.insert(buffer).second) {
|
||||||
|
++dumpsEmitted;
|
||||||
|
SystemBridge::DumpCpuState(cpu);
|
||||||
|
if (dumpsEmitted == kMaxWarningDumps) {
|
||||||
|
std::cerr << "[runtime] guest warning context dumps capped at " << kMaxWarningDumps
|
||||||
|
<< "; further warnings log the message only." << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ namespace fs = std::filesystem;
|
|||||||
// The extracted ISO "DATA" folder. We map its "files" subfolder to the DVD
|
// The extracted ISO "DATA" folder. We map its "files" subfolder to the DVD
|
||||||
// root "/" and its "sys" subfolder to "/sys/". The root is user-owned input:
|
// root "/" and its "sys" subfolder to "/sys/". The root is user-owned input:
|
||||||
// it is never embedded into or copied by the public runtime.
|
// it is never embedded into or copied by the public runtime.
|
||||||
static std::string g_dvdRoot;
|
static fs::path g_dvdRoot;
|
||||||
static std::once_flag g_dvdRootOnce;
|
static std::once_flag g_dvdRootOnce;
|
||||||
|
|
||||||
static uint32_t CurrentDiscGameCode() {
|
static uint32_t CurrentDiscGameCode() {
|
||||||
@@ -66,7 +66,7 @@ static uint32_t CurrentDiscGameCode() {
|
|||||||
#define DVD_FILEINFO_OFFSET_LEN 0x34
|
#define DVD_FILEINFO_OFFSET_LEN 0x34
|
||||||
|
|
||||||
struct DVDFileEntry {
|
struct DVDFileEntry {
|
||||||
std::string hostPath; // Full Windows path
|
fs::path hostPath;
|
||||||
std::string dvdPath; // Virtual Wii path (e.g., "/Race/Course.szs")
|
std::string dvdPath; // Virtual Wii path (e.g., "/Race/Course.szs")
|
||||||
uint32_t size;
|
uint32_t size;
|
||||||
uint32_t discOffsetWords = 0;
|
uint32_t discOffsetWords = 0;
|
||||||
@@ -78,7 +78,7 @@ struct FstFileEntry {
|
|||||||
uint32_t end;
|
uint32_t end;
|
||||||
uint32_t size;
|
uint32_t size;
|
||||||
std::string dvdPath;
|
std::string dvdPath;
|
||||||
std::string hostPath;
|
fs::path hostPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Global State
|
// Global State
|
||||||
@@ -111,11 +111,9 @@ static void CopyToGuestAsDma(uint32_t dest, const uint8_t* data, size_t size) {
|
|||||||
GxNotifyGuestRamDmaWrite(dest, static_cast<uint32_t>(size));
|
GxNotifyGuestRamDmaWrite(dest, static_cast<uint32_t>(size));
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string NormalizeDvdHostPath(std::string path) {
|
// Host path strings only ever leave this module as UTF-8 display text.
|
||||||
while (!path.empty() && (path.back() == '\\' || path.back() == '/')) {
|
static std::string HostPathText(const fs::path& path) {
|
||||||
path.pop_back();
|
return RuntimeConfigFile::PathToUtf8(path);
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsDvdDataRoot(const fs::path& path) {
|
static bool IsDvdDataRoot(const fs::path& path) {
|
||||||
@@ -140,7 +138,7 @@ static bool IsDvdDataRoot(const fs::path& path) {
|
|||||||
[[noreturn]] static void FailDvdRoot(const char* source, const fs::path& path = {}) {
|
[[noreturn]] static void FailDvdRoot(const char* source, const fs::path& path = {}) {
|
||||||
RT_LOGF(RT_TAG_DVD, "ERROR: %s", source);
|
RT_LOGF(RT_TAG_DVD, "ERROR: %s", source);
|
||||||
if (!path.empty()) {
|
if (!path.empty()) {
|
||||||
std::fprintf(stderr, ": %s", path.string().c_str());
|
std::fprintf(stderr, ": %s", HostPathText(path).c_str());
|
||||||
}
|
}
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"\n[dvd] Set [paths] dvd_root in Config.toml "
|
"\n[dvd] Set [paths] dvd_root in Config.toml "
|
||||||
@@ -148,14 +146,14 @@ static bool IsDvdDataRoot(const fs::path& path) {
|
|||||||
std::string details = source ? source : "The configured DVD root could not be opened.";
|
std::string details = source ? source : "The configured DVD root could not be opened.";
|
||||||
if (!path.empty()) {
|
if (!path.empty()) {
|
||||||
details += "\n\nPath: ";
|
details += "\n\nPath: ";
|
||||||
details += path.string();
|
details += HostPathText(path);
|
||||||
}
|
}
|
||||||
details += "\n\nSet [paths] dvd_root in Config.toml to the extracted "
|
details += "\n\nSet [paths] dvd_root in Config.toml to the extracted "
|
||||||
"Mario Kart Wii DATA directory.";
|
"Mario Kart Wii DATA directory.";
|
||||||
FailDvd("dvd_root", "DVD data is unavailable", details);
|
FailDvd("dvd_root", "DVD data is unavailable", details);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const std::string& GetDvdRoot() {
|
static const fs::path& GetDvdRoot() {
|
||||||
std::call_once(g_dvdRootOnce, []() {
|
std::call_once(g_dvdRootOnce, []() {
|
||||||
const fs::path path = RuntimeConfigFile::ResolvedDvdRoot();
|
const fs::path path = RuntimeConfigFile::ResolvedDvdRoot();
|
||||||
if (path.empty()) {
|
if (path.empty()) {
|
||||||
@@ -164,14 +162,14 @@ static const std::string& GetDvdRoot() {
|
|||||||
if (!IsDvdDataRoot(path)) {
|
if (!IsDvdDataRoot(path)) {
|
||||||
FailDvdRoot("Configured DVD root is not an extracted DATA directory", path);
|
FailDvdRoot("Configured DVD root is not an extracted DATA directory", path);
|
||||||
}
|
}
|
||||||
g_dvdRoot = NormalizeDvdHostPath(path.string());
|
g_dvdRoot = path;
|
||||||
});
|
});
|
||||||
|
|
||||||
return g_dvdRoot;
|
return g_dvdRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string NormalizePath(const std::string& path);
|
static std::string NormalizePath(const std::string& path);
|
||||||
static std::string ResolveDvdMappedHostPath(const std::string& dvdPath, const std::string& fallbackHostPath);
|
static fs::path ResolveDvdMappedHostPath(const std::string& dvdPath, const fs::path& fallbackHostPath);
|
||||||
|
|
||||||
static void InvokeDvdCallback(uint32_t callbackPtr, int32_t result, uint32_t fileInfoPtr) {
|
static void InvokeDvdCallback(uint32_t callbackPtr, int32_t result, uint32_t fileInfoPtr) {
|
||||||
if (callbackPtr == 0) {
|
if (callbackPtr == 0) {
|
||||||
@@ -240,7 +238,7 @@ static void LoadFstIndex() {
|
|||||||
}
|
}
|
||||||
g_fstLoaded = true;
|
g_fstLoaded = true;
|
||||||
|
|
||||||
fs::path fstPath = fs::path(GetDvdRoot()) / "sys" / "fst.bin";
|
const fs::path fstPath = GetDvdRoot() / "sys" / "fst.bin";
|
||||||
std::ifstream fstFile(fstPath, std::ios::binary);
|
std::ifstream fstFile(fstPath, std::ios::binary);
|
||||||
if (!fstFile.is_open()) {
|
if (!fstFile.is_open()) {
|
||||||
return;
|
return;
|
||||||
@@ -333,7 +331,7 @@ static void LoadFstIndex() {
|
|||||||
entry.end = endBytes;
|
entry.end = endBytes;
|
||||||
entry.size = fileSize;
|
entry.size = fileSize;
|
||||||
entry.dvdPath = "/" + relPath;
|
entry.dvdPath = "/" + relPath;
|
||||||
const std::string baseHostPath = (fs::path(GetDvdRoot()) / "files" / fs::path(relPath)).string();
|
const fs::path baseHostPath = GetDvdRoot() / "files" / fs::path(relPath);
|
||||||
entry.hostPath = ResolveDvdMappedHostPath(entry.dvdPath, baseHostPath);
|
entry.hostPath = ResolveDvdMappedHostPath(entry.dvdPath, baseHostPath);
|
||||||
g_fstFiles.push_back(std::move(entry));
|
g_fstFiles.push_back(std::move(entry));
|
||||||
}
|
}
|
||||||
@@ -408,7 +406,7 @@ static void RegisterFileEntry(std::string dvdPath, const fs::path& hostPath, uin
|
|||||||
dvdPath = DvdFstContract::CanonicalizePath(dvdPath);
|
dvdPath = DvdFstContract::CanonicalizePath(dvdPath);
|
||||||
|
|
||||||
DVDFileEntry fileEntry;
|
DVDFileEntry fileEntry;
|
||||||
fileEntry.hostPath = hostPath.string();
|
fileEntry.hostPath = hostPath;
|
||||||
fileEntry.dvdPath = dvdPath;
|
fileEntry.dvdPath = dvdPath;
|
||||||
fileEntry.size = size;
|
fileEntry.size = size;
|
||||||
|
|
||||||
@@ -445,7 +443,7 @@ static void WalkDirectory(const fs::path& root, bool recursive, bool announceErr
|
|||||||
fs::recursive_directory_iterator it(root, fs::directory_options::skip_permission_denied, ec);
|
fs::recursive_directory_iterator it(root, fs::directory_options::skip_permission_denied, ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
if (announceErrors) {
|
if (announceErrors) {
|
||||||
RT_LOG(RT_TAG_DVD) << "WARNING: cannot enumerate " << root.string() << ": "
|
RT_LOG(RT_TAG_DVD) << "WARNING: cannot enumerate " << HostPathText(root) << ": "
|
||||||
<< ec.message() << std::endl;
|
<< ec.message() << std::endl;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -458,7 +456,7 @@ static void WalkDirectory(const fs::path& root, bool recursive, bool announceErr
|
|||||||
it.increment(ec);
|
it.increment(ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
if (announceErrors) {
|
if (announceErrors) {
|
||||||
RT_LOG(RT_TAG_DVD) << "WARNING: stopped enumerating " << root.string() << ": "
|
RT_LOG(RT_TAG_DVD) << "WARNING: stopped enumerating " << HostPathText(root) << ": "
|
||||||
<< ec.message() << std::endl;
|
<< ec.message() << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -488,7 +486,7 @@ static void ScanDirectory(const fs::path& root, const std::string& virtualPrefix
|
|||||||
|
|
||||||
const std::uintmax_t size = fs::file_size(entry.path(), entryEc);
|
const std::uintmax_t size = fs::file_size(entry.path(), entryEc);
|
||||||
if (entryEc) {
|
if (entryEc) {
|
||||||
RT_LOG(RT_TAG_DVD) << "WARNING: skipping " << entry.path().string() << ": "
|
RT_LOG(RT_TAG_DVD) << "WARNING: skipping " << HostPathText(entry.path()) << ": "
|
||||||
<< entryEc.message() << std::endl;
|
<< entryEc.message() << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -502,7 +500,7 @@ static void ScanDirectory(const fs::path& root, const std::string& virtualPrefix
|
|||||||
if (prefix.back() != '/' && prefix.back() != '\\') {
|
if (prefix.back() != '/' && prefix.back() != '\\') {
|
||||||
prefix += "/";
|
prefix += "/";
|
||||||
}
|
}
|
||||||
std::string dvdPath = prefix + relative.string();
|
std::string dvdPath = prefix + HostPathText(relative);
|
||||||
if (!addNewFiles && !DvdEntryExists(dvdPath)) {
|
if (!addNewFiles && !DvdEntryExists(dvdPath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -536,7 +534,7 @@ static void ApplyFolderByNameMapping(const RuntimeRiivolution::Mapping& mapping)
|
|||||||
if (!entry.is_regular_file(entryEc) || entryEc) {
|
if (!entry.is_regular_file(entryEc) || entryEc) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::string name = entry.path().filename().string();
|
std::string name = HostPathText(entry.path().filename());
|
||||||
RuntimeHle::LowerInPlace(name);
|
RuntimeHle::LowerInPlace(name);
|
||||||
const auto matches = discPathsByName.find(name);
|
const auto matches = discPathsByName.find(name);
|
||||||
if (matches == discPathsByName.end()) {
|
if (matches == discPathsByName.end()) {
|
||||||
@@ -554,7 +552,7 @@ static void ApplyFolderByNameMapping(const RuntimeRiivolution::Mapping& mapping)
|
|||||||
|
|
||||||
WalkDirectory(mapping.hostPath, mapping.recursive, /*announceErrors=*/false, applyEntry);
|
WalkDirectory(mapping.hostPath, mapping.recursive, /*announceErrors=*/false, applyEntry);
|
||||||
|
|
||||||
RT_LOG(RT_TAG_DVD) << mapping.hostPath.string() << ": replaced " << replaced
|
RT_LOG(RT_TAG_DVD) << HostPathText(mapping.hostPath) << ": replaced " << replaced
|
||||||
<< " disc file(s) by filename" << std::endl;
|
<< " disc file(s) by filename" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,7 +560,7 @@ static void ScanOverlayRoot(const RuntimeRiivolution::Overlay& overlay) {
|
|||||||
if (!overlay.patches) {
|
if (!overlay.patches) {
|
||||||
// Fallback for mod roots that mirror the disc filesystem directly (not a
|
// Fallback for mod roots that mirror the disc filesystem directly (not a
|
||||||
// Riivolution pack, which wouldn't map anything useful this way).
|
// Riivolution pack, which wouldn't map anything useful this way).
|
||||||
RT_LOG(RT_TAG_DVD) << overlay.root.string()
|
RT_LOG(RT_TAG_DVD) << HostPathText(overlay.root)
|
||||||
<< ": no Riivolution XML found, treating the root as a disc-shaped overlay"
|
<< ": no Riivolution XML found, treating the root as a disc-shaped overlay"
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
ScanDirectory(overlay.root, "/");
|
ScanDirectory(overlay.root, "/");
|
||||||
@@ -592,7 +590,7 @@ static void ScanOverlayRoot(const RuntimeRiivolution::Overlay& overlay) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string ResolveDvdMappedHostPath(const std::string& dvdPath, const std::string& fallbackHostPath) {
|
static fs::path ResolveDvdMappedHostPath(const std::string& dvdPath, const fs::path& fallbackHostPath) {
|
||||||
const std::string normalized = NormalizePath(dvdPath);
|
const std::string normalized = NormalizePath(dvdPath);
|
||||||
const auto it = g_pathToEntry.find(normalized);
|
const auto it = g_pathToEntry.find(normalized);
|
||||||
if (it != g_pathToEntry.end() && it->second >= 0 &&
|
if (it != g_pathToEntry.end() && it->second >= 0 &&
|
||||||
@@ -604,7 +602,7 @@ static std::string ResolveDvdMappedHostPath(const std::string& dvdPath, const st
|
|||||||
const fs::path candidate = overlay.root / fs::path(normalized.substr(1));
|
const fs::path candidate = overlay.root / fs::path(normalized.substr(1));
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (fs::is_regular_file(candidate, ec)) {
|
if (fs::is_regular_file(candidate, ec)) {
|
||||||
return candidate.string();
|
return candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,7 +738,7 @@ extern "C" const char* DVDResolveHostPathForTest(const char* dvdPath)
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolved = g_fileEntries[it->second].hostPath;
|
resolved = HostPathText(g_fileEntries[it->second].hostPath);
|
||||||
return resolved.c_str();
|
return resolved.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,7 +806,7 @@ extern "C" void DVDInit_8015EA1C()
|
|||||||
Memory::Write16(diskHeader + 0x04, 0x3031); // '01' (Maker)
|
Memory::Write16(diskHeader + 0x04, 0x3031); // '01' (Maker)
|
||||||
Memory::Write8(diskHeader + 0x06, 0x01); // Disk #1
|
Memory::Write8(diskHeader + 0x06, 0x01); // Disk #1
|
||||||
// 4. Scan Files
|
// 4. Scan Files
|
||||||
fs::path rootPath(GetDvdRoot());
|
const fs::path& rootPath = GetDvdRoot();
|
||||||
|
|
||||||
// Map "<dvd_root>/files" -> "/"
|
// Map "<dvd_root>/files" -> "/"
|
||||||
ScanDirectory(rootPath / "files", "/");
|
ScanDirectory(rootPath / "files", "/");
|
||||||
@@ -879,7 +877,7 @@ extern "C" int32_t DVDReadPrio_8015E834(uint32_t fileInfoPtr, uint32_t bufferPtr
|
|||||||
const DVDFileEntry& entry = g_fileEntries[extent->entryIndex];
|
const DVDFileEntry& entry = g_fileEntries[extent->entryIndex];
|
||||||
|
|
||||||
if (offset < 0 || length < 0) {
|
if (offset < 0 || length < 0) {
|
||||||
return DvdReadFatal(fileInfoPtr, entry.hostPath, offset,
|
return DvdReadFatal(fileInfoPtr, HostPathText(entry.hostPath), offset,
|
||||||
length > 0 ? static_cast<uint32_t>(length) : 0,
|
length > 0 ? static_cast<uint32_t>(length) : 0,
|
||||||
"negative DVD read offset or length");
|
"negative DVD read offset or length");
|
||||||
}
|
}
|
||||||
@@ -889,7 +887,7 @@ extern "C" int32_t DVDReadPrio_8015E834(uint32_t fileInfoPtr, uint32_t bufferPtr
|
|||||||
uint32_t uLength = (uint32_t)length;
|
uint32_t uLength = (uint32_t)length;
|
||||||
|
|
||||||
if (requestedOffset >= entry.size) {
|
if (requestedOffset >= entry.size) {
|
||||||
return DvdReadFatal(fileInfoPtr, entry.hostPath, offset, uLength,
|
return DvdReadFatal(fileInfoPtr, HostPathText(entry.hostPath), offset, uLength,
|
||||||
"read offset is outside the indexed DVD file");
|
"read offset is outside the indexed DVD file");
|
||||||
}
|
}
|
||||||
const uint32_t uOffset = static_cast<uint32_t>(requestedOffset);
|
const uint32_t uOffset = static_cast<uint32_t>(requestedOffset);
|
||||||
@@ -899,14 +897,14 @@ extern "C" int32_t DVDReadPrio_8015E834(uint32_t fileInfoPtr, uint32_t bufferPtr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uLength != 0 && !Memory::Contains(bufferPtr, uLength)) {
|
if (uLength != 0 && !Memory::Contains(bufferPtr, uLength)) {
|
||||||
return DvdReadFatal(fileInfoPtr, entry.hostPath, offset, uLength,
|
return DvdReadFatal(fileInfoPtr, HostPathText(entry.hostPath), offset, uLength,
|
||||||
"DVD read destination is outside guest memory");
|
"DVD read destination is outside guest memory");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> tempBuf;
|
std::vector<uint8_t> tempBuf;
|
||||||
DvdReadContract::HostReadFailure failure;
|
DvdReadContract::HostReadFailure failure;
|
||||||
if (!DvdReadContract::ReadExact(entry.hostPath, uOffset, uLength, tempBuf, failure)) {
|
if (!DvdReadContract::ReadExact(entry.hostPath, uOffset, uLength, tempBuf, failure)) {
|
||||||
return DvdReadFatal(fileInfoPtr, entry.hostPath, offset, uLength,
|
return DvdReadFatal(fileInfoPtr, HostPathText(entry.hostPath), offset, uLength,
|
||||||
DvdReadContract::Describe(failure));
|
DvdReadContract::Describe(failure));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -962,11 +960,11 @@ extern "C" int32_t DVD__ReadAbsAsyncPrio_HLE_801628cc(uint32_t cmdBlockPtr,
|
|||||||
requestedLength,
|
requestedLength,
|
||||||
"absolute DVD read offset is not mapped to a host file");
|
"absolute DVD read offset is not mapped to a host file");
|
||||||
} else if (requestedLength != 0 && readInfo.readLength != requestedLength) {
|
} else if (requestedLength != 0 && readInfo.readLength != requestedLength) {
|
||||||
bytesRead = DvdReadFatal(cmdBlockPtr, readInfo.entry->hostPath,
|
bytesRead = DvdReadFatal(cmdBlockPtr, HostPathText(readInfo.entry->hostPath),
|
||||||
readInfo.fileOffset, requestedLength,
|
readInfo.fileOffset, requestedLength,
|
||||||
"requested range extends beyond the indexed DVD file");
|
"requested range extends beyond the indexed DVD file");
|
||||||
} else if (requestedLength != 0 && !Memory::Contains(bufferPtr, requestedLength)) {
|
} else if (requestedLength != 0 && !Memory::Contains(bufferPtr, requestedLength)) {
|
||||||
bytesRead = DvdReadFatal(cmdBlockPtr, readInfo.entry->hostPath,
|
bytesRead = DvdReadFatal(cmdBlockPtr, HostPathText(readInfo.entry->hostPath),
|
||||||
readInfo.fileOffset, requestedLength,
|
readInfo.fileOffset, requestedLength,
|
||||||
"DVD read destination is outside guest memory");
|
"DVD read destination is outside guest memory");
|
||||||
} else {
|
} else {
|
||||||
@@ -977,7 +975,7 @@ extern "C" int32_t DVD__ReadAbsAsyncPrio_HLE_801628cc(uint32_t cmdBlockPtr,
|
|||||||
readInfo.readLength,
|
readInfo.readLength,
|
||||||
tempBuf,
|
tempBuf,
|
||||||
failure)) {
|
failure)) {
|
||||||
bytesRead = DvdReadFatal(cmdBlockPtr, readInfo.entry->hostPath,
|
bytesRead = DvdReadFatal(cmdBlockPtr, HostPathText(readInfo.entry->hostPath),
|
||||||
readInfo.fileOffset, readInfo.readLength,
|
readInfo.fileOffset, readInfo.readLength,
|
||||||
DvdReadContract::Describe(failure));
|
DvdReadContract::Describe(failure));
|
||||||
} else {
|
} else {
|
||||||
@@ -1076,12 +1074,12 @@ extern "C" int32_t DVDLowRead_80166330(uint32_t buffer, uint32_t length, uint32_
|
|||||||
return finish(false);
|
return finish(false);
|
||||||
}
|
}
|
||||||
if (readInfo.readLength != length) {
|
if (readInfo.readLength != length) {
|
||||||
ReportDvdReadError(readInfo.entry->hostPath, readInfo.fileOffset, length,
|
ReportDvdReadError(HostPathText(readInfo.entry->hostPath), readInfo.fileOffset, length,
|
||||||
"requested range extends beyond the indexed DVD file");
|
"requested range extends beyond the indexed DVD file");
|
||||||
return finish(false);
|
return finish(false);
|
||||||
}
|
}
|
||||||
if (!Memory::Contains(buffer, length)) {
|
if (!Memory::Contains(buffer, length)) {
|
||||||
ReportDvdReadError(readInfo.entry->hostPath, readInfo.fileOffset, length,
|
ReportDvdReadError(HostPathText(readInfo.entry->hostPath), readInfo.fileOffset, length,
|
||||||
"DVD read destination is outside guest memory");
|
"DVD read destination is outside guest memory");
|
||||||
return finish(false);
|
return finish(false);
|
||||||
}
|
}
|
||||||
@@ -1093,7 +1091,7 @@ extern "C" int32_t DVDLowRead_80166330(uint32_t buffer, uint32_t length, uint32_
|
|||||||
readInfo.readLength,
|
readInfo.readLength,
|
||||||
tempBuf,
|
tempBuf,
|
||||||
failure)) {
|
failure)) {
|
||||||
ReportDvdReadError(readInfo.entry->hostPath, readInfo.fileOffset,
|
ReportDvdReadError(HostPathText(readInfo.entry->hostPath), readInfo.fileOffset,
|
||||||
readInfo.readLength, DvdReadContract::Describe(failure));
|
readInfo.readLength, DvdReadContract::Describe(failure));
|
||||||
return finish(false);
|
return finish(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
|
|||||||
return NAND_RESULT_INVALID;
|
return NAND_RESULT_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string hostPath = TranslateNandPath(path);
|
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||||
|
|
||||||
// Existing-file write opens go through a shadow copy seeded from the original, so a
|
// Existing-file write opens go through a shadow copy seeded from the original, so a
|
||||||
// crash between NANDWrite and NANDClose cannot leave a torn file (the game patches
|
// crash between NANDWrite and NANDClose cannot leave a torn file (the game patches
|
||||||
@@ -101,23 +101,23 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
|
|||||||
// Another live handle already refers to this file. A shadow would hide the
|
// Another live handle already refers to this file. A shadow would hide the
|
||||||
// writes from that handle, so stay in place for this open.
|
// writes from that handle, so stay in place for this open.
|
||||||
LogNandWarning("NANDOpen", "WARNING: '%s' already has a live handle, writing in place",
|
LogNandWarning("NANDOpen", "WARNING: '%s' already has a live handle, writing in place",
|
||||||
hostPath.c_str());
|
HostPathText(hostPath).c_str());
|
||||||
} else {
|
} else {
|
||||||
const std::string tempPath = SafeTempPathFor(hostPath);
|
const std::filesystem::path tempPath = SafeTempPathFor(hostPath);
|
||||||
if (DiscardStaleSafeTemp(tempPath)) {
|
if (DiscardStaleSafeTemp(tempPath)) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::copy_file(hostPath, tempPath,
|
std::filesystem::copy_file(hostPath, tempPath,
|
||||||
std::filesystem::copy_options::overwrite_existing, ec);
|
std::filesystem::copy_options::overwrite_existing, ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
LogNandWarning("NANDOpen", "WARNING: could not seed shadow '%s' (%s), writing in place",
|
LogNandWarning("NANDOpen", "WARNING: could not seed shadow '%s' (%s), writing in place",
|
||||||
tempPath.c_str(), ec.message().c_str());
|
HostPathText(tempPath).c_str(), ec.message().c_str());
|
||||||
std::remove(tempPath.c_str());
|
NandRemove(tempPath);
|
||||||
} else {
|
} else {
|
||||||
FILE* shadow = std::fopen(tempPath.c_str(), "r+b");
|
FILE* shadow = NandFopen(tempPath, "r+b");
|
||||||
if (!shadow) {
|
if (!shadow) {
|
||||||
LogNandWarning("NANDOpen", "WARNING: could not open shadow '%s', writing in place",
|
LogNandWarning("NANDOpen", "WARNING: could not open shadow '%s', writing in place",
|
||||||
tempPath.c_str());
|
HostPathText(tempPath).c_str());
|
||||||
std::remove(tempPath.c_str());
|
NandRemove(tempPath);
|
||||||
} else {
|
} else {
|
||||||
const int32_t shadowFd = AllocateFd(tempPath, shadow, static_cast<int32_t>(mode));
|
const int32_t shadowFd = AllocateFd(tempPath, shadow, static_cast<int32_t>(mode));
|
||||||
{
|
{
|
||||||
@@ -141,20 +141,20 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
|
|||||||
else if (mode == 2) fopenMode = "r+b";
|
else if (mode == 2) fopenMode = "r+b";
|
||||||
else if (mode == 3) fopenMode = "r+b";
|
else if (mode == 3) fopenMode = "r+b";
|
||||||
|
|
||||||
FILE* file = std::fopen(hostPath.c_str(), fopenMode);
|
FILE* file = NandFopen(hostPath, fopenMode);
|
||||||
if (!file && mode >= 2) {
|
if (!file && mode >= 2) {
|
||||||
// Try creating for write modes
|
// Try creating for write modes
|
||||||
file = std::fopen(hostPath.c_str(), "w+b");
|
file = NandFopen(hostPath, "w+b");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create parent directories and retry
|
// Create parent directories and retry
|
||||||
if (!file && CreateParentDirectories(hostPath)) {
|
if (!file && CreateParentDirectories(hostPath)) {
|
||||||
file = std::fopen(hostPath.c_str(), mode >= 2 ? "w+b" : "rb");
|
file = NandFopen(hostPath, mode >= 2 ? "w+b" : "rb");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
if (IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
|
if (IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
|
||||||
file = std::fopen(hostPath.c_str(), fopenMode);
|
file = NandFopen(hostPath, fopenMode);
|
||||||
}
|
}
|
||||||
if (!file) {
|
if (!file) {
|
||||||
LogNandError("NANDOpen", "FAILED to open");
|
LogNandError("NANDOpen", "FAILED to open");
|
||||||
@@ -282,7 +282,7 @@ extern "C" int32_t NANDCreate_HLE(uint32_t pathPtr, uint32_t perm, uint32_t attr
|
|||||||
return NAND_RESULT_INVALID;
|
return NAND_RESULT_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string hostPath = TranslateNandPath(path);
|
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||||
CreateParentDirectories(hostPath);
|
CreateParentDirectories(hostPath);
|
||||||
|
|
||||||
// Check if file already exists
|
// Check if file already exists
|
||||||
@@ -291,7 +291,7 @@ extern "C" int32_t NANDCreate_HLE(uint32_t pathPtr, uint32_t perm, uint32_t attr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create empty file
|
// Create empty file
|
||||||
FILE* f = std::fopen(hostPath.c_str(), "wb");
|
FILE* f = NandFopen(hostPath, "wb");
|
||||||
if (!f) {
|
if (!f) {
|
||||||
return NAND_RESULT_UNKNOWN;
|
return NAND_RESULT_UNKNOWN;
|
||||||
}
|
}
|
||||||
@@ -307,13 +307,13 @@ extern "C" int32_t NANDDelete_HLE(uint32_t pathPtr) {
|
|||||||
return NAND_RESULT_INVALID;
|
return NAND_RESULT_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string hostPath = TranslateNandPath(path);
|
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||||
|
|
||||||
if (!PathExists(hostPath)) {
|
if (!PathExists(hostPath)) {
|
||||||
return NAND_RESULT_NOEXISTS;
|
return NAND_RESULT_NOEXISTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std::remove(hostPath.c_str()) == 0) {
|
if (NandRemove(hostPath)) {
|
||||||
return NAND_RESULT_OK;
|
return NAND_RESULT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +327,7 @@ extern "C" int32_t NANDCreateDir_HLE(uint32_t pathPtr, uint32_t perm, uint32_t a
|
|||||||
return NAND_RESULT_INVALID;
|
return NAND_RESULT_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string hostPath = TranslateNandPath(path);
|
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||||
|
|
||||||
if (PathExists(hostPath)) {
|
if (PathExists(hostPath)) {
|
||||||
if (IsDirectory(hostPath)) {
|
if (IsDirectory(hostPath)) {
|
||||||
@@ -337,11 +337,6 @@ extern "C" int32_t NANDCreateDir_HLE(uint32_t pathPtr, uint32_t perm, uint32_t a
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (CreateDirectoryPath(hostPath)) {
|
if (CreateDirectoryPath(hostPath)) {
|
||||||
#ifdef _WIN32
|
|
||||||
_mkdir(hostPath.c_str());
|
|
||||||
#else
|
|
||||||
mkdir(hostPath.c_str(), 0755);
|
|
||||||
#endif
|
|
||||||
return NAND_RESULT_OK;
|
return NAND_RESULT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,13 +364,13 @@ extern "C" int32_t NANDMove_HLE(uint32_t srcPathPtr, uint32_t dstPathPtr) {
|
|||||||
// filename (for example /tmp/banner.bin -> <title home>/banner.bin).
|
// filename (for example /tmp/banner.bin -> <title home>/banner.bin).
|
||||||
const std::filesystem::path dstHost = dstDirectoryHost / srcName;
|
const std::filesystem::path dstHost = dstDirectoryHost / srcName;
|
||||||
|
|
||||||
if (!PathExists(srcHost.string())) {
|
if (!PathExists(srcHost)) {
|
||||||
return NAND_RESULT_NOEXISTS;
|
return NAND_RESULT_NOEXISTS;
|
||||||
}
|
}
|
||||||
if (!IsDirectory(dstDirectoryHost.string())) {
|
if (!IsDirectory(dstDirectoryHost)) {
|
||||||
return NAND_RESULT_NOEXISTS;
|
return NAND_RESULT_NOEXISTS;
|
||||||
}
|
}
|
||||||
if (PathExists(dstHost.string())) {
|
if (PathExists(dstHost)) {
|
||||||
return NAND_RESULT_EXISTS;
|
return NAND_RESULT_EXISTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,7 +391,7 @@ extern "C" int32_t NANDGetStatus_HLE(uint32_t pathPtr, uint32_t outStatusPtr) {
|
|||||||
return NAND_RESULT_INVALID;
|
return NAND_RESULT_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string hostPath = TranslateNandPath(path);
|
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||||
|
|
||||||
if (!PathExists(hostPath)) {
|
if (!PathExists(hostPath)) {
|
||||||
return NAND_RESULT_NOEXISTS;
|
return NAND_RESULT_NOEXISTS;
|
||||||
@@ -417,7 +412,7 @@ extern "C" int32_t NANDGetType_HLE(uint32_t pathPtr, uint32_t outTypePtr) {
|
|||||||
return NAND_RESULT_INVALID;
|
return NAND_RESULT_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string hostPath = TranslateNandPath(path);
|
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||||
|
|
||||||
if (!PathExists(hostPath)) {
|
if (!PathExists(hostPath)) {
|
||||||
return NAND_RESULT_NOEXISTS;
|
return NAND_RESULT_NOEXISTS;
|
||||||
|
|||||||
@@ -234,8 +234,10 @@ PPC_NATIVE_OVERRIDE(8019E7B4, NANDPrivateGetTypeAsync_HLE, int32_t,
|
|||||||
|
|
||||||
static const char kNandSafeTempSuffix[] = ".nandsafe.tmp";
|
static const char kNandSafeTempSuffix[] = ".nandsafe.tmp";
|
||||||
|
|
||||||
std::string SafeTempPathFor(const std::string& hostPath) {
|
std::filesystem::path SafeTempPathFor(const std::filesystem::path& hostPath) {
|
||||||
return hostPath + kNandSafeTempSuffix;
|
std::filesystem::path tempPath = hostPath;
|
||||||
|
tempPath += kNandSafeTempSuffix;
|
||||||
|
return tempPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push the CRT buffer out and then force the OS to put it on the platter, so the data is
|
// Push the CRT buffer out and then force the OS to put it on the platter, so the data is
|
||||||
@@ -264,24 +266,25 @@ static bool FlushFileToDisk(FILE* file) {
|
|||||||
|
|
||||||
// Replace `targetPath` with `tempPath` in one step. Either the old or the new contents
|
// Replace `targetPath` with `tempPath` in one step. Either the old or the new contents
|
||||||
// survive a crash; there is no window where the target is truncated or partial.
|
// survive a crash; there is no window where the target is truncated or partial.
|
||||||
static bool AtomicReplaceHostFile(const char* who, const std::string& tempPath,
|
static bool AtomicReplaceHostFile(const char* who, const std::filesystem::path& tempPath,
|
||||||
const std::string& targetPath) {
|
const std::filesystem::path& targetPath) {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
if (MoveFileExA(tempPath.c_str(), targetPath.c_str(),
|
if (MoveFileExW(tempPath.c_str(), targetPath.c_str(),
|
||||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
|
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
LogNandError(who, "ERROR: MoveFileEx('%s' -> '%s') failed (err=%lu)",
|
LogNandError(who, "ERROR: MoveFileEx('%s' -> '%s') failed (err=%lu)",
|
||||||
tempPath.c_str(), targetPath.c_str(), static_cast<unsigned long>(GetLastError()));
|
HostPathText(tempPath).c_str(), HostPathText(targetPath).c_str(),
|
||||||
|
static_cast<unsigned long>(GetLastError()));
|
||||||
return false;
|
return false;
|
||||||
#else
|
#else
|
||||||
if (std::rename(tempPath.c_str(), targetPath.c_str()) != 0) {
|
if (!NandRename(tempPath, targetPath)) {
|
||||||
LogNandError(who, "ERROR: rename('%s' -> '%s') failed",
|
LogNandError(who, "ERROR: rename('%s' -> '%s') failed",
|
||||||
tempPath.c_str(), targetPath.c_str());
|
HostPathText(tempPath).c_str(), HostPathText(targetPath).c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Durably record the directory entry so the rename itself survives a crash.
|
// Durably record the directory entry so the rename itself survives a crash.
|
||||||
const std::string directory = std::filesystem::path(targetPath).parent_path().string();
|
const std::string directory = targetPath.parent_path().string();
|
||||||
const int dirFd = open(directory.c_str(), O_RDONLY);
|
const int dirFd = open(directory.c_str(), O_RDONLY);
|
||||||
if (dirFd >= 0) {
|
if (dirFd >= 0) {
|
||||||
fsync(dirFd);
|
fsync(dirFd);
|
||||||
@@ -293,23 +296,24 @@ static bool AtomicReplaceHostFile(const char* who, const std::string& tempPath,
|
|||||||
|
|
||||||
// Remove a scratch file left behind by a previous run that died between safe open and
|
// Remove a scratch file left behind by a previous run that died between safe open and
|
||||||
// safe close. Its contents are worthless: the original was never replaced.
|
// safe close. Its contents are worthless: the original was never replaced.
|
||||||
bool DiscardStaleSafeTemp(const std::string& tempPath) {
|
bool DiscardStaleSafeTemp(const std::filesystem::path& tempPath) {
|
||||||
if (!PathExists(tempPath)) {
|
if (!PathExists(tempPath)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
LogNandWarning("nand-shadow", "WARNING: discarding stale scratch file '%s' from a previous run",
|
LogNandWarning("nand-shadow", "WARNING: discarding stale scratch file '%s' from a previous run",
|
||||||
tempPath.c_str());
|
HostPathText(tempPath).c_str());
|
||||||
if (std::remove(tempPath.c_str()) == 0) {
|
if (NandRemove(tempPath)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
LogNandError("nand-shadow", "FAILED to remove stale scratch file '%s'", tempPath.c_str());
|
LogNandError("nand-shadow", "FAILED to remove stale scratch file '%s'",
|
||||||
|
HostPathText(tempPath).c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// True when any live handle already refers to `hostPath`, either directly or as the
|
// True when any live handle already refers to `hostPath`, either directly or as the
|
||||||
// commit target of a shadow. Used to keep shadow writes from hiding data behind a second
|
// commit target of a shadow. Used to keep shadow writes from hiding data behind a second
|
||||||
// handle on the same file.
|
// handle on the same file.
|
||||||
bool IsHostPathOpen(const std::string& hostPath) {
|
bool IsHostPathOpen(const std::filesystem::path& hostPath) {
|
||||||
std::lock_guard<std::mutex> lock(g_fdMutex);
|
std::lock_guard<std::mutex> lock(g_fdMutex);
|
||||||
for (const auto& entry : g_fileHandles) {
|
for (const auto& entry : g_fileHandles) {
|
||||||
if (entry.second.path == hostPath || entry.second.safeCommitPath == hostPath) {
|
if (entry.second.path == hostPath || entry.second.safeCommitPath == hostPath) {
|
||||||
@@ -324,8 +328,8 @@ bool IsHostPathOpen(const std::string& hostPath) {
|
|||||||
// dropped and the original is left exactly as it was, and the error is returned so the
|
// dropped and the original is left exactly as it was, and the error is returned so the
|
||||||
// guest's close call fails instead of silently reporting success.
|
// guest's close call fails instead of silently reporting success.
|
||||||
int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) {
|
int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) {
|
||||||
std::string tempPath;
|
std::filesystem::path tempPath;
|
||||||
std::string commitPath;
|
std::filesystem::path commitPath;
|
||||||
FILE* file = nullptr;
|
FILE* file = nullptr;
|
||||||
int32_t mode = 0;
|
int32_t mode = 0;
|
||||||
|
|
||||||
@@ -358,7 +362,7 @@ int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) {
|
|||||||
|
|
||||||
if (!needsCommit) {
|
if (!needsCommit) {
|
||||||
if (!flushed) {
|
if (!flushed) {
|
||||||
LogNandError(who, "ERROR: flush of '%s' failed", tempPath.c_str());
|
LogNandError(who, "ERROR: flush of '%s' failed", HostPathText(tempPath).c_str());
|
||||||
return NAND_RESULT_UNKNOWN;
|
return NAND_RESULT_UNKNOWN;
|
||||||
}
|
}
|
||||||
return NAND_RESULT_OK;
|
return NAND_RESULT_OK;
|
||||||
@@ -366,13 +370,13 @@ int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) {
|
|||||||
|
|
||||||
if (!flushed) {
|
if (!flushed) {
|
||||||
LogNandError(who, "ERROR: flush of '%s' failed, discarding it and leaving '%s' untouched",
|
LogNandError(who, "ERROR: flush of '%s' failed, discarding it and leaving '%s' untouched",
|
||||||
tempPath.c_str(), commitPath.c_str());
|
HostPathText(tempPath).c_str(), HostPathText(commitPath).c_str());
|
||||||
std::remove(tempPath.c_str());
|
NandRemove(tempPath);
|
||||||
return NAND_RESULT_UNKNOWN;
|
return NAND_RESULT_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!AtomicReplaceHostFile(who, tempPath, commitPath)) {
|
if (!AtomicReplaceHostFile(who, tempPath, commitPath)) {
|
||||||
std::remove(tempPath.c_str());
|
NandRemove(tempPath);
|
||||||
return NAND_RESULT_UNKNOWN;
|
return NAND_RESULT_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,7 +398,7 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
|
|||||||
return NAND_RESULT_INVALID;
|
return NAND_RESULT_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string hostPath = TranslateNandPath(path);
|
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||||
if (hostPath.empty()) {
|
if (hostPath.empty()) {
|
||||||
LogNandError("NANDSafeOpen", "FAILED to translate path '%s'", path);
|
LogNandError("NANDSafeOpen", "FAILED to translate path '%s'", path);
|
||||||
return NAND_RESULT_INVALID;
|
return NAND_RESULT_INVALID;
|
||||||
@@ -407,12 +411,13 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
|
|||||||
if (mode == 1) {
|
if (mode == 1) {
|
||||||
// Read-only safe open reads the original in place; the library builds no scratch
|
// Read-only safe open reads the original in place; the library builds no scratch
|
||||||
// copy for this case.
|
// copy for this case.
|
||||||
FILE* file = std::fopen(hostPath.c_str(), "rb");
|
FILE* file = NandFopen(hostPath, "rb");
|
||||||
if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
|
if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
|
||||||
file = std::fopen(hostPath.c_str(), "rb");
|
file = NandFopen(hostPath, "rb");
|
||||||
}
|
}
|
||||||
if (!file) {
|
if (!file) {
|
||||||
LogNandError("NANDSafeOpen", "FAILED to open '%s' for reading", hostPath.c_str());
|
LogNandError("NANDSafeOpen", "FAILED to open '%s' for reading",
|
||||||
|
HostPathText(hostPath).c_str());
|
||||||
return NAND_RESULT_NOEXISTS;
|
return NAND_RESULT_NOEXISTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,15 +430,16 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
|
|||||||
// Write modes. The library queries the attributes of the original first, so a safe
|
// Write modes. The library queries the attributes of the original first, so a safe
|
||||||
// open of a file that does not exist fails instead of creating one.
|
// open of a file that does not exist fails instead of creating one.
|
||||||
if (!PathExists(hostPath)) {
|
if (!PathExists(hostPath)) {
|
||||||
LogNandError("NANDSafeOpen", "FAILED: '%s' does not exist, safe open never creates it", hostPath.c_str());
|
LogNandError("NANDSafeOpen", "FAILED: '%s' does not exist, safe open never creates it",
|
||||||
|
HostPathText(hostPath).c_str());
|
||||||
return NAND_RESULT_NOEXISTS;
|
return NAND_RESULT_NOEXISTS;
|
||||||
}
|
}
|
||||||
if (IsDirectory(hostPath)) {
|
if (IsDirectory(hostPath)) {
|
||||||
LogNandError("NANDSafeOpen", "FAILED: '%s' is a directory", hostPath.c_str());
|
LogNandError("NANDSafeOpen", "FAILED: '%s' is a directory", HostPathText(hostPath).c_str());
|
||||||
return NAND_RESULT_INVALID;
|
return NAND_RESULT_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string tempPath = SafeTempPathFor(hostPath);
|
const std::filesystem::path tempPath = SafeTempPathFor(hostPath);
|
||||||
if (!DiscardStaleSafeTemp(tempPath)) {
|
if (!DiscardStaleSafeTemp(tempPath)) {
|
||||||
return NAND_RESULT_ACCESS;
|
return NAND_RESULT_ACCESS;
|
||||||
}
|
}
|
||||||
@@ -445,15 +451,17 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
|
|||||||
std::filesystem::copy_options::overwrite_existing, ec);
|
std::filesystem::copy_options::overwrite_existing, ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
LogNandError("NANDSafeOpen", "FAILED to seed scratch file '%s' from '%s': %s",
|
LogNandError("NANDSafeOpen", "FAILED to seed scratch file '%s' from '%s': %s",
|
||||||
tempPath.c_str(), hostPath.c_str(), ec.message().c_str());
|
HostPathText(tempPath).c_str(), HostPathText(hostPath).c_str(),
|
||||||
std::remove(tempPath.c_str());
|
ec.message().c_str());
|
||||||
|
NandRemove(tempPath);
|
||||||
return NAND_RESULT_UNKNOWN;
|
return NAND_RESULT_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
FILE* file = std::fopen(tempPath.c_str(), "r+b");
|
FILE* file = NandFopen(tempPath, "r+b");
|
||||||
if (!file) {
|
if (!file) {
|
||||||
LogNandError("NANDSafeOpen", "FAILED to open scratch file '%s'", tempPath.c_str());
|
LogNandError("NANDSafeOpen", "FAILED to open scratch file '%s'",
|
||||||
std::remove(tempPath.c_str());
|
HostPathText(tempPath).c_str());
|
||||||
|
NandRemove(tempPath);
|
||||||
return NAND_RESULT_UNKNOWN;
|
return NAND_RESULT_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Base path for the host Wii NAND directory (resolved at runtime).
|
// Base path for the host Wii NAND directory (resolved at runtime).
|
||||||
static std::string g_dolphinWiiBase;
|
static std::filesystem::path g_dolphinWiiBase;
|
||||||
static std::once_flag g_dolphinWiiBaseOnce;
|
static std::once_flag g_dolphinWiiBaseOnce;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -47,7 +47,7 @@ std::map<int32_t, FileHandle> g_fileHandles;
|
|||||||
static int32_t g_nextFd = 100; // Start at 100 to avoid confusion with stdio fds
|
static int32_t g_nextFd = 100; // Start at 100 to avoid confusion with stdio fds
|
||||||
std::mutex g_fdMutex;
|
std::mutex g_fdMutex;
|
||||||
|
|
||||||
int32_t AllocateFd(const std::string& path, FILE* file, int32_t mode) {
|
int32_t AllocateFd(const std::filesystem::path& path, FILE* file, int32_t mode) {
|
||||||
std::lock_guard<std::mutex> lock(g_fdMutex);
|
std::lock_guard<std::mutex> lock(g_fdMutex);
|
||||||
int32_t fd = g_nextFd++;
|
int32_t fd = g_nextFd++;
|
||||||
g_fileHandles[fd] = {file, path, mode, 0};
|
g_fileHandles[fd] = {file, path, mode, 0};
|
||||||
@@ -88,29 +88,50 @@ std::string CurrentNandDataDir() {
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string& GetNandBasePath() {
|
const std::filesystem::path& GetNandBasePath() {
|
||||||
std::call_once(g_dolphinWiiBaseOnce, []() {
|
std::call_once(g_dolphinWiiBaseOnce, []() {
|
||||||
g_dolphinWiiBase = RuntimeNandPath::DiscoverNandRootString();
|
g_dolphinWiiBase = RuntimeNandPath::DiscoverNandRootPath();
|
||||||
});
|
});
|
||||||
|
|
||||||
return g_dolphinWiiBase;
|
return g_dolphinWiiBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string BuildHostNandPath(std::string wiiPathStr) {
|
std::string HostPathText(const std::filesystem::path& path) {
|
||||||
std::string hostPath = GetNandBasePath();
|
return RuntimeConfigFile::PathToUtf8(path);
|
||||||
for (char& c : wiiPathStr) {
|
}
|
||||||
if (c == '/') {
|
|
||||||
#ifdef _WIN32
|
|
||||||
c = '\\';
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wiiPathStr.empty() && (wiiPathStr[0] == '\\' || wiiPathStr[0] == '/')) {
|
FILE* NandFopen(const std::filesystem::path& path, const char* mode) {
|
||||||
hostPath += wiiPathStr;
|
#ifdef _WIN32
|
||||||
} else {
|
const std::wstring wideMode(mode, mode + std::strlen(mode));
|
||||||
hostPath += "\\";
|
return _wfopen(path.c_str(), wideMode.c_str());
|
||||||
hostPath += wiiPathStr;
|
#else
|
||||||
|
return std::fopen(path.c_str(), mode);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NandRemove(const std::filesystem::path& path) {
|
||||||
|
std::error_code ec;
|
||||||
|
return std::filesystem::remove(path, ec) && !ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NandRename(const std::filesystem::path& from, const std::filesystem::path& to) {
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::rename(from, to, ec);
|
||||||
|
return !ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guest paths are absolute and already lexically resolved against the NAND root, so
|
||||||
|
// they are appended as relative components instead of replacing the root.
|
||||||
|
static std::filesystem::path BuildHostNandPath(const std::string& wiiPathStr) {
|
||||||
|
std::filesystem::path hostPath = GetNandBasePath();
|
||||||
|
size_t cursor = 0;
|
||||||
|
while (cursor < wiiPathStr.size()) {
|
||||||
|
const size_t slash = wiiPathStr.find('/', cursor);
|
||||||
|
const size_t end = slash == std::string::npos ? wiiPathStr.size() : slash;
|
||||||
|
if (end != cursor) {
|
||||||
|
hostPath /= wiiPathStr.substr(cursor, end - cursor);
|
||||||
|
}
|
||||||
|
cursor = end + 1;
|
||||||
}
|
}
|
||||||
return hostPath;
|
return hostPath;
|
||||||
}
|
}
|
||||||
@@ -169,7 +190,7 @@ static std::string NormalizeAbsoluteWiiPath(const char* wiiPath) {
|
|||||||
struct RiivolutionSaveRedirect {
|
struct RiivolutionSaveRedirect {
|
||||||
bool enabled = false;
|
bool enabled = false;
|
||||||
bool clone = false;
|
bool clone = false;
|
||||||
std::string hostDirectory;
|
std::filesystem::path hostDirectory;
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::once_flag g_riivolutionSaveRedirectOnce;
|
static std::once_flag g_riivolutionSaveRedirectOnce;
|
||||||
@@ -187,7 +208,7 @@ static const RiivolutionSaveRedirect& GetRiivolutionSaveRedirect() {
|
|||||||
}
|
}
|
||||||
g_riivolutionSaveRedirect.enabled = true;
|
g_riivolutionSaveRedirect.enabled = true;
|
||||||
g_riivolutionSaveRedirect.clone = redirect->clone;
|
g_riivolutionSaveRedirect.clone = redirect->clone;
|
||||||
g_riivolutionSaveRedirect.hostDirectory = redirect->hostDirectory.string();
|
g_riivolutionSaveRedirect.hostDirectory = redirect->hostDirectory;
|
||||||
// Riivolution creates the redirect folder if it does not exist.
|
// Riivolution creates the redirect folder if it does not exist.
|
||||||
CreateDirectoryPath(g_riivolutionSaveRedirect.hostDirectory);
|
CreateDirectoryPath(g_riivolutionSaveRedirect.hostDirectory);
|
||||||
});
|
});
|
||||||
@@ -195,8 +216,8 @@ static const RiivolutionSaveRedirect& GetRiivolutionSaveRedirect() {
|
|||||||
return g_riivolutionSaveRedirect;
|
return g_riivolutionSaveRedirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void CloneRiivolutionSaveIfNeeded(const std::string& sourceHostPath,
|
static void CloneRiivolutionSaveIfNeeded(const std::filesystem::path& sourceHostPath,
|
||||||
const std::string& redirectedHostPath,
|
const std::filesystem::path& redirectedHostPath,
|
||||||
const RiivolutionSaveRedirect& redirect) {
|
const RiivolutionSaveRedirect& redirect) {
|
||||||
if (!redirect.clone || PathExists(redirectedHostPath) || !PathExists(sourceHostPath)) {
|
if (!redirect.clone || PathExists(redirectedHostPath) || !PathExists(sourceHostPath)) {
|
||||||
return;
|
return;
|
||||||
@@ -209,11 +230,13 @@ static void CloneRiivolutionSaveIfNeeded(const std::string& sourceHostPath,
|
|||||||
std::filesystem::copy_options::skip_existing, ec);
|
std::filesystem::copy_options::skip_existing, ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
LogNandWarning("RiivolutionSave", "WARNING: failed to clone '%s' -> '%s': %s",
|
LogNandWarning("RiivolutionSave", "WARNING: failed to clone '%s' -> '%s': %s",
|
||||||
sourceHostPath.c_str(), redirectedHostPath.c_str(), ec.message().c_str());
|
HostPathText(sourceHostPath).c_str(),
|
||||||
|
HostPathText(redirectedHostPath).c_str(), ec.message().c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool ResolveRiivolutionSaveHostPath(const std::string& absoluteWiiPath, std::string& outHostPath) {
|
static bool ResolveRiivolutionSaveHostPath(const std::string& absoluteWiiPath,
|
||||||
|
std::filesystem::path& outHostPath) {
|
||||||
const RiivolutionSaveRedirect& redirect = GetRiivolutionSaveRedirect();
|
const RiivolutionSaveRedirect& redirect = GetRiivolutionSaveRedirect();
|
||||||
if (!redirect.enabled) {
|
if (!redirect.enabled) {
|
||||||
return false;
|
return false;
|
||||||
@@ -232,23 +255,23 @@ static bool ResolveRiivolutionSaveHostPath(const std::string& absoluteWiiPath, s
|
|||||||
relative = absoluteWiiPath.substr(dataDir.size() + 1);
|
relative = absoluteWiiPath.substr(dataDir.size() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path redirected(redirect.hostDirectory);
|
std::filesystem::path redirected = redirect.hostDirectory;
|
||||||
if (!relative.empty()) {
|
if (!relative.empty()) {
|
||||||
redirected /= std::filesystem::path(relative);
|
redirected /= std::filesystem::path(relative);
|
||||||
}
|
}
|
||||||
|
|
||||||
outHostPath = redirected.string();
|
outHostPath = redirected;
|
||||||
CloneRiivolutionSaveIfNeeded(BuildHostNandPath(absoluteWiiPath), outHostPath, redirect);
|
CloneRiivolutionSaveIfNeeded(BuildHostNandPath(absoluteWiiPath), outHostPath, redirect);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string TranslateNandPath(const char* wiiPath) {
|
std::filesystem::path TranslateNandPath(const char* wiiPath) {
|
||||||
std::string wiiPathStr = NormalizeAbsoluteWiiPath(wiiPath);
|
std::string wiiPathStr = NormalizeAbsoluteWiiPath(wiiPath);
|
||||||
if (wiiPathStr.empty()) {
|
if (wiiPathStr.empty()) {
|
||||||
return "";
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string redirectedHostPath;
|
std::filesystem::path redirectedHostPath;
|
||||||
if (ResolveRiivolutionSaveHostPath(wiiPathStr, redirectedHostPath)) {
|
if (ResolveRiivolutionSaveHostPath(wiiPathStr, redirectedHostPath)) {
|
||||||
return redirectedHostPath;
|
return redirectedHostPath;
|
||||||
}
|
}
|
||||||
@@ -266,7 +289,8 @@ struct U8Node {
|
|||||||
uint32_t size;
|
uint32_t size;
|
||||||
};
|
};
|
||||||
|
|
||||||
static bool ExtractFromU8(const std::string& archivePath, const char* targetName, std::vector<uint8_t>& outData) {
|
static bool ExtractFromU8(const std::filesystem::path& archivePath, const char* targetName,
|
||||||
|
std::vector<uint8_t>& outData) {
|
||||||
std::ifstream file(archivePath, std::ios::binary);
|
std::ifstream file(archivePath, std::ios::binary);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return false;
|
return false;
|
||||||
@@ -352,13 +376,13 @@ static bool ExtractFromU8(const std::string& archivePath, const char* targetName
|
|||||||
// as an argument and the path predicate below hard-codes the same name.
|
// as an argument and the path predicate below hard-codes the same name.
|
||||||
static constexpr char kFaceLibResourceName[] = "RFL_Res.dat";
|
static constexpr char kFaceLibResourceName[] = "RFL_Res.dat";
|
||||||
|
|
||||||
bool SeedFaceLibResource(const std::string& hostPath) {
|
bool SeedFaceLibResource(const std::filesystem::path& hostPath) {
|
||||||
std::vector<uint8_t> payload;
|
std::vector<uint8_t> payload;
|
||||||
if (const auto dvdRoot = RuntimeConfigFile::ResolvedDvdRoot(); !dvdRoot.empty()) {
|
if (const auto dvdRoot = RuntimeConfigFile::ResolvedDvdRoot(); !dvdRoot.empty()) {
|
||||||
const auto arcPath = dvdRoot / "files" / "contents" / "RFLRes01.arc";
|
const auto arcPath = dvdRoot / "files" / "contents" / "RFLRes01.arc";
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (std::filesystem::exists(arcPath, ec)) {
|
if (std::filesystem::exists(arcPath, ec)) {
|
||||||
ExtractFromU8(arcPath.string(), kFaceLibResourceName, payload);
|
ExtractFromU8(arcPath, kFaceLibResourceName, payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,12 +395,12 @@ bool SeedFaceLibResource(const std::string& hostPath) {
|
|||||||
|
|
||||||
std::ofstream out(hostPath, std::ios::binary);
|
std::ofstream out(hostPath, std::ios::binary);
|
||||||
if (!out) {
|
if (!out) {
|
||||||
LogNandError("FaceLibSeed", "Failed to create %s", hostPath.c_str());
|
LogNandError("FaceLibSeed", "Failed to create %s", HostPathText(hostPath).c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
out.write(reinterpret_cast<const char*>(payload.data()), static_cast<std::streamsize>(payload.size()));
|
out.write(reinterpret_cast<const char*>(payload.data()), static_cast<std::streamsize>(payload.size()));
|
||||||
if (!out) {
|
if (!out) {
|
||||||
LogNandError("FaceLibSeed", "Failed to write %s", hostPath.c_str());
|
LogNandError("FaceLibSeed", "Failed to write %s", HostPathText(hostPath).c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,45 +412,33 @@ bool IsFaceLibResourcePath(const char* path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create directories recursively
|
// Create directories recursively
|
||||||
bool CreateDirectoryPath(const std::string& path) {
|
bool CreateDirectoryPath(const std::filesystem::path& path) {
|
||||||
if (path.empty()) {
|
if (path.empty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::create_directories(path, ec);
|
std::filesystem::create_directories(path, ec);
|
||||||
return !ec || std::filesystem::is_directory(path);
|
return !ec || std::filesystem::is_directory(path, ec);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a path exists
|
// Check if a path exists
|
||||||
bool PathExists(const std::string& path) {
|
bool PathExists(const std::filesystem::path& path) {
|
||||||
#ifdef _WIN32
|
std::error_code ec;
|
||||||
return GetFileAttributesA(path.c_str()) != INVALID_FILE_ATTRIBUTES;
|
return std::filesystem::exists(path, ec) && !ec;
|
||||||
#else
|
|
||||||
return access(path.c_str(), F_OK) == 0;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if path is a directory
|
// Check if path is a directory
|
||||||
bool IsDirectory(const std::string& path) {
|
bool IsDirectory(const std::filesystem::path& path) {
|
||||||
#ifdef _WIN32
|
std::error_code ec;
|
||||||
DWORD attrs = GetFileAttributesA(path.c_str());
|
return std::filesystem::is_directory(path, ec) && !ec;
|
||||||
return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY);
|
|
||||||
#else
|
|
||||||
struct stat st;
|
|
||||||
return stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CreateParentDirectories(const std::string& path) {
|
bool CreateParentDirectories(const std::filesystem::path& path) {
|
||||||
size_t lastSlash = path.rfind('\\');
|
if (!path.has_parent_path()) {
|
||||||
if (lastSlash == std::string::npos) {
|
|
||||||
lastSlash = path.rfind('/');
|
|
||||||
}
|
|
||||||
if (lastSlash == std::string::npos) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
CreateDirectoryPath(path.substr(0, lastSlash));
|
CreateDirectoryPath(path.parent_path());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,15 +29,10 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <sys/stat.h>
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <direct.h>
|
|
||||||
#include <io.h>
|
#include <io.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#define mkdir(path, mode) _mkdir(path)
|
|
||||||
#define access _access
|
|
||||||
#define F_OK 0
|
|
||||||
#else
|
#else
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
@@ -67,19 +62,19 @@ void LogNandWarning(const char* func, const char* fmt, ...);
|
|||||||
|
|
||||||
struct FileHandle {
|
struct FileHandle {
|
||||||
FILE* file = nullptr;
|
FILE* file = nullptr;
|
||||||
std::string path;
|
std::filesystem::path path;
|
||||||
int32_t mode = 0; // 1=read, 2=write, 3=read/write
|
int32_t mode = 0; // 1=read, 2=write, 3=read/write
|
||||||
uint32_t position = 0;
|
uint32_t position = 0;
|
||||||
// Non-empty only for write-mode NANDSafeOpen handles. `path` then points at the
|
// Non-empty only for write-mode NANDSafeOpen handles. `path` then points at the
|
||||||
// sibling scratch file the guest is writing into, and this is the original file it
|
// sibling scratch file the guest is writing into, and this is the original file it
|
||||||
// atomically replaces on NANDSafeClose.
|
// atomically replaces on NANDSafeClose.
|
||||||
std::string safeCommitPath;
|
std::filesystem::path safeCommitPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern std::map<int32_t, FileHandle> g_fileHandles;
|
extern std::map<int32_t, FileHandle> g_fileHandles;
|
||||||
extern std::mutex g_fdMutex;
|
extern std::mutex g_fdMutex;
|
||||||
|
|
||||||
int32_t AllocateFd(const std::string& path, FILE* file, int32_t mode);
|
int32_t AllocateFd(const std::filesystem::path& path, FILE* file, int32_t mode);
|
||||||
FileHandle* GetHandle(int32_t fd);
|
FileHandle* GetHandle(int32_t fd);
|
||||||
void CloseFd(int32_t fd);
|
void CloseFd(int32_t fd);
|
||||||
|
|
||||||
@@ -89,18 +84,27 @@ void CloseFd(int32_t fd);
|
|||||||
|
|
||||||
uint32_t CurrentMkwTitleIdLo();
|
uint32_t CurrentMkwTitleIdLo();
|
||||||
std::string CurrentNandDataDir();
|
std::string CurrentNandDataDir();
|
||||||
const std::string& GetNandBasePath();
|
const std::filesystem::path& GetNandBasePath();
|
||||||
std::string TranslateNandPath(const char* wiiPath);
|
std::filesystem::path TranslateNandPath(const char* wiiPath);
|
||||||
|
|
||||||
bool CreateDirectoryPath(const std::string& path);
|
bool CreateDirectoryPath(const std::filesystem::path& path);
|
||||||
bool PathExists(const std::string& path);
|
bool PathExists(const std::filesystem::path& path);
|
||||||
bool IsDirectory(const std::string& path);
|
bool IsDirectory(const std::filesystem::path& path);
|
||||||
|
|
||||||
// Create the directory that contains `path`. False when `path` has no directory
|
// Create the directory that contains `path`. False when `path` has no directory
|
||||||
// component, i.e. there was nothing to create.
|
// component, i.e. there was nothing to create.
|
||||||
bool CreateParentDirectories(const std::string& path);
|
bool CreateParentDirectories(const std::filesystem::path& path);
|
||||||
|
|
||||||
bool SeedFaceLibResource(const std::string& hostPath);
|
// Host paths keep their native encoding end to end; these are the only places a NAND
|
||||||
|
// path is narrowed, and they narrow to UTF-8 for display.
|
||||||
|
std::string HostPathText(const std::filesystem::path& path);
|
||||||
|
|
||||||
|
// fopen takes an ANSI-codepage name on Windows, which cannot express every path.
|
||||||
|
FILE* NandFopen(const std::filesystem::path& path, const char* mode);
|
||||||
|
bool NandRemove(const std::filesystem::path& path);
|
||||||
|
bool NandRename(const std::filesystem::path& from, const std::filesystem::path& to);
|
||||||
|
|
||||||
|
bool SeedFaceLibResource(const std::filesystem::path& hostPath);
|
||||||
bool IsFaceLibResourcePath(const char* path);
|
bool IsFaceLibResourcePath(const char* path);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -178,9 +182,9 @@ enum ISFSResult {
|
|||||||
int32_t ISFS_OpenLib_Initialize(CpuContext* ctx);
|
int32_t ISFS_OpenLib_Initialize(CpuContext* ctx);
|
||||||
|
|
||||||
// Shadow-write machinery, defined with the NANDSafeOpen/NANDSafeClose section below.
|
// Shadow-write machinery, defined with the NANDSafeOpen/NANDSafeClose section below.
|
||||||
std::string SafeTempPathFor(const std::string& hostPath);
|
std::filesystem::path SafeTempPathFor(const std::filesystem::path& hostPath);
|
||||||
bool DiscardStaleSafeTemp(const std::string& tempPath);
|
bool DiscardStaleSafeTemp(const std::filesystem::path& tempPath);
|
||||||
bool IsHostPathOpen(const std::string& hostPath);
|
bool IsHostPathOpen(const std::filesystem::path& hostPath);
|
||||||
int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError);
|
int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError);
|
||||||
|
|
||||||
// Synchronous NAND library entry points (defined in nand_api.cpp); the async
|
// Synchronous NAND library entry points (defined in nand_api.cpp); the async
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// It's a NAND file path
|
// It's a NAND file path
|
||||||
std::string hostPath = TranslateNandPath(path);
|
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||||
|
|
||||||
// Seed FaceLib resources before the existence check so every open mode can
|
// Seed FaceLib resources before the existence check so every open mode can
|
||||||
// still find them on a fresh managed NAND.
|
// still find them on a fresh managed NAND.
|
||||||
@@ -346,16 +346,16 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
|
|||||||
if (mode == 2 || mode == 3) {
|
if (mode == 2 || mode == 3) {
|
||||||
if (!PathExists(hostPath)) {
|
if (!PathExists(hostPath)) {
|
||||||
LogNandWarning("IOS_Open", "'%s' does not exist; open mode %u never creates it",
|
LogNandWarning("IOS_Open", "'%s' does not exist; open mode %u never creates it",
|
||||||
hostPath.c_str(), mode);
|
HostPathText(hostPath).c_str(), mode);
|
||||||
return ISFS_ENOENT;
|
return ISFS_ENOENT;
|
||||||
}
|
}
|
||||||
fopenMode = "r+b"; // Write-only opens still need read for seeks
|
fopenMode = "r+b"; // Write-only opens still need read for seeks
|
||||||
}
|
}
|
||||||
|
|
||||||
FILE* file = std::fopen(hostPath.c_str(), fopenMode);
|
FILE* file = NandFopen(hostPath, fopenMode);
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
LogNandError("IOS_Open", "FAILED to open '%s'", hostPath.c_str());
|
LogNandError("IOS_Open", "FAILED to open '%s'", HostPathText(hostPath).c_str());
|
||||||
return ISFS_ENOENT;
|
return ISFS_ENOENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,14 +516,9 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
|
|||||||
return ISFS_EINVAL;
|
return ISFS_EINVAL;
|
||||||
}
|
}
|
||||||
const char* path = (const char*)Memory::GetPointer(inBufPtr + 6);
|
const char* path = (const char*)Memory::GetPointer(inBufPtr + 6);
|
||||||
std::string hostPath = TranslateNandPath(path);
|
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||||
|
|
||||||
if (CreateDirectoryPath(hostPath)) {
|
if (CreateDirectoryPath(hostPath)) {
|
||||||
#ifdef _WIN32
|
|
||||||
_mkdir(hostPath.c_str());
|
|
||||||
#else
|
|
||||||
mkdir(hostPath.c_str(), 0755);
|
|
||||||
#endif
|
|
||||||
return ISFS_OK;
|
return ISFS_OK;
|
||||||
}
|
}
|
||||||
return ISFS_EIO;
|
return ISFS_EIO;
|
||||||
@@ -534,16 +529,11 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
|
|||||||
return ISFS_EINVAL;
|
return ISFS_EINVAL;
|
||||||
}
|
}
|
||||||
const char* path = (const char*)Memory::GetPointer(inBufPtr);
|
const char* path = (const char*)Memory::GetPointer(inBufPtr);
|
||||||
std::string hostPath = TranslateNandPath(path);
|
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||||
|
|
||||||
if (IsDirectory(hostPath)) {
|
// fs::remove refuses a non-empty directory, matching rmdir.
|
||||||
#ifdef _WIN32
|
if (NandRemove(hostPath)) {
|
||||||
if (RemoveDirectoryA(hostPath.c_str())) return ISFS_OK;
|
return ISFS_OK;
|
||||||
#else
|
|
||||||
if (rmdir(hostPath.c_str()) == 0) return ISFS_OK;
|
|
||||||
#endif
|
|
||||||
} else {
|
|
||||||
if (std::remove(hostPath.c_str()) == 0) return ISFS_OK;
|
|
||||||
}
|
}
|
||||||
return ISFS_ENOENT;
|
return ISFS_ENOENT;
|
||||||
}
|
}
|
||||||
@@ -553,7 +543,7 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
|
|||||||
return ISFS_EINVAL;
|
return ISFS_EINVAL;
|
||||||
}
|
}
|
||||||
const char* path = (const char*)Memory::GetPointer(inBufPtr);
|
const char* path = (const char*)Memory::GetPointer(inBufPtr);
|
||||||
std::string hostPath = TranslateNandPath(path);
|
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||||
|
|
||||||
if (!PathExists(hostPath)) {
|
if (!PathExists(hostPath)) {
|
||||||
return ISFS_ENOENT;
|
return ISFS_ENOENT;
|
||||||
@@ -582,11 +572,11 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
|
|||||||
return ISFS_EINVAL;
|
return ISFS_EINVAL;
|
||||||
}
|
}
|
||||||
const char* path = (const char*)Memory::GetPointer(inBufPtr + 6);
|
const char* path = (const char*)Memory::GetPointer(inBufPtr + 6);
|
||||||
std::string hostPath = TranslateNandPath(path);
|
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||||
CreateParentDirectories(hostPath);
|
CreateParentDirectories(hostPath);
|
||||||
|
|
||||||
// Create empty file
|
// Create empty file
|
||||||
FILE* f = std::fopen(hostPath.c_str(), "wb");
|
FILE* f = NandFopen(hostPath, "wb");
|
||||||
if (f) {
|
if (f) {
|
||||||
std::fclose(f);
|
std::fclose(f);
|
||||||
return ISFS_OK;
|
return ISFS_OK;
|
||||||
@@ -606,10 +596,10 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
|
|||||||
}
|
}
|
||||||
const char* srcPath = (const char*)Memory::GetPointer(inBufPtr);
|
const char* srcPath = (const char*)Memory::GetPointer(inBufPtr);
|
||||||
const char* dstPath = (const char*)Memory::GetPointer(inBufPtr + 0x40);
|
const char* dstPath = (const char*)Memory::GetPointer(inBufPtr + 0x40);
|
||||||
std::string srcHost = TranslateNandPath(srcPath);
|
const std::filesystem::path srcHost = TranslateNandPath(srcPath);
|
||||||
std::string dstHost = TranslateNandPath(dstPath);
|
const std::filesystem::path dstHost = TranslateNandPath(dstPath);
|
||||||
|
|
||||||
if (std::rename(srcHost.c_str(), dstHost.c_str()) == 0) {
|
if (NandRename(srcHost, dstHost)) {
|
||||||
return ISFS_OK;
|
return ISFS_OK;
|
||||||
}
|
}
|
||||||
return ISFS_EIO;
|
return ISFS_EIO;
|
||||||
@@ -805,11 +795,11 @@ int32_t ISFS_OpenLib_Initialize(CpuContext* ctx) {
|
|||||||
g_isfsInitialized = true;
|
g_isfsInitialized = true;
|
||||||
|
|
||||||
// Create the title data directory if it doesn't exist
|
// Create the title data directory if it doesn't exist
|
||||||
char titlePath[256];
|
char titleId[32];
|
||||||
const std::string& base = GetNandBasePath();
|
std::snprintf(titleId, sizeof(titleId), "%08x", kNandTitleIdHi);
|
||||||
std::snprintf(titlePath, sizeof(titlePath), "%s\\title\\%08x\\%08x\\data",
|
char gameId[32];
|
||||||
base.c_str(), kNandTitleIdHi, CurrentMkwTitleIdLo());
|
std::snprintf(gameId, sizeof(gameId), "%08x", CurrentMkwTitleIdLo());
|
||||||
CreateDirectoryPath(titlePath);
|
CreateDirectoryPath(GetNandBasePath() / "title" / titleId / gameId / "data");
|
||||||
|
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return ISFS_OK;
|
return ISFS_OK;
|
||||||
@@ -899,6 +889,74 @@ REGISTER_NATIVE_FUNCTION_AS(0x80169BCC, ISFS_OpenLib_HLE_80169BCC, "ISFS_OpenLib
|
|||||||
// IOS_Ioctlv HLE - Vector Ioctl for complex ISFS operations
|
// IOS_Ioctlv HLE - Vector Ioctl for complex ISFS operations
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
static int32_t HandleIsfsReadDir(uint32_t numIn, uint32_t numOut, uint32_t vectorPtr) {
|
||||||
|
const bool countOnly = (numIn == 1 && numOut == 1);
|
||||||
|
if (!countOnly && !(numIn == 2 && numOut == 2)) {
|
||||||
|
LogNandWarning("IOS_Ioctlv", "READDIR unsupported vector shape numIn=%u numOut=%u",
|
||||||
|
numIn, numOut);
|
||||||
|
return ISFS_EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IosVector pathVec = ReadIosVector(vectorPtr, 0);
|
||||||
|
const std::string wiiPath = ReadGuestCString(pathVec.address, 64);
|
||||||
|
if (wiiPath.empty()) {
|
||||||
|
return ISFS_EINVAL;
|
||||||
|
}
|
||||||
|
const std::filesystem::path hostPath = TranslateNandPath(wiiPath.c_str());
|
||||||
|
if (!IsDirectory(hostPath)) {
|
||||||
|
return ISFS_ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NAND names are at most 12 characters; longer host names cannot exist on
|
||||||
|
// a real NAND (this also hides *.nandsafe.tmp write shadows).
|
||||||
|
constexpr size_t kMaxNandNameLength = 12;
|
||||||
|
std::vector<std::string> names;
|
||||||
|
std::error_code ec;
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(hostPath, ec)) {
|
||||||
|
std::string name = HostPathText(entry.path().filename());
|
||||||
|
if (name.empty() || name.size() > kMaxNandNameLength) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
names.push_back(std::move(name));
|
||||||
|
}
|
||||||
|
std::sort(names.begin(), names.end());
|
||||||
|
|
||||||
|
if (countOnly) {
|
||||||
|
const IosVector countOut = ReadIosVector(vectorPtr, 1);
|
||||||
|
if (countOut.size < 4 || !Memory::Contains(countOut.address, 4)) {
|
||||||
|
return ISFS_EINVAL;
|
||||||
|
}
|
||||||
|
Memory::Write32(countOut.address, static_cast<uint32_t>(names.size()));
|
||||||
|
return ISFS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IosVector maxVec = ReadIosVector(vectorPtr, 1);
|
||||||
|
const IosVector namesOut = ReadIosVector(vectorPtr, 2);
|
||||||
|
const IosVector countOut = ReadIosVector(vectorPtr, 3);
|
||||||
|
if (maxVec.size < 4 || !Memory::Contains(maxVec.address, 4) ||
|
||||||
|
countOut.size < 4 || !Memory::Contains(countOut.address, 4) ||
|
||||||
|
!IsValidGuestRange(namesOut.address, namesOut.size)) {
|
||||||
|
return ISFS_EINVAL;
|
||||||
|
}
|
||||||
|
const uint32_t maxCount = Memory::Read32(maxVec.address);
|
||||||
|
|
||||||
|
constexpr uint32_t kEntryWindow = 13; // 12 chars + terminator
|
||||||
|
uint32_t cursor = 0;
|
||||||
|
uint32_t written = 0;
|
||||||
|
for (const std::string& name : names) {
|
||||||
|
if (written >= maxCount || cursor + kEntryWindow > namesOut.size) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint8_t* out = Memory::GetPointer(namesOut.address + cursor, kEntryWindow);
|
||||||
|
std::memset(out, 0, kEntryWindow);
|
||||||
|
std::memcpy(out, name.data(), name.size());
|
||||||
|
cursor += static_cast<uint32_t>(name.size()) + 1;
|
||||||
|
++written;
|
||||||
|
}
|
||||||
|
Memory::Write32(countOut.address, written);
|
||||||
|
return ISFS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" int32_t NAND_IOS_Ioctlv_HLE(
|
extern "C" int32_t NAND_IOS_Ioctlv_HLE(
|
||||||
uint32_t fd,
|
uint32_t fd,
|
||||||
uint32_t cmd,
|
uint32_t cmd,
|
||||||
@@ -919,6 +977,16 @@ extern "C" int32_t NAND_IOS_Ioctlv_HLE(
|
|||||||
return HandleDolphinIoctlv(cmd, numIn, numOut, vectorPtr);
|
return HandleDolphinIoctlv(cmd, numIn, numOut, vectorPtr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fd == ISFS_DEV_FD) {
|
||||||
|
if (!vectorPtr || !Memory::Contains(vectorPtr, static_cast<size_t>(numIn + numOut) * 8u)) {
|
||||||
|
return ISFS_EINVAL;
|
||||||
|
}
|
||||||
|
if (cmd == ISFS_IOCTL_READDIR) {
|
||||||
|
return HandleIsfsReadDir(numIn, numOut, vectorPtr);
|
||||||
|
}
|
||||||
|
return ISFS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
if (fd == ES_DEV_FD) {
|
if (fd == ES_DEV_FD) {
|
||||||
if (!vectorPtr || !Memory::Contains(vectorPtr, static_cast<size_t>(numIn + numOut) * 8u)) {
|
if (!vectorPtr || !Memory::Contains(vectorPtr, static_cast<size_t>(numIn + numOut) * 8u)) {
|
||||||
return ISFS_EINVAL;
|
return ISFS_EINVAL;
|
||||||
|
|||||||
@@ -74,8 +74,18 @@ std::string RiivoGameId() {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every narrow path string here is UTF-8, including the ones the XML halves of
|
||||||
|
// resolved paths are concatenated with.
|
||||||
|
using RuntimeConfigFile::PathFromUtf8;
|
||||||
|
using RuntimeConfigFile::PathToUtf8;
|
||||||
|
|
||||||
|
std::string RiivoGenericText(const fs::path& path) {
|
||||||
|
const std::u8string text = path.generic_u8string();
|
||||||
|
return std::string(text.begin(), text.end());
|
||||||
|
}
|
||||||
|
|
||||||
std::string RiivoComparablePath(const fs::path& path) {
|
std::string RiivoComparablePath(const fs::path& path) {
|
||||||
std::string text = path.lexically_normal().generic_string();
|
std::string text = RiivoGenericText(path.lexically_normal());
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
RuntimeHle::LowerInPlace(text);
|
RuntimeHle::LowerInPlace(text);
|
||||||
#endif
|
#endif
|
||||||
@@ -86,6 +96,8 @@ void RiivoAddRoot(std::vector<RuntimeRiivolution::Overlay>& overlays, fs::path r
|
|||||||
const char* source) {
|
const char* source) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (!fs::is_directory(root, ec)) {
|
if (!fs::is_directory(root, ec)) {
|
||||||
|
RT_LOG(RT_TAG_RIIVOLUTION) << "rejected overlay root (" << (source ? source : "unknown")
|
||||||
|
<< "): " << PathToUtf8(root) << " is not a reachable directory" << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +116,7 @@ void RiivoAddRoot(std::vector<RuntimeRiivolution::Overlay>& overlays, fs::path r
|
|||||||
}
|
}
|
||||||
|
|
||||||
RT_LOG(RT_TAG_RIIVOLUTION) << "overlay root (" << (source ? source : "unknown")
|
RT_LOG(RT_TAG_RIIVOLUTION) << "overlay root (" << (source ? source : "unknown")
|
||||||
<< "): " << normalized.string() << std::endl;
|
<< "): " << PathToUtf8(normalized) << std::endl;
|
||||||
overlays.push_back({std::move(normalized), std::nullopt});
|
overlays.push_back({std::move(normalized), std::nullopt});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +145,7 @@ std::vector<RuntimeRiivolution::Overlay> RiivoDiscoverRoots() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& root : RecompMod::DvdOverlayRoots()) {
|
for (const auto& root : RecompMod::DvdOverlayRoots()) {
|
||||||
RiivoAddRoot(overlays, fs::path(root), "recomp mod manifest");
|
RiivoAddRoot(overlays, root, "recomp mod manifest");
|
||||||
}
|
}
|
||||||
|
|
||||||
return overlays;
|
return overlays;
|
||||||
@@ -154,7 +166,7 @@ std::optional<RiivoXmlSet> RiivoFindXmls(const fs::path& overlayRoot) {
|
|||||||
// <sd>/RetroRewind6), so externals resolve against the root's parent.
|
// <sd>/RetroRewind6), so externals resolve against the root's parent.
|
||||||
const std::string& configured = RecompMod::RiivolutionXml();
|
const std::string& configured = RecompMod::RiivolutionXml();
|
||||||
if (!configured.empty()) {
|
if (!configured.empty()) {
|
||||||
const fs::path configuredXml = overlayRoot / fs::path(configured);
|
const fs::path configuredXml = overlayRoot / PathFromUtf8(configured);
|
||||||
if (fs::is_regular_file(configuredXml, ec)) {
|
if (fs::is_regular_file(configuredXml, ec)) {
|
||||||
return RiivoXmlSet{overlayRoot.parent_path(), {configuredXml}};
|
return RiivoXmlSet{overlayRoot.parent_path(), {configuredXml}};
|
||||||
}
|
}
|
||||||
@@ -211,7 +223,7 @@ void RiivoCollectMappings(const RiivolutionContract::Patch& patch, const std::st
|
|||||||
++set.skippedExternals;
|
++set.skippedExternals;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const fs::path hostFile(*resolved);
|
const fs::path hostFile = PathFromUtf8(*resolved);
|
||||||
if (!fs::is_regular_file(hostFile, ec)) {
|
if (!fs::is_regular_file(hostFile, ec)) {
|
||||||
++set.skippedExternals;
|
++set.skippedExternals;
|
||||||
continue;
|
continue;
|
||||||
@@ -227,7 +239,7 @@ void RiivoCollectMappings(const RiivolutionContract::Patch& patch, const std::st
|
|||||||
++set.skippedExternals;
|
++set.skippedExternals;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const fs::path hostFolder(*resolved);
|
const fs::path hostFolder = PathFromUtf8(*resolved);
|
||||||
if (!fs::is_directory(hostFolder, ec)) {
|
if (!fs::is_directory(hostFolder, ec)) {
|
||||||
++set.skippedExternals;
|
++set.skippedExternals;
|
||||||
continue;
|
continue;
|
||||||
@@ -246,7 +258,7 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::string gameId = RiivoGameId();
|
const std::string gameId = RiivoGameId();
|
||||||
const std::string sdRootGeneric = xmlSet->sdRoot.generic_string();
|
const std::string sdRootGeneric = RiivoGenericText(xmlSet->sdRoot);
|
||||||
const auto manifestSelections = RiivoManifestSelections();
|
const auto manifestSelections = RiivoManifestSelections();
|
||||||
|
|
||||||
// Dolphin-compatible remembered choices; a recomp.yml pin overrides them.
|
// Dolphin-compatible remembered choices; a recomp.yml pin overrides them.
|
||||||
@@ -261,19 +273,20 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
|
|||||||
for (const fs::path& xmlFile : xmlSet->xmlFiles) {
|
for (const fs::path& xmlFile : xmlSet->xmlFiles) {
|
||||||
const auto text = RiivoReadFile(xmlFile);
|
const auto text = RiivoReadFile(xmlFile);
|
||||||
if (!text) {
|
if (!text) {
|
||||||
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: cannot read " << xmlFile.string() << std::endl;
|
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: cannot read " << PathToUtf8(xmlFile)
|
||||||
|
<< std::endl;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto disc = RiivolutionContract::ParseString(*text);
|
auto disc = RiivolutionContract::ParseString(*text);
|
||||||
if (!disc) {
|
if (!disc) {
|
||||||
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << xmlFile.string()
|
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << PathToUtf8(xmlFile)
|
||||||
<< " is not a valid Riivolution XML (version 1 wiidisc); ignoring it"
|
<< " is not a valid Riivolution XML (version 1 wiidisc); ignoring it"
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!disc->IsValidForGame(gameId, std::nullopt, std::nullopt)) {
|
if (!disc->IsValidForGame(gameId, std::nullopt, std::nullopt)) {
|
||||||
RT_LOG(RT_TAG_RIIVOLUTION) << xmlFile.string() << ": not valid for " << gameId
|
RT_LOG(RT_TAG_RIIVOLUTION) << PathToUtf8(xmlFile) << ": not valid for " << gameId
|
||||||
<< ", skipped" << std::endl;
|
<< ", skipped" << std::endl;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -284,7 +297,7 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
|
|||||||
RiivolutionContract::ApplySelections(*disc, manifestSelections);
|
RiivolutionContract::ApplySelections(*disc, manifestSelections);
|
||||||
|
|
||||||
const auto activePatches = disc->GeneratePatches(gameId);
|
const auto activePatches = disc->GeneratePatches(gameId);
|
||||||
const std::string xmlDirGeneric = xmlFile.parent_path().generic_string();
|
const std::string xmlDirGeneric = RiivoGenericText(xmlFile.parent_path());
|
||||||
|
|
||||||
const size_t before = set.mappings.size();
|
const size_t before = set.mappings.size();
|
||||||
for (const auto& patch : activePatches) {
|
for (const auto& patch : activePatches) {
|
||||||
@@ -297,9 +310,10 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
|
|||||||
if (const auto resolvedSave = RiivolutionContract::MakeAbsoluteFromRelative(
|
if (const auto resolvedSave = RiivolutionContract::MakeAbsoluteFromRelative(
|
||||||
sdRootGeneric, xmlDirGeneric, savegame->external)) {
|
sdRootGeneric, xmlDirGeneric, savegame->external)) {
|
||||||
state.saveRedirect =
|
state.saveRedirect =
|
||||||
RuntimeRiivolution::SaveRedirect{fs::path(*resolvedSave), savegame->clone};
|
RuntimeRiivolution::SaveRedirect{PathFromUtf8(*resolvedSave),
|
||||||
|
savegame->clone};
|
||||||
RT_LOG(RT_TAG_RIIVOLUTION) << "savegame redirect: "
|
RT_LOG(RT_TAG_RIIVOLUTION) << "savegame redirect: "
|
||||||
<< state.saveRedirect->hostDirectory.string()
|
<< PathToUtf8(state.saveRedirect->hostDirectory)
|
||||||
<< (savegame->clone ? " (clone)" : "") << std::endl;
|
<< (savegame->clone ? " (clone)" : "") << std::endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,11 +322,11 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
|
|||||||
// A pack whose XML parses but activates nothing is the most confusing
|
// A pack whose XML parses but activates nothing is the most confusing
|
||||||
// failure this layer has: the game boots, plays, and quietly shows
|
// failure this layer has: the game boots, plays, and quietly shows
|
||||||
// vanilla content. Always say what happened.
|
// vanilla content. Always say what happened.
|
||||||
RT_LOG(RT_TAG_RIIVOLUTION) << xmlFile.string() << ": " << activePatches.size()
|
RT_LOG(RT_TAG_RIIVOLUTION) << PathToUtf8(xmlFile) << ": " << activePatches.size()
|
||||||
<< " active patch(es), " << (set.mappings.size() - before) << " mapping(s)"
|
<< " active patch(es), " << (set.mappings.size() - before) << " mapping(s)"
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
if (activePatches.empty()) {
|
if (activePatches.empty()) {
|
||||||
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << xmlFile.string()
|
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << PathToUtf8(xmlFile)
|
||||||
<< " has no enabled options for " << gameId
|
<< " has no enabled options for " << gameId
|
||||||
<< "; check the riivolution option selections (recomp.yml) or "
|
<< "; check the riivolution option selections (recomp.yml) or "
|
||||||
<< sdRootGeneric << "/riivolution/config/" << gameId.substr(0, 4) << ".xml"
|
<< sdRootGeneric << "/riivolution/config/" << gameId.substr(0, 4) << ".xml"
|
||||||
@@ -321,7 +335,7 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (set.skippedExternals != 0) {
|
if (set.skippedExternals != 0) {
|
||||||
RT_LOG(RT_TAG_RIIVOLUTION) << overlayRoot.string() << ": skipped "
|
RT_LOG(RT_TAG_RIIVOLUTION) << PathToUtf8(overlayRoot) << ": skipped "
|
||||||
<< set.skippedExternals << " mapping(s) whose external path does not exist"
|
<< set.skippedExternals << " mapping(s) whose external path does not exist"
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -259,6 +259,12 @@ uint32_t ExtractTvFormat(uint32_t tvMode) {
|
|||||||
// Re-entry guard to prevent AdvanceRetrace calling itself via OSWakeupThread -> SelectThread
|
// Re-entry guard to prevent AdvanceRetrace calling itself via OSWakeupThread -> SelectThread
|
||||||
static std::atomic<bool> s_inAdvanceRetrace{false};
|
static std::atomic<bool> s_inAdvanceRetrace{false};
|
||||||
|
|
||||||
|
// Set while VI_HLE_PresentFrame runs its seal/pace/pre-warm sequence. Guest callbacks
|
||||||
|
// serviced during that window (pace-loop alarms, GX timing polls) still see the stale
|
||||||
|
// hasValidXfb/g_auroraFrameActive flags; a retrace-context present fired from them would
|
||||||
|
// end the freshly pre-warmed empty frame and show it as a black frame group.
|
||||||
|
static std::atomic<bool> s_presentSequenceActive{false};
|
||||||
|
|
||||||
void AdvanceRetrace(CpuContext* ctx, Clock::time_point retraceStamp, bool serviceAurora) {
|
void AdvanceRetrace(CpuContext* ctx, Clock::time_point retraceStamp, bool serviceAurora) {
|
||||||
// Prevent re-entry - this can happen if OSWakeupThread triggers SelectThread
|
// Prevent re-entry - this can happen if OSWakeupThread triggers SelectThread
|
||||||
// which goes idle and calls ProcessTimerEvents again
|
// which goes idle and calls ProcessTimerEvents again
|
||||||
@@ -347,7 +353,7 @@ void AdvanceRetrace(CpuContext* ctx, Clock::time_point retraceStamp, bool servic
|
|||||||
// VISetBlack(TRUE) keeps frame submission running but shows only the clear color: GX render work is
|
// VISetBlack(TRUE) keeps frame submission running but shows only the clear color: GX render work is
|
||||||
// skipped and Aurora's end_frame() clears to black, matching the hardware manual's "signal continues,
|
// skipped and Aurora's end_frame() clears to black, matching the hardware manual's "signal continues,
|
||||||
// pixels go black" behavior. When not black, submission waits for hasXfbReady (GXCopyDisp done).
|
// pixels go black" behavior. When not black, submission waits for hasXfbReady (GXCopyDisp done).
|
||||||
if (serviceAurora) {
|
if (serviceAurora && !s_presentSequenceActive.load(std::memory_order_acquire)) {
|
||||||
const bool frameActive = g_auroraFrameActive.load(std::memory_order_acquire);
|
const bool frameActive = g_auroraFrameActive.load(std::memory_order_acquire);
|
||||||
const bool xfbMatches = (readyXfb != 0 && readyXfb == currentFb);
|
const bool xfbMatches = (readyXfb != 0 && readyXfb == currentFb);
|
||||||
const bool shouldPresentXfb = hasXfbReady && !isBlack && xfbMatches;
|
const bool shouldPresentXfb = hasXfbReady && !isBlack && xfbMatches;
|
||||||
@@ -512,6 +518,12 @@ void PaceToRetraceBoundary(Clock::time_point deadline) {
|
|||||||
// producer to the VI retrace boundary, and pre-warms the next frame. Paced from GXCopyDisp; unpaced for
|
// producer to the VI retrace boundary, and pre-warms the next frame. Paced from GXCopyDisp; unpaced for
|
||||||
// the retrace-context black/boot present path in AdvanceRetrace.
|
// the retrace-context black/boot present path in AdvanceRetrace.
|
||||||
void VI_HLE_PresentFrame(bool presentedXfb, bool paceToRetrace) {
|
void VI_HLE_PresentFrame(bool presentedXfb, bool paceToRetrace) {
|
||||||
|
if (s_presentSequenceActive.exchange(true, std::memory_order_acq_rel)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
struct SequenceGuard {
|
||||||
|
~SequenceGuard() { s_presentSequenceActive.store(false, std::memory_order_release); }
|
||||||
|
} sequenceGuard;
|
||||||
Clock::time_point paceDeadline{};
|
Clock::time_point paceDeadline{};
|
||||||
bool paceThisFrame = false;
|
bool paceThisFrame = false;
|
||||||
if (paceToRetrace) {
|
if (paceToRetrace) {
|
||||||
|
|||||||
+35
-11
@@ -47,6 +47,7 @@
|
|||||||
#include "system_bridge.h"
|
#include "system_bridge.h"
|
||||||
#include "ppc_runtime.h"
|
#include "ppc_runtime.h"
|
||||||
#include "aurora_events.h"
|
#include "aurora_events.h"
|
||||||
|
#include "wup028_adapter.h"
|
||||||
#include "fiber_manager.h"
|
#include "fiber_manager.h"
|
||||||
#include "hle_stubs.h"
|
#include "hle_stubs.h"
|
||||||
#include "runtime_config.h"
|
#include "runtime_config.h"
|
||||||
@@ -394,7 +395,7 @@ void InitializeProcessTranscript(int argc, char** argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::filesystem::path path = GetRunLogDirectory() / "console.log";
|
const std::filesystem::path path = GetRunLogDirectory() / "console.log";
|
||||||
state.file.open(path.string(), std::ios::out | std::ios::trunc | std::ios::binary);
|
state.file.open(path, std::ios::out | std::ios::trunc | std::ios::binary);
|
||||||
if (!state.file) {
|
if (!state.file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -570,14 +571,31 @@ std::string FormatHostStackTrace(unsigned framesToSkip) {
|
|||||||
for (USHORT i = 0; i < captured; ++i) {
|
for (USHORT i = 0; i < captured; ++i) {
|
||||||
const DWORD64 addr = reinterpret_cast<DWORD64>(frames[i]);
|
const DWORD64 addr = reinterpret_cast<DWORD64>(frames[i]);
|
||||||
HMODULE module = nullptr;
|
HMODULE module = nullptr;
|
||||||
char modulePath[MAX_PATH] = "?";
|
std::string modulePath = "?";
|
||||||
DWORD64 moduleBase = 0;
|
DWORD64 moduleBase = 0;
|
||||||
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||||
reinterpret_cast<LPCSTR>(frames[i]),
|
reinterpret_cast<LPCWSTR>(frames[i]),
|
||||||
&module) != 0 &&
|
&module) != 0 &&
|
||||||
module != nullptr) {
|
module != nullptr) {
|
||||||
moduleBase = reinterpret_cast<DWORD64>(module);
|
moduleBase = reinterpret_cast<DWORD64>(module);
|
||||||
(void)GetModuleFileNameA(module, modulePath, MAX_PATH);
|
std::wstring modulePathBuffer(MAX_PATH, L'\0');
|
||||||
|
for (;;) {
|
||||||
|
const DWORD length =
|
||||||
|
GetModuleFileNameW(module, modulePathBuffer.data(), static_cast<DWORD>(modulePathBuffer.size()));
|
||||||
|
if (length == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (length < modulePathBuffer.size()) {
|
||||||
|
modulePathBuffer.resize(length);
|
||||||
|
modulePath = RuntimeConfigFile::PathToUtf8(std::filesystem::path(modulePathBuffer));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Truncated; retry with a larger buffer up to the extended path limit.
|
||||||
|
if (modulePathBuffer.size() >= 32768) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
modulePathBuffer.resize(modulePathBuffer.size() * 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* symbolName = "?";
|
const char* symbolName = "?";
|
||||||
@@ -641,7 +659,7 @@ void WriteFatalLogImpl(std::string_view reason, std::string_view extraDetails =
|
|||||||
std::string fileName = "crash_";
|
std::string fileName = "crash_";
|
||||||
fileName.append(reason);
|
fileName.append(reason);
|
||||||
fileName.append(".txt");
|
fileName.append(".txt");
|
||||||
std::ofstream out((runDirectory / fileName).string(), std::ios::out | std::ios::trunc);
|
std::ofstream out(runDirectory / fileName, std::ios::out | std::ios::trunc);
|
||||||
if (!out) {
|
if (!out) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -680,11 +698,12 @@ void WriteFatalLogImpl(std::string_view reason, std::string_view extraDetails =
|
|||||||
// are large.
|
// are large.
|
||||||
static std::atomic_bool s_memorySnapshotWritten{false};
|
static std::atomic_bool s_memorySnapshotWritten{false};
|
||||||
if (!s_memorySnapshotWritten.exchange(true, std::memory_order_acq_rel)) {
|
if (!s_memorySnapshotWritten.exchange(true, std::memory_order_acq_rel)) {
|
||||||
SystemBridge::WriteGuestMemorySnapshot(out, (runDirectory / "mem1.bin").string().c_str());
|
SystemBridge::WriteGuestMemorySnapshot(out, runDirectory / "mem1.bin");
|
||||||
}
|
}
|
||||||
|
|
||||||
out.flush();
|
out.flush();
|
||||||
RT_LOG(RT_TAG_RUNTIME) << "crash artifacts written to " << runDirectory.string() << std::endl;
|
RT_LOG(RT_TAG_RUNTIME) << "crash artifacts written to "
|
||||||
|
<< RuntimeConfigFile::PathToUtf8(runDirectory) << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetRuntimeExitCodeImpl(int code) {
|
void SetRuntimeExitCodeImpl(int code) {
|
||||||
@@ -1183,10 +1202,11 @@ int RuntimeMain(int argc, char** argv) {
|
|||||||
std::filesystem::create_directories(rendererCacheDirectory, rendererPathError);
|
std::filesystem::create_directories(rendererCacheDirectory, rendererPathError);
|
||||||
if (rendererPathError) {
|
if (rendererPathError) {
|
||||||
RT_LOG(RT_TAG_RUNTIME) << "Unable to create renderer cache directory "
|
RT_LOG(RT_TAG_RUNTIME) << "Unable to create renderer cache directory "
|
||||||
<< rendererCacheDirectory << ": " << rendererPathError.message() << std::endl;
|
<< RuntimeConfigFile::PathToUtf8(rendererCacheDirectory) << ": "
|
||||||
|
<< rendererPathError.message() << std::endl;
|
||||||
}
|
}
|
||||||
const std::string auroraUserPath = applicationDataDirectory.string();
|
const std::string auroraUserPath = RuntimeConfigFile::PathToUtf8(applicationDataDirectory);
|
||||||
const std::string auroraCachePath = rendererCacheDirectory.string();
|
const std::string auroraCachePath = RuntimeConfigFile::PathToUtf8(rendererCacheDirectory);
|
||||||
auroraConfig.userPath = auroraUserPath.c_str();
|
auroraConfig.userPath = auroraUserPath.c_str();
|
||||||
auroraConfig.cachePath = auroraCachePath.c_str();
|
auroraConfig.cachePath = auroraCachePath.c_str();
|
||||||
auroraConfig.logCallback = &RuntimeAuroraLogCallback;
|
auroraConfig.logCallback = &RuntimeAuroraLogCallback;
|
||||||
@@ -1252,6 +1272,7 @@ int RuntimeMain(int argc, char** argv) {
|
|||||||
}
|
}
|
||||||
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
|
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
|
||||||
GxGuestWrite::InstallAuroraHooks();
|
GxGuestWrite::InstallAuroraHooks();
|
||||||
|
Wup028Adapter::Initialize();
|
||||||
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
|
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
|
||||||
auroraInfo.windowSize.native_fb_height);
|
auroraInfo.windowSize.native_fb_height);
|
||||||
settings_overlay::InitializeRuntimeSettings();
|
settings_overlay::InitializeRuntimeSettings();
|
||||||
@@ -1293,6 +1314,7 @@ int RuntimeMain(int argc, char** argv) {
|
|||||||
// Shutdown fiber system
|
// Shutdown fiber system
|
||||||
Fiber::GuestFiberManager::Shutdown();
|
Fiber::GuestFiberManager::Shutdown();
|
||||||
WindowPlacementPersistence::Flush(true);
|
WindowPlacementPersistence::Flush(true);
|
||||||
|
Wup028Adapter::Shutdown();
|
||||||
aurora_shutdown();
|
aurora_shutdown();
|
||||||
SetRuntimeExitCodeImpl(0);
|
SetRuntimeExitCodeImpl(0);
|
||||||
ShutdownProcessTranscript();
|
ShutdownProcessTranscript();
|
||||||
@@ -1310,6 +1332,7 @@ int RuntimeMain(int argc, char** argv) {
|
|||||||
SetRuntimeExitCodeImpl(1);
|
SetRuntimeExitCodeImpl(1);
|
||||||
Fiber::GuestFiberManager::Shutdown();
|
Fiber::GuestFiberManager::Shutdown();
|
||||||
WindowPlacementPersistence::Flush(true);
|
WindowPlacementPersistence::Flush(true);
|
||||||
|
Wup028Adapter::Shutdown();
|
||||||
aurora_shutdown();
|
aurora_shutdown();
|
||||||
ShutdownProcessTranscript();
|
ShutdownProcessTranscript();
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1321,6 +1344,7 @@ int RuntimeMain(int argc, char** argv) {
|
|||||||
SetRuntimeExitCodeImpl(1);
|
SetRuntimeExitCodeImpl(1);
|
||||||
Fiber::GuestFiberManager::Shutdown();
|
Fiber::GuestFiberManager::Shutdown();
|
||||||
WindowPlacementPersistence::Flush(true);
|
WindowPlacementPersistence::Flush(true);
|
||||||
|
Wup028Adapter::Shutdown();
|
||||||
aurora_shutdown();
|
aurora_shutdown();
|
||||||
ShutdownProcessTranscript();
|
ShutdownProcessTranscript();
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ std::vector<RecompMod::InitializerFn>& PostRelInitializers() {
|
|||||||
return initializers;
|
return initializers;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string>& OverlayRoots() {
|
std::vector<std::filesystem::path>& OverlayRoots() {
|
||||||
static std::vector<std::string> roots;
|
static std::vector<std::filesystem::path> roots;
|
||||||
return roots;
|
return roots;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,17 +279,17 @@ void RegisterDvdOverlayRoot(std::string root) {
|
|||||||
// against the executable directory instead.
|
// against the executable directory instead.
|
||||||
const std::filesystem::path base =
|
const std::filesystem::path base =
|
||||||
RuntimeConfigFile::ExecutableDirectory().value_or(std::filesystem::current_path());
|
RuntimeConfigFile::ExecutableDirectory().value_or(std::filesystem::current_path());
|
||||||
root = RuntimeConfigFile::ResolveRelativeTo(base, root).string();
|
std::filesystem::path resolved = RuntimeConfigFile::ResolveRelativeTo(base, root);
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(ModMutex());
|
std::lock_guard<std::mutex> lock(ModMutex());
|
||||||
auto& roots = OverlayRoots();
|
auto& roots = OverlayRoots();
|
||||||
const auto it = std::find(roots.begin(), roots.end(), root);
|
const auto it = std::find(roots.begin(), roots.end(), resolved);
|
||||||
if (it == roots.end()) {
|
if (it == roots.end()) {
|
||||||
roots.push_back(std::move(root));
|
roots.push_back(std::move(resolved));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<std::string>& DvdOverlayRoots() {
|
const std::vector<std::filesystem::path>& DvdOverlayRoots() {
|
||||||
return OverlayRoots();
|
return OverlayRoots();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "settings_overlay.h"
|
#include "settings_overlay.h"
|
||||||
|
#include "wup028_adapter.h"
|
||||||
#include "audio_backend.h"
|
#include "audio_backend.h"
|
||||||
|
#include "controller_mapping_wizard.h"
|
||||||
#include "game_graphics_options.h"
|
#include "game_graphics_options.h"
|
||||||
#include "music_attenuation.h"
|
#include "music_attenuation.h"
|
||||||
#include "runtime_config.h"
|
#include "runtime_config.h"
|
||||||
@@ -310,6 +312,34 @@ void ApplyConfiguredMappings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DrawGameCubeAdapterInfo() {
|
||||||
|
ImGui::Separator();
|
||||||
|
if (!ImGui::BeginMenu("GameCube adapter info")) return;
|
||||||
|
|
||||||
|
const auto adapter = Wup028Adapter::GetInfo();
|
||||||
|
const char* state = adapter.state == Wup028Adapter::ConnectionState::Connected
|
||||||
|
? "Connected"
|
||||||
|
: adapter.state == Wup028Adapter::ConnectionState::DriverError ? "Driver error"
|
||||||
|
: "Searching";
|
||||||
|
ImGui::Text("Status: %s", state);
|
||||||
|
if (!adapter.deviceName.empty()) {
|
||||||
|
ImGui::Text("Device: %s", adapter.deviceName.c_str());
|
||||||
|
}
|
||||||
|
ImGui::TextWrapped("%s", adapter.detail.c_str());
|
||||||
|
if (adapter.state == Wup028Adapter::ConnectionState::Connected) {
|
||||||
|
ImGui::Text("Poll rate: %.1f reports/s", adapter.pollRateHz);
|
||||||
|
ImGui::Text("Endpoints: IN 0x%02X, OUT 0x%02X", adapter.inputEndpoint, adapter.outputEndpoint);
|
||||||
|
for (size_t port = 0; port < adapter.ports.size(); ++port) {
|
||||||
|
const uint8_t type = adapter.portStatus[port] & 0x30;
|
||||||
|
const char* typeName = type == 0x10 ? "wired" : type == 0x20 ? "wireless" : "none";
|
||||||
|
ImGui::Text("Adapter port %u: %s (type %s, raw 0x%02X)", static_cast<unsigned>(port + 1),
|
||||||
|
adapter.ports[port] ? "Controller connected" : "Empty", typeName,
|
||||||
|
adapter.portStatus[port]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
void DrawControllerSettings() {
|
void DrawControllerSettings() {
|
||||||
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||||
const std::string label = "Port " + std::to_string(port + 1);
|
const std::string label = "Port " + std::to_string(port + 1);
|
||||||
@@ -320,20 +350,60 @@ void DrawControllerSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort);
|
||||||
|
const int adapterAssignment = Wup028Adapter::GetPortAssignment(selectedGamePort);
|
||||||
|
if (adapterAssignment >= 0) {
|
||||||
|
ImGui::Text("Assigned: GameCube adapter port %d", adapterAssignment + 1);
|
||||||
|
} else {
|
||||||
|
const char* currentName = PADGetName(selectedGamePort);
|
||||||
|
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
|
||||||
|
}
|
||||||
|
if (ImGui::BeginMenu("Assign GameCube adapter port")) {
|
||||||
|
if (ImGui::MenuItem("None", nullptr, adapterAssignment < 0)) {
|
||||||
|
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
|
||||||
|
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
|
||||||
|
}
|
||||||
|
const auto adapter = Wup028Adapter::GetInfo();
|
||||||
|
for (int physicalPort = 0; physicalPort < PAD_CHANMAX; ++physicalPort) {
|
||||||
|
const std::string label = "Adapter port " + std::to_string(physicalPort + 1) +
|
||||||
|
(adapter.ports[static_cast<size_t>(physicalPort)] ? " (connected)" : " (empty)");
|
||||||
|
if (ImGui::MenuItem(label.c_str(), nullptr, adapterAssignment == physicalPort)) {
|
||||||
|
for (uint32_t gamePort = 0; gamePort < PAD_CHANMAX; ++gamePort) {
|
||||||
|
if (gamePort != selectedGamePort && Wup028Adapter::GetPortAssignment(gamePort) == physicalPort) {
|
||||||
|
RuntimeConfigFile::SetGameCubeAdapterPort(gamePort, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PADClearPort(selectedGamePort);
|
||||||
|
Wup028Adapter::SetPortAssignment(selectedGamePort, physicalPort);
|
||||||
|
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, physicalPort);
|
||||||
|
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem("Unassign controller")) {
|
||||||
|
PADClearPort(selectedGamePort);
|
||||||
|
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
|
||||||
|
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
|
||||||
|
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
controller_mapping_wizard::DrawSetupList();
|
||||||
const uint32_t controllerCount = PADCount();
|
const uint32_t controllerCount = PADCount();
|
||||||
if (controllerCount == 0) {
|
if (controllerCount == 0) {
|
||||||
ImGui::TextDisabled("No controller connected");
|
ImGui::TextDisabled("No controller connected");
|
||||||
|
DrawGameCubeAdapterInfo();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* currentName = PADGetName(static_cast<uint32_t>(g_controllerPort));
|
|
||||||
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
|
|
||||||
if (ImGui::BeginMenu("Assign connected controller")) {
|
if (ImGui::BeginMenu("Assign connected controller")) {
|
||||||
for (uint32_t index = 0; index < controllerCount; ++index) {
|
for (uint32_t index = 0; index < controllerCount; ++index) {
|
||||||
const char* name = PADGetNameForControllerIndex(index);
|
const char* name = PADGetNameForControllerIndex(index);
|
||||||
ImGui::PushID(static_cast<int>(index));
|
ImGui::PushID(static_cast<int>(index));
|
||||||
if (ImGui::MenuItem(name != nullptr ? name : "Unknown controller")) {
|
if (ImGui::MenuItem(name != nullptr ? name : "Unknown controller")) {
|
||||||
PADSetPortForIndex(index, static_cast<uint32_t>(g_controllerPort));
|
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
|
||||||
|
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
|
||||||
|
PADSetPortForIndex(index, selectedGamePort);
|
||||||
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
||||||
ApplyConfiguredMappings();
|
ApplyConfiguredMappings();
|
||||||
}
|
}
|
||||||
@@ -346,6 +416,7 @@ void DrawControllerSettings() {
|
|||||||
PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount);
|
PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount);
|
||||||
if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) {
|
if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) {
|
||||||
ImGui::TextDisabled("Assign a controller to edit its buttons");
|
ImGui::TextDisabled("Assign a controller to edit its buttons");
|
||||||
|
DrawGameCubeAdapterInfo();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,7 +556,7 @@ void DrawControllerSettings() {
|
|||||||
ImGui::TextUnformatted(kControllerButtons[i].label);
|
ImGui::TextUnformatted(kControllerButtons[i].label);
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
DrawGameCubeAdapterInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawAudioSettings() {
|
void DrawAudioSettings() {
|
||||||
@@ -840,6 +911,7 @@ void PersistDisplayModeIfChanged() {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void InitializeRuntimeSettings() noexcept {
|
void InitializeRuntimeSettings() noexcept {
|
||||||
|
controller_mapping_wizard::LoadPersistedMappings();
|
||||||
ApplyConfiguredMappings();
|
ApplyConfiguredMappings();
|
||||||
AudioBackend::Instance().SetMasterVolume(static_cast<float>(g_audioVolumePercent) / 100.0f);
|
AudioBackend::Instance().SetMasterVolume(static_cast<float>(g_audioVolumePercent) / 100.0f);
|
||||||
AudioBackend::Instance().SetMuted(g_audioMuted);
|
AudioBackend::Instance().SetMuted(g_audioMuted);
|
||||||
@@ -872,6 +944,7 @@ void HandleEvents(const AuroraEvent* events) noexcept {
|
|||||||
if (ev->type != AURORA_SDL_EVENT) {
|
if (ev->type != AURORA_SDL_EVENT) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
controller_mapping_wizard::HandleSdlEvent(ev->sdl);
|
||||||
if (IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) {
|
if (IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) {
|
||||||
SetTopBarVisible(!g_topBarVisible);
|
SetTopBarVisible(!g_topBarVisible);
|
||||||
}
|
}
|
||||||
@@ -893,6 +966,10 @@ void Draw() noexcept {
|
|||||||
}
|
}
|
||||||
DrawFpsOverlay();
|
DrawFpsOverlay();
|
||||||
DrawTopBar();
|
DrawTopBar();
|
||||||
|
controller_mapping_wizard::Draw();
|
||||||
|
// The wizard captures raw presses; keep them out of the game even when the
|
||||||
|
// top bar is hidden mid-setup.
|
||||||
|
PADBlockInput(g_topBarVisible || controller_mapping_wizard::IsActive());
|
||||||
DrawStartupScreen();
|
DrawStartupScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "memory.h"
|
#include "memory.h"
|
||||||
#include "ppc_runtime.h"
|
#include "ppc_runtime.h"
|
||||||
#include "recomp_mod_loader.h"
|
#include "recomp_mod_loader.h"
|
||||||
|
#include "runtime_config.h"
|
||||||
#include "runtime_log.h"
|
#include "runtime_log.h"
|
||||||
#include "runtime_product.h"
|
#include "runtime_product.h"
|
||||||
#include "timebase_contract.h"
|
#include "timebase_contract.h"
|
||||||
@@ -409,14 +410,14 @@ void SystemBridge::Initialize() {
|
|||||||
RT_LOG(RT_TAG_RUNTIME) << "Executed " << count << " static constructors." << std::endl;
|
RT_LOG(RT_TAG_RUNTIME) << "Executed " << count << " static constructors." << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const char* mem1Path) {
|
void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const std::filesystem::path& mem1Path) {
|
||||||
constexpr uint32_t kMem1Base = 0x80000000u;
|
constexpr uint32_t kMem1Base = 0x80000000u;
|
||||||
constexpr uint32_t kMem1Size = 0x01800000u;
|
constexpr uint32_t kMem1Size = 0x01800000u;
|
||||||
if (Memory::Contains(kMem1Base, kMem1Size)) {
|
if (Memory::Contains(kMem1Base, kMem1Size)) {
|
||||||
std::ofstream dump(mem1Path, std::ios::binary | std::ios::trunc);
|
std::ofstream dump(mem1Path, std::ios::binary | std::ios::trunc);
|
||||||
dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem1Base, kMem1Size)),
|
dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem1Base, kMem1Size)),
|
||||||
kMem1Size);
|
kMem1Size);
|
||||||
os << "[runtime] MEM1 snapshot written to " << mem1Path
|
os << "[runtime] MEM1 snapshot written to " << RuntimeConfigFile::PathToUtf8(mem1Path)
|
||||||
<< (dump.good() ? "" : " (write failed)") << std::endl;
|
<< (dump.good() ? "" : " (write failed)") << std::endl;
|
||||||
}
|
}
|
||||||
constexpr uint32_t kMem2Base = 0x90000000u;
|
constexpr uint32_t kMem2Base = 0x90000000u;
|
||||||
@@ -424,11 +425,12 @@ void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const char* mem1Pa
|
|||||||
if (!Memory::Contains(kMem2Base, mem2Size)) {
|
if (!Memory::Contains(kMem2Base, mem2Size)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const std::string mem2Path = std::string(mem1Path) + ".mem2";
|
std::filesystem::path mem2Path = mem1Path;
|
||||||
|
mem2Path += ".mem2";
|
||||||
std::ofstream dump(mem2Path, std::ios::binary | std::ios::trunc);
|
std::ofstream dump(mem2Path, std::ios::binary | std::ios::trunc);
|
||||||
dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem2Base, mem2Size)),
|
dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem2Base, mem2Size)),
|
||||||
mem2Size);
|
mem2Size);
|
||||||
os << "[runtime] MEM2 snapshot written to " << mem2Path
|
os << "[runtime] MEM2 snapshot written to " << RuntimeConfigFile::PathToUtf8(mem2Path)
|
||||||
<< (dump.good() ? "" : " (write failed)") << std::endl;
|
<< (dump.good() ? "" : " (write failed)") << std::endl;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,145 +1,77 @@
|
|||||||
|
|
||||||
# Wiicompiled Static Recompiler
|
# Wiicompiled Static Recompiler
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The translator parses GameCube/Wii DOL files, decodes PowerPC instructions, lifts them through
|
The translator parses GameCube/Wii DOL files, decodes PowerPC instructions, lifts them through
|
||||||
|
|
||||||
IR/SSA and type inference, and emits C++ that is compiled into a native executable together with
|
IR/SSA and type inference, and emits C++ that is compiled into a native executable together with
|
||||||
|
|
||||||
a runtime in `runtime/`. Project-specific paths and addresses are supplied through a versioned
|
a runtime in `runtime/`. Project-specific paths and addresses are supplied through a versioned
|
||||||
|
|
||||||
YAML manifest, so the translator itself contains no game-specific data.
|
YAML manifest, so the translator itself contains no game-specific data.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
> Translating a DOL does not exempt you from owning the game it came from.
|
> Translating a DOL does not exempt you from owning the game it came from.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## How translating a DOL works
|
## How translating a DOL works
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
A project manifest (YAML) names the input DOL and pins its layout; everything else is derived.
|
A project manifest (YAML) names the input DOL and pins its layout; everything else is derived.
|
||||||
|
|
||||||
Translation is four commands:
|
Translation is four commands:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
1. **`translate-recursive <entry-point> --project <manifest>`** - walks the call graph from the
|
1. **`translate-recursive <entry-point> --project <manifest>`** - walks the call graph from the
|
||||||
|
|
||||||
entry point, decodes every reachable function, and emits C++ (plus JSON metadata describing
|
entry point, decodes every reachable function, and emits C++ (plus JSON metadata describing
|
||||||
|
|
||||||
what was emitted).
|
what was emitted).
|
||||||
|
|
||||||
2. **`generate-data-init --project <manifest>`** - writes the embedded `.data`/`.rodata`/`.sdata`
|
2. **`generate-data-init --project <manifest>`** - writes the embedded `.data`/`.rodata`/`.sdata`
|
||||||
|
|
||||||
section initializer and `RuntimeConfig.h`.
|
section initializer and `RuntimeConfig.h`.
|
||||||
|
|
||||||
3. **`emit-build-shards --project <manifest>`** - emits the CMake build graph (`shards.cmake`)
|
3. **`emit-build-shards --project <manifest>`** - emits the CMake build graph (`shards.cmake`)
|
||||||
|
|
||||||
covering both generated sources and `runtime/src`.
|
covering both generated sources and `runtime/src`.
|
||||||
|
|
||||||
4. **CMake + Ninja with Clang** compiles `runtime/` plus the generated output into one executable.
|
4. **CMake + Ninja with Clang** compiles `runtime/` plus the generated output into one executable.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Discovery is purely recursive from the entry point unless the manifest provides an optional
|
Discovery is purely recursive from the entry point unless the manifest provides an optional
|
||||||
|
|
||||||
`function_map` (one `hexaddr name` per line) that seeds additional function boundaries. Unsupported
|
`function_map` (one `hexaddr name` per line) that seeds additional function boundaries. Unsupported
|
||||||
|
|
||||||
instructions fail translation by default.
|
instructions fail translation by default.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
See `projects/examples/generic-dol.yml` for a minimal manifest driven by `RECOMP_GENERIC_DOL`.
|
See `projects/examples/generic-dol.yml` for a minimal manifest driven by `RECOMP_GENERIC_DOL`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| Tool | Notes |
|
| Tool | Notes |
|
||||||
|
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|
||||||
| .NET 8 SDK | Builds and runs the translator. |
|
| .NET 8 SDK | Builds and runs the translator. |
|
||||||
|
|
||||||
| CMake ≥ 3.16 and Ninja | Configures and drives the native build. |
|
| CMake ≥ 3.16 and Ninja | Configures and drives the native build. |
|
||||||
|
|
||||||
| Clang / LLVM | The shipped build uses LLVM-MinGW targeting `x86-64-v3`. MSVC is not the tested path. |
|
| Clang / LLVM | The shipped build uses LLVM-MinGW targeting `x86-64-v3`. MSVC is not the tested path. |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Build the CLI once and invoke the assembly directly:
|
Build the CLI once and invoke the assembly directly:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|
||||||
dotnet build translator/src/Translator.Cli/Translator.Cli.csproj -c Release
|
dotnet build translator/src/Translator.Cli/Translator.Cli.csproj -c Release
|
||||||
|
|
||||||
$translator = 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll'
|
$translator = 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll'
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Manifest essentials
|
## Manifest essentials
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- `inputs.dol.path` - the DOL to translate; optional SHA-256 pinning rejects wrong revisions.
|
- `inputs.dol.path` - the DOL to translate; optional SHA-256 pinning rejects wrong revisions.
|
||||||
|
|
||||||
- `memory.base` / `size` - guest address space.
|
- `memory.base` / `size` - guest address space.
|
||||||
|
|
||||||
- `memory.sda_base` / `sda2_base` - the r13/r2 Small Data Area bases your DOL's boot code installs
|
- `memory.sda_base` / `sda2_base` - the r13/r2 Small Data Area bases your DOL's boot code installs
|
||||||
|
|
||||||
(`lis`/`ori` pairs in `__init_registers`). Required by any command that writes `RuntimeConfig.h`;
|
(`lis`/`ori` pairs in `__init_registers`). Required by any command that writes `RuntimeConfig.h`;
|
||||||
|
|
||||||
the translator does not guess them.
|
the translator does not guess them.
|
||||||
|
|
||||||
- `translation.function_map.path` - optional symbol map used as the discovery oracle.
|
- `translation.function_map.path` - optional symbol map used as the discovery oracle.
|
||||||
|
|
||||||
- `translation.allow_unsupported_instructions` - off by default; enabling it emits runtime traps
|
- `translation.allow_unsupported_instructions` - off by default; enabling it emits runtime traps
|
||||||
|
|
||||||
instead of failing, and such a build can never ship.
|
instead of failing, and such a build can never ship.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Relative paths resolve from `workspace_root`, which itself resolves from the manifest directory.
|
Relative paths resolve from `workspace_root`, which itself resolves from the manifest directory.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- `info [--project path]`
|
- `info [--project path]`
|
||||||
|
|
||||||
- `translate-recursive <address> --project path`
|
- `translate-recursive <address> --project path`
|
||||||
|
|
||||||
- `generate-data-init --project path`
|
- `generate-data-init --project path`
|
||||||
|
|
||||||
- `emit-base-manifest --project path`
|
- `emit-base-manifest --project path`
|
||||||
|
|
||||||
- `emit-build-shards --project path`
|
- `emit-build-shards --project path`
|
||||||
|
|
||||||
- `translate-mod --project path [--profile name] ...` - static Kamek/Pulsar module translation
|
- `translate-mod --project path [--profile name] ...` - static Kamek/Pulsar module translation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Any command prints its own option list with `--help`.
|
Any command prints its own option list with `--help`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Test
|
## Test
|
||||||
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|
||||||
dotnet test translator/Translator.sln -c Release
|
dotnet test translator/Translator.sln -c Release
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user