mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-11 09:25:05 -04:00
4f4716be5a
also restores automatic port assignment on non-Windows. This should be the default as https://github.com/patchzyy/Wiicompiled/pull/42 was never a necessary change
1471 lines
57 KiB
C++
1471 lines
57 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)
|
|
#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>
|
|
#include <ucontext.h>
|
|
#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 "wup028_adapter.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
|
|
|
|
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;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
#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);
|
|
}
|
|
|
|
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();
|
|
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;
|
|
};
|
|
static constexpr std::array<GraphicsBackendEntry, 3> kGraphicsBackends{{
|
|
{"auto", BACKEND_AUTO}, {"d3d12", BACKEND_D3D12}, {"vulkan", BACKEND_VULKAN},
|
|
}};
|
|
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;
|
|
|
|
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();
|
|
#if defined(_WIN32)
|
|
Wup028Adapter::Initialize();
|
|
#endif
|
|
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);
|
|
#if defined(_WIN32)
|
|
Wup028Adapter::Shutdown();
|
|
#endif
|
|
aurora_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);
|
|
#if defined(_WIN32)
|
|
Wup028Adapter::Shutdown();
|
|
#endif
|
|
aurora_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);
|
|
#if defined(_WIN32)
|
|
Wup028Adapter::Shutdown();
|
|
#endif
|
|
aurora_shutdown();
|
|
ShutdownProcessTranscript();
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
return RuntimeMain(argc, argv);
|
|
}
|
|
extern "C" bool g_dynamicAspectRatioEnabled = false;
|