/** ****************************************************************************** * 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 "xenia/emulator.h" #include #include #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 "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/cpu/backend/code_cache.h" #include "xenia/cpu/backend/null_backend.h" #include "xenia/cpu/cpu_flags.h" #include "xenia/cpu/thread_state.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/user_module.h" #include "xenia/kernel/util/gameinfo_utils.h" #include "xenia/kernel/util/xdbf_utils.h" #include "xenia/kernel/xam/xam_module.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/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" #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_int32(user_language); DECLARE_bool(allow_plugins); 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_(), graphics_system_(), input_system_(), export_resolver_(), file_system_(), kernel_state_(), main_thread_(), title_id_(std::nullopt), paused_(false), restoring_(false), restore_fence_() { #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-project/xenia/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. // 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(); 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(); // Create memory system first, as it is required for other systems. memory_ = std::make_unique(); if (!memory_->Initialize()) { return false; } // 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()); } #endif // XE_ARCH if (cvars::cpu == "any") { if (!backend) { #if XE_ARCH_AMD64 backend.reset(new xe::cpu::backend::x64::X64Backend()); #endif // XE_ARCH } } if (!backend && !require_cpu_backend) { backend.reset(new xe::cpu::backend::NullBackend()); } // Initialize the CPU. processor_ = std::make_unique(memory_.get(), export_resolver_.get()); if (!processor_->Setup(std::move(backend))) { return X_STATUS_UNSUCCESSFUL; } // Initialize the APU. if (audio_system_factory) { audio_system_ = audio_system_factory(processor_.get()); if (!audio_system_) { return X_STATUS_NOT_IMPLEMENTED; } } // Initialize the GPU. graphics_system_ = graphics_system_factory(); if (!graphics_system_) { return X_STATUS_NOT_IMPLEMENTED; } // Initialize the HID. input_system_ = std::make_unique(display_window_); if (!input_system_) { 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) { auto& input_driver = input_drivers[i]; input_driver->set_is_active_callback( []() -> bool { return !xe::kernel::xam::xeXamIsUIActive(); }); input_system_->AddDriver(std::move(input_driver)); } } result = input_system_->Setup(); if (result) { return result; } // Bring up the virtual filesystem used by the kernel. file_system_ = std::make_unique(); patcher_ = std::make_unique(storage_root_); // Shared kernel state. kernel_state_ = std::make_unique(this); #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"); // 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) { return result; } if (audio_system_) { result = audio_system_->Setup(kernel_state_.get()); if (result) { return result; } } // 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 {}.", xe::path_to_utf8(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); const int64_t header_size = 4; if (file_size < header_size) { return FileSignatureType::Unknown; } char file_magic[header_size]; fread_s(file_magic, sizeof(file_magic), 1, header_size, 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_s(file_magic, sizeof(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; } return FileSignatureType::Unknown; } 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\\Cdrom0"); 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 = "game:\\" + xe::path_to_utf8(file_name); X_STATUS result = CompleteLaunch(path, fs_path); kernel_state_->deployment_type_ = XDeploymentType::kHardDrive; 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::kGoD; 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); kernel_state_->deployment_type_ = XDeploymentType::kHardDrive; return result; } X_STATUS Emulator::InstallContentPackage(const std::filesystem::path& path) { std::unique_ptr device = vfs::XContentContainerDevice::CreateContentDevice("", path); if (!device || !device->Initialize()) { XELOGE("Failed to initialize device"); return X_STATUS_INVALID_PARAMETER; } const vfs::XContentContainerDevice* dev = (vfs::XContentContainerDevice*)device.get(); std::filesystem::path installation_path = content_root() / fmt::format("{:08X}", dev->title_id()) / fmt::format("{:08X}", dev->content_type()) / path.filename(); std::filesystem::path header_path = content_root() / fmt::format("{:08X}", dev->title_id()) / "Headers" / fmt::format("{:08X}", dev->content_type()) / path.filename(); 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) { return error_code.value(); } } vfs::VirtualFileSystem::ExtractContentHeader(device.get(), header_path); return vfs::VirtualFileSystem::ExtractContentFiles(device.get(), installation_path); } 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; } bool Emulator::TitleRequested() { auto xam = kernel_state()->GetKernelModule("xam.xex"); return xam->loader_data().launch_data_present; } void Emulator::LaunchNextTitle() { auto xam = kernel_state()->GetKernelModule("xam.xex"); auto next_title = xam->loader_data().launch_path; // Swap disk doesn't require reloading // This function should be purged? CompleteLaunch("", next_title); } 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()))); 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("game:\\"); 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; } std::string default_module("default.xex"); auto gameinfo_entry(file_system_->ResolvePath(path + "GameInfo.bin")); if (gameinfo_entry) { vfs::File* file = nullptr; X_STATUS result = gameinfo_entry->Open(vfs::FileAccess::kGenericRead, &file); if (XSUCCEEDED(result)) { std::vector buffer(gameinfo_entry->size()); size_t bytes_read = 0; result = file->ReadSync(buffer.data(), buffer.size(), 0, &bytes_read); if (XSUCCEEDED(result)) { kernel::util::GameInfo info(buffer); if (info.is_valid()) { XELOGI("Found virtual title {}", info.virtual_title_id()); const std::string xna_id("584E07D1"); auto xna_id_entry(file_system_->ResolvePath(path + xna_id)); if (xna_id_entry) { default_module = xna_id + "\\" + info.module_name(); } else { XELOGE("Could not find fixed XNA path {}", xna_id); } } } } } return path + default_module; } static std::string format_version(xex2_version version) { // fmt::format doesn't like bit fields uint32_t major, minor, build, qfe; major = version.major; minor = version.minor; build = version.build; qfe = version.qfe; if (qfe) { return fmt::format("{}.{}.{}.{}", major, minor, build, qfe); } if (build) { return fmt::format("{}.{}.{}", major, minor, build); } return fmt::format("{}.{}", major, minor); } 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. assert_true(display_window_->app_context().IsInUIThread()); // 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 {}", xe::path_to_utf8(path)); return X_STATUS_NOT_FOUND; } X_RESULT result = kernel_state_->ApplyTitleUpdate(module); if (XFAILED(result)) { XELOGE("Failed to apply title update! Cannot run module {}", xe::path_to_utf8(path)); return result; } result = kernel_state_->FinishLoadingUserModule(module); if (XFAILED(result)) { XELOGE("Failed to initialize user module {}", xe::path_to_utf8(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 kernel::util::XdbfGameData db = kernel_state_->module_xdbf(module); if (db.is_valid()) { XLanguage language = db.GetExistingLanguage(static_cast(cvars::user_language)); title_name_ = db.title(language); XELOGI("Title name: {}", title_name_); // Show achievments data tabulate::Table table; table.format().multi_byte_characters(true); table.add_row({"ID", "Title", "Description", "Gamerscore"}); const std::vector achievement_list = db.GetAchievements(); for (const kernel::util::XdbfAchievementTableEntry& entry : achievement_list) { std::string label = string_util::remove_eol(string_util::trim( db.GetStringTableEntry(language, entry.label_id))); std::string desc = string_util::remove_eol(string_util::trim( db.GetStringTableEntry(language, entry.description_id))); table.add_row({fmt::format("{}", entry.id), label, desc, fmt::format("{}", entry.gamerscore)}); } XELOGI("-------------------- ACHIEVEMENTS --------------------\n{}", table.str()); const std::vector properties_list = db.GetProperties(); table = tabulate::Table(); table.format().multi_byte_characters(true); table.add_row({"ID", "Name", "Data Size"}); for (const kernel::util::XdbfPropertyTableEntry& entry : properties_list) { std::string label = string_util::remove_eol(string_util::trim( db.GetStringTableEntry(language, entry.string_id))); table.add_row({fmt::format("{:08X}", entry.id), label, fmt::format("{}", entry.data_size)}); } XELOGI("-------------------- PROPERTIES --------------------\n{}", table.str()); const std::vector contexts_list = db.GetContexts(); table = tabulate::Table(); table.format().multi_byte_characters(true); table.add_row({"ID", "Name", "Default Value", "Max Value"}); for (const kernel::util::XdbfContextTableEntry& entry : contexts_list) { std::string label = string_util::remove_eol(string_util::trim( db.GetStringTableEntry(language, entry.string_id))); table.add_row({fmt::format("{:08X}", entry.id), label, fmt::format("{}", entry.default_value), fmt::format("{}", entry.max_value)}); } XELOGI("-------------------- CONTEXTS --------------------\n{}", table.str()); auto icon_block = db.icon(); if (icon_block) { display_window_->SetIcon(icon_block.buffer, icon_block.size); } } } // Initializing the shader storage in a blocking way so the user doesn't // miss the initial seconds - for instance, sound from an intro video may // start playing before the video can be seen if doing this in parallel with // the main thread. on_shader_storage_initialization(true); graphics_system_->InitializeShaderStorage(cache_root_, title_id_.value(), true); 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()); } } return X_STATUS_SUCCESS; } } // namespace xe