Files
wiicompiled/runtime/src/main.cpp
T
Javier R Bueno ca7d126a13 Bluetooth Wii Remote support: Wii Remote / Wii Wheel, Nunchuk and Classic Controller through KPAD (#73)
* 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>
2026-09-01 21:47:31 +02:00

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;