Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m34s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
Snapshot for handoff. Contains the mission-audio + threading fixes and the
crash-investigation instrumentation (all diagnostic cvars default-OFF).
Fixes (behavioral):
- threading_posix.cc: reap-once guard on PosixCondition<Thread>::post_execution
(double pthread_join at mission teardown -> fault loop -> audio death + freeze).
- xma_decoder.cc: work_event_->Set() in Pause() so the idle XMA worker observes
paused_ and signals pause_fence_ (Pause() deadlock -> permanent audio death).
- audio_system / xma_context_master / xboxkrnl_audio / apu_flags / alsa: mission
audio keepalive + guest_audio_flags + watchdogs.
Instrumentation (additive, default-off): xboxkrnl_debug cache-throw diag +
guest-catch dispatcher, xex_module PE/PDATA/EH scans, kernel_state mem_watch
(NOTE: mem_watch DEFAULTS TRUE -- an always-on host poll thread; prime crash suspect).
NEW ORACLE (see HANDOFF-crash-oracle-2026-07-16.md): stock 6e5b8324f built with
our toolchain + zero custom code = NO crash, NO sound-stop, plays the Ready Room.
=> the Ready-Room out_of_range crash is introduced by THESE changes, not the game
and not the (LTO-broken) build chain. Bisection plan + suspect ranking in the note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1861 lines
64 KiB
C++
1861 lines
64 KiB
C++
/**
|
|
******************************************************************************
|
|
* 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 <ranges>
|
|
|
|
#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<std::unique_ptr<apu::AudioSystem>(cpu::Processor*)>
|
|
audio_system_factory,
|
|
std::function<std::unique_ptr<gpu::GraphicsSystem>()>
|
|
graphics_system_factory,
|
|
std::function<std::vector<std::unique_ptr<hid::InputDriver>>(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<Memory>();
|
|
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<xe::cpu::ExportResolver>();
|
|
|
|
std::unique_ptr<xe::cpu::backend::Backend> 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<xe::cpu::Processor>(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<xe::hid::InputSystem>(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<xe::vfs::VirtualFileSystem>();
|
|
|
|
patcher_ = std::make_unique<xe::patcher::Patcher>(storage_root_);
|
|
|
|
XELOGI("{}: Initializing Kernel...", __func__);
|
|
// Shared kernel state.
|
|
kernel_state_ = std::make_unique<xe::kernel::KernelState>(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<uint64_t>(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<void>(kernel_state_->LoadKernelModule<kernel::t>())
|
|
// 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<xe::patcher::PluginLoader>(
|
|
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<apu::AudioMediaPlayer>(
|
|
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<vfs::Device> 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<vfs::HostPathDevice>(
|
|
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<vfs::DiscImageDevice>(mount_path, path);
|
|
} break;
|
|
case FileSignatureType::ZAR: {
|
|
return std::make_unique<vfs::DiscZarchiveDevice>(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<LPBYTE>(&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<const BYTE*>(&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<vfs::Device> device =
|
|
std::make_unique<vfs::DiscImageDevice>("", 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<kernel::XThread>();
|
|
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<uint32_t>(ctx->lr), static_cast<uint32_t>(ctx->ctr),
|
|
static_cast<uint32_t>(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<uint32_t>(ctx->r[3]), static_cast<uint32_t>(ctx->r[4]),
|
|
static_cast<uint32_t>(ctx->r[5]), static_cast<uint32_t>(ctx->r[6]),
|
|
static_cast<uint32_t>(ctx->r[7]), static_cast<uint32_t>(ctx->r[8]),
|
|
static_cast<uint32_t>(ctx->r[9]), static_cast<uint32_t>(ctx->r[10]),
|
|
static_cast<uint32_t>(ctx->r[11]), static_cast<uint32_t>(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<uint32_t>(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<uint32_t>(frame);
|
|
const uint32_t saved_lr = xe::load_and_swap<uint32_t>(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 <pc> ====");
|
|
}
|
|
|
|
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<uint8_t>(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<uint32_t>(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<uint32_t>(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<XContentType>(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<const uint8_t>(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<vfs::XContentContainerDevice> 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<vfs::Device> device =
|
|
std::make_unique<vfs::DiscZarchiveDevice>("", 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<uint8_t> 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<PackContext*>(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<PackContext*>(ctx);
|
|
packContext->currentOutputFile.write(
|
|
reinterpret_cast<const char*>(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::XThread>(
|
|
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::XThread>(
|
|
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<uint32_t>() != kEmulatorSaveSignature) {
|
|
return false;
|
|
}
|
|
|
|
auto has_title_id = stream.Read<bool>();
|
|
std::optional<uint32_t> title_id;
|
|
if (!has_title_id) {
|
|
title_id = {};
|
|
} else {
|
|
title_id = stream.Read<uint32_t>();
|
|
}
|
|
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<kernel::XThread>();
|
|
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<Emulator*>(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<uint64_t*>(&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<kernel::xam::XamModule>("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<vfs::NullDevice>("\\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<kernel::xam::XamModule>("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<kernel::util::GameInfoDatabase>(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<XLanguage>(
|
|
kernel_state_->xconfig()->ReadSetting<uint32_t>(
|
|
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<kernel::util::GameInfoDatabase::Achievement>
|
|
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<kernel::util::GameInfoDatabase::Property>
|
|
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<kernel::util::GameInfoDatabase::Context> 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<kernel::util::GameInfoDatabase::StatsView> 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<kernel::util::GameInfoDatabase::PresenceMode>
|
|
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
|