mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-13 09:59:47 -04:00
ca7d126a13
* Bluetooth Wii Remote support: the game reads a real Wii Remote through KPAD Enable SDL3's HIDAPI Wii driver and hand a paired Wii Remote (bare or with Nunchuk) to the game as a real Wii Remote: WPADProbe reports CORE/FREESTYLE and KPADRead fills KPADStatus[0] from SDL every frame (buttons, accelerometer in KPAD's g frame, Nunchuk stick and accelerometer), while the GameCube pad view of that port reports no controller. The game's own motion code then handles wheelies, tricks and Wii Wheel steering. Classic Controllers and Wii U Pro Controllers keep going through the GameCube pad path with a default button table picked by name. SDL's Wii driver drops a remote on a failed Bluetooth read or when the Nunchuk is plugged or unplugged and never re-adds it, so the runtime keeps rescanning (Dolphin style) while no Wii controller is present by toggling the driver hint off and, a few frames later, on again; a dropped remote is back within 1-2 s. Settings live in the F10 overlay under Wii Remotes (Bluetooth) and in Config.toml (wii_remotes, wii_continuous_scan). * Fix Wii U Pro / Classic Controller ZL and ZR not registering SDL's Wii driver reports ZL/ZR as the LEFT_TRIGGER/RIGHT_TRIGGER analog axes, never as digital shoulder buttons. Binding them to LEFT_SHOULDER/RIGHT_SHOULDER meant they never fired and also disabled aurora's own analog-trigger fallback (a button table entry for PAD_TRIGGER_L/R marks the trigger as "handled", even when the bound digital button never actually presses). Leaving them unbound lets the default axis mapping drive them like every other analog-trigger pad. Reported by an end-to-end tester connecting a real Classic Controller to a Wii Remote. * Wii Remotes menu: live raw D-pad/ZL/ZR readout for Classic Controller / Wii U Pro Diagnostic aid for a reported issue where the Classic Controller's D-pad does not do anything in-game (no wheelies). Shows what SDL itself sees so a driver-level problem (nothing lights up) can be told apart from a mapping problem (it lights up but the game does not react). * Fix Classic Controller D-pad input * Address CodeRabbit review on PR #73 - PADRead: hide KPAD-served ports even while input is blocked so the port error state does not flip when the overlay opens/closes. - WPADProbe: run the Wii Remote rescan state machine before probing so a reconnect probe before the next PADRead can see the remote. - EnsureSensors: only cache the gamepad id once every accelerometer enabled, so a failed activation is retried. - ConfigureSdlHints: reset the in-flight rescan bookkeeping. - Settings overlay: disable "Rescan now" while Wii Remotes are turned off. * Bluetooth Wii Remote: fix wheel steering, native Classic Controller, extension hot-swap Accelerometer - The SDL -> KPAD conversion negated the wrong axis: SDL's z is the remote's +Y (towards the user), so KPAD acc is (-wiiX, -wiiZ, +wiiY). Fixes mirrored Wii Wheel steering. - Drop reports whose accelerometer bytes arrive zeroed (+-5.12 g on every axis, a few times a minute over Bluetooth) and repeat the last good sample; they read as a full-lock steer plus a 9 g shake. - One-button zero-point calibration in the overlay (remote flat, buttons up), stored in Config.toml as wii_accel_offset_x/y/z. SDL's read of the remote's factory calibration times out over Bluetooth and falls back to a nominal zero point, which left a per-axis bias of up to ~0.3 g on the tested remote. - Live accelerometer readout and an optional per-frame CSV trace (wii_accel_trace = true) for debugging. Classic Controller through KPAD/WPAD - WPADProbe reports WPAD_DEV_CLASSIC; KPADRead fills ex_status.cl and KPADGetUnifiedWpadStatus the raw WPADCLStatus (WPAD_CL_BUTTON_* bits, sticks in the SDK's signed -512..511 range, triggers), so the game shows the Classic layout and icons and no button mapping is involved. Ports served through KPAD are hidden from PADRead; only the Wii U Pro Controller stays a GameCube pad. Extension hot-swap - SDL's Wii driver destroys the joystick on an extension change but keeps the HID handle open, and HIDAPI never re-creates a joystick for such a device. Patch the vendored SDL at configure time (AuroraSDL3Patches.cmake, wired into AuroraSDL3Provider.cmake for both the downloaded tarball and a pre-provided FETCHCONTENT_SOURCE_DIR_SDL) so the joystick is rebuilt in place with the new extension type, without touching the Bluetooth handle. - Keep a vanished remote's channel alive with neutral input for up to 3 s while SDL re-creates the joystick, so the game never sees a disconnection. The driver-hint rescan stays as a fallback for real drops, starting 3 s after the loss, and also runs from the overlay's per-frame Draw. Log rescans. Mappings / overlay - Do not apply the shared positional [controller] bindings to Wii pads: that override is what made a Classic Controller's A/B and X/Y look swapped. - Raw D-pad fallback also for the Wii U Pro Controller; overlay readouts read joystick buttons directly (SDL's generated HIDAPI mapping expects a hat). - Overlay: Classic Controller readout, accelerometer readout and calibration. - README: Bluetooth Wii Remote section and known limitations. * Review pass on the Wii Remote input path - EffectiveKind: stop bridging an extension swap once a different controller has taken the port, and note that everything touching the scanner state runs on the guest thread. - KPADGetUnifiedWpadStatus: fill every requested entry (the SDK returns `count` recent samples), capped at KPAD's 16 read buffers. - IsKpadKind gets internal linkage; the calibration accessors get their comments; clarify why Draw() also runs Poll(). * Drop the dead Classic-Controller-as-GameCube-pad matching A Wii Remote with a Classic Controller is served through KPAD and its port is hidden from PADRead, so the name matches that once gave it a GameCube button table and the raw D-pad fallback could never take effect any more. Both now match only the Wii U Pro Controller, and the default table is renamed accordingly (g_defaultButtonsWiiUPro). --------- Co-authored-by: LOL <andresguerra2k26@gmail.com> Co-authored-by: Nick <89667145+Nick1232345@users.noreply.github.com>
1516 lines
59 KiB
C++
1516 lines
59 KiB
C++
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cctype>
|
|
#include <chrono>
|
|
#include <csignal>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <exception>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <array>
|
|
#include <limits>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <mutex>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
#if !defined(_WIN32)
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#if defined(_WIN32)
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <fcntl.h>
|
|
#include <io.h>
|
|
#include <crtdbg.h>
|
|
#include <windows.h>
|
|
#include <mmsystem.h>
|
|
#include <dbghelp.h>
|
|
#else
|
|
#include <signal.h>
|
|
#if defined(__x86_64__)
|
|
// Only the x86 POSIX fault path inspects ucontext_t to recover the page-fault
|
|
// write bit. macOS deprecates ucontext and requires _XOPEN_SOURCE just to
|
|
// include the header, while the arm64 handler does not use it at all.
|
|
#include <ucontext.h>
|
|
#endif
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include "abi_bridge.h"
|
|
#include "guest_flat_memory.h"
|
|
#include "gx_guest_write.h"
|
|
#include "memory.h"
|
|
#include "system_bridge.h"
|
|
#include "ppc_runtime.h"
|
|
#include "aurora_events.h"
|
|
#include "wii_remote_input.h"
|
|
#include "discord_presence.h"
|
|
#include "fiber_manager.h"
|
|
#include "hle_stubs.h"
|
|
#include "runtime_config.h"
|
|
#include "runtime_log.h"
|
|
#include "runtime_product.h"
|
|
#include "recomp_mod_loader.h"
|
|
#include <aurora/aurora.h>
|
|
#include <aurora/gfx.h>
|
|
#include <dolphin/gx/GXAurora.h>
|
|
#include <dolphin/vi.h>
|
|
|
|
// Defined in `runtime/src/hle/vi.cpp` (used by GX/VI HLE).
|
|
extern std::atomic_bool g_auroraFrameActive;
|
|
extern "C" int g_gxFrameCount;
|
|
extern "C" const char* DVDResolveHostPathForTest(const char* dvdPath);
|
|
bool OS_HLE_InterruptsEnabled() noexcept;
|
|
|
|
namespace {
|
|
|
|
// Defined below, beside the fatal-log machinery.
|
|
std::string FormatHostStackTrace(unsigned framesToSkip = 0);
|
|
|
|
void ServiceGuestTimingDuringAuroraFrameWait() {
|
|
// Aurora can block inside FIFO drains before control returns to GX HLE, for as long as a
|
|
// whole display period. Keep VI retraces, alarms and audio moving at wall-clock cadence here
|
|
// while still suppressing guest rescheduling and recursive Aurora work.
|
|
VI_HLE_ProcessRetracesDeferred(8);
|
|
OS_HLE_ProcessAlarmsDeferred(8);
|
|
Audio_HLE_PollDeferred();
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
int __cdecl WindowsCrtReportHook(int reportType, char* message, int* returnValue) {
|
|
if (returnValue) {
|
|
*returnValue = 0;
|
|
}
|
|
|
|
RT_LOG(RT_TAG_RUNTIME) << "CRT report type=" << reportType;
|
|
if (message) {
|
|
std::cerr << ": " << message;
|
|
} else {
|
|
std::cerr << '\n';
|
|
}
|
|
|
|
RT_LOG(RT_TAG_RUNTIME) << "CRT report stack:\n" << FormatHostStackTrace(1);
|
|
std::cerr.flush();
|
|
return TRUE;
|
|
}
|
|
|
|
void ConfigureWindowsFatalDialogBehavior() {
|
|
::SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);
|
|
_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
|
|
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
|
|
_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
|
|
_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE);
|
|
_CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR);
|
|
_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
|
|
_CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
|
|
_CrtSetReportHook(WindowsCrtReportHook);
|
|
}
|
|
|
|
class WindowsTimerResolutionGuard {
|
|
public:
|
|
WindowsTimerResolutionGuard() {
|
|
const MMRESULT result = ::timeBeginPeriod(1);
|
|
if (result == TIMERR_NOERROR) {
|
|
armed_ = true;
|
|
} else {
|
|
RT_LOG(RT_TAG_RUNTIME) << "timeBeginPeriod(1) failed: " << result << std::endl;
|
|
}
|
|
}
|
|
|
|
~WindowsTimerResolutionGuard() {
|
|
if (armed_) {
|
|
::timeEndPeriod(1);
|
|
}
|
|
}
|
|
|
|
WindowsTimerResolutionGuard(const WindowsTimerResolutionGuard&) = delete;
|
|
WindowsTimerResolutionGuard& operator=(const WindowsTimerResolutionGuard&) = delete;
|
|
|
|
private:
|
|
bool armed_ = false;
|
|
};
|
|
#endif
|
|
} // namespace
|
|
|
|
namespace {
|
|
|
|
std::string g_lastEntryLabel;
|
|
std::atomic_bool g_auroraInitialized{false};
|
|
std::atomic_flag g_abortSignalHandled = ATOMIC_FLAG_INIT;
|
|
std::atomic_bool g_fatalErrorReported{false};
|
|
std::atomic_bool g_fatalPopupShown{false};
|
|
std::atomic<int> g_lastExitCode{0};
|
|
std::atomic_bool g_exitCodeSet{false};
|
|
|
|
struct ProcessTranscriptState {
|
|
bool enabled = false;
|
|
std::filesystem::path path;
|
|
std::ofstream file;
|
|
std::mutex fileMutex;
|
|
std::atomic_bool initialized{false};
|
|
int savedStdoutFd = -1;
|
|
int savedStderrFd = -1;
|
|
int stdoutPipeReadFd = -1;
|
|
int stdoutPipeWriteFd = -1;
|
|
int stderrPipeReadFd = -1;
|
|
int stderrPipeWriteFd = -1;
|
|
std::thread stdoutThread;
|
|
std::thread stderrThread;
|
|
};
|
|
|
|
std::filesystem::path GetDefaultRuntimeLogDirectory() {
|
|
return RuntimeConfigFile::ApplicationDataDirectory() / "Logs";
|
|
}
|
|
|
|
// Every entry in the Logs root - both the per-run folders written by this
|
|
// scheme and any flat .log files left over from the previous one - is removed
|
|
// once it is older than the retention window.
|
|
void PruneOldRunLogs(const std::filesystem::path& logRoot) {
|
|
constexpr auto kRetention = std::chrono::hours(24 * 4);
|
|
|
|
std::error_code ec;
|
|
const auto now = std::filesystem::file_time_type::clock::now();
|
|
for (const auto& entry : std::filesystem::directory_iterator(logRoot, ec)) {
|
|
std::error_code entryEc;
|
|
const auto writeTime = std::filesystem::last_write_time(entry.path(), entryEc);
|
|
if (entryEc) {
|
|
continue;
|
|
}
|
|
if (now - writeTime > kRetention) {
|
|
std::filesystem::remove_all(entry.path(), entryEc);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Logs/<product>_<epochSeconds>_pid<pid>/ - one folder per run. The console
|
|
// transcript and every crash artifact for the run land in here, so "zip this
|
|
// folder" is a complete diagnostic. Created lazily so even a crash before
|
|
// transcript setup still has somewhere to write.
|
|
const std::filesystem::path& GetRunLogDirectory() {
|
|
static const std::filesystem::path runDirectory = [] {
|
|
const std::filesystem::path logRoot = GetDefaultRuntimeLogDirectory();
|
|
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(logRoot, ec);
|
|
PruneOldRunLogs(logRoot);
|
|
|
|
#if defined(_WIN32)
|
|
const unsigned long pid = ::GetCurrentProcessId();
|
|
#else
|
|
const auto pid = static_cast<unsigned long>(::getpid());
|
|
#endif
|
|
const auto now = std::chrono::system_clock::now();
|
|
const auto secs = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
|
|
|
|
std::ostringstream name;
|
|
name << (RuntimeProduct::IsRetroRewind() ? "retro_rewind" : "base")
|
|
<< "_" << secs << "_pid" << pid;
|
|
const std::filesystem::path directory = logRoot / name.str();
|
|
std::filesystem::create_directories(directory, ec);
|
|
return directory;
|
|
}();
|
|
return runDirectory;
|
|
}
|
|
|
|
ProcessTranscriptState& GetProcessTranscriptState() {
|
|
static ProcessTranscriptState state;
|
|
return state;
|
|
}
|
|
|
|
void WriteProcessTranscriptChunk(ProcessTranscriptState& state, const char* data, size_t size) {
|
|
if (!state.enabled || !state.file || size == 0) {
|
|
return;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(state.fileMutex);
|
|
state.file.write(data, static_cast<std::streamsize>(size));
|
|
state.file.flush();
|
|
}
|
|
|
|
void PumpTranscriptPipe(ProcessTranscriptState& state, int readFd, int mirrorFd) {
|
|
std::array<char, 4096> buffer{};
|
|
for (;;) {
|
|
#if defined(_WIN32)
|
|
const int bytesRead = _read(readFd, buffer.data(), static_cast<unsigned int>(buffer.size()));
|
|
#else
|
|
const ssize_t bytesRead = ::read(readFd, buffer.data(), buffer.size());
|
|
#endif
|
|
if (bytesRead <= 0) {
|
|
break;
|
|
}
|
|
|
|
if (mirrorFd >= 0) {
|
|
size_t offset = 0;
|
|
while (offset < static_cast<size_t>(bytesRead)) {
|
|
#if defined(_WIN32)
|
|
const int written = _write(mirrorFd,
|
|
buffer.data() + offset,
|
|
static_cast<unsigned int>(static_cast<size_t>(bytesRead) - offset));
|
|
#else
|
|
const ssize_t written = ::write(mirrorFd,
|
|
buffer.data() + offset,
|
|
static_cast<size_t>(bytesRead) - offset);
|
|
#endif
|
|
if (written <= 0) {
|
|
break;
|
|
}
|
|
offset += static_cast<size_t>(written);
|
|
}
|
|
}
|
|
|
|
WriteProcessTranscriptChunk(state, buffer.data(), static_cast<size_t>(bytesRead));
|
|
}
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
int GetFileDescriptor(FILE* file) {
|
|
return _fileno(file);
|
|
}
|
|
|
|
int DuplicateFileDescriptor(int fd) {
|
|
return _dup(fd);
|
|
}
|
|
|
|
int DuplicateFileDescriptorTo(int sourceFd, int targetFd) {
|
|
return _dup2(sourceFd, targetFd);
|
|
}
|
|
|
|
void CloseFileDescriptor(int fd) {
|
|
_close(fd);
|
|
}
|
|
#else
|
|
int GetFileDescriptor(FILE* file) {
|
|
return fileno(file);
|
|
}
|
|
|
|
int DuplicateFileDescriptor(int fd) {
|
|
return ::dup(fd);
|
|
}
|
|
|
|
int DuplicateFileDescriptorTo(int sourceFd, int targetFd) {
|
|
return ::dup2(sourceFd, targetFd);
|
|
}
|
|
|
|
void CloseFileDescriptor(int fd) {
|
|
::close(fd);
|
|
}
|
|
#endif
|
|
|
|
bool InstallTranscriptPipe(int& outReadFd, int& outWriteFd, int targetFd) {
|
|
#if defined(_WIN32)
|
|
int pipeFds[2]{-1, -1};
|
|
if (_pipe(pipeFds, 8192, _O_BINARY) != 0) {
|
|
return false;
|
|
}
|
|
#else
|
|
int pipeFds[2]{-1, -1};
|
|
if (::pipe(pipeFds) != 0) {
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
outReadFd = pipeFds[0];
|
|
outWriteFd = pipeFds[1];
|
|
if (targetFd == GetFileDescriptor(stdout)) {
|
|
std::fflush(stdout);
|
|
} else if (targetFd == GetFileDescriptor(stderr)) {
|
|
std::fflush(stderr);
|
|
}
|
|
|
|
if (DuplicateFileDescriptorTo(outWriteFd, targetFd) < 0) {
|
|
CloseFileDescriptor(outReadFd);
|
|
CloseFileDescriptor(outWriteFd);
|
|
outReadFd = -1;
|
|
outWriteFd = -1;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
void AttachParentConsoleForDiagnostics() {
|
|
// GUI-subsystem products get no console and no bound stdout/stderr unless a parent already
|
|
// redirected them (pipe/file: leave alone) or has a console to attach to (bind only the
|
|
// streams still unbound). With no parent console, fall back to NUL rather than leaving
|
|
// _fileno(stdout) == -2, which would stop InitializeProcessTranscript from redirecting into
|
|
// the log file at all.
|
|
const HANDLE outHandle = ::GetStdHandle(STD_OUTPUT_HANDLE);
|
|
const HANDLE errHandle = ::GetStdHandle(STD_ERROR_HANDLE);
|
|
const bool haveOut = outHandle != nullptr && outHandle != INVALID_HANDLE_VALUE;
|
|
const bool haveErr = errHandle != nullptr && errHandle != INVALID_HANDLE_VALUE;
|
|
if (haveOut && haveErr) {
|
|
return;
|
|
}
|
|
|
|
const bool attached =
|
|
::GetConsoleWindow() != nullptr || ::AttachConsole(ATTACH_PARENT_PROCESS) != 0;
|
|
const char* const sink = attached ? "CONOUT$" : "NUL";
|
|
if (!haveOut) {
|
|
(void)std::freopen(sink, "w", stdout);
|
|
}
|
|
if (!haveErr) {
|
|
(void)std::freopen(sink, "w", stderr);
|
|
}
|
|
std::cout.clear();
|
|
std::cerr.clear();
|
|
}
|
|
#else
|
|
void AttachParentConsoleForDiagnostics() {}
|
|
#endif
|
|
|
|
// The setup writes build-fingerprint.json beside every product executable; its SetupVersion is
|
|
// the only version identity the runtime has (products are compiled locally, so nothing is baked
|
|
// into the binary). Surface it at the top of the transcript so every attached log self-identifies.
|
|
std::string ReadInstalledSetupVersion() {
|
|
const auto directory = RuntimeConfigFile::ExecutableDirectory();
|
|
if (!directory) {
|
|
return {};
|
|
}
|
|
std::ifstream file(*directory / "build-fingerprint.json", std::ios::binary);
|
|
if (!file) {
|
|
return {};
|
|
}
|
|
const std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
|
constexpr std::string_view kKey = "\"SetupVersion\"";
|
|
const auto keyPos = text.find(kKey);
|
|
if (keyPos == std::string::npos) {
|
|
return {};
|
|
}
|
|
const auto colon = text.find(':', keyPos + kKey.size());
|
|
const auto open = colon == std::string::npos ? std::string::npos : text.find('"', colon + 1);
|
|
const auto close = open == std::string::npos ? std::string::npos : text.find('"', open + 1);
|
|
if (close == std::string::npos) {
|
|
return {};
|
|
}
|
|
return text.substr(open + 1, close - open - 1);
|
|
}
|
|
|
|
void InitializeProcessTranscript(int argc, char** argv) {
|
|
auto& state = GetProcessTranscriptState();
|
|
if (state.initialized.exchange(true, std::memory_order_acq_rel)) {
|
|
return;
|
|
}
|
|
|
|
const std::filesystem::path path = GetRunLogDirectory() / "console.log";
|
|
state.file.open(path, std::ios::out | std::ios::trunc | std::ios::binary);
|
|
if (!state.file) {
|
|
return;
|
|
}
|
|
|
|
state.enabled = true;
|
|
state.path = path;
|
|
|
|
#if defined(_WIN32)
|
|
const unsigned long pid = ::GetCurrentProcessId();
|
|
#else
|
|
const auto pid = static_cast<unsigned long>(::getpid());
|
|
#endif
|
|
|
|
const std::string setupVersion = ReadInstalledSetupVersion();
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(state.fileMutex);
|
|
state.file << "[runtime] WiiCompiled "
|
|
<< (setupVersion.empty() ? "version unknown" : setupVersion) << "\n";
|
|
state.file << "[runtime] process transcript started\n";
|
|
state.file << "[runtime] pid=" << pid << "\n";
|
|
state.file << "[runtime] argv=";
|
|
for (int i = 0; i < argc; ++i) {
|
|
if (i != 0) {
|
|
state.file << ' ';
|
|
}
|
|
state.file << argv[i];
|
|
}
|
|
state.file << "\n";
|
|
state.file.flush();
|
|
}
|
|
|
|
state.savedStdoutFd = DuplicateFileDescriptor(GetFileDescriptor(stdout));
|
|
state.savedStderrFd = DuplicateFileDescriptor(GetFileDescriptor(stderr));
|
|
|
|
std::setvbuf(stdout, nullptr, _IONBF, 0);
|
|
std::setvbuf(stderr, nullptr, _IONBF, 0);
|
|
|
|
auto restoreFailedSetup = [&state]() {
|
|
if (state.savedStdoutFd >= 0) {
|
|
DuplicateFileDescriptorTo(state.savedStdoutFd, GetFileDescriptor(stdout));
|
|
}
|
|
if (state.savedStderrFd >= 0) {
|
|
DuplicateFileDescriptorTo(state.savedStderrFd, GetFileDescriptor(stderr));
|
|
}
|
|
if (state.stdoutPipeReadFd >= 0) {
|
|
CloseFileDescriptor(state.stdoutPipeReadFd);
|
|
state.stdoutPipeReadFd = -1;
|
|
}
|
|
if (state.stdoutPipeWriteFd >= 0) {
|
|
CloseFileDescriptor(state.stdoutPipeWriteFd);
|
|
state.stdoutPipeWriteFd = -1;
|
|
}
|
|
if (state.stderrPipeReadFd >= 0) {
|
|
CloseFileDescriptor(state.stderrPipeReadFd);
|
|
state.stderrPipeReadFd = -1;
|
|
}
|
|
if (state.stderrPipeWriteFd >= 0) {
|
|
CloseFileDescriptor(state.stderrPipeWriteFd);
|
|
state.stderrPipeWriteFd = -1;
|
|
}
|
|
if (state.savedStdoutFd >= 0) {
|
|
CloseFileDescriptor(state.savedStdoutFd);
|
|
state.savedStdoutFd = -1;
|
|
}
|
|
if (state.savedStderrFd >= 0) {
|
|
CloseFileDescriptor(state.savedStderrFd);
|
|
state.savedStderrFd = -1;
|
|
}
|
|
state.enabled = false;
|
|
state.file.close();
|
|
};
|
|
|
|
if (!InstallTranscriptPipe(state.stdoutPipeReadFd, state.stdoutPipeWriteFd, GetFileDescriptor(stdout)) ||
|
|
!InstallTranscriptPipe(state.stderrPipeReadFd, state.stderrPipeWriteFd, GetFileDescriptor(stderr))) {
|
|
restoreFailedSetup();
|
|
return;
|
|
}
|
|
|
|
state.stdoutThread = std::thread([&state]() {
|
|
PumpTranscriptPipe(state, state.stdoutPipeReadFd, state.savedStdoutFd);
|
|
});
|
|
state.stderrThread = std::thread([&state]() {
|
|
PumpTranscriptPipe(state, state.stderrPipeReadFd, state.savedStderrFd);
|
|
});
|
|
}
|
|
|
|
void ShutdownProcessTranscript() {
|
|
auto& state = GetProcessTranscriptState();
|
|
if (!state.enabled) {
|
|
return;
|
|
}
|
|
|
|
std::fflush(stdout);
|
|
std::fflush(stderr);
|
|
std::cout.flush();
|
|
std::cerr.flush();
|
|
|
|
if (state.savedStdoutFd >= 0) {
|
|
DuplicateFileDescriptorTo(state.savedStdoutFd, GetFileDescriptor(stdout));
|
|
}
|
|
if (state.savedStderrFd >= 0) {
|
|
DuplicateFileDescriptorTo(state.savedStderrFd, GetFileDescriptor(stderr));
|
|
}
|
|
|
|
if (state.stdoutPipeWriteFd >= 0) {
|
|
CloseFileDescriptor(state.stdoutPipeWriteFd);
|
|
state.stdoutPipeWriteFd = -1;
|
|
}
|
|
if (state.stderrPipeWriteFd >= 0) {
|
|
CloseFileDescriptor(state.stderrPipeWriteFd);
|
|
state.stderrPipeWriteFd = -1;
|
|
}
|
|
|
|
if (state.stdoutThread.joinable()) {
|
|
state.stdoutThread.join();
|
|
}
|
|
if (state.stderrThread.joinable()) {
|
|
state.stderrThread.join();
|
|
}
|
|
|
|
if (state.stdoutPipeReadFd >= 0) {
|
|
CloseFileDescriptor(state.stdoutPipeReadFd);
|
|
state.stdoutPipeReadFd = -1;
|
|
}
|
|
if (state.stderrPipeReadFd >= 0) {
|
|
CloseFileDescriptor(state.stderrPipeReadFd);
|
|
state.stderrPipeReadFd = -1;
|
|
}
|
|
if (state.savedStdoutFd >= 0) {
|
|
CloseFileDescriptor(state.savedStdoutFd);
|
|
state.savedStdoutFd = -1;
|
|
}
|
|
if (state.savedStderrFd >= 0) {
|
|
CloseFileDescriptor(state.savedStderrFd);
|
|
state.savedStderrFd = -1;
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(state.fileMutex);
|
|
state.file << "\n[runtime] process transcript ended\n";
|
|
state.file.flush();
|
|
}
|
|
state.file.close();
|
|
state.enabled = false;
|
|
}
|
|
|
|
// The one host stack walker. Every caller - the CRT report hook, the fatal log
|
|
// and the stderr crash dump - goes through this, so the log and the console see
|
|
// exactly the same frames, including the TranslatedFunctionRegistry fallback for
|
|
// addresses DbgHelp cannot name.
|
|
std::string FormatHostStackTrace(unsigned framesToSkip) {
|
|
#if defined(_WIN32)
|
|
static std::atomic_bool s_symbolsReady{false};
|
|
HANDLE process = GetCurrentProcess();
|
|
if (!s_symbolsReady.load(std::memory_order_acquire)) {
|
|
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES);
|
|
if (SymInitialize(process, nullptr, TRUE)) {
|
|
s_symbolsReady.store(true, std::memory_order_release);
|
|
}
|
|
}
|
|
const bool symbolsReady = s_symbolsReady.load(std::memory_order_acquire);
|
|
|
|
void* frames[64]{};
|
|
const USHORT captured = CaptureStackBackTrace(static_cast<DWORD>(framesToSkip),
|
|
static_cast<DWORD>(std::size(frames)), frames, nullptr);
|
|
std::ostringstream out;
|
|
if (captured == 0) {
|
|
out << "[runtime] host stack trace unavailable (CaptureStackBackTrace returned 0)\n";
|
|
return out.str();
|
|
}
|
|
out << "[runtime] host stack trace (most recent call first):\n";
|
|
for (USHORT i = 0; i < captured; ++i) {
|
|
const DWORD64 addr = reinterpret_cast<DWORD64>(frames[i]);
|
|
HMODULE module = nullptr;
|
|
std::string modulePath = "?";
|
|
DWORD64 moduleBase = 0;
|
|
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
|
reinterpret_cast<LPCWSTR>(frames[i]),
|
|
&module) != 0 &&
|
|
module != nullptr) {
|
|
moduleBase = reinterpret_cast<DWORD64>(module);
|
|
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 = "?";
|
|
std::string translatedFuncName;
|
|
uint32_t ppcAddress = 0;
|
|
DWORD64 symbolDisp = 0;
|
|
std::array<char, sizeof(SYMBOL_INFO) + MAX_SYM_NAME> symbolBuffer{};
|
|
auto* symbol = reinterpret_cast<SYMBOL_INFO*>(symbolBuffer.data());
|
|
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
|
|
symbol->MaxNameLen = MAX_SYM_NAME;
|
|
if (symbolsReady && SymFromAddr(process, addr, &symbolDisp, symbol)) {
|
|
symbolName = symbol->Name;
|
|
} else if (auto info = TranslatedFunctionRegistry::FindByHostAddress(static_cast<uintptr_t>(addr))) {
|
|
// DbgHelp could not name it; the translated-function registry can.
|
|
translatedFuncName = info->name;
|
|
ppcAddress = info->address;
|
|
if (!translatedFuncName.empty()) {
|
|
symbolName = translatedFuncName.c_str();
|
|
symbolDisp = addr - reinterpret_cast<DWORD64>(info->entryPoint);
|
|
}
|
|
}
|
|
|
|
IMAGEHLP_LINE64 line{};
|
|
line.SizeOfStruct = sizeof(line);
|
|
DWORD lineDisp = 0;
|
|
const char* fileName = nullptr;
|
|
DWORD lineNumber = 0;
|
|
if (symbolsReady && SymGetLineFromAddr64(process, addr, &lineDisp, &line)) {
|
|
fileName = line.FileName;
|
|
lineNumber = line.LineNumber;
|
|
}
|
|
|
|
out << " ["
|
|
<< std::setw(2) << std::setfill('0') << static_cast<unsigned>(i)
|
|
<< std::setfill(' ') << "] "
|
|
<< modulePath << "!" << symbolName
|
|
<< " + 0x" << std::hex << std::uppercase << symbolDisp
|
|
<< " (0x" << addr;
|
|
if (moduleBase != 0) {
|
|
out << ", module+0x" << (addr - moduleBase);
|
|
}
|
|
if (ppcAddress != 0) {
|
|
out << ", PPC:0x" << std::setw(8) << std::setfill('0') << ppcAddress << std::setfill(' ');
|
|
}
|
|
out << std::dec << std::nouppercase;
|
|
if (fileName) {
|
|
out << ", " << fileName << ":" << lineNumber;
|
|
}
|
|
out << ")\n";
|
|
}
|
|
return out.str();
|
|
#else
|
|
(void)framesToSkip;
|
|
return {};
|
|
#endif
|
|
}
|
|
|
|
void WriteFatalLogImpl(std::string_view reason, std::string_view extraDetails = {},
|
|
const uint32_t* missingGuestTarget = nullptr) {
|
|
const std::filesystem::path runDirectory = GetRunLogDirectory();
|
|
std::string fileName = "crash_";
|
|
fileName.append(reason);
|
|
fileName.append(".txt");
|
|
std::ofstream out(runDirectory / fileName, std::ios::out | std::ios::trunc);
|
|
if (!out) {
|
|
return;
|
|
}
|
|
|
|
const auto now = std::chrono::system_clock::now();
|
|
const auto nowSecs = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
|
|
|
|
out << "[runtime] fatal log" << std::endl;
|
|
out << "[runtime] reason: " << reason << std::endl;
|
|
out << "[runtime] timestamp(seconds): " << nowSecs << std::endl;
|
|
out << "[runtime] entry: " << (g_lastEntryLabel.empty() ? "<unknown>" : g_lastEntryLabel) << std::endl;
|
|
if (!extraDetails.empty()) {
|
|
out << "[runtime] details: " << extraDetails << std::endl;
|
|
}
|
|
if (missingGuestTarget) {
|
|
out << "[runtime] guest jump target: 0x" << std::hex << std::uppercase
|
|
<< *missingGuestTarget << std::dec << std::endl;
|
|
}
|
|
|
|
const CpuContext* cpu = TryGetCpuContext();
|
|
if (!cpu) {
|
|
// Exceptions can unwind CpuContextScope before we get here.
|
|
// Fall back to the persistent CPU context snapshot so fatal logs still include registers.
|
|
cpu = &GetPersistentCpuContext();
|
|
}
|
|
if (cpu) {
|
|
SystemBridge::DumpCrashHeuristics(out, cpu, missingGuestTarget);
|
|
SystemBridge::DumpCpuState(out, cpu);
|
|
} else {
|
|
out << "[runtime] CPU context unavailable." << std::endl;
|
|
}
|
|
|
|
// Guest memory snapshots so the heap/object state can be walked offline.
|
|
// Written once per process: several fatal paths can fire in sequence
|
|
// (e.g. an exception followed by the exit-code report) and the snapshots
|
|
// are large.
|
|
static std::atomic_bool s_memorySnapshotWritten{false};
|
|
if (!s_memorySnapshotWritten.exchange(true, std::memory_order_acq_rel)) {
|
|
SystemBridge::WriteGuestMemorySnapshot(out, runDirectory / "mem1.bin");
|
|
}
|
|
|
|
out.flush();
|
|
RT_LOG(RT_TAG_RUNTIME) << "crash artifacts written to "
|
|
<< RuntimeConfigFile::PathToUtf8(runDirectory) << std::endl;
|
|
}
|
|
|
|
void SetRuntimeExitCodeImpl(int code) {
|
|
g_lastExitCode.store(code, std::memory_order_relaxed);
|
|
g_exitCodeSet.store(true, std::memory_order_relaxed);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace {
|
|
|
|
void DumpHostStackTrace() {
|
|
#if defined(_WIN32)
|
|
static std::atomic_flag s_inProgress = ATOMIC_FLAG_INIT;
|
|
if (s_inProgress.test_and_set()) {
|
|
return;
|
|
}
|
|
const std::string trace = FormatHostStackTrace(1);
|
|
std::fputs(trace.c_str(), stderr);
|
|
std::fflush(stderr);
|
|
s_inProgress.clear();
|
|
#else
|
|
RT_LOGF(RT_TAG_RUNTIME, "Host stack trace unavailable on this platform\n");
|
|
std::fflush(stderr);
|
|
#endif
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" void DumpHostStackTraceForRuntimeHelper() {
|
|
DumpHostStackTrace();
|
|
}
|
|
|
|
void MarkFatalErrorReported() {
|
|
g_fatalErrorReported.store(true, std::memory_order_release);
|
|
}
|
|
|
|
void ShowRuntimeFatalPopup(std::string_view category, std::string_view details) noexcept {
|
|
if (g_fatalPopupShown.exchange(true, std::memory_order_acq_rel)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
std::string message;
|
|
message.reserve(category.size() + details.size() + 220);
|
|
message.append("The game stopped because ");
|
|
message.append(category.empty() ? "a fatal error occurred." : category);
|
|
message.append(".\n\n");
|
|
if (details.empty()) {
|
|
message.append("No additional details were available.");
|
|
} else {
|
|
constexpr size_t kMaxPopupDetails = 4096;
|
|
message.append(details.data(), std::min(details.size(), kMaxPopupDetails));
|
|
if (details.size() > kMaxPopupDetails) {
|
|
message.append("\n\n[Additional details were written to the crash log.]");
|
|
}
|
|
}
|
|
message.append("\n\nSee the WiiCompiled Logs folder for the full diagnostic.");
|
|
#if defined(_WIN32)
|
|
::MessageBoxA(nullptr, message.c_str(), "WiiCompiled - Fatal Error",
|
|
MB_OK | MB_ICONERROR | MB_SETFOREGROUND | MB_TASKMODAL);
|
|
#else
|
|
// The shipped product is Windows-first. Keep non-Windows builds safe
|
|
// and retain the console diagnostic when no native dialog is available.
|
|
RT_LOGF(RT_TAG_RUNTIME, "fatal dialog: %s\n", message.c_str());
|
|
#endif
|
|
} catch (...) {
|
|
// Reporting a crash must never throw or mask the original failure.
|
|
}
|
|
}
|
|
|
|
namespace RuntimeCrash {
|
|
|
|
void WriteCrashArtifacts(std::string_view reason, std::string_view extraDetails,
|
|
const uint32_t* missingGuestTarget) noexcept {
|
|
try {
|
|
WriteFatalLogImpl(reason, extraDetails, missingGuestTarget);
|
|
} catch (...) {
|
|
// Crash reporting must never mask the original failure.
|
|
}
|
|
}
|
|
|
|
[[noreturn]] void FatalMissingGuestTarget(uint32_t target, CpuContext* cpu) noexcept {
|
|
RT_LOG(RT_TAG_RUNTIME) << "InvokeIndirectCpu: target 0x" << std::hex << target
|
|
<< " not translated (missing function)" << std::dec << std::endl;
|
|
RT_LOG(RT_TAG_RUNTIME) << "Caller LR = 0x" << std::hex << (cpu ? cpu->lr : 0u)
|
|
<< std::dec << std::endl;
|
|
try {
|
|
SystemBridge::DumpCrashHeuristics(std::cerr, cpu, &target);
|
|
SystemBridge::DumpCpuState(cpu);
|
|
} catch (...) {
|
|
}
|
|
std::fflush(stderr);
|
|
|
|
std::ostringstream message;
|
|
message << "The game stopped because it tried to execute guest address 0x"
|
|
<< std::hex << target
|
|
<< ", but that function was not translated or registered.\n\n"
|
|
<< "Caller LR: 0x" << (cpu ? cpu->lr : 0u);
|
|
if (target == 0) {
|
|
message << "\n\nA jump to address 0 usually means a virtual call through a bad "
|
|
"object pointer; the crash log heuristics have details.";
|
|
}
|
|
WriteCrashArtifacts("missing_target", message.str(), &target);
|
|
ShowRuntimeFatalPopup("Missing translated function", message.str());
|
|
MarkFatalErrorReported();
|
|
std::exit(EXIT_FAILURE);
|
|
}
|
|
|
|
} // namespace RuntimeCrash
|
|
|
|
|
|
namespace {
|
|
|
|
void RuntimeAuroraLogCallback(AuroraLogLevel level, const char* module,
|
|
const char* message, unsigned int len) {
|
|
const std::string_view moduleView = module != nullptr ? std::string_view(module) : std::string_view{};
|
|
const std::string_view messageView = message != nullptr ? std::string_view(message, len) : std::string_view{};
|
|
std::cerr << "[aurora] [" << static_cast<int>(level) << "] [" << moduleView << "] "
|
|
<< messageView << std::endl;
|
|
if (level == LOG_FATAL) {
|
|
ShowRuntimeFatalPopup("Aurora reported a fatal renderer error", messageView);
|
|
}
|
|
}
|
|
|
|
const TranslatedFunctionInfo* ResolveEntry() {
|
|
const auto* entry = TranslatedFunctionRegistry::FindByAddressPtr(kDefaultEntryAddress);
|
|
if (!entry) {
|
|
std::ostringstream oss;
|
|
oss << "No translated function registered at address 0x" << std::hex << kDefaultEntryAddress;
|
|
throw std::runtime_error(oss.str());
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
void SeedCpuContext(CpuContext& cpu) {
|
|
cpu.gpr[1] = 0x81700000u;
|
|
}
|
|
|
|
void DumpAccessViolationReport(const Memory::AccessViolation& ex,
|
|
std::string_view entryLabel) {
|
|
const uint32_t address = ex.address();
|
|
const size_t length = ex.length();
|
|
const CpuContext* cpu = TryGetCpuContext();
|
|
|
|
RT_LOG(RT_TAG_RUNTIME) << "===== Memory Access Violation =====" << std::endl;
|
|
RT_LOG(RT_TAG_RUNTIME) << "Reason : " << ex.reason() << std::endl;
|
|
std::cerr << std::hex << std::uppercase;
|
|
RT_LOG(RT_TAG_RUNTIME) << "Address: 0x" << std::setw(8) << std::setfill('0') << address
|
|
<< " (+0x" << length << ")" << std::dec << std::setfill(' ') << std::endl;
|
|
RT_LOG(RT_TAG_RUNTIME) << "Entry : " << (entryLabel.empty() ? "(unknown)" : std::string(entryLabel)) << std::endl;
|
|
RT_LOG(RT_TAG_RUNTIME) << "Mode : strict (trap on unmapped)" << std::endl;
|
|
RT_LOG(RT_TAG_RUNTIME) << "r1 seed: 0x81700000" << std::endl;
|
|
if (cpu) {
|
|
RT_LOG(RT_TAG_RUNTIME) << "CurrentCpuContext: " << cpu << " r1=0x"
|
|
<< std::hex << std::uppercase << cpu->gpr[1] << std::dec << std::nouppercase << std::endl;
|
|
RT_LOG(RT_TAG_RUNTIME) << "Last recorded PC : 0x" << std::hex << std::uppercase << cpu->pc
|
|
<< std::dec << std::nouppercase << std::endl;
|
|
} else {
|
|
RT_LOG(RT_TAG_RUNTIME) << "CurrentCpuContext: (null)" << std::endl;
|
|
}
|
|
|
|
SystemBridge::DumpCpuState(cpu);
|
|
|
|
const auto regions = Memory::DescribeRegions();
|
|
if (regions.empty()) {
|
|
RT_LOG(RT_TAG_RUNTIME) << "Memory not initialized; no regions mapped." << std::endl;
|
|
return;
|
|
}
|
|
|
|
RT_LOG(RT_TAG_RUNTIME) << "Mapped regions:" << std::endl;
|
|
uint64_t bestDistance = std::numeric_limits<uint64_t>::max();
|
|
std::string bestRegion;
|
|
bool insideRegion = false;
|
|
|
|
for (const auto& region : regions) {
|
|
const uint64_t base = region.baseAddress;
|
|
const uint64_t end = base + region.sizeBytes;
|
|
const bool contains = address >= base && address < end;
|
|
if (contains) {
|
|
insideRegion = true;
|
|
bestDistance = 0;
|
|
bestRegion = region.name;
|
|
} else {
|
|
const uint64_t distance = address < base ? base - address : address - end + 1;
|
|
if (distance < bestDistance) {
|
|
bestDistance = distance;
|
|
bestRegion = region.name;
|
|
}
|
|
}
|
|
|
|
std::cerr << " - " << region.name
|
|
<< " 0x" << std::hex << std::setw(8) << std::setfill('0') << region.baseAddress
|
|
<< " .. 0x" << std::setw(8) << (end - 1)
|
|
<< std::dec << std::setfill(' ')
|
|
<< " (" << region.sizeBytes / 1024 << " KiB";
|
|
if (contains) {
|
|
std::cerr << ", <-- access landed here";
|
|
}
|
|
std::cerr << ")" << std::endl;
|
|
}
|
|
|
|
if (!bestRegion.empty() && !insideRegion) {
|
|
RT_LOG(RT_TAG_RUNTIME) << "Nearest region: " << bestRegion << " (" << bestDistance << " bytes away)" << std::endl;
|
|
}
|
|
|
|
RT_LOG(RT_TAG_RUNTIME) << "Verify that the installed game data and runtime build match." << std::endl;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
PVOID g_vectoredSehHandle = nullptr;
|
|
constexpr DWORD kCppExceptionCodeGcc = 0x20474343; // "GCC" exception code
|
|
constexpr DWORD kCppExceptionCodeMsvc = 0xE06D7363;
|
|
// AddressSanitizer uses STATUS_FATAL_APP_EXIT when it detects an error and wants to report it.
|
|
// We must let ASan's handler run so it can print file/line information.
|
|
constexpr DWORD kAsanFatalAppExit = 0x40000015; // STATUS_FATAL_APP_EXIT
|
|
LONG ReportFatalSehAndExit(EXCEPTION_POINTERS* info);
|
|
|
|
void ReportStructuredException(EXCEPTION_POINTERS* info) {
|
|
if (!info || !info->ExceptionRecord) {
|
|
RT_LOG(RT_TAG_RUNTIME) << "Structured exception occurred, but no diagnostic info was captured." << std::endl;
|
|
return;
|
|
}
|
|
|
|
const auto* record = info->ExceptionRecord;
|
|
const auto code = record->ExceptionCode;
|
|
std::cerr << std::hex << std::uppercase;
|
|
RT_LOG(RT_TAG_RUNTIME) << "Structured exception 0x" << code;
|
|
if (!g_lastEntryLabel.empty()) {
|
|
std::cerr << " while executing " << g_lastEntryLabel;
|
|
}
|
|
std::cerr << std::dec << std::nouppercase << std::endl;
|
|
|
|
const auto faultAddress = reinterpret_cast<uintptr_t>(record->ExceptionAddress);
|
|
std::cerr << std::hex << std::uppercase;
|
|
RT_LOG(RT_TAG_RUNTIME) << "Fault address: 0x" << faultAddress << std::dec << std::nouppercase << std::endl;
|
|
|
|
if (code == EXCEPTION_ACCESS_VIOLATION && record->NumberParameters >= 2) {
|
|
const auto accessType = record->ExceptionInformation[0];
|
|
const auto accessed = record->ExceptionInformation[1];
|
|
RT_LOG(RT_TAG_RUNTIME) << "Access type: " << (accessType ? "write" : "read")
|
|
<< " at 0x" << std::hex << std::uppercase << accessed << std::dec << std::nouppercase << std::endl;
|
|
|
|
// Guardrail: flag raw GX gather pipe touches (0xCC00_8xxx) which must be routed through HLE.
|
|
// Direct stores to this MMIO region (e.g., translated stfs/stb -0x8000(r4) with r4=0xCC010000)
|
|
// will fault on the host. Emit an explicit hint so we know to fix the translation/HLE path instead
|
|
// of chasing generic access violations.
|
|
constexpr uintptr_t kGxGatherLo = 0xCC008000;
|
|
constexpr uintptr_t kGxGatherHi = 0xCC009000; // one page past the 0x100-byte gather range for clarity
|
|
if (accessed >= kGxGatherLo && accessed < kGxGatherHi) {
|
|
RT_LOG(RT_TAG_RUNTIME) << "HINT: guest attempted a direct GX gather pipe write (0xCC00_8xxx)." << std::endl;
|
|
RT_LOG(RT_TAG_RUNTIME) << " These must go through GX_HLE_FIFO_Write*; check the translated function for" << std::endl;
|
|
RT_LOG(RT_TAG_RUNTIME) << " literal stores to -0x8000(r4) after lis r4,0xCC01 and route them via HLE." << std::endl;
|
|
}
|
|
}
|
|
|
|
#if defined(_M_X64) || defined(__x86_64__)
|
|
const auto rip = info->ContextRecord ? info->ContextRecord->Rip : 0;
|
|
const auto rsp = info->ContextRecord ? info->ContextRecord->Rsp : 0;
|
|
std::cerr << std::hex << std::uppercase;
|
|
RT_LOG(RT_TAG_RUNTIME) << "RIP=0x" << rip << " RSP=0x" << rsp << std::dec << std::nouppercase << std::endl;
|
|
#elif defined(_M_IX86)
|
|
const auto eip = info->ContextRecord ? info->ContextRecord->Eip : 0;
|
|
const auto esp = info->ContextRecord ? info->ContextRecord->Esp : 0;
|
|
std::cerr << std::hex << std::uppercase;
|
|
RT_LOG(RT_TAG_RUNTIME) << "EIP=0x" << eip << " ESP=0x" << esp << std::dec << std::nouppercase << std::endl;
|
|
#endif
|
|
|
|
// Always dump CPU state on crash.
|
|
RT_LOG(RT_TAG_RUNTIME) << "===== DUMPING CPU STATE =====" << std::endl;
|
|
SystemBridge::DumpCpuState(TryGetCpuContext());
|
|
std::cerr.flush();
|
|
|
|
RT_LOG(RT_TAG_RUNTIME) << "Enable /DEBUG builds or capture a dump for full stack details." << std::endl;
|
|
std::cerr.flush();
|
|
}
|
|
|
|
|
|
|
|
LONG CALLBACK SehLogger(EXCEPTION_POINTERS* info) {
|
|
// Guest-space faults are the flat memory interception mechanism (MMIO,
|
|
// deferred EFB reads, the executable-write guard, unmapped pages). The
|
|
// flat module registers its own handler first, but registration order is
|
|
// not guaranteed once another VEH is installed later, so consult it here
|
|
// too - resolving a fault twice is a no-op.
|
|
if (info->ExceptionRecord != nullptr &&
|
|
info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION &&
|
|
info->ExceptionRecord->NumberParameters >= 2 &&
|
|
GuestFlat::HandleAccessViolation(
|
|
reinterpret_cast<void*>(info->ExceptionRecord->ExceptionInformation[1]),
|
|
info->ExceptionRecord->ExceptionInformation[0] != 0)) {
|
|
return EXCEPTION_CONTINUE_EXECUTION;
|
|
}
|
|
if (g_suppressSehReporting && g_sehJumpTarget) {
|
|
g_sehLastExceptionCode = info->ExceptionRecord->ExceptionCode;
|
|
g_sehLastExceptionAddress = reinterpret_cast<uintptr_t>(info->ExceptionRecord->ExceptionAddress);
|
|
g_sehLastAccessType = 0;
|
|
g_sehLastAccessedAddress = 0;
|
|
if (g_sehLastExceptionCode == EXCEPTION_ACCESS_VIOLATION && info->ExceptionRecord->NumberParameters >= 2) {
|
|
g_sehLastAccessType = static_cast<uint32_t>(info->ExceptionRecord->ExceptionInformation[0]);
|
|
g_sehLastAccessedAddress = static_cast<uintptr_t>(info->ExceptionRecord->ExceptionInformation[1]);
|
|
}
|
|
longjmp(*g_sehJumpTarget, 1);
|
|
}
|
|
if (g_suppressSehReporting) {
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
// Let C++ exceptions propagate to std::terminate so we can log their what().
|
|
if (info->ExceptionRecord->ExceptionCode == kCppExceptionCodeGcc ||
|
|
info->ExceptionRecord->ExceptionCode == kCppExceptionCodeMsvc) {
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
if (info->ExceptionRecord->ExceptionCode == 0x40010006 || // DBG_PRINTEXCEPTION_C
|
|
info->ExceptionRecord->ExceptionCode == 0x4001000A || // DBG_PRINTEXCEPTION_WIDE_C (OutputDebugStringW)
|
|
info->ExceptionRecord->ExceptionCode == 0x406D1388 || // SetThreadName
|
|
info->ExceptionRecord->ExceptionCode == kAsanFatalAppExit) { // ASan reporting - let it print first
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
// Software-raised exceptions (customer bit set) are used for internal control flow by
|
|
// system DLLs (e.g. msxml6 while mscms parses a display colour profile) and are caught
|
|
// by their own frame handlers. Only hardware faults are fatal at first chance; anything
|
|
// else that truly goes unhandled reaches UnhandledSehFilter.
|
|
if ((info->ExceptionRecord->ExceptionCode & 0x20000000u) != 0) {
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
return ReportFatalSehAndExit(info);
|
|
}
|
|
|
|
LONG WINAPI UnhandledSehFilter(EXCEPTION_POINTERS* info) {
|
|
if (info == nullptr || info->ExceptionRecord == nullptr) {
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
const DWORD code = info->ExceptionRecord->ExceptionCode;
|
|
if (code == kCppExceptionCodeGcc || code == kCppExceptionCodeMsvc || code == kAsanFatalAppExit) {
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
return ReportFatalSehAndExit(info);
|
|
}
|
|
|
|
LONG ReportFatalSehAndExit(EXCEPTION_POINTERS* info) {
|
|
// Guard against re-entrancy: if we crash while reporting, don't recurse
|
|
static std::atomic_flag s_inCrashHandler = ATOMIC_FLAG_INIT;
|
|
if (s_inCrashHandler.test_and_set()) {
|
|
std::_Exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// Report the structured exception with detailed information
|
|
ReportStructuredException(info);
|
|
const auto* record = info->ExceptionRecord;
|
|
const DWORD code = record != nullptr ? record->ExceptionCode : 0;
|
|
std::ostringstream popupDetails;
|
|
popupDetails << "A native Windows exception (0x" << std::hex << std::uppercase << code << ") occurred";
|
|
if (!g_lastEntryLabel.empty()) {
|
|
popupDetails << " while executing " << g_lastEntryLabel;
|
|
}
|
|
if (code == EXCEPTION_ACCESS_VIOLATION && record->NumberParameters >= 2) {
|
|
popupDetails << ".\n\nThe game attempted a "
|
|
<< (record->ExceptionInformation[0] ? "write" : "read")
|
|
<< " at host address 0x" << record->ExceptionInformation[1];
|
|
}
|
|
popupDetails << ".\n\nThe process transcript and crash log contain the full CPU and stack diagnostics.";
|
|
ShowRuntimeFatalPopup("a native crash occurred", popupDetails.str());
|
|
DumpHostStackTrace();
|
|
|
|
WriteFatalLogImpl("seh");
|
|
|
|
// CRITICAL: Explicitly flush all output to ensure visibility with PowerShell redirection
|
|
std::cerr << '\n';
|
|
RT_LOG(RT_TAG_RUNTIME) << "===== FLUSHING OUTPUT BEFORE EXIT =====" << std::endl;
|
|
std::cerr.flush();
|
|
std::cout.flush();
|
|
std::fflush(stdout);
|
|
std::fflush(stderr);
|
|
|
|
std::_Exit(EXIT_FAILURE);
|
|
}
|
|
|
|
void InstallSehLogger() {
|
|
if (!g_vectoredSehHandle) {
|
|
g_vectoredSehHandle = AddVectoredExceptionHandler(1, SehLogger);
|
|
SetUnhandledExceptionFilter(UnhandledSehFilter);
|
|
}
|
|
}
|
|
#else
|
|
// POSIX counterpart to SehLogger above. Unlike Windows' AddVectoredExceptionHandler, which lets
|
|
// GuestFlat and this module each install their own handler and defensively re-check each other,
|
|
// sigaction only allows one handler per signal - the second registration replaces the first
|
|
// instead of chaining. So this is the single SIGSEGV/SIGBUS handler for the whole process, and it
|
|
// owns checking GuestFlat's fault-interception logic first, exactly mirroring the order SehLogger
|
|
// already uses on Windows.
|
|
void ReportUnhandledSignalFault(int sig, void* faultAddress) {
|
|
RT_LOG(RT_TAG_RUNTIME) << "Signal " << sig << " (fault address 0x" << std::hex
|
|
<< reinterpret_cast<uintptr_t>(faultAddress) << std::dec << ")";
|
|
if (!g_lastEntryLabel.empty()) {
|
|
std::cerr << " while executing " << g_lastEntryLabel;
|
|
}
|
|
std::cerr << std::endl;
|
|
if (const CpuContext* cpu = TryGetCpuContext()) {
|
|
RT_LOG(RT_TAG_RUNTIME) << "===== DUMPING CPU STATE =====" << std::endl;
|
|
SystemBridge::DumpCpuState(cpu);
|
|
}
|
|
std::cerr.flush();
|
|
}
|
|
|
|
void PosixMemoryFaultHandler(int sig, siginfo_t* info, void* ucontextVoid) {
|
|
void* faultAddress = info != nullptr ? info->si_addr : nullptr;
|
|
bool isWrite = false;
|
|
#if defined(__x86_64__)
|
|
// Standard glibc technique for a POSIX fastmem-style handler: bit 1 (0x2) of the hardware
|
|
// error code x86 pushes on a page fault records whether it was a write.
|
|
if (ucontextVoid != nullptr) {
|
|
auto* uc = static_cast<ucontext_t*>(ucontextVoid);
|
|
isWrite = (uc->uc_mcontext.gregs[REG_ERR] & 0x2) != 0;
|
|
}
|
|
#endif
|
|
|
|
// Guest-space faults are the flat memory interception mechanism (MMIO, deferred EFB reads,
|
|
// the executable-write guard, unmapped pages). Resolving one here means resuming the
|
|
// faulting instruction, which just returning from the handler does.
|
|
if (faultAddress != nullptr && GuestFlat::HandleAccessViolation(faultAddress, isWrite)) {
|
|
return;
|
|
}
|
|
|
|
if (g_suppressSehReporting && g_sehJumpTarget) {
|
|
g_sehLastExceptionCode = static_cast<uint32_t>(sig);
|
|
g_sehLastExceptionAddress = reinterpret_cast<uintptr_t>(faultAddress);
|
|
g_sehLastAccessType = isWrite ? 1u : 0u;
|
|
g_sehLastAccessedAddress = reinterpret_cast<uintptr_t>(faultAddress);
|
|
siglongjmp(*g_sehJumpTarget, 1);
|
|
}
|
|
if (g_suppressSehReporting) {
|
|
// Reporting suppressed but nobody armed a recovery jump: restore the default disposition
|
|
// and re-raise so the process still terminates, instead of returning into the same fault.
|
|
signal(sig, SIG_DFL);
|
|
raise(sig);
|
|
return;
|
|
}
|
|
|
|
// Guard against re-entrancy: if we crash while reporting, don't recurse.
|
|
static std::atomic_flag s_inCrashHandler = ATOMIC_FLAG_INIT;
|
|
if (s_inCrashHandler.test_and_set()) {
|
|
std::_Exit(EXIT_FAILURE);
|
|
}
|
|
|
|
ReportUnhandledSignalFault(sig, faultAddress);
|
|
std::ostringstream popupDetails;
|
|
popupDetails << "A native signal (" << sig << ") occurred";
|
|
if (!g_lastEntryLabel.empty()) {
|
|
popupDetails << " while executing " << g_lastEntryLabel;
|
|
}
|
|
if (faultAddress != nullptr) {
|
|
popupDetails << ".\n\nThe game attempted a " << (isWrite ? "write" : "read")
|
|
<< " at host address 0x" << std::hex
|
|
<< reinterpret_cast<uintptr_t>(faultAddress) << std::dec;
|
|
}
|
|
popupDetails << ".\n\nThe process transcript and crash log contain the full CPU and stack "
|
|
"diagnostics.";
|
|
ShowRuntimeFatalPopup("a native crash occurred", popupDetails.str());
|
|
DumpHostStackTrace();
|
|
WriteFatalLogImpl(sig == SIGBUS ? "sigbus" : "sigsegv");
|
|
|
|
std::cerr.flush();
|
|
std::cout.flush();
|
|
std::fflush(stdout);
|
|
std::fflush(stderr);
|
|
std::_Exit(EXIT_FAILURE);
|
|
}
|
|
|
|
void InstallPosixMemoryFaultHandler() {
|
|
struct sigaction action {};
|
|
action.sa_sigaction = PosixMemoryFaultHandler;
|
|
action.sa_flags = SA_SIGINFO;
|
|
sigemptyset(&action.sa_mask);
|
|
sigaction(SIGSEGV, &action, nullptr);
|
|
// A touch beyond a memfd-backed mapping's ftruncate()'d size raises SIGBUS rather than
|
|
// SIGSEGV on Linux; region sizing should make this unreachable, but routing it to the same
|
|
// handler costs nothing and avoids a silent gap if it ever isn't.
|
|
sigaction(SIGBUS, &action, nullptr);
|
|
}
|
|
#endif
|
|
|
|
void AbortSignalHandler(int signum) {
|
|
// Guard against re-entrancy if multiple aborts are raised in quick succession
|
|
if (g_abortSignalHandled.test_and_set()) {
|
|
std::_Exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// abort() bypasses atexit, so the guest-memory fault summary has to be
|
|
// emitted here too. It is idempotent, so a later AtExitHandler is a no-op.
|
|
GuestFlat::LogFaultSummary();
|
|
|
|
ShowRuntimeFatalPopup("a fatal internal error occurred",
|
|
"The process called abort while running the game or Aurora renderer.\n\n"
|
|
"This usually means an unimplemented function, failed renderer assertion, "
|
|
"or another unrecoverable runtime condition was reached.");
|
|
|
|
// Skip detailed dump if already reported by another handler
|
|
if (g_fatalErrorReported.load(std::memory_order_acquire)) {
|
|
std::fflush(stderr);
|
|
std::fflush(stdout);
|
|
std::_Exit(EXIT_FAILURE);
|
|
}
|
|
|
|
WriteFatalLogImpl("sigabrt");
|
|
|
|
std::fflush(stderr);
|
|
std::fflush(stdout);
|
|
std::_Exit(EXIT_FAILURE);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// Retained as the thin public wrapper over WriteFatalLogImpl (declared in
|
|
// system_bridge.h); it has no in-tree callers because every fatal path now goes
|
|
// through RuntimeCrash::WriteCrashArtifacts.
|
|
void WriteFatalLog(std::string_view reason) {
|
|
WriteFatalLogImpl(reason);
|
|
}
|
|
|
|
void SetRuntimeExitCode(int code) {
|
|
SetRuntimeExitCodeImpl(code);
|
|
}
|
|
|
|
// Global handler called via atexit() to flush buffers before any exit
|
|
static void AtExitHandler() {
|
|
// End-of-run guest memory report. This runs before the fatal-report check
|
|
// below because the counters describe the whole session and are just as
|
|
// interesting after a crash as after a clean exit.
|
|
GuestFlat::LogFaultSummary();
|
|
|
|
// Skip if already reported by another handler
|
|
if (g_fatalErrorReported.load(std::memory_order_acquire)) {
|
|
return;
|
|
}
|
|
if (g_exitCodeSet.load(std::memory_order_relaxed) &&
|
|
g_lastExitCode.load(std::memory_order_relaxed) != 0) {
|
|
ShowRuntimeFatalPopup("the runtime exited with an error",
|
|
"The game stopped after reporting a fatal error. Check the process transcript and crash log for details.");
|
|
WriteFatalLogImpl("exitcode");
|
|
}
|
|
|
|
std::cerr.flush();
|
|
std::cout.flush();
|
|
std::fflush(stdout);
|
|
std::fflush(stderr);
|
|
}
|
|
|
|
// Global terminate handler for uncaught exceptions
|
|
static void TerminateHandler() {
|
|
// Skip detailed dump if already reported
|
|
if (g_fatalErrorReported.load(std::memory_order_acquire)) {
|
|
std::fflush(stderr);
|
|
std::_Exit(EXIT_FAILURE);
|
|
}
|
|
std::string terminateDetails;
|
|
if (auto ex = std::current_exception()) {
|
|
try {
|
|
std::rethrow_exception(ex);
|
|
} catch (const std::exception& e) {
|
|
terminateDetails = std::string("Unhandled C++ exception: ") + e.what();
|
|
RT_LOG(RT_TAG_RUNTIME) << "Unhandled C++ exception: " << e.what() << std::endl;
|
|
} catch (...) {
|
|
terminateDetails = "Unhandled non-std C++ exception.";
|
|
RT_LOG(RT_TAG_RUNTIME) << "Unhandled non-std C++ exception." << std::endl;
|
|
}
|
|
} else {
|
|
terminateDetails = "std::terminate() without current exception.";
|
|
}
|
|
ShowRuntimeFatalPopup("an unhandled C++ exception occurred", terminateDetails);
|
|
const std::string hostStackSummary = FormatHostStackTrace(1);
|
|
if (!hostStackSummary.empty()) {
|
|
terminateDetails.append("\n");
|
|
terminateDetails.append(hostStackSummary);
|
|
}
|
|
WriteFatalLogImpl("terminate", terminateDetails);
|
|
RT_LOG(RT_TAG_RUNTIME) << "std::terminate() called - program exiting" << std::endl;
|
|
std::cerr.flush();
|
|
DumpHostStackTrace();
|
|
if (const CpuContext* cpu = TryGetCpuContext()) {
|
|
RT_LOG(RT_TAG_RUNTIME) << "CPU state at terminate:" << std::endl;
|
|
SystemBridge::DumpCpuState(cpu);
|
|
}
|
|
std::fflush(stderr);
|
|
std::_Exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// Runtime entry point: loads the configuration, brings up aurora and runs the game.
|
|
int RuntimeMain(int argc, char** argv) {
|
|
// Must run before the transcript duplicates stdout/stderr: it decides what
|
|
// those descriptors are mirrored to now that the products are GUI-subsystem.
|
|
AttachParentConsoleForDiagnostics();
|
|
#if defined(_WIN32)
|
|
ConfigureWindowsFatalDialogBehavior();
|
|
InstallSehLogger();
|
|
WindowsTimerResolutionGuard timerResolutionGuard;
|
|
#else
|
|
InstallPosixMemoryFaultHandler();
|
|
#endif
|
|
InitializeProcessTranscript(argc, argv);
|
|
std::signal(SIGABRT, AbortSignalHandler);
|
|
// Install exit/terminate handlers to ensure we get crash info
|
|
std::atexit(AtExitHandler);
|
|
std::set_terminate(TerminateHandler);
|
|
|
|
std::string currentEntryLabel;
|
|
|
|
try {
|
|
if (argc != 1) {
|
|
throw std::invalid_argument("The game runtime does not accept command-line options; use Config.toml through the installed host.");
|
|
}
|
|
RuntimeConfigFile::LogLoadedConfig();
|
|
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
|
|
DiscordPresence::Initialize(RuntimeConfigFile::DiscordClientId(), "Mario Kart Wii");
|
|
}
|
|
SystemBridge::Initialize();
|
|
TranslatedFunctionRegistry::Finalize();
|
|
|
|
// Initialize Aurora (graphics backend)
|
|
// We use auto backend (or specific if needed) and set a default window size.
|
|
// This is required for GX commands (like texture loading) to work.
|
|
AuroraConfig auroraConfig = {};
|
|
auroraConfig.appName = RuntimeProduct::Active().displayName.data();
|
|
const auto applicationDataDirectory = RuntimeConfigFile::ApplicationDataDirectory();
|
|
const auto rendererCacheDirectory = applicationDataDirectory / "Cache";
|
|
std::error_code rendererPathError;
|
|
std::filesystem::create_directories(rendererCacheDirectory, rendererPathError);
|
|
if (rendererPathError) {
|
|
RT_LOG(RT_TAG_RUNTIME) << "Unable to create renderer cache directory "
|
|
<< RuntimeConfigFile::PathToUtf8(rendererCacheDirectory) << ": "
|
|
<< rendererPathError.message() << std::endl;
|
|
}
|
|
const std::string auroraUserPath = RuntimeConfigFile::PathToUtf8(applicationDataDirectory);
|
|
const std::string auroraCachePath = RuntimeConfigFile::PathToUtf8(rendererCacheDirectory);
|
|
auroraConfig.userPath = auroraUserPath.c_str();
|
|
auroraConfig.cachePath = auroraCachePath.c_str();
|
|
auroraConfig.logCallback = &RuntimeAuroraLogCallback;
|
|
auroraConfig.logLevel = LOG_DEBUG;
|
|
const bool configWidescreen = RuntimeConfigFile::WidescreenEnabled(true);
|
|
auroraConfig.windowWidth = configWidescreen ? 854 : 640;
|
|
auroraConfig.windowHeight = 480;
|
|
auroraConfig.windowWidth = RuntimeConfigFile::WindowWidth(auroraConfig.windowWidth);
|
|
auroraConfig.windowHeight = RuntimeConfigFile::WindowHeight(auroraConfig.windowHeight);
|
|
auroraConfig.hasWindowPosition = RuntimeConfigFile::WindowPosition(
|
|
auroraConfig.windowPosX, auroraConfig.windowPosY);
|
|
auroraConfig.allowJoystickBackgroundEvents = true;
|
|
auroraConfig.disableCopyFilter = RuntimeConfigFile::DisableCopyFilter(true);
|
|
// Dolphin-style custom textures. Aurora indexes <userPath>/texture_replacements
|
|
// once during aurora_initialize, so both knobs only take effect on the next launch.
|
|
// Dumps name each unmatched texture the way its replacement would have to be named,
|
|
// which is only useful while the index is live - hence the conjunction.
|
|
auroraConfig.allowTextureReplacements = RuntimeConfigFile::TextureReplacements(false);
|
|
auroraConfig.allowTextureDumps = auroraConfig.allowTextureReplacements &&
|
|
RuntimeConfigFile::TextureDumps(false);
|
|
// No vsync knob: aurora always configures a non-blocking present mode.
|
|
auroraConfig.desiredBackend = BACKEND_AUTO;
|
|
const float resolutionMultiplier = RuntimeConfigFile::ResolutionMultiplier(1.0f);
|
|
ConfigureMkwDynamicAspect(configWidescreen, auroraConfig.windowWidth, auroraConfig.windowHeight);
|
|
VISetFrameBufferScale(resolutionMultiplier);
|
|
// One table for both directions. RuntimeConfigFile::IsSupportedGraphicsApi
|
|
// whitelists exactly these config names, so an unrecognised value has
|
|
// already been rejected (and reported) at parse time.
|
|
struct GraphicsBackendEntry {
|
|
const char* configName;
|
|
AuroraBackend backend;
|
|
};
|
|
#if defined(__APPLE__)
|
|
static constexpr std::array<GraphicsBackendEntry, 2> kGraphicsBackends{{
|
|
{"auto", BACKEND_AUTO}, {"metal", BACKEND_METAL},
|
|
}};
|
|
// only vulkan for linux
|
|
#elif defined(__linux__)
|
|
static constexpr std::array<GraphicsBackendEntry, 2> kGraphicsBackends{{
|
|
{"auto", BACKEND_AUTO}, {"vulkan", BACKEND_VULKAN},
|
|
}};
|
|
#elif defined(_WIN32)
|
|
static constexpr std::array<GraphicsBackendEntry, 3> kGraphicsBackends{{
|
|
{"auto", BACKEND_AUTO}, {"d3d12", BACKEND_D3D12}, {"vulkan", BACKEND_VULKAN},
|
|
}};
|
|
|
|
#endif
|
|
const auto backendDisplayName = [](AuroraBackend value) -> const char* {
|
|
for (const auto& entry : kGraphicsBackends) {
|
|
if (entry.backend == value) {
|
|
return entry.configName;
|
|
}
|
|
}
|
|
return "unknown";
|
|
};
|
|
|
|
const std::string backend = RuntimeConfigFile::GraphicsApi("auto");
|
|
for (const auto& entry : kGraphicsBackends) {
|
|
if (backend == entry.configName) {
|
|
auroraConfig.desiredBackend = entry.backend;
|
|
break;
|
|
}
|
|
}
|
|
const AuroraBackend requestedBackend = auroraConfig.desiredBackend;
|
|
|
|
// SDL only reads its Wii driver hint when the joystick subsystem starts, which
|
|
// aurora_initialize does; a Bluetooth Wii Remote paired before launch must be
|
|
// visible on that first scan.
|
|
WiiRemoteInput::ConfigureSdlHints(RuntimeConfigFile::WiiRemotesEnabled(true));
|
|
|
|
const AuroraInfo auroraInfo = aurora_initialize(0, nullptr, &auroraConfig);
|
|
if (requestedBackend != BACKEND_AUTO && auroraInfo.backend != requestedBackend) {
|
|
RT_LOG(RT_TAG_RUNTIME) << "graphics_api=\"" << backend
|
|
<< "\" is not available on this system; aurora fell back to \""
|
|
<< backendDisplayName(auroraInfo.backend)
|
|
<< "\". See the [aurora::gpu] lines above for the reason." << std::endl;
|
|
} else {
|
|
RT_LOG(RT_TAG_RUNTIME) << "graphics backend: " << backendDisplayName(auroraInfo.backend)
|
|
<< std::endl;
|
|
}
|
|
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
|
|
GxGuestWrite::InstallAuroraHooks();
|
|
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
|
|
auroraInfo.windowSize.native_fb_height);
|
|
settings_overlay::InitializeRuntimeSettings();
|
|
RT_LOG(RT_TAG_CONFIG) << "video.widescreen=" << (configWidescreen ? "true" : "false")
|
|
<< " SCGetAspectRatio=" << (configWidescreen ? 1 : 0)
|
|
<< " resolutionMultiplier=" << resolutionMultiplier
|
|
<< " window=" << auroraInfo.windowSize.width << "x" << auroraInfo.windowSize.height
|
|
<< " native=" << auroraInfo.windowSize.native_fb_width << "x"
|
|
<< auroraInfo.windowSize.native_fb_height
|
|
<< " viewportPolicy=" << (g_dynamicAspectRatioEnabled ? "stretch" : "fit")
|
|
<< " presentAspect="
|
|
<< (g_dynamicAspectRatioEnabled ? "surface (dynamic EGG canvas)" : "4:3")
|
|
<< std::endl;
|
|
g_auroraInitialized.store(true, std::memory_order_release);
|
|
|
|
auto entry = ResolveEntry();
|
|
InitializePersistentCpuContext();
|
|
auto& cpu = GetPersistentCpuContext();
|
|
SeedCpuContext(cpu);
|
|
|
|
// Initialize the fiber-based threading system
|
|
Fiber::GuestFiberManager::Initialize();
|
|
|
|
CpuContextScope cpuScope(&cpu);
|
|
|
|
std::string label = entry->name;
|
|
if (label.empty()) {
|
|
std::ostringstream oss;
|
|
oss << "0x" << std::hex << entry->address;
|
|
label = oss.str();
|
|
}
|
|
currentEntryLabel = label;
|
|
g_lastEntryLabel = currentEntryLabel;
|
|
|
|
InvokeIndirectCpu(entry->address, &cpu);
|
|
const uint32_t result = cpu.gpr[3];
|
|
RT_LOG(RT_TAG_RUNTIME) << label << " => 0x" << std::hex << result << std::dec << " (" << result << ")" << std::endl;
|
|
|
|
// Shutdown fiber system
|
|
Fiber::GuestFiberManager::Shutdown();
|
|
WindowPlacementPersistence::Flush(true);
|
|
aurora_shutdown();
|
|
DiscordPresence::Shutdown();
|
|
SetRuntimeExitCodeImpl(0);
|
|
ShutdownProcessTranscript();
|
|
return 0;
|
|
} catch (const Memory::AccessViolation& ex) {
|
|
std::cerr << "Runtime error: " << ex.what() << std::endl;
|
|
DumpAccessViolationReport(ex, currentEntryLabel);
|
|
std::ostringstream details;
|
|
details << "addr=0x" << std::hex << std::uppercase << ex.address()
|
|
<< " len=0x" << ex.length()
|
|
<< std::dec << std::nouppercase
|
|
<< " reason=" << ex.reason();
|
|
ShowRuntimeFatalPopup("a guest memory access was out of bounds", details.str());
|
|
WriteFatalLogImpl("access_violation", details.str());
|
|
SetRuntimeExitCodeImpl(1);
|
|
Fiber::GuestFiberManager::Shutdown();
|
|
WindowPlacementPersistence::Flush(true);
|
|
aurora_shutdown();
|
|
DiscordPresence::Shutdown();
|
|
ShutdownProcessTranscript();
|
|
return 1;
|
|
} catch (const std::exception& ex) {
|
|
std::cerr << "Runtime error: " << ex.what() << std::endl;
|
|
SystemBridge::DumpCpuState(TryGetCpuContext());
|
|
ShowRuntimeFatalPopup("a runtime exception occurred", ex.what());
|
|
WriteFatalLogImpl("exception");
|
|
SetRuntimeExitCodeImpl(1);
|
|
Fiber::GuestFiberManager::Shutdown();
|
|
WindowPlacementPersistence::Flush(true);
|
|
aurora_shutdown();
|
|
DiscordPresence::Shutdown();
|
|
ShutdownProcessTranscript();
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
return RuntimeMain(argc, argv);
|
|
}
|
|
extern "C" bool g_dynamicAspectRatioEnabled = false;
|