#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) #ifndef NOMINMAX #define NOMINMAX #endif #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include #include #include #include #include #else #include #include #include #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 #include #include #include // 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 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/__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(::getpid()); #endif const auto now = std::chrono::system_clock::now(); const auto secs = std::chrono::duration_cast(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 lock(state.fileMutex); state.file.write(data, static_cast(size)); state.file.flush(); } void PumpTranscriptPipe(ProcessTranscriptState& state, int readFd, int mirrorFd) { std::array buffer{}; for (;;) { #if defined(_WIN32) const int bytesRead = _read(readFd, buffer.data(), static_cast(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(bytesRead)) { #if defined(_WIN32) const int written = _write(mirrorFd, buffer.data() + offset, static_cast(static_cast(bytesRead) - offset)); #else const ssize_t written = ::write(mirrorFd, buffer.data() + offset, static_cast(bytesRead) - offset); #endif if (written <= 0) { break; } offset += static_cast(written); } } WriteProcessTranscriptChunk(state, buffer.data(), static_cast(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(file)), std::istreambuf_iterator()); 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(::getpid()); #endif const std::string setupVersion = ReadInstalledSetupVersion(); { std::lock_guard 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 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(framesToSkip), static_cast(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(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(frames[i]), &module) != 0 && module != nullptr) { moduleBase = reinterpret_cast(module); std::wstring modulePathBuffer(MAX_PATH, L'\0'); for (;;) { const DWORD length = GetModuleFileNameW(module, modulePathBuffer.data(), static_cast(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 symbolBuffer{}; auto* symbol = reinterpret_cast(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(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(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(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(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() ? "" : 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(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::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(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(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(info->ExceptionRecord->ExceptionAddress); g_sehLastAccessType = 0; g_sehLastAccessedAddress = 0; if (g_sehLastExceptionCode == EXCEPTION_ACCESS_VIOLATION && info->ExceptionRecord->NumberParameters >= 2) { g_sehLastAccessType = static_cast(info->ExceptionRecord->ExceptionInformation[0]); g_sehLastAccessedAddress = static_cast(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(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(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(sig); g_sehLastExceptionAddress = reinterpret_cast(faultAddress); g_sehLastAccessType = isWrite ? 1u : 0u; g_sehLastAccessedAddress = reinterpret_cast(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(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 /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 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;