mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 10:00:27 +02:00
When no backend could create a window and a WebGPU device, aurora hit an ASSERT and the process aborted with only SDL's last error in the log. Keep the first adapter, device or window error, return it from aurora_initialize with a new initializationStatus, and have the runtime throw it so the existing fatal popup and fatal log record the reason. ImGui shutdown now skips backends that never initialized, so the shutdown that follows a failed start does not crash. From upstream patchzyy/Wiicompiled 6f14bde (#244, KartPad batch). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wg7mB8ogCWmp9GH19Uc82B
1684 lines
66 KiB
C++
1684 lines
66 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>
|
|
#if defined(__GLIBC__)
|
|
// The host side of a native crash on desktop Linux: the faulting thread, its pc and lr, and a
|
|
// backtrace, each as module + offset for addr2line against the unstripped executable.
|
|
#include <dlfcn.h>
|
|
#include <execinfo.h>
|
|
#include <pthread.h>
|
|
#include <ucontext.h>
|
|
#endif
|
|
#endif
|
|
|
|
#include "abi_bridge.h"
|
|
#include "guest_flat_memory.h"
|
|
#include "gx_guest_write.h"
|
|
#include "gx_thread.h"
|
|
#include <aurora/imgui.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 "physical_wheel.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 "vr/openxr_integration.h"
|
|
#include <aurora/aurora.h>
|
|
#include <aurora/gfx.h>
|
|
#include <dolphin/gx/GXAurora.h>
|
|
|
|
#if defined(__ANDROID__)
|
|
#include <android/log.h>
|
|
// Renames main() to SDL_main(), which SDLActivity's nativeRunMain resolves
|
|
// from libmain.so with dlsym once the Java side has set up the surface.
|
|
#include <SDL3/SDL_main.h>
|
|
#endif
|
|
#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();
|
|
}
|
|
|
|
void GxThreadFrameLog(char* buffer, uint32_t bufferSize, double windowSeconds, uint32_t frames) {
|
|
if (!GxThread::Enabled()) {
|
|
return;
|
|
}
|
|
const std::string line = GxThread::FormatStatsAndReset(windowSeconds, frames);
|
|
std::snprintf(buffer, bufferSize, "%s", line.c_str());
|
|
}
|
|
|
|
#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{};
|
|
#if defined(__ANDROID__)
|
|
// Android sends stdout/stderr to /dev/null, so the mirror fd shows nothing;
|
|
// forward complete lines to logcat as well (tag WiiCompiled).
|
|
std::string pendingLine;
|
|
#endif
|
|
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(__ANDROID__)
|
|
pendingLine.append(buffer.data(), static_cast<size_t>(bytesRead));
|
|
size_t lineStart = 0;
|
|
for (size_t newline = pendingLine.find('\n'); newline != std::string::npos;
|
|
newline = pendingLine.find('\n', lineStart)) {
|
|
const std::string line = pendingLine.substr(lineStart, newline - lineStart);
|
|
__android_log_write(ANDROID_LOG_INFO, "WiiCompiled", line.c_str());
|
|
lineStart = newline + 1;
|
|
}
|
|
pendingLine.erase(0, lineStart);
|
|
if (pendingLine.size() > 3500) {
|
|
__android_log_write(ANDROID_LOG_INFO, "WiiCompiled", pendingLine.c_str());
|
|
pendingLine.clear();
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#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 {
|
|
|
|
#if defined(__GLIBC__)
|
|
// One host code address as module + offset (what `addr2line -f -C -e <module> <offset>` takes),
|
|
// with the nearest exported symbol when there is one.
|
|
void LogHostAddress(const char* label, int index, const void* address) {
|
|
Dl_info info{};
|
|
if (address != nullptr && dladdr(address, &info) != 0 && info.dli_fname != nullptr) {
|
|
const char* slash = std::strrchr(info.dli_fname, '/');
|
|
const char* module = slash != nullptr ? slash + 1 : info.dli_fname;
|
|
const auto offset = reinterpret_cast<uintptr_t>(address) - reinterpret_cast<uintptr_t>(info.dli_fbase);
|
|
RT_LOGF(RT_TAG_RUNTIME, "%s%d %p %s+0x%zx%s%s\n", label, index, address, module, static_cast<size_t>(offset),
|
|
info.dli_sname != nullptr ? " " : "", info.dli_sname != nullptr ? info.dli_sname : "");
|
|
} else {
|
|
RT_LOGF(RT_TAG_RUNTIME, "%s%d %p\n", label, index, address);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
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();
|
|
#elif defined(__GLIBC__)
|
|
static std::atomic_flag s_inProgress = ATOMIC_FLAG_INIT;
|
|
if (s_inProgress.test_and_set()) {
|
|
return;
|
|
}
|
|
std::array<void*, 64> frames{};
|
|
const int count = backtrace(frames.data(), static_cast<int>(frames.size()));
|
|
RT_LOGF(RT_TAG_RUNTIME, "Host stack trace (%d frames):\n", count);
|
|
for (int i = 0; i < count; ++i) {
|
|
LogHostAddress(" #", i, frames[static_cast<size_t>(i)]);
|
|
}
|
|
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);
|
|
|
|
// Exceptions the processor itself raises for the faulting instruction.
|
|
bool IsCpuFaultException(DWORD code) noexcept {
|
|
switch (code) {
|
|
case EXCEPTION_ACCESS_VIOLATION:
|
|
case EXCEPTION_IN_PAGE_ERROR:
|
|
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
|
case EXCEPTION_PRIV_INSTRUCTION:
|
|
case EXCEPTION_INT_DIVIDE_BY_ZERO:
|
|
case EXCEPTION_INT_OVERFLOW:
|
|
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
|
|
case EXCEPTION_DATATYPE_MISALIGNMENT:
|
|
case EXCEPTION_STACK_OVERFLOW:
|
|
case EXCEPTION_FLT_DENORMAL_OPERAND:
|
|
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
|
case EXCEPTION_FLT_INEXACT_RESULT:
|
|
case EXCEPTION_FLT_INVALID_OPERATION:
|
|
case EXCEPTION_FLT_OVERFLOW:
|
|
case EXCEPTION_FLT_STACK_CHECK:
|
|
case EXCEPTION_FLT_UNDERFLOW:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
// This handler sees every exception on every thread before any frame handler
|
|
// does, so only CPU faults are fatal here. Everything else is raised in
|
|
// software and is often caught by its own frames: C++ exceptions (left to
|
|
// reach std::terminate so their what() is logged), OutputDebugString,
|
|
// SetThreadName, ASan reports, msxml6's customer codes while mscms parses a
|
|
// display colour profile, and RPC's 0x6BA (RPC_S_SERVER_UNAVAILABLE) inside
|
|
// the shell folder picker behind F10 > Diagnostics > Export Logs. Any of
|
|
// them that truly goes unhandled still reaches UnhandledSehFilter.
|
|
if (!IsCpuFaultException(info->ExceptionRecord->ExceptionCode)) {
|
|
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());
|
|
#if defined(__GLIBC__)
|
|
{
|
|
char threadName[32] = "?";
|
|
pthread_getname_np(pthread_self(), threadName, sizeof(threadName));
|
|
RT_LOGF(RT_TAG_RUNTIME, "Faulting host thread: '%s' (tid %ld)\n", threadName, static_cast<long>(gettid()));
|
|
if (ucontextVoid != nullptr) {
|
|
const auto* uc = static_cast<const ucontext_t*>(ucontextVoid);
|
|
#if defined(__aarch64__)
|
|
LogHostAddress("Faulting pc #", 0, reinterpret_cast<const void*>(uc->uc_mcontext.pc));
|
|
LogHostAddress("Faulting lr #", 0, reinterpret_cast<const void*>(uc->uc_mcontext.regs[30]));
|
|
#elif defined(__x86_64__)
|
|
LogHostAddress("Faulting pc #", 0, reinterpret_cast<const void*>(uc->uc_mcontext.gregs[REG_RIP]));
|
|
#endif
|
|
}
|
|
}
|
|
#endif
|
|
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();
|
|
#if defined(__ANDROID__)
|
|
// Aurora defaults resourcesPath to SDL_GetBasePath(), which is empty on
|
|
// Android; the transferable pipeline cache lives with the other bundled
|
|
// resources the activity unpacked.
|
|
std::string auroraResourcesPath;
|
|
if (const auto resources = RuntimeConfigFile::ExecutableDirectory()) {
|
|
auroraResourcesPath = RuntimeConfigFile::PathToUtf8(*resources);
|
|
auroraConfig.resourcesPath = auroraResourcesPath.c_str();
|
|
}
|
|
#endif
|
|
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 mkw::vr::OpenXRStartupResult openxrStartup =
|
|
mkw::vr::OpenXRPrepareAurora(auroraConfig);
|
|
if (openxrStartup == mkw::vr::OpenXRStartupResult::Unavailable) {
|
|
const std::string error = mkw::vr::OpenXRLastError();
|
|
if (RuntimeConfigFile::VrRequired(false)) {
|
|
throw std::runtime_error("OpenXR is required but unavailable: " + error);
|
|
}
|
|
RT_LOG(RT_TAG_RUNTIME) << "OpenXR unavailable; continuing with desktop rendering: "
|
|
<< error << std::endl;
|
|
}
|
|
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 (auroraInfo.initializationStatus != AURORA_INITIALIZATION_SUCCESS) {
|
|
throw std::runtime_error(auroraInfo.initializationError != nullptr
|
|
? auroraInfo.initializationError : "No supported graphics backend is available");
|
|
}
|
|
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();
|
|
GxThread::Configure(RuntimeConfigFile::GxThread());
|
|
GxThread::SetWaitCallback(ServiceGuestTimingDuringAuroraFrameWait);
|
|
GxThread::Start();
|
|
if (GxThread::Enabled()) {
|
|
// The GX thread is aurora's producer: the game thread never waits
|
|
// inside aurora any more, and it pumps SDL itself (aurora_update).
|
|
aurora_set_frame_worker_wait_callback(nullptr);
|
|
aurora_set_host_event_pump(true);
|
|
}
|
|
aurora_set_frame_log_callback(GxThreadFrameLog);
|
|
// The desktop overlay's ImGui frames belong to this thread from the
|
|
// first frame on; the seal replays a copy of their draw data.
|
|
aurora_imgui_host_frame_begin();
|
|
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);
|
|
|
|
if (openxrStartup == mkw::vr::OpenXRStartupResult::Prepared &&
|
|
!mkw::vr::OpenXRStartAfterAurora(auroraInfo.backend)) {
|
|
const std::string error = mkw::vr::OpenXRLastError();
|
|
if (RuntimeConfigFile::VrRequired(false)) {
|
|
throw std::runtime_error("OpenXR graphics binding failed: " + error);
|
|
}
|
|
RT_LOG(RT_TAG_RUNTIME)
|
|
<< "OpenXR graphics binding failed; continuing with the desktop mirror: "
|
|
<< error << std::endl;
|
|
}
|
|
|
|
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);
|
|
GxThread::Stop();
|
|
mkw::vr::OpenXRShutdownBeforeAurora();
|
|
physical_wheel::Shutdown();
|
|
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);
|
|
GxThread::Stop();
|
|
mkw::vr::OpenXRShutdownBeforeAurora();
|
|
physical_wheel::Shutdown();
|
|
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);
|
|
GxThread::Stop();
|
|
mkw::vr::OpenXRShutdownBeforeAurora();
|
|
physical_wheel::Shutdown();
|
|
aurora_shutdown();
|
|
DiscordPresence::Shutdown();
|
|
ShutdownProcessTranscript();
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
return RuntimeMain(argc, argv);
|
|
}
|
|
extern "C" bool g_dynamicAspectRatioEnabled = false;
|