/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2023 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #include #include "xenia/emulator.h" #include "config.h" #include "third_party/fmt/include/fmt/format.h" #include "third_party/tabulate/single_include/tabulate/tabulate.hpp" #include "third_party/zarchive/include/zarchive/zarchivecommon.h" #include "third_party/zarchive/include/zarchive/zarchivewriter.h" #include "third_party/zarchive/src/sha_256.h" #include "xenia/apu/audio_system.h" #include "xenia/base/assert.h" #include "xenia/base/byte_stream.h" #include "xenia/base/clock.h" #include "xenia/base/cvar.h" #include "xenia/base/debugging.h" #include "xenia/base/exception_handler.h" #include "xenia/base/literals.h" #include "xenia/base/logging.h" #include "xenia/base/mapped_memory.h" #include "xenia/base/platform.h" #include "xenia/base/string.h" #include "xenia/base/system.h" #include "xenia/base/guest_liveness.h" #include "xenia/cpu/backend/code_cache.h" #include "xenia/cpu/backend/null_backend.h" #include "xenia/cpu/cpu_flags.h" #include "xenia/cpu/ppc/ppc_context.h" #include "xenia/cpu/thread_state.h" #include "xenia/kernel/xthread.h" #include "xenia/gpu/command_processor.h" #include "xenia/gpu/graphics_system.h" #include "xenia/hid/input_driver.h" #include "xenia/hid/input_system.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/title_id_utils.h" #include "xenia/kernel/user_module.h" #include "xenia/kernel/xam/achievement_manager.h" #include "xenia/kernel/xam/xam_module.h" #include "xenia/kernel/xam/xdbf/spa_info.h" #include "xenia/kernel/xbdm/xbdm_module.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_module.h" #include "xenia/memory.h" #include "xenia/ui/file_picker.h" #include "xenia/ui/imgui_dialog.h" #include "xenia/ui/imgui_drawer.h" #include "xenia/ui/imgui_host_notification.h" #include "xenia/ui/window.h" #include "xenia/ui/windowed_app_context.h" #include "xenia/vfs/device.h" #include "xenia/vfs/devices/disc_image_device.h" #include "xenia/vfs/devices/disc_zarchive_device.h" #include "xenia/vfs/devices/host_path_device.h" #include "xenia/vfs/devices/null_device.h" #include "xenia/vfs/devices/xcontent_container_device.h" #include "xenia/vfs/virtual_file_system.h" #if XE_ARCH_AMD64 #include "xenia/cpu/backend/x64/x64_backend.h" #elif XE_ARCH_ARM64 #include "xenia/cpu/backend/a64/a64_backend.h" #endif // XE_ARCH DEFINE_double(time_scalar, 1.0, "Scalar used to speed or slow time (1x, 2x, 1/2x, etc).", "General"); DEFINE_string( launch_module, "", "Executable to launch from the .iso or the package instead of default.xex " "or the module specified by the game. Leave blank to launch the default " "module.", "General"); DEFINE_bool(allow_game_relative_writes, false, "Not useful to non-developers. Allows code to write to paths " "relative to game://. Used for " "generating test data to compare with original hardware. ", "General"); DECLARE_bool(allow_plugins); DEFINE_int32(priority_class, 0, "Forces Xenia to use different process priority than default one. " "It might affect performance and cause unexpected bugs. Possible " "values: 0 - Normal, 1 - Above normal, 2 - High", "General"); DEFINE_int32(hang_watchdog_secs, 0, "If the guest stops presenting frames for this many seconds, dump " "every guest thread's registers and guest call stack to the log " "(0 = off). Diagnostics only; requires no debugger.", "General"); namespace xe { using namespace xe::literals; Emulator::GameConfigLoadCallback::GameConfigLoadCallback(Emulator& emulator) : emulator_(emulator) { emulator_.AddGameConfigLoadCallback(this); } Emulator::GameConfigLoadCallback::~GameConfigLoadCallback() { emulator_.RemoveGameConfigLoadCallback(this); } Emulator::Emulator(const std::filesystem::path& command_line, const std::filesystem::path& storage_root, const std::filesystem::path& content_root, const std::filesystem::path& cache_root) : on_launch(), on_terminate(), on_exit(), command_line_(command_line), storage_root_(storage_root), content_root_(content_root), cache_root_(cache_root), title_name_(), title_version_(), display_window_(nullptr), memory_(), audio_system_(), audio_media_player_(), graphics_system_(), input_system_(), export_resolver_(), file_system_(), kernel_state_(), main_thread_(), title_id_(std::nullopt), game_info_database_(), paused_(false), restoring_(false), restore_fence_() { if (cvars::priority_class != 0) { if (SetProcessPriorityClass(cvars::priority_class)) { XELOGI("Higher priority class request: Successful. New priority: {}", cvars::priority_class); } } #if XE_PLATFORM_WIN32 == 1 // Show a disclaimer that links to the quickstart // guide the first time they ever open the emulator uint64_t persistent_flags = GetPersistentEmulatorFlags(); if (!(persistent_flags & EmulatorFlagDisclaimerAcknowledged)) { if ((MessageBoxW( nullptr, L"DISCLAIMER: Xenia is not for enabling illegal activity, and " "support is unavailable for illegally obtained software.\n\n" "Please respect this policy as no further reminders will be " "given.\n\nThe quickstart guide explains how to use digital or " "physical games from your Xbox 360 console.\n\nWould you like " "to open it?", L"Xenia", MB_YESNO | MB_ICONQUESTION) == IDYES)) { LaunchWebBrowser( "https://github.com/xenia-canary/xenia-canary/wiki/" "Quickstart#how-to-rip-games"); } SetPersistentEmulatorFlags(persistent_flags | EmulatorFlagDisclaimerAcknowledged); } #endif } Emulator::~Emulator() { // Note that we delete things in the reverse order they were initialized. if (hang_watchdog_running_) { hang_watchdog_running_ = false; if (hang_watchdog_thread_.joinable()) { hang_watchdog_thread_.join(); } } // Give the systems time to shutdown before we delete them. if (graphics_system_) { graphics_system_->Shutdown(); } if (audio_system_) { audio_system_->Shutdown(); } input_system_.reset(); graphics_system_.reset(); audio_system_.reset(); audio_media_player_.reset(); kernel_state_.reset(); file_system_.reset(); processor_.reset(); export_resolver_.reset(); ExceptionHandler::Uninstall(Emulator::ExceptionCallbackThunk, this); } X_STATUS Emulator::Setup( ui::Window* display_window, ui::ImGuiDrawer* imgui_drawer, bool require_cpu_backend, std::function(cpu::Processor*)> audio_system_factory, std::function()> graphics_system_factory, std::function>(ui::Window*)> input_driver_factory) { X_STATUS result = X_STATUS_UNSUCCESSFUL; display_window_ = display_window; imgui_drawer_ = imgui_drawer; // Initialize clock. // 360 uses a 50MHz clock. Clock::set_guest_tick_frequency(50000000); // We could reset this with save state data/constant value to help replays. Clock::set_guest_system_time_base(Clock::QueryHostSystemTime()); // This can be adjusted dynamically, as well. Clock::set_guest_time_scalar(cvars::time_scalar); // Before we can set thread affinity we must enable the process to use all // logical processors. xe::threading::EnableAffinityConfiguration(); XELOGI("{}: Initializing Memory...", __func__); // Create memory system first, as it is required for other systems. memory_ = std::make_unique(); if (!memory_->Initialize()) { XELOGE("{}: Cannot initalize memory!", __func__); return result; } XELOGI("{}: Initializing Exports...", __func__); // Shared export resolver used to attach and query for HLE exports. export_resolver_ = std::make_unique(); std::unique_ptr backend; #if XE_ARCH_AMD64 if (cvars::cpu == "x64") { backend.reset(new xe::cpu::backend::x64::X64Backend()); } #elif XE_ARCH_ARM64 if (cvars::cpu == "a64") { backend.reset(new xe::cpu::backend::a64::A64Backend()); } #endif // XE_ARCH if (cvars::cpu == "any") { if (!backend) { #if XE_ARCH_AMD64 backend.reset(new xe::cpu::backend::x64::X64Backend()); #elif XE_ARCH_ARM64 backend.reset(new xe::cpu::backend::a64::A64Backend()); #endif // XE_ARCH } } if (!backend && !require_cpu_backend) { backend.reset(new xe::cpu::backend::NullBackend()); } XELOGI("{}: Initializing Processor...", __func__); // Initialize the CPU. processor_ = std::make_unique(memory_.get(), export_resolver_.get()); if (!processor_->Setup(std::move(backend))) { XELOGE("{}: Cannot initalize processor!", __func__); return X_STATUS_UNSUCCESSFUL; } XELOGI("{}: Initializing Audio...", __func__); // Initialize the APU. if (audio_system_factory) { audio_system_ = audio_system_factory(processor_.get()); if (!audio_system_) { XELOGE("{}: Cannot initalize audio_system!", __func__); return X_STATUS_NOT_IMPLEMENTED; } } XELOGI("{}: Initializing Graphics...", __func__); // Initialize the GPU. graphics_system_ = graphics_system_factory(); if (!graphics_system_) { XELOGE("{}: Cannot initalize graphics_system!", __func__); return X_STATUS_NOT_IMPLEMENTED; } XELOGI("{}: Initializing HID...", __func__); // Initialize the HID. input_system_ = std::make_unique(display_window_); if (!input_system_) { XELOGE("{}: Cannot initalize input_system!", __func__); return X_STATUS_NOT_IMPLEMENTED; } if (input_driver_factory) { auto input_drivers = input_driver_factory(display_window_); for (size_t i = 0; i < input_drivers.size(); ++i) { input_system_->AddDriver(std::move(input_drivers[i])); } } result = input_system_->Setup(); if (result) { return result; } // Add inputSystem to UI imgui_drawer_->LoadInputSystem(input_system_.get()); XELOGI("{}: Initializing VFS...", __func__); // Bring up the virtual filesystem used by the kernel. file_system_ = std::make_unique(); patcher_ = std::make_unique(storage_root_); XELOGI("{}: Initializing Kernel...", __func__); // Shared kernel state. kernel_state_ = std::make_unique(this); // Hang watchdog (--hang_watchdog_secs, diagnostics, default off). // // The tutorial->menu freeze is a GUEST hang, not a host deadlock: the host // keeps running while a guest thread spins on a memory flag nobody sets. A // host debugger is awkward to attach to that (ptrace is locked down, and the // window may die before we get to it), but the emulator can simply read its // own guest state. When the guest stops presenting frames, dump every guest // thread's registers plus a walk of its guest stack -- enough to name the // spinning function and the address it is polling. if (cvars::hang_watchdog_secs > 0) { hang_watchdog_running_ = true; hang_watchdog_thread_ = std::thread([this]() { xe::threading::set_name("Hang Watchdog"); const uint64_t limit = static_cast(cvars::hang_watchdog_secs); uint64_t last_swaps = 0; uint64_t stalled_s = 0; bool dumped = false; while (hang_watchdog_running_) { std::this_thread::sleep_for(std::chrono::seconds(1)); if (!hang_watchdog_running_) { break; } const uint64_t swaps = xe::liveness::guest_swaps.load(std::memory_order_relaxed); if (swaps != last_swaps) { last_swaps = swaps; if (stalled_s >= limit) { XELOGE("HANG-WD guest recovered after {}s (swaps resumed)", stalled_s); } stalled_s = 0; dumped = false; continue; } // Nothing presented for another second. if (++stalled_s < limit || dumped) { continue; } dumped = true; // one dump per hang, not one per second DumpGuestHang(stalled_s); } }); XELOGI("HANG-WD armed: dump guest state after {}s without a frame", cvars::hang_watchdog_secs); } #define LOAD_KERNEL_MODULE(t) \ static_cast(kernel_state_->LoadKernelModule()) // HLE kernel modules. LOAD_KERNEL_MODULE(xboxkrnl::XboxkrnlModule); LOAD_KERNEL_MODULE(xam::XamModule); LOAD_KERNEL_MODULE(xbdm::XbdmModule); #undef LOAD_KERNEL_MODULE plugin_loader_ = std::make_unique( kernel_state_.get(), storage_root() / "plugins"); XELOGI("{}: Starting graphics_system...", __func__); // Setup the core components. result = graphics_system_->Setup( processor_.get(), kernel_state_.get(), display_window_ ? &display_window_->app_context() : nullptr, display_window_ != nullptr); if (result) { XELOGE("{}: Failed to setup graphics_system!", __func__); return result; } if (audio_system_) { XELOGI("{}: Starting audio_system...", __func__); result = audio_system_->Setup(kernel_state_.get()); if (result) { XELOGE("{}: Failed to setup audio_system!", __func__); return result; } audio_media_player_ = std::make_unique( audio_system_.get(), kernel_state_.get()); audio_media_player_->Setup(); } // Initialize emulator fallback exception handling last. ExceptionHandler::Install(Emulator::ExceptionCallbackThunk, this); return result; } X_STATUS Emulator::TerminateTitle() { if (!is_title_open()) { return X_STATUS_UNSUCCESSFUL; } kernel_state_->TerminateTitle(); title_id_ = std::nullopt; title_name_ = ""; title_version_ = ""; on_terminate(); return X_STATUS_SUCCESS; } const std::unique_ptr Emulator::CreateVfsDevice( const std::filesystem::path& path, const std::string_view mount_path) { // Must check if the type has changed e.g. XamSwapDisc switch (GetFileSignature(path)) { case FileSignatureType::XEX1: case FileSignatureType::XEX2: case FileSignatureType::ELF: { auto parent_path = path.parent_path(); return std::make_unique( mount_path, parent_path, !cvars::allow_game_relative_writes); } break; case FileSignatureType::LIVE: case FileSignatureType::CON: case FileSignatureType::PIRS: { return vfs::XContentContainerDevice::CreateContentDevice(mount_path, path); } break; case FileSignatureType::XISO: { return std::make_unique(mount_path, path); } break; case FileSignatureType::ZAR: { return std::make_unique(mount_path, path); } break; case FileSignatureType::EXE: case FileSignatureType::Unknown: default: return nullptr; break; } } uint64_t Emulator::GetPersistentEmulatorFlags() { #if XE_PLATFORM_WIN32 == 1 uint64_t value = 0; DWORD value_size = sizeof(value); HKEY xenia_hkey = nullptr; LSTATUS lstat = RegOpenKeyA(HKEY_CURRENT_USER, "SOFTWARE\\Xenia", &xenia_hkey); if (!xenia_hkey) { // let the Set function create the key and initialize it to 0 SetPersistentEmulatorFlags(0ULL); return 0ULL; } lstat = RegQueryValueExA(xenia_hkey, "XEFLAGS", 0, NULL, reinterpret_cast(&value), &value_size); RegCloseKey(xenia_hkey); if (lstat) { return 0ULL; } return value; #else return EmulatorFlagDisclaimerAcknowledged; #endif } void Emulator::SetPersistentEmulatorFlags(uint64_t new_flags) { #if XE_PLATFORM_WIN32 == 1 uint64_t value = new_flags; DWORD value_size = sizeof(value); HKEY xenia_hkey = nullptr; LSTATUS lstat = RegOpenKeyA(HKEY_CURRENT_USER, "SOFTWARE\\Xenia", &xenia_hkey); if (!xenia_hkey) { lstat = RegCreateKeyA(HKEY_CURRENT_USER, "SOFTWARE\\Xenia", &xenia_hkey); } lstat = RegSetValueExA(xenia_hkey, "XEFLAGS", 0, REG_QWORD, reinterpret_cast(&value), 8); RegFlushKey(xenia_hkey); RegCloseKey(xenia_hkey); #endif } X_STATUS Emulator::MountPath(const std::filesystem::path& path, const std::string_view mount_path) { auto device = CreateVfsDevice(path, mount_path); if (!device || !device->Initialize()) { XELOGE( "Unable to mount the selected file, it is an unsupported format or " "corrupted."); return X_STATUS_NO_SUCH_FILE; } if (!file_system_->RegisterDevice(std::move(device))) { XELOGE("Unable to register the input file to {}.", mount_path); return X_STATUS_NO_SUCH_FILE; } file_system_->UnregisterSymbolicLink(kDefaultPartitionSymbolicLink); file_system_->UnregisterSymbolicLink(kDefaultGameSymbolicLink); file_system_->UnregisterSymbolicLink("plugins:"); // Create symlinks to the device. file_system_->RegisterSymbolicLink(kDefaultGameSymbolicLink, mount_path); file_system_->RegisterSymbolicLink(kDefaultPartitionSymbolicLink, mount_path); return X_STATUS_SUCCESS; } Emulator::FileSignatureType Emulator::GetFileSignature( const std::filesystem::path& path) { FILE* file = xe::filesystem::OpenFile(path, "rb"); if (!file) { return FileSignatureType::Unknown; } const uint64_t file_size = std::filesystem::file_size(path); constexpr int64_t header_size = 4; if (file_size < header_size) { return FileSignatureType::Unknown; } char file_magic[header_size]; fread(file_magic, sizeof(file_magic), 1, file); fourcc_t magic_value = make_fourcc(file_magic[0], file_magic[1], file_magic[2], file_magic[3]); fclose(file); switch (magic_value) { case xe::cpu::kXEX1Signature: return FileSignatureType::XEX1; case xe::cpu::kXEX2Signature: return FileSignatureType::XEX2; case xe::vfs::kCONSignature: return FileSignatureType::CON; case xe::vfs::kLIVESignature: return FileSignatureType::LIVE; case xe::vfs::kPIRSSignature: return FileSignatureType::PIRS; case xe::vfs::kXSFSignature: return FileSignatureType::XISO; case xe::cpu::kElfSignature: return FileSignatureType::ELF; default: break; } magic_value = make_fourcc(file_magic[0], file_magic[1], 0, 0); if (xe::kernel::kEXESignature == magic_value) { return FileSignatureType::EXE; } file = xe::filesystem::OpenFile(path, "rb"); xe::filesystem::Seek(file, -header_size, SEEK_END); fread(file_magic, 1, header_size, file); fclose(file); magic_value = make_fourcc(file_magic[0], file_magic[1], file_magic[2], file_magic[3]); if (xe::vfs::kZarMagic == magic_value) { return FileSignatureType::ZAR; } // Check if XISO std::unique_ptr device = std::make_unique("", path); XELOGI("Checking for XISO"); if (device->Initialize()) { return FileSignatureType::XISO; } XELOGE("{}: {} ({:08X})", __func__, path.extension(), magic_value); return FileSignatureType::Unknown; } void Emulator::DumpGuestHang(uint64_t stalled_s) { // Read-only autopsy of a hung guest. Racy by nature (the threads keep // running); that is fine -- a spinning thread's stack pointer and link // register are stable, which is exactly what we need. constexpr uint32_t kCodeLo = 0x82000000u; constexpr uint32_t kCodeHi = 0x84000000u; XELOGE("=========================== HANG-WD ==========================="); XELOGE("HANG-WD no frame presented for {}s -- dumping guest threads.", stalled_s); auto threads = kernel_state()->object_table()->GetObjectsByType(); for (const auto& thread : threads) { if (!thread || !thread->is_guest_thread()) { continue; } auto thread_state = thread->thread_state(); if (!thread_state) { continue; } auto ctx = thread_state->context(); if (!ctx) { continue; } XELOGE( "HANG-WD tid={:04X} '{}' running={} lr={:08X} ctr={:08X} r1={:08X}", thread->thread_id(), thread->name(), thread->is_running(), static_cast(ctx->lr), static_cast(ctx->ctr), static_cast(ctx->r[1])); XELOGE( "HANG-WD r3={:08X} r4={:08X} r5={:08X} r6={:08X} r7={:08X} " "r8={:08X} r9={:08X} r10={:08X} r11={:08X} r12={:08X}", static_cast(ctx->r[3]), static_cast(ctx->r[4]), static_cast(ctx->r[5]), static_cast(ctx->r[6]), static_cast(ctx->r[7]), static_cast(ctx->r[8]), static_cast(ctx->r[9]), static_cast(ctx->r[10]), static_cast(ctx->r[11]), static_cast(ctx->r[12])); // Walk the guest stack: PowerPC back chain -- [sp] = caller sp, // [sp+4] = saved LR. Stop on anything that stops looking like a frame. std::string frames; uint32_t sp = static_cast(ctx->r[1]); for (int depth = 0; depth < 16; ++depth) { if (sp < 0x1000 || !memory()->TranslateVirtual(sp)) { break; } auto frame = memory()->TranslateVirtual(sp); const uint32_t next_sp = xe::load_and_swap(frame); const uint32_t saved_lr = xe::load_and_swap(frame + 4); if (saved_lr >= kCodeLo && saved_lr < kCodeHi) { frames += fmt::format(" {:08X}", saved_lr); } if (next_sp <= sp || next_sp - sp > 0x10000) { break; // not a plausible back chain anymore } sp = next_sp; } if (!frames.empty()) { XELOGE("HANG-WD guest stack:{}", frames); } } XELOGE("HANG-WD ==== resolve these PCs with: zq.py fn ===="); } X_STATUS Emulator::LaunchPath(const std::filesystem::path& path) { X_STATUS mount_result = X_STATUS_SUCCESS; switch (GetFileSignature(path)) { case FileSignatureType::XEX1: case FileSignatureType::XEX2: case FileSignatureType::ELF: { mount_result = MountPath(path, "\\Device\\Harddisk0\\Partition1"); return mount_result ? mount_result : LaunchXexFile(path); } break; case FileSignatureType::LIVE: case FileSignatureType::CON: case FileSignatureType::PIRS: { mount_result = MountPath(path, "\\Device\\Package_0"); return mount_result ? mount_result : LaunchStfsContainer(path); } break; case FileSignatureType::XISO: { mount_result = MountPath(path, "\\Device\\Cdrom0"); return mount_result ? mount_result : LaunchDiscImage(path); } break; case FileSignatureType::ZAR: { mount_result = MountPath(path, "\\Device\\Cdrom0"); return mount_result ? mount_result : LaunchDiscArchive(path); } break; case FileSignatureType::EXE: case FileSignatureType::Unknown: default: return X_STATUS_NOT_SUPPORTED; break; } } X_STATUS Emulator::LaunchXexFile(const std::filesystem::path& path) { // We create a virtual filesystem pointing to its directory and symlink // that to the game filesystem. // e.g., /my/files/foo.xex will get a local fs at: // \\Device\\Harddisk0\\Partition1 // and then get that symlinked to game:\, so // -> game:\foo.xex // Get just the filename (foo.xex). auto file_name = path.filename(); // Launch the game. auto fs_path = fmt::format("{}\\", kDefaultGameSymbolicLink) + xe::path_to_utf8(file_name); X_STATUS result = CompleteLaunch(path, fs_path); if (XFAILED(result)) { return result; } kernel_state_->deployment_type_ = XDeploymentType::kInstalledToHDD; if (!kernel::IsSystemTitle(kernel_state_->title_id())) { return result; } const std::string mount_path = utf8::find_base_guest_path(kernel_state_->GetExecutableModule()->path()); // System related symlinks. This should point to dashboard location in the // future. file_system_->RegisterSymbolicLink("\\SystemRoot", mount_path); auto module = kernel_state_->LoadUserModule("xam.xex"); if (!module) { module = kernel_state_->LoadUserModule("$flash_xam.xex"); } if (module) { result = kernel_state_->FinishLoadingUserModule(module, false); } return result; } X_STATUS Emulator::LaunchDiscImage(const std::filesystem::path& path) { std::string module_path = FindLaunchModule(); X_STATUS result = CompleteLaunch(path, module_path); if (result == X_STATUS_NOT_FOUND && !cvars::launch_module.empty()) { return LaunchDefaultModule(path); } kernel_state_->deployment_type_ = XDeploymentType::kOpticalDisc; return result; } X_STATUS Emulator::LaunchDiscArchive(const std::filesystem::path& path) { std::string module_path = FindLaunchModule(); X_STATUS result = CompleteLaunch(path, module_path); if (result == X_STATUS_NOT_FOUND && !cvars::launch_module.empty()) { return LaunchDefaultModule(path); } kernel_state_->deployment_type_ = XDeploymentType::kOpticalDisc; return result; } X_STATUS Emulator::LaunchStfsContainer(const std::filesystem::path& path) { std::string module_path = FindLaunchModule(); X_STATUS result = CompleteLaunch(path, module_path); if (result == X_STATUS_NOT_FOUND && !cvars::launch_module.empty()) { return LaunchDefaultModule(path); } kernel_state_->deployment_type_ = XDeploymentType::kDownload; return result; } X_STATUS Emulator::LaunchDefaultModule(const std::filesystem::path& path) { cvars::launch_module = ""; std::string module_path = FindLaunchModule(); X_STATUS result = CompleteLaunch(path, module_path); if (XSUCCEEDED(result)) { kernel_state_->deployment_type_ = XDeploymentType::kInstalledToHDD; auto title_id = kernel_state_->title_id(); if (!kernel::IsSystemTitle(title_id)) { // Assumption that any loaded game is loaded as a disc. kernel_state_->deployment_type_ = XDeploymentType::kOpticalDisc; } } return result; } X_STATUS Emulator::DataMigration(const uint64_t xuid) { uint32_t failure_count = 0; const std::string xuid_string = fmt::format("{:016X}", xuid); const std::string common_xuid_string = fmt::format("{:016X}", 0); const std::filesystem::path path_to_profile_data = content_root_ / xuid_string / "FFFE07D1" / "00010000" / xuid_string; // Filter directories inside. First we need to find any content type // directories. // Savefiles must go to user specific directory // Everything else goes to common const auto titles_to_move = xe::filesystem::FilterByName( xe::filesystem::ListDirectories(content_root_), std::regex("[A-F0-9]{8}")); for (const auto& title : titles_to_move) { if (xe::path_to_utf8(title.name) == "FFFE07D1" || xe::path_to_utf8(title.name) == "00000000") { // SKip any dashboard/profile related data that was previously installed continue; } const auto content_type_dirs = xe::filesystem::FilterByName( xe::filesystem::ListDirectories(title.path / title.name), std::regex("[A-F0-9]{8}")); for (const auto& content_type : content_type_dirs) { const std::string used_xuid = xe::path_to_utf8(content_type.name) == "00000001" ? xuid_string : common_xuid_string; const auto previous_path = content_root_ / title.name / content_type.name; const auto path = content_root_ / used_xuid / title.name; if (!std::filesystem::exists(path)) { std::filesystem::create_directories(path); } std::error_code ec; std::filesystem::rename(previous_path, path / content_type.name, ec); if (ec) { failure_count++; XELOGW("{}: Moving from: {} to: {} failed! Error message: {} ({:08X})", __func__, previous_path, path / content_type.name, ec.message(), ec.value()); } } // Other directories: // Headers - Just copy everything to both common and xuid locations // profile - ? if (std::filesystem::exists(title.path / title.name / "Headers")) { const auto xuid_path = content_root_ / xuid_string / title.name / "Headers"; std::filesystem::create_directories(xuid_path); std::error_code ec; // Copy to specific user std::filesystem::copy(title.path / title.name / "Headers", xuid_path, std::filesystem::copy_options::recursive | std::filesystem::copy_options::skip_existing, ec); if (ec) { failure_count++; XELOGW("{}: Copying from: {} to: {} failed! Error message: {} ({:08X})", __func__, title.path / title.name / "Headers", xuid_path, ec.message(), ec.value()); } const auto header_types = xe::filesystem::ListDirectories(title.path / title.name / "Headers"); if (!(header_types.size() == 1 && header_types.at(0).name == "00000001")) { const auto common_path = content_root_ / common_xuid_string / title.name / "Headers"; std::filesystem::create_directories(common_path); // Copy to common, skip cases where only savefile header is available std::filesystem::copy(title.path / title.name / "Headers", common_path, std::filesystem::copy_options::recursive | std::filesystem::copy_options::skip_existing, ec); if (ec) { failure_count++; XELOGW( "{}: Copying from: {} to: {} failed! Error message: {} ({:08X})", __func__, title.path / title.name / "Headers", common_path, ec.message(), ec.value()); } } if (!ec) { // Remove previous directory std::error_code ec; std::filesystem::remove_all(title.path / title.name / "Headers", ec); } } if (std::filesystem::exists(title.path / title.name / "profile")) { // Find directory with previous username. There should be only one! const auto old_profile_data = xe::filesystem::ListDirectories(title.path / title.name / "profile"); xe::filesystem::FileInfo entry_to_copy = xe::filesystem::FileInfo(); if (old_profile_data.size() != 1) { for (const auto& entry : old_profile_data) { if (entry.name == "User") { entry_to_copy = entry; } } } else { entry_to_copy = old_profile_data.front(); } const auto path_from = title.path / title.name / "profile" / entry_to_copy.name; std::error_code ec; // Move files from inside to outside for convenience std::filesystem::rename(path_from, path_to_profile_data / title.name, ec); if (ec) { failure_count++; XELOGW("{}: Moving from: {} to: {} failed! Error message: {} ({:08X})", __func__, path_from, path_to_profile_data / title.name, ec.message(), ec.value()); } else { std::error_code ec; std::filesystem::remove_all(title.path / title.name / "profile", ec); } } const auto remaining_file_list = xe::filesystem::ListDirectories(title.path / title.name); if (remaining_file_list.empty()) { std::error_code ec; std::filesystem::remove_all(title.path / title.name, ec); } } std::string migration_status_message = fmt::format("Migration finished with {} {}.", failure_count, failure_count == 1 ? "error" : "errors"); if (failure_count) { migration_status_message.append( " For more information check xenia.log file."); } new xe::ui::HostNotificationWindow(imgui_drawer_, "Migration Status", migration_status_message, 0); return X_STATUS_SUCCESS; } X_STATUS Emulator::ProcessContentPackageHeader( const std::filesystem::path& path, ContentInstallEntry& installation_info) { installation_info.name_ = "Invalid Content Package!"; installation_info.content_type_ = XContentType::kInvalid; installation_info.data_installation_path_ = xe::path_to_utf8(path.filename()); const auto header = vfs::XContentContainerDevice::ReadContainerHeader(path); if (!header || !header->content_header.is_magic_valid()) { installation_info.installation_state_ = InstallState::failed; installation_info.installation_result_ = X_STATUS_INVALID_PARAMETER; installation_info.installation_error_message_ = "Invalid Package Type!"; XELOGE("Failed to initialize device"); return X_STATUS_INVALID_PARAMETER; } // Always install savefiles to user signed to slot 0. const auto profile = kernel_state_->xam_state()->profile_manager()->GetProfile( static_cast(0)); uint64_t xuid = header->content_metadata.profile_id; if (header->content_metadata.content_type == XContentType::kSavedGame && profile) { xuid = profile->xuid(); } installation_info.data_installation_path_ = fmt::format( "{:016X}/{:08X}/{:08X}/{}", xuid, header->content_metadata.execution_info.title_id.get(), static_cast(header->content_metadata.content_type.get()), path.filename()); installation_info.header_installation_path_ = fmt::format( "{:016X}/{:08X}/Headers/{:08X}/{}", xuid, header->content_metadata.execution_info.title_id.get(), static_cast(header->content_metadata.content_type.get()), path.filename()); installation_info.name_ = xe::to_utf8(header->content_metadata.display_name(XLanguage::kEnglish)); installation_info.content_type_ = static_cast(header->content_metadata.content_type); installation_info.content_size_ = header->content_metadata.content_size; installation_info.installation_state_ = InstallState::pending; installation_info.icon_ = imgui_drawer_->LoadImGuiIcon( std::span(header->content_metadata.title_thumbnail, header->content_metadata.title_thumbnail_size)); return X_STATUS_SUCCESS; } X_STATUS Emulator::InstallContentPackage( const std::filesystem::path& path, ContentInstallEntry& installation_info) { installation_info.installation_state_ = InstallState::preparing; std::unique_ptr device = vfs::XContentContainerDevice::CreateContentDevice("", path); if (!device || !device->Initialize()) { installation_info.installation_state_ = InstallState::failed; installation_info.installation_error_message_ = "Device initialization failed!"; installation_info.installation_result_ = X_STATUS_ACCESS_DENIED; XELOGE("Failed to initialize device"); return X_STATUS_INVALID_PARAMETER; } const std::filesystem::path installation_path = content_root() / installation_info.data_installation_path_; const std::filesystem::path header_path = content_root() / installation_info.header_installation_path_; if (!std::filesystem::exists(content_root())) { const std::error_code ec = xe::filesystem::CreateFolder(content_root()); if (ec) { installation_info.installation_state_ = InstallState::failed; installation_info.installation_error_message_ = ec.message(); installation_info.installation_result_ = X_STATUS_ACCESS_DENIED; return X_STATUS_ACCESS_DENIED; } } const auto disk_space = std::filesystem::space(content_root()); if (disk_space.available < installation_info.content_size_ * 1.1f) { installation_info.installation_state_ = InstallState::failed; installation_info.installation_error_message_ = "Insufficient disk space!"; installation_info.installation_result_ = X_STATUS_DISK_FULL; return X_STATUS_DISK_FULL; } if (std::filesystem::exists(installation_path)) { // TODO(Gliniak): Popup // Do you want to overwrite already existing data? } else { std::error_code error_code; std::filesystem::create_directories(installation_path, error_code); if (error_code) { installation_info.installation_state_ = InstallState::failed; installation_info.installation_error_message_ = "Cannot Create Content Directory!"; installation_info.installation_result_ = error_code.value(); return error_code.value(); } } installation_info.content_size_ = device->data_size(); installation_info.installation_state_ = InstallState::installing; vfs::VirtualFileSystem::ExtractContentHeader(device.get(), header_path); X_STATUS error_code = vfs::VirtualFileSystem::ExtractContentFiles( device.get(), installation_path, installation_info.currently_installed_size_); if (error_code != X_ERROR_SUCCESS) { installation_info.installation_state_ = InstallState::failed; return error_code; } installation_info.installation_state_ = InstallState::installed; installation_info.currently_installed_size_ = installation_info.content_size_; kernel_state()->BroadcastNotification(kXNotificationLiveContentInstalled, 0); if (installation_info.content_type_ == XContentType::kProfile) { kernel_state_->xam_state()->profile_manager()->ReloadProfiles(); } return error_code; } X_STATUS Emulator::ExtractZarchivePackage( const std::filesystem::path& path, const std::filesystem::path& extract_dir) { std::unique_ptr device = std::make_unique("", path); if (!device->Initialize()) { XELOGE("Failed to initialize device"); return X_STATUS_INVALID_PARAMETER; } if (std::filesystem::exists(extract_dir)) { // TODO(Gliniak): Popup // Do you want to overwrite already existing data? } else { std::error_code error_code; std::filesystem::create_directories(extract_dir, error_code); if (error_code) { return error_code.value(); } } uint64_t progress = 0; return vfs::VirtualFileSystem::ExtractContentFiles(device.get(), extract_dir, progress); } X_STATUS Emulator::CreateZarchivePackage( const std::filesystem::path& inputDirectory, const std::filesystem::path& outputFile) { std::vector buffer; buffer.resize(64 * 1024); std::error_code ec; PackContext packContext; packContext.outputFilePath = outputFile; ZArchiveWriter zWriter( [](int32_t partIndex, void* ctx) { PackContext* packContext = reinterpret_cast(ctx); packContext->currentOutputFile = std::ofstream(packContext->outputFilePath, std::ios::binary); if (!packContext->currentOutputFile.is_open()) { XELOGI("Failed to create output file: {}\n", packContext->outputFilePath.string()); packContext->hasError = true; } }, [](const void* data, size_t length, void* ctx) { PackContext* packContext = reinterpret_cast(ctx); packContext->currentOutputFile.write( reinterpret_cast(data), length); }, &packContext); if (packContext.hasError) { return X_STATUS_UNSUCCESSFUL; } for (auto const& dirEntry : std::filesystem::recursive_directory_iterator(inputDirectory)) { std::filesystem::path pathEntry = std::filesystem::relative(dirEntry.path(), inputDirectory, ec); if (ec) { XELOGI("Failed to get relative path {}\n", pathEntry.string()); return X_STATUS_UNSUCCESSFUL; } if (dirEntry.is_directory()) { if (!zWriter.MakeDir(pathEntry.generic_string().c_str(), false)) { XELOGI("Failed to create directory {}\n", pathEntry.string()); return X_STATUS_UNSUCCESSFUL; } } else if (dirEntry.is_regular_file()) { // Don't pack itself to prevent infinite packing. if (dirEntry == outputFile) { continue; } XELOGI("Adding file: {}\n", pathEntry.string()); if (!zWriter.StartNewFile(pathEntry.generic_string().c_str())) { XELOGI("Failed to create archive file {}\n", pathEntry.string()); return X_STATUS_UNSUCCESSFUL; } std::filesystem::path file_to_pack_path = inputDirectory / pathEntry; FILE* file = xe::filesystem::OpenFile(file_to_pack_path, "rb"); if (!file) { XELOGI("Failed to open input file {}\n", pathEntry.string()); return X_STATUS_UNSUCCESSFUL; } const uint64_t file_size = std::filesystem::file_size(file_to_pack_path); uint64_t total_bytes_read = 0; while (total_bytes_read < file_size) { uint64_t bytes_read = fread(buffer.data(), 1, buffer.size(), file); total_bytes_read += bytes_read; zWriter.AppendData(buffer.data(), bytes_read); } fclose(file); } if (packContext.hasError) { return X_STATUS_UNSUCCESSFUL; } } zWriter.Finalize(); return X_STATUS_SUCCESS; } void Emulator::Pause() { if (paused_) { return; } paused_ = true; // Don't hold the lock on this (so any waits follow through) graphics_system_->Pause(); audio_system_->Pause(); auto lock = global_critical_region::AcquireDirect(); auto threads = kernel_state()->object_table()->GetObjectsByType( kernel::XObject::Type::Thread); auto current_thread = kernel::XThread::IsInThread() ? kernel::XThread::GetCurrentThread() : nullptr; for (auto thread : threads) { // Don't pause ourself or host threads. if (thread == current_thread || !thread->can_debugger_suspend()) { continue; } if (thread->is_running()) { thread->thread()->Suspend(nullptr); } } XELOGD("! EMULATOR PAUSED !"); } void Emulator::Resume() { if (!paused_) { return; } paused_ = false; XELOGD("! EMULATOR RESUMED !"); graphics_system_->Resume(); audio_system_->Resume(); auto threads = kernel_state()->object_table()->GetObjectsByType( kernel::XObject::Type::Thread); for (auto thread : threads) { if (!thread->can_debugger_suspend()) { // Don't pause host threads. continue; } if (!thread->is_running()) { thread->thread()->Resume(nullptr); } } } bool Emulator::SaveToFile(const std::filesystem::path& path) { Pause(); filesystem::CreateEmptyFile(path); auto map = MappedMemory::Open(path, MappedMemory::Mode::kReadWrite, 0, 2_GiB); if (!map) { return false; } // Save the emulator state to a file ByteStream stream(map->data(), map->size()); stream.Write(kEmulatorSaveSignature); stream.Write(title_id_.has_value()); if (title_id_.has_value()) { stream.Write(title_id_.value()); } // It's important we don't hold the global lock here! XThreads need to step // forward (possibly through guarded regions) without worry! processor_->Save(&stream); graphics_system_->Save(&stream); audio_system_->Save(&stream); kernel_state_->Save(&stream); memory_->Save(&stream); map->Close(stream.offset()); Resume(); return true; } bool Emulator::RestoreFromFile(const std::filesystem::path& path) { // Restore the emulator state from a file auto map = MappedMemory::Open(path, MappedMemory::Mode::kReadWrite); if (!map) { return false; } restoring_ = true; // Terminate any loaded titles. Pause(); kernel_state_->TerminateTitle(); auto lock = global_critical_region::AcquireDirect(); ByteStream stream(map->data(), map->size()); if (stream.Read() != kEmulatorSaveSignature) { return false; } auto has_title_id = stream.Read(); std::optional title_id; if (!has_title_id) { title_id = {}; } else { title_id = stream.Read(); } if (title_id_.has_value() != title_id.has_value() || title_id_.value() != title_id.value()) { // Swapping between titles is unsupported at the moment. assert_always(); return false; } if (!processor_->Restore(&stream)) { XELOGE("Could not restore processor!"); return false; } if (!graphics_system_->Restore(&stream)) { XELOGE("Could not restore graphics system!"); return false; } if (!audio_system_->Restore(&stream)) { XELOGE("Could not restore audio system!"); return false; } if (!kernel_state_->Restore(&stream)) { XELOGE("Could not restore kernel state!"); return false; } if (!memory_->Restore(&stream)) { XELOGE("Could not restore memory!"); return false; } // Update the main thread. auto threads = kernel_state_->object_table()->GetObjectsByType(); for (auto thread : threads) { if (thread->main_thread()) { main_thread_ = thread; break; } } Resume(); restore_fence_.Signal(); restoring_ = false; return true; } const std::filesystem::path Emulator::GetNewDiscPath( std::string window_message) { std::filesystem::path path = ""; auto file_picker = xe::ui::FilePicker::Create(); file_picker->set_mode(ui::FilePicker::Mode::kOpen); file_picker->set_type(ui::FilePicker::Type::kFile); file_picker->set_multi_selection(false); file_picker->set_title(!window_message.empty() ? window_message : "Select Content Package"); file_picker->set_extensions({ {"Supported Files", "*.iso;*.xex;*.xcp;*.*"}, {"Disc Image (*.iso)", "*.iso"}, {"Xbox Executable (*.xex)", "*.xex"}, {"All Files (*.*)", "*.*"}, }); if (file_picker->Show()) { auto selected_files = file_picker->selected_files(); if (!selected_files.empty()) { path = selected_files[0]; } } return path; } bool Emulator::ExceptionCallbackThunk(Exception* ex, void* data) { return reinterpret_cast(data)->ExceptionCallback(ex); } bool Emulator::ExceptionCallback(Exception* ex) { // Check to see if the exception occurred in guest code. auto code_cache = processor()->backend()->code_cache(); auto code_base = code_cache->execute_base_address(); auto code_end = code_base + code_cache->total_size(); if (!processor()->is_debugger_attached() && debugging::IsDebuggerAttached()) { // If Xenia's debugger isn't attached but another one is, pass it to that // debugger. return false; } else if (processor()->is_debugger_attached()) { // Let the debugger handle this exception. It may decide to continue past // it (if it was a stepping breakpoint, etc). return processor()->OnUnhandledException(ex); } if (!(ex->pc() >= code_base && ex->pc() < code_end)) { // Didn't occur in guest code. Let it pass. return false; } // Within range. Pause the emulator and eat the exception. Pause(); // Dump information into the log. auto current_thread = kernel::XThread::GetCurrentThread(); assert_not_null(current_thread); auto guest_function = code_cache->LookupFunction(ex->pc()); assert_not_null(guest_function); auto context = current_thread->thread_state()->context(); std::string crash_msg; crash_msg.append("==== CRASH DUMP ====\n"); crash_msg.append(fmt::format("Thread ID (Host: 0x{:08X} / Guest: 0x{:08X})\n", current_thread->thread()->system_id(), current_thread->thread_id())); crash_msg.append( fmt::format("Thread Handle: 0x{:08X}\n", current_thread->handle())); crash_msg.append( fmt::format("PC: 0x{:08X}\n", guest_function->MapMachineCodeToGuestAddress(ex->pc()))); if (ex->code() == Exception::Code::kAccessViolation) { const char* op_str = "unknown"; if (ex->access_violation_operation() == Exception::AccessViolationOperation::kRead) { op_str = "read"; } else if (ex->access_violation_operation() == Exception::AccessViolationOperation::kWrite) { op_str = "write"; } crash_msg.append(fmt::format("Access Violation: {} at 0x{:016X}\n", op_str, ex->fault_address())); } else if (ex->code() == Exception::Code::kIllegalInstruction) { crash_msg.append("Illegal Instruction\n"); } crash_msg.append("Registers:\n"); for (int i = 0; i < 32; i++) { crash_msg.append(fmt::format(" r{:<3} = {:016X}\n", i, context->r[i])); } for (int i = 0; i < 32; i++) { crash_msg.append(fmt::format(" f{:<3} = {:016X} = (double){} = (float){}\n", i, *reinterpret_cast(&context->f[i]), context->f[i], *(float*)&context->f[i])); } for (int i = 0; i < 128; i++) { crash_msg.append( fmt::format(" v{:<3} = [0x{:08X}, 0x{:08X}, 0x{:08X}, 0x{:08X}]\n", i, context->v[i].u32[0], context->v[i].u32[1], context->v[i].u32[2], context->v[i].u32[3])); } XELOGE("{}", crash_msg); std::string crash_dlg = fmt::format( "The guest has crashed.\n\n" "Xenia has now paused itself.\n\n" "{}", crash_msg); // Display a dialog telling the user the guest has crashed. if (display_window_ && imgui_drawer_) { display_window_->app_context().CallInUIThreadSynchronous([this, &crash_dlg]() { xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer_, "Uh-oh!", crash_dlg); }); } // Now suspend ourself (we should be a guest thread). current_thread->Suspend(nullptr); // We should not arrive here! assert_always(); return false; } void Emulator::WaitUntilExit() { while (true) { if (main_thread_) { xe::threading::Wait(main_thread_->thread(), false); } if (restoring_) { restore_fence_.Wait(); } else { // Not restoring and the thread exited. We're finished. break; } } on_exit(); } void Emulator::AddGameConfigLoadCallback(GameConfigLoadCallback* callback) { assert_not_null(callback); // Game config load callbacks handling is entirely in the UI thread. assert_true(!display_window_ || display_window_->app_context().IsInUIThread()); // Check if already added. if (std::find(game_config_load_callbacks_.cbegin(), game_config_load_callbacks_.cend(), callback) != game_config_load_callbacks_.cend()) { return; } game_config_load_callbacks_.push_back(callback); } void Emulator::RemoveGameConfigLoadCallback(GameConfigLoadCallback* callback) { assert_not_null(callback); // Game config load callbacks handling is entirely in the UI thread. assert_true(!display_window_ || display_window_->app_context().IsInUIThread()); auto it = std::find(game_config_load_callbacks_.cbegin(), game_config_load_callbacks_.cend(), callback); if (it == game_config_load_callbacks_.cend()) { return; } if (game_config_load_callback_loop_next_index_ != SIZE_MAX) { // Actualize the next callback index after the erasure from the vector. size_t existing_index = size_t(std::distance(game_config_load_callbacks_.cbegin(), it)); if (game_config_load_callback_loop_next_index_ > existing_index) { --game_config_load_callback_loop_next_index_; } } game_config_load_callbacks_.erase(it); } std::string Emulator::FindLaunchModule() { std::string path(fmt::format("{}\\", kDefaultGameSymbolicLink)); auto xam = kernel_state()->GetKernelModule("xam.xex"); if (!xam->loader_data().launch_path.empty()) { std::string symbolic_link_path; if (kernel_state_->file_system()->FindSymbolicLink(kDefaultGameSymbolicLink, symbolic_link_path)) { std::filesystem::path file_path = symbolic_link_path; // Remove previous symbolic links. // Some titles can provide root within specific directory. kernel_state_->file_system()->UnregisterSymbolicLink( kDefaultPartitionSymbolicLink); kernel_state_->file_system()->UnregisterSymbolicLink( kDefaultGameSymbolicLink); file_path /= std::filesystem::path(xam->loader_data().launch_path); kernel_state_->file_system()->RegisterSymbolicLink( kDefaultPartitionSymbolicLink, xe::path_to_utf8(file_path.parent_path())); kernel_state_->file_system()->RegisterSymbolicLink( kDefaultGameSymbolicLink, xe::path_to_utf8(file_path.parent_path())); return xe::path_to_utf8(file_path); } } if (!cvars::launch_module.empty()) { return path + cvars::launch_module; } return path + "default.xex"; } static std::string format_version(xex2_version version) { // fmt::format doesn't like bit fields we use + to bypass it return fmt::format("{}.{}.{}.{}", +version.major, +version.minor, +version.build, +version.qfe); } X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, const std::string_view module_path) { // Making changes to the UI (setting the icon) and executing game config // load callbacks which expect to be called from the UI thread. // If not on UI thread, dispatch to it synchronously. if (!display_window_->app_context().IsInUIThread()) { X_STATUS result = X_STATUS_UNSUCCESSFUL; display_window_->app_context().CallInUIThreadSynchronous( [this, &path, &module_path, &result]() { result = CompleteLaunch(path, module_path); }); return result; } // Setup NullDevices for raw HDD partition accesses // Cache/STFC code baked into games tries reading/writing to these // By using a NullDevice that just returns success to all IO requests it // should allow games to believe cache/raw disk was accessed successfully // NOTE: this should probably be moved to xenia_main.cc, but right now we // need to register the \Device\Harddisk0\ NullDevice _after_ the // \Device\Harddisk0\Partition1 HostPathDevice, otherwise requests to // Partition1 will go to this. Registering during CompleteLaunch allows us // to make sure any HostPathDevices are ready beforehand. (see comment above // cache:\ device registration for more info about why) auto null_paths = {std::string("\\Partition0"), std::string("\\Cache0"), std::string("\\Cache1")}; auto null_device = std::make_unique("\\Device\\Harddisk0", null_paths); if (null_device->Initialize()) { file_system_->RegisterDevice(std::move(null_device)); } // Reset state. title_id_ = std::nullopt; title_name_ = ""; title_version_ = ""; display_window_->SetIcon(nullptr, 0); // Allow xam to request module loads. auto xam = kernel_state()->GetKernelModule("xam.xex"); XELOGI("Loading module {}", module_path); auto module = kernel_state_->LoadUserModule(module_path); if (!module) { XELOGE("Failed to load user module {}", path); return X_STATUS_NOT_FOUND; } if (!module->is_executable()) { kernel_state_->UnloadUserModule(module, false); XELOGE("Failed to load user module {}", path); return X_STATUS_NOT_SUPPORTED; } X_RESULT result = kernel_state_->ApplyTitleUpdate(module); if (XFAILED(result)) { XELOGE("Failed to apply title update! Cannot run module {}", path); return result; } result = kernel_state_->FinishLoadingUserModule(module); if (XFAILED(result)) { XELOGE("Failed to initialize user module {}", path); return result; } // Grab the current title ID. xex2_opt_execution_info* info = nullptr; uint32_t workspace_address = 0; module->GetOptHeader(XEX_HEADER_EXECUTION_INFO, &info); kernel_state_->memory() ->LookupHeapByType(false, 0x1000) ->Alloc(module->workspace_size(), 0x1000, kMemoryAllocationReserve | kMemoryAllocationCommit, kMemoryProtectRead | kMemoryProtectWrite, false, &workspace_address); if (!info) { title_id_ = 0; } else { title_id_ = info->title_id; auto title_version = info->version(); if (title_version.value != 0) { title_version_ = format_version(title_version); } } // Try and load the resource database (xex only). if (module->title_id()) { auto title_id = fmt::format("{:08X}", module->title_id()); // Load the per-game configuration file and make sure updates are handled // by the callbacks. config::LoadGameConfig(title_id); assert_true(game_config_load_callback_loop_next_index_ == SIZE_MAX); game_config_load_callback_loop_next_index_ = 0; while (game_config_load_callback_loop_next_index_ < game_config_load_callbacks_.size()) { game_config_load_callbacks_[game_config_load_callback_loop_next_index_++] ->PostGameConfigLoad(); } game_config_load_callback_loop_next_index_ = SIZE_MAX; const auto db = kernel_state_->module_xdbf(module); game_info_database_ = std::make_unique(db.get()); kernel_state_->xam_state()->LoadSpaInfo(db.get()); kernel_state_->xam_state()->user_tracker()->AddTitleToPlayedList(); if (game_info_database_->IsValid()) { title_name_ = game_info_database_->GetTitleName(static_cast( kernel_state_->xconfig()->ReadSetting( kernel::XCONFIG_USER_CATEGORY, kernel::XCONFIG_USER_LANGUAGE))); XELOGI("Title name: {}", title_name_); // Show achievments data tabulate::Table table; table.format().multi_byte_characters(true); table.add_row({"ID", "Title", "Description", "Type", "Gamerscore"}); const std::vector achievement_list = game_info_database_->GetAchievements(); for (const kernel::util::GameInfoDatabase::Achievement& entry : achievement_list) { const std::string type = GetAchievementTypeName( kernel::xam::GetAchievementType(entry.flags)); table.add_row({fmt::format("{}", entry.id), entry.label, entry.description, type, fmt::format("{}", entry.gamerscore)}); } XELOGI("\n-------------------- ACHIEVEMENTS --------------------\n{}", table.str()); const std::vector properties_list = game_info_database_->GetProperties(); // 4D5307DC SPA contains a lot of properties, limit properties to log. const auto properties_list_limit = properties_list | std::views::take(150); table = tabulate::Table(); table.format().multi_byte_characters(true); table.add_row({"ID", "Name", "Matchmaking", "Data Size"}); for (const kernel::util::GameInfoDatabase::Property& entry : properties_list_limit) { std::string label = string_util::remove_eol(string_util::trim(entry.description)); table.add_row({fmt::format("{:08X}", entry.id), label, entry.is_matchmaking ? "True" : "False", fmt::format("{}", entry.data_size)}); } std::string properties_totals; if (properties_list.size() > properties_list_limit.size()) { properties_totals = fmt::format("\nProperties: {}/{}", properties_list_limit.size(), properties_list.size()); } XELOGI("\n-------------------- PROPERTIES --------------------{}\n{}", properties_totals.c_str(), table.str()); const std::vector contexts_list = game_info_database_->GetContexts(); table = tabulate::Table(); table.format().multi_byte_characters(true); table.add_row( {"ID", "Name", "Matchmaking", "Default Value", "Max Value"}); for (const kernel::util::GameInfoDatabase::Context& entry : contexts_list) { std::string label = string_util::remove_eol(string_util::trim(entry.description)); table.add_row({fmt::format("{:08X}", entry.id), label, entry.is_matchmaking ? "True" : "False", fmt::format("{}", entry.default_value), fmt::format("{}", entry.max_value)}); } XELOGI("\n-------------------- CONTEXTS --------------------\n{}", table.str()); const std::vector stats_views = game_info_database_->GetStatsViews(); // 4D5307EA SPA contains a lot of stats, limit views to log. const auto stats_views_limit = stats_views | std::views::take(100); table = tabulate::Table(); table.format().multi_byte_characters(true); table.add_row({"ID", "View Type", "Name", "Skilled", "Arbitrated", "Hidden", "Team View", "Online Only"}); for (const kernel::util::GameInfoDatabase::StatsView& entry : stats_views_limit) { const std::string name = string_util::remove_eol(string_util::trim(entry.view.name)); const std::string view_type = kernel::xam::GetViewTypeName(entry.view.view_type); table.add_row({fmt::format("{:08X}", entry.view.id), view_type, name, entry.view.skilled ? "True" : "False", entry.view.arbitrated ? "True" : "False", entry.view.hidden ? "True" : "False", entry.view.team_view ? "True" : "False", entry.view.online_only ? "True" : "False"}); } std::string stats_view_totals; if (stats_views.size() > stats_views_limit.size()) { stats_view_totals = fmt::format( "\nViews: {}/{}", stats_views_limit.size(), stats_views.size()); } XELOGI("\n-------------------- STATS VIEWS --------------------{}\n{}", stats_view_totals.c_str(), table.str()); const std::vector presence_modes = game_info_database_->GetPresenceModes(); table = tabulate::Table(); table.format().multi_byte_characters(true); table.add_row({"Context Value", "Contexts Count", "Properties Count"}); for (const kernel::util::GameInfoDatabase::PresenceMode& entry : presence_modes) { table.add_row( {fmt::format("{}", entry.context_value), fmt::format("{}", entry.property_bag.contexts.size()), fmt::format("{}", entry.property_bag.properties.size())}); } XELOGI("\n-------------------- PRESENCE MODES --------------------\n{}", table.str()); auto icon_block = game_info_database_->GetIcon(); if (!icon_block.empty()) { display_window_->SetIcon(icon_block.data(), icon_block.size()); } } } // Initialize shader storage asynchronously - pipeline compilation happens in // background while the game goes through its normal startup (loading screens, // intro videos, etc.). With async_shader_compilation enabled, draws are // skipped until pipelines are ready, so this is safe. By the time actual // gameplay starts, most cached pipelines should be compiled. if (graphics_system_) { on_shader_storage_initialization(true); graphics_system_->InitializeShaderStorage( cache_root_, title_id_.value(), false, [this]() { on_shader_storage_initialization(false); }); } auto main_thread = kernel_state_->LaunchModule(module); if (!main_thread) { return X_STATUS_UNSUCCESSFUL; } main_thread_ = main_thread; on_launch(title_id_.value(), title_name_); // Plugins must be loaded after calling LaunchModule() and // FinishLoadingUserModule() which will apply TUs and patching to the main // xex. if (cvars::allow_plugins) { if (plugin_loader_->IsAnyPluginForTitleAvailable(title_id_.value(), module->hash().value())) { plugin_loader_->LoadTitlePlugins(title_id_.value(), module->hash().value()); } } // Resume the main thread now. // If the debugger has requested a suspend this will just decrement the // suspend count without resuming it until the debugger wants. main_thread_->Resume(); return X_STATUS_SUCCESS; } } // namespace xe