From b5a7767aae8a49ef1957634eaf3977f93a31f1cd Mon Sep 17 00:00:00 2001 From: Gliniak <153369+Gliniak@users.noreply.github.com> Date: Sat, 7 Mar 2026 23:39:39 +0100 Subject: [PATCH 01/31] [XAM] Added some missing user settings. Names put according to findings in: 54510896 --- src/xenia/kernel/xam/user_settings.h | 41 +++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/xenia/kernel/xam/user_settings.h b/src/xenia/kernel/xam/user_settings.h index c748581b9..3c0471b84 100644 --- a/src/xenia/kernel/xam/user_settings.h +++ b/src/xenia/kernel/xam/user_settings.h @@ -101,11 +101,34 @@ enum class UserSettingId : uint32_t { X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3E), // 0x1004003E, XPROFILE_GAMERCARD_SERVICE_TYPE_FLAGS = SettingKey( X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x3F), // 0x1004003F, + XPROFILE_ENABLE_TUTORIALS = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x40), // 0x10040040, + XPROFILE_ENABLE_SUBTITLES = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x41), // 0x10040041, + XPROFILE_UNKNOWN_42 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x42), // 0x10040042, + XPROFILE_UNKNOWN_43 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x43), // 0x10040043, + XPROFILE_AIM_SENSITIVITY_XAXIS = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x44), // 0x10040044, + XPROFILE_AIM_SENSITIVITY_YAXIS = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x45), // 0x10040045, + XPROFILE_UNKNOWN_46 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x46), // 0x10040046, XPROFILE_TENURE_LEVEL = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x47), // 0x10040047, XPROFILE_TENURE_MILESTONE = SettingKey( X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x48), // 0x10040048, + XPROFILE_UNKNOWN_49 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x49), // 0x10040049, + XPROFILE_SHOW_DAMAGE_INDICATORS = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x4A), // 0x1004004A, XPROFILE_SUBSCRIPTION_TYPE_LENGTH_IN_MONTHS = SettingKey( X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x4B), // 0x1004004B, XPROFILE_SUBSCRIPTION_PAYMENT_TYPE = SettingKey( @@ -117,6 +140,11 @@ enum class UserSettingId : uint32_t { 0x4E), // 0x1004004E, set by XamUserNuiEnableBiometric XPROFILE_GFWL_VADNORMAL = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x4F), // 0x1004004F, + XPROFILE_UNKNOWN_50 = SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x50), // 0x10040050, + XPROFILE_MINIMAP_AUTOROTATE = + SettingKey(X_USER_DATA_TYPE::INT32, sizeof(uint32_t), + 0x51), // 0x10040051, XPROFILE_BEACONS_SOCIAL_NETWORK_SHARING = SettingKey( X_USER_DATA_TYPE::INT32, sizeof(uint32_t), 0x52), // 0x10040052, XPROFILE_USER_PREFERENCES = SettingKey( @@ -269,7 +297,7 @@ enum class UserSettingId : uint32_t { 0x4F), // 0x7008004F, named "LastOnLIVE" in Velocity }; -inline constexpr std::array known_settings = { +inline constexpr std::array known_settings = { UserSettingId::XPROFILE_PERMISSIONS, UserSettingId::XPROFILE_GAMER_TYPE, UserSettingId::XPROFILE_GAMER_YAXIS_INVERSION, @@ -303,13 +331,24 @@ inline constexpr std::array known_settings = { UserSettingId::XPROFILE_SAVE_WINDOWS_LIVE_PASSWORD, UserSettingId::XPROFILE_FRIENDSAPP_SHOW_BUDDIES, UserSettingId::XPROFILE_GAMERCARD_SERVICE_TYPE_FLAGS, + UserSettingId::XPROFILE_ENABLE_TUTORIALS, + UserSettingId::XPROFILE_ENABLE_SUBTITLES, + UserSettingId::XPROFILE_UNKNOWN_42, + UserSettingId::XPROFILE_UNKNOWN_43, + UserSettingId::XPROFILE_AIM_SENSITIVITY_XAXIS, + UserSettingId::XPROFILE_AIM_SENSITIVITY_YAXIS, + UserSettingId::XPROFILE_UNKNOWN_46, UserSettingId::XPROFILE_TENURE_LEVEL, UserSettingId::XPROFILE_TENURE_MILESTONE, + UserSettingId::XPROFILE_UNKNOWN_49, + UserSettingId::XPROFILE_SHOW_DAMAGE_INDICATORS, UserSettingId::XPROFILE_SUBSCRIPTION_TYPE_LENGTH_IN_MONTHS, UserSettingId::XPROFILE_SUBSCRIPTION_PAYMENT_TYPE, UserSettingId::XPROFILE_PEC_INFO, UserSettingId::XPROFILE_NUI_BIOMETRIC_SIGNIN, UserSettingId::XPROFILE_GFWL_VADNORMAL, + UserSettingId::XPROFILE_UNKNOWN_50, + UserSettingId::XPROFILE_MINIMAP_AUTOROTATE, UserSettingId::XPROFILE_BEACONS_SOCIAL_NETWORK_SHARING, UserSettingId::XPROFILE_USER_PREFERENCES, UserSettingId::XPROFILE_XBOXONE_GAMERSCORE, From eb914ae77d07c2f36018c9cdd44e1aea4d973844 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 10 Feb 2026 00:22:42 +0900 Subject: [PATCH 02/31] [Kernel] Return null on handle type mismatch instead of asserting --- src/xenia/kernel/util/object_table.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/xenia/kernel/util/object_table.h b/src/xenia/kernel/util/object_table.h index c055210a4..694251384 100644 --- a/src/xenia/kernel/util/object_table.h +++ b/src/xenia/kernel/util/object_table.h @@ -51,7 +51,10 @@ class ObjectTable { object_ref LookupObject(X_HANDLE handle, bool already_locked = false) { auto object = LookupObject(handle, already_locked); if (object) { - assert_true(object->type() == T::kObjectType); + if (object->type() != T::kObjectType) { + object->Release(); + return object_ref(); + } } auto result = object_ref(reinterpret_cast(object)); return result; From de63bb18a68d1ff386fb43bac7c36336f0e85bd5 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 10 Feb 2026 00:18:54 +0900 Subject: [PATCH 03/31] [Base] Use function-local static for global_mutex File-scope static initialization order is undefined across translation units. Code that acquires the global critical region during early startup could run before the mutex constructor, operating on uninitialized state. A function-local static is constructed on first use, guaranteeing the mutex is initialized before any caller can access it. --- src/xenia/base/mutex.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/xenia/base/mutex.cc b/src/xenia/base/mutex.cc index b975e4bc3..4fc37de9b 100644 --- a/src/xenia/base/mutex.cc +++ b/src/xenia/base/mutex.cc @@ -65,9 +65,9 @@ bool xe_fast_mutex::try_lock() { return TryEnterCriticalSection(fast_crit(this)); } #endif -// chrispy: moved this out of body of function to eliminate the initialization -// guards -static global_mutex_type global_mutex; -global_mutex_type& global_critical_region::mutex() { return global_mutex; } +global_mutex_type& global_critical_region::mutex() { + static global_mutex_type global_mutex; + return global_mutex; +} } // namespace xe From 77535b7cecf33d58b98294fae304b24bb6c2940a Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:10:03 +0900 Subject: [PATCH 04/31] [Base] Replace CRITICAL_SECTION with SRWLOCK based mutexes xe_fast_mutex now also asserts on recursive lock attempts --- src/xenia/base/mutex.cc | 82 ++++++++++++++++++++++------------------- src/xenia/base/mutex.h | 35 +++++++++--------- 2 files changed, 62 insertions(+), 55 deletions(-) diff --git a/src/xenia/base/mutex.cc b/src/xenia/base/mutex.cc index 4fc37de9b..5f62ae39b 100644 --- a/src/xenia/base/mutex.cc +++ b/src/xenia/base/mutex.cc @@ -14,55 +14,61 @@ namespace xe { #if XE_PLATFORM_WIN32 == 1 && XE_ENABLE_FAST_WIN32_MUTEX == 1 -// default spincount for entercriticalsection is insane on windows, 0x20007D0i64 -// (33556432 times!!) when a lock is highly contended performance degrades -// sharply on some processors todo: perhaps we should have a set of optional -// jobs that processors can do instead of spinning, for instance, sorting a list -// so we have better locality later or something -#define XE_CRIT_SPINCOUNT 128 -/* -chrispy: todo, if a thread exits before releasing the global mutex we need to -check this and release the mutex one way to do this is by using FlsAlloc and -PFLS_CALLBACK_FUNCTION, which gets called with the fiber local data when a -thread exits -*/ - -static CRITICAL_SECTION* global_critical_section(xe_global_mutex* mutex) { - return reinterpret_cast(mutex); -} - -xe_global_mutex::xe_global_mutex() { - InitializeCriticalSectionEx(global_critical_section(this), XE_CRIT_SPINCOUNT, - CRITICAL_SECTION_NO_DEBUG_INFO); -} -xe_global_mutex ::~xe_global_mutex() { - DeleteCriticalSection(global_critical_section(this)); -} +// xe_global_mutex: recursive mutex via SRWLOCK void xe_global_mutex::lock() { - EnterCriticalSection(global_critical_section(this)); + DWORD self = GetCurrentThreadId(); + if (owner_thread_ == self) { + ++recursion_count_; + return; + } + AcquireSRWLockExclusive(&srwlock_); + owner_thread_ = self; + recursion_count_ = 1; } + void xe_global_mutex::unlock() { - LeaveCriticalSection(global_critical_section(this)); + if (--recursion_count_ == 0) { + owner_thread_ = 0; + ReleaseSRWLockExclusive(&srwlock_); + } } + bool xe_global_mutex::try_lock() { - BOOL success = TryEnterCriticalSection(global_critical_section(this)); - return success; + DWORD self = GetCurrentThreadId(); + if (owner_thread_ == self) { + ++recursion_count_; + return true; + } + if (TryAcquireSRWLockExclusive(&srwlock_)) { + owner_thread_ = self; + recursion_count_ = 1; + return true; + } + return false; } -CRITICAL_SECTION* fast_crit(xe_fast_mutex* mutex) { - return reinterpret_cast(mutex); +// xe_fast_mutex: non-recursive mutex via SRWLOCK +void xe_fast_mutex::lock() { + DWORD self = GetCurrentThreadId(); + if (owner_thread_ == self) { + assert_always("xe_fast_mutex: recursive lock detected"); + } + AcquireSRWLockExclusive(&srwlock_); + owner_thread_ = self; +} + +void xe_fast_mutex::unlock() { + owner_thread_ = 0; + ReleaseSRWLockExclusive(&srwlock_); } -xe_fast_mutex::xe_fast_mutex() { - InitializeCriticalSectionEx(fast_crit(this), XE_CRIT_SPINCOUNT, - CRITICAL_SECTION_NO_DEBUG_INFO); -} -xe_fast_mutex::~xe_fast_mutex() { DeleteCriticalSection(fast_crit(this)); } -void xe_fast_mutex::lock() { EnterCriticalSection(fast_crit(this)); } -void xe_fast_mutex::unlock() { LeaveCriticalSection(fast_crit(this)); } bool xe_fast_mutex::try_lock() { - return TryEnterCriticalSection(fast_crit(this)); + if (TryAcquireSRWLockExclusive(&srwlock_)) { + owner_thread_ = GetCurrentThreadId(); + return true; + } + return false; } #endif global_mutex_type& global_critical_region::mutex() { diff --git a/src/xenia/base/mutex.h b/src/xenia/base/mutex.h index d5e691c60..665f38f97 100644 --- a/src/xenia/base/mutex.h +++ b/src/xenia/base/mutex.h @@ -10,27 +10,27 @@ #ifndef XENIA_BASE_MUTEX_H_ #define XENIA_BASE_MUTEX_H_ #include -#include "memory.h" +#include #include "platform.h" +#if XE_PLATFORM_WIN32 +#include "platform_win.h" +#elif XE_PLATFORM_LINUX +#include +#endif +#include "memory.h" #define XE_ENABLE_FAST_WIN32_MUTEX 1 namespace xe { #if XE_PLATFORM_WIN32 == 1 && XE_ENABLE_FAST_WIN32_MUTEX == 1 -/* - must conform to - BasicLockable:https://en.cppreference.com/w/cpp/named_req/BasicLockable as - well as Lockable: https://en.cppreference.com/w/cpp/named_req/Lockable - - this emulates a recursive mutex, except with far less overhead -*/ - +// Recursive mutex using SRWLOCK. class alignas(4096) xe_global_mutex { - XE_MAYBE_UNUSED - char detail[64]; + SRWLOCK srwlock_ = SRWLOCK_INIT; + DWORD owner_thread_ = 0; + uint32_t recursion_count_ = 0; public: - xe_global_mutex(); - ~xe_global_mutex(); + xe_global_mutex() = default; + ~xe_global_mutex() = default; void lock(); void unlock(); @@ -38,13 +38,14 @@ class alignas(4096) xe_global_mutex { }; using global_mutex_type = xe_global_mutex; +// Non-recursive mutex using SRWLOCK. class alignas(64) xe_fast_mutex { - XE_MAYBE_UNUSED - char detail[64]; + SRWLOCK srwlock_ = SRWLOCK_INIT; + DWORD owner_thread_ = 0; public: - xe_fast_mutex(); - ~xe_fast_mutex(); + xe_fast_mutex() = default; + ~xe_fast_mutex() = default; void lock(); void unlock(); From 6dfb323fe6eb2573e61ee23f9c9dbaf7c8d7cb55 Mon Sep 17 00:00:00 2001 From: Gliniak <153369+Gliniak@users.noreply.github.com> Date: Sun, 8 Mar 2026 12:57:11 +0100 Subject: [PATCH 05/31] [Cmake] Filter Configuration presets. This removes VS preset visibility on linux --- CMakePresets.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CMakePresets.json b/CMakePresets.json index 090e39003..25ee7a029 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -16,6 +16,11 @@ "name": "vs", "displayName": "Visual Studio 2022", "generator": "Visual Studio 17 2022", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, "architecture": "x64", "binaryDir": "${sourceDir}/build" } @@ -23,16 +28,19 @@ "buildPresets": [ { "name": "debug", + "displayName": "Debug", "configurePreset": "default", "configuration": "Debug" }, { "name": "release", + "displayName": "Release", "configurePreset": "default", "configuration": "Release" }, { "name": "checked", + "displayName": "Checked", "configurePreset": "default", "configuration": "Checked" }, From 54400cbc2cf3273c9f9c307497215cb89a8521d0 Mon Sep 17 00:00:00 2001 From: Gliniak <153369+Gliniak@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:38:28 +0100 Subject: [PATCH 06/31] [Build] Integration with CLion --- xenia-build.py | 130 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 119 insertions(+), 11 deletions(-) diff --git a/xenia-build.py b/xenia-build.py index f9b230ef6..5baf3f86a 100755 --- a/xenia-build.py +++ b/xenia-build.py @@ -24,6 +24,53 @@ __author__ = "ben.vanik@gmail.com (Ben Vanik)" self_path = os.path.dirname(os.path.abspath(__file__)) +# TODO: Automate getting these entries +XENIA_TARGETS = [ + "aes_128", + "capstone", + "dxbc", + "discord-rpc", + "fmt", + "glslang-spirv", + "imgui", + "libavcodec", + "libavformat", + "libavutil", + "mspack", + "pugixml", + "snappy", + "xxhash", + "zarchive", + "zlib-ng", + "zstd", + "xenia-app", + "xenia-app-discord", + "xenia-apu", + "xenia-apu-alsa", + "xenia-apu-nop", + "xenia-apu-sdl", + "xenia-base", + "xenia-core", + "xenia-cpu", + "xenia-cpu-backend-x64", + "xenia-debug-ui", + "xenia-gpu", + "xenia-gpu-null", + "xenia-gpu-vulkan", + "xenia-gpu-vulkan-spirv-shaders", + "xenia-helper-sdl", + "xenia-hid", + "xenia-hid-nop", + "xenia-hid-sdl", + "xenia-hid-skylander", + "xenia-kernel", + "xenia-patcher", + "xenia-ui", + "xenia-ui-vulkan", + "xenia-ui-vulkan-spirv-shaders", + "xenia-vfs", +] + class bcolors: # HEADER = "\033[95m" # OKBLUE = "\033[94m" @@ -690,26 +737,86 @@ def create_clion_workspace(): with open(os.path.join(".idea", "misc.xml"), "w") as f: f.write(""" - - + + + """) - # Set available configurations - # TODO Find a way to trigger a cmake reload + # Create workspace.xml file with open(os.path.join(".idea", "workspace.xml"), "w") as f: + # Write CmakePresetLoader f.write(""" - - - - - - + +""") + f.write(""" + -""") +""") + # Write ConfigurationManager + f.write(""" \n""") + f.write(" \n") + # Loop over every entry + for target in XENIA_TARGETS: + f.write(f' \n') + f.write(" \n") + f.write(" \n") + + # Write CMakeSettings + f.write(""" \n""") + f.write(" \n") + f.write(""" \n""") + f.write(""" \n""") + f.write(""" \n""") + f.write(" \n") + f.write(" \n") + + # Write RunManager + # Write basic xenia-app manually + f.write(""" \n""") + f.write(""" \n""") + f.write(""" \n""") + f.write(""" \n""") + f.write(""" \n""") + + for target in XENIA_TARGETS: + if target != "xenia-app": + f.write(f' \n') + else: + f.write(f' \n') + + f.write(""" \n""") + f.write(""" \n""") + f.write(""" \n""") + + # Write itemvalue list + f.write(""" \n""") + for target in XENIA_TARGETS: + f.write(f' \n') + + f.write(""" \n""") + f.write(""" \n""") + f.write("""\n""") + + os.makedirs(os.path.join(".idea", "codeStyles"), exist_ok=True) + with open(os.path.join(".idea", "codeStyles", "Project.xml"), "w") as f: + f.write(""" + + + + + + +""") return True @@ -1789,6 +1896,7 @@ class DevenvCommand(Command): print("Launching Visual Studio...") elif has_bin("clion") or has_bin("clion.sh"): print("Launching CLion...") + create_clion_workspace() else: print("IDE not detected. CMakeLists.txt is in the project root.") From 02fccd9113e66e9db5672e16ecaca778bc05c717 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:42:11 +0900 Subject: [PATCH 07/31] [Kernel/XMP] Add title-specific file I/O hooks for XMP volume sync Some games persist BGM volume through their own save files but never call XMPSetVolume. Add a TitlePatch system that intercepts NtReadFile/ NtWriteFile to apply game-specific logic as well as logic to identify the memory location that the volume data is read into and monitor that memory location for changes, updating XMP player volume if it changes during gameplay. Includes patches for known games with this issue: Dead or Alive Xtreme 2 (544307D2), PGR4 (4D5307F9) and PGR3 (4D5307D1) --- src/xenia/kernel/kernel_state.cc | 8 ++ src/xenia/kernel/kernel_state.h | 5 + src/xenia/kernel/util/xmp_volume_patch.cc | 130 ++++++++++++++++++++++ src/xenia/kernel/util/xmp_volume_patch.h | 70 ++++++++++++ src/xenia/kernel/xam/xam_input.cc | 19 +++- src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc | 18 +++ 6 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 src/xenia/kernel/util/xmp_volume_patch.cc create mode 100644 src/xenia/kernel/util/xmp_volume_patch.h diff --git a/src/xenia/kernel/kernel_state.cc b/src/xenia/kernel/kernel_state.cc index e7a646674..3bbad27be 100644 --- a/src/xenia/kernel/kernel_state.cc +++ b/src/xenia/kernel/kernel_state.cc @@ -407,6 +407,9 @@ void KernelState::SetExecutableModule(object_ref module) { xboxkrnl::XboxkrnlModule::kExLoadedCommandLineSize); } + // Initialize file I/O hooks for XMP volume title-specific patches. + InitXmpVolumePatch(); + // Spin up deferred dispatch worker. // TODO(benvanik): move someplace more appropriate (out of ctor, but around // here). @@ -738,8 +741,13 @@ void KernelState::UnloadUserModule(const object_ref& module, object_table()->ReleaseHandleInLock(module->handle()); } +void KernelState::InitXmpVolumePatch() { + xmp_volume_patch_ = XmpVolumePatch::CreateForTitle(title_id(), this); +} + void KernelState::TerminateTitle() { XELOGD("KernelState::TerminateTitle"); + xmp_volume_patch_.reset(); auto global_lock = global_critical_region_.Acquire(); // Call terminate routines. diff --git a/src/xenia/kernel/kernel_state.h b/src/xenia/kernel/kernel_state.h index 8095c3fa7..2e1a99a76 100644 --- a/src/xenia/kernel/kernel_state.h +++ b/src/xenia/kernel/kernel_state.h @@ -24,6 +24,7 @@ #include "xenia/kernel/util/kernel_fwd.h" #include "xenia/kernel/util/native_list.h" #include "xenia/kernel/util/object_table.h" +#include "xenia/kernel/util/xmp_volume_patch.h" #include "xenia/kernel/xam/achievement_manager.h" #include "xenia/kernel/xam/app_manager.h" #include "xenia/kernel/xam/content_manager.h" @@ -196,6 +197,9 @@ class KernelState { return xam_state()->content_manager(); } + XmpVolumePatch* xmp_volume_patch() const { return xmp_volume_patch_.get(); } + void InitXmpVolumePatch(); + std::bitset<4> GetConnectedUsers() const; // Access must be guarded by the global critical region. @@ -351,6 +355,7 @@ class KernelState { vfs::VirtualFileSystem* file_system_; std::unique_ptr xam_state_; std::unique_ptr smc_; + std::unique_ptr xmp_volume_patch_; KernelVersion kernel_version_; diff --git a/src/xenia/kernel/util/xmp_volume_patch.cc b/src/xenia/kernel/util/xmp_volume_patch.cc new file mode 100644 index 000000000..02fa661e0 --- /dev/null +++ b/src/xenia/kernel/util/xmp_volume_patch.cc @@ -0,0 +1,130 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/kernel/util/xmp_volume_patch.h" + +#include "xenia/apu/audio_media_player.h" +#include "xenia/base/logging.h" +#include "xenia/emulator.h" +#include "xenia/kernel/kernel_state.h" +#include "xenia/memory.h" + +namespace xe { +namespace kernel { + +// Convert a volume byte to a float in [0.0001, 1.0]. Uses a near-zero floor +// instead of 0.0f so the audio player doesn't interpret it as "never set" +// and override with the default volume. +static float ByteToVolume(uint8_t value, uint8_t max_value) { + if (value == 0) { + return 0.0001f; + } + return static_cast(value) / static_cast(max_value); +} + +bool XmpVolumePatch::Matches(const std::string& path, uint32_t length) const { + if (path.find(config_.file_name) == std::string::npos) { + return false; + } + if (config_.write_length != 0) { + return length == config_.write_length; + } + return length >= config_.offset + 1; +} + +void XmpVolumePatch::ApplyVolume(const uint8_t* buffer, uint32_t length) { + uint8_t volume_byte = buffer[config_.offset]; + if (volume_byte > config_.max_value) { + return; + } + + float volume = ByteToVolume(volume_byte, config_.max_value); + + XELOGD("XmpVolumePatch: volume={}/{} -> {:.2f}", volume_byte, + config_.max_value, volume); + + last_volume_ = volume_byte; + auto* player = kernel_state_->emulator()->audio_media_player(); + if (player) { + player->SetVolume(volume); + } +} + +void XmpVolumePatch::PollGuestVolume() { + if (!settings_guest_addr_) { + return; + } + + auto* mem = kernel_state_->memory(); + uint32_t vol_addr = settings_guest_addr_ + config_.offset; + uint8_t current = mem->TranslateVirtual(vol_addr)[0]; + + if (current == last_volume_ || current > config_.max_value) { + return; + } + + last_volume_ = current; + float volume = ByteToVolume(current, config_.max_value); + + XELOGD("XmpVolumePatch: polled volume change={}/{} -> {:.2f}", current, + config_.max_value, volume); + + auto* player = kernel_state_->emulator()->audio_media_player(); + if (player) { + player->SetVolume(volume); + } +} + +void XmpVolumePatch::OnFileWrite(const std::string& path, const uint8_t* buffer, + uint32_t length, uint32_t guest_address) { + if (Matches(path, length)) { + ApplyVolume(buffer, length); + } +} + +void XmpVolumePatch::OnFileRead(const std::string& path, const uint8_t* buffer, + uint32_t length, uint32_t guest_address) { + if (Matches(path, length)) { + settings_guest_addr_ = guest_address; + ApplyVolume(buffer, length); + } +} + +void XmpVolumePatch::OnInputPoll(uint32_t packet_number) { + if (packet_number == last_packet_number_) { + return; + } + last_packet_number_ = packet_number; + PollGuestVolume(); +} + +std::unique_ptr XmpVolumePatch::CreateForTitle( + uint32_t title_id, KernelState* ks) { + XmpVolumeConfig config; + + switch (title_id) { + case 0x544307D2: // DOAX2 + config = {"ups.dat", 0, 0x50, 100}; + break; + case 0x4D5307D1: // PGR3 + config = {"profile.save", 528, 19, 10}; + break; + case 0x4D5307F9: // PGR4 + config = {"savegame", 344, 2, 10}; + break; + default: + return nullptr; + } + + XELOGI("XmpVolumePatch: Loading for title {:08X}", title_id); + return std::make_unique(ks, config); +} + +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/util/xmp_volume_patch.h b/src/xenia/kernel/util/xmp_volume_patch.h new file mode 100644 index 000000000..9ff9a9fa9 --- /dev/null +++ b/src/xenia/kernel/util/xmp_volume_patch.h @@ -0,0 +1,70 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_KERNEL_UTIL_XMP_VOLUME_PATCH_H_ +#define XENIA_KERNEL_UTIL_XMP_VOLUME_PATCH_H_ + +#include +#include +#include + +namespace xe { +namespace kernel { + +class KernelState; + +// Patch for games that store their BGM volume in a save file but never call +// XMPSetVolume. Each game just needs to specify: +// - file_name: substring to match in the file path +// - write_length: exact length of the write containing the volume byte +// (0 to match any write >= offset + 1) +// - offset: byte offset of the volume value within the write buffer +// - max_value: maximum volume value (maps to 1.0f) +// +// On savegame read/write, the patch reads the volume byte and syncs it to +// the XMP audio player. It also captures the guest memory address of the +// buffer and re-reads it on controller input changes to detect in-game +// volume adjustments before they are saved. +struct XmpVolumeConfig { + const char* file_name; + uint32_t write_length; + uint32_t offset; + uint8_t max_value; +}; + +class XmpVolumePatch { + public: + XmpVolumePatch(KernelState* kernel_state, const XmpVolumeConfig& config) + : kernel_state_(kernel_state), config_(config) {} + + void OnFileWrite(const std::string& path, const uint8_t* buffer, + uint32_t length, uint32_t guest_address); + void OnFileRead(const std::string& path, const uint8_t* buffer, + uint32_t length, uint32_t guest_address); + void OnInputPoll(uint32_t packet_number); + + static std::unique_ptr CreateForTitle(uint32_t title_id, + KernelState* ks); + + private: + bool Matches(const std::string& path, uint32_t length) const; + void ApplyVolume(const uint8_t* buffer, uint32_t length); + void PollGuestVolume(); + + KernelState* kernel_state_; + XmpVolumeConfig config_; + uint32_t settings_guest_addr_ = 0; + uint8_t last_volume_ = 0xFF; + uint32_t last_packet_number_ = 0; +}; + +} // namespace kernel +} // namespace xe + +#endif // XENIA_KERNEL_UTIL_XMP_VOLUME_PATCH_H_ diff --git a/src/xenia/kernel/xam/xam_input.cc b/src/xenia/kernel/xam/xam_input.cc index 2a54e58fd..4d8487b15 100644 --- a/src/xenia/kernel/xam/xam_input.cc +++ b/src/xenia/kernel/xam/xam_input.cc @@ -121,11 +121,22 @@ dword_result_t XamInputGetState_entry(dword_t user_index, dword_t flags, actual_user_index = 0; } + X_RESULT result; auto input_system = kernel_state()->emulator()->input_system(); - auto lock = input_system->lock(); - return input_system->GetState( - user_index, !flags ? X_INPUT_FLAG::X_INPUT_FLAG_GAMEPAD : flags, - input_state); + { + auto lock = input_system->lock(); + result = input_system->GetState( + user_index, !flags ? X_INPUT_FLAG::X_INPUT_FLAG_GAMEPAD : flags, + input_state); + } + + if (input_state && result == X_ERROR_SUCCESS) { + if (auto patch = kernel_state()->xmp_volume_patch()) { + patch->OnInputPoll(input_state->packet_number); + } + } + + return result; } DECLARE_XAM_EXPORT2(XamInputGetState, kInput, kImplemented, kHighFrequency); diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc index 63755db85..bed943647 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc @@ -171,6 +171,15 @@ dword_result_t NtReadFile_entry(dword_t file_handle, dword_t event_handle, // Mark that we should signal the event now. We do this after // we have written the info out. signal_event = true; + + if (XSUCCEEDED(result)) { + if (auto patch = kernel_state()->xmp_volume_patch()) { + auto host_buf = + kernel_memory()->TranslateVirtual(buffer.guest_address()); + patch->OnFileRead(file->entry()->name(), host_buf, buffer_length, + buffer.guest_address()); + } + } } else { // TODO(benvanik): async. @@ -346,6 +355,15 @@ dword_result_t NtWriteFile_entry(dword_t file_handle, dword_t event_handle, // Mark that we should signal the event now. We do this after // we have written the info out. signal_event = true; + + if (XSUCCEEDED(result)) { + if (auto patch = kernel_state()->xmp_volume_patch()) { + auto host_buf = + kernel_memory()->TranslateVirtual(buffer.guest_address()); + patch->OnFileWrite(file->entry()->name(), host_buf, buffer_length, + buffer.guest_address()); + } + } } else { // X_STATUS_PENDING if not returning immediately. result = X_STATUS_PENDING; From 9a704bff1e5d6189d5a4ba44ce8f6ffc281c625c Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sun, 21 Sep 2025 20:10:03 +0900 Subject: [PATCH 08/31] Fix menu hotkeys on Linux --- src/xenia/ui/window_gtk.cc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/xenia/ui/window_gtk.cc b/src/xenia/ui/window_gtk.cc index 0517705d5..233ae94d3 100644 --- a/src/xenia/ui/window_gtk.cc +++ b/src/xenia/ui/window_gtk.cc @@ -484,8 +484,12 @@ VirtualKey GTKWindow::TranslateVirtualKey(guint keyval) { return VirtualKey::kSpace; case GDK_KEY_Caps_Lock: return VirtualKey::kCapital; + case GDK_KEY_Escape: + return VirtualKey::kEscape; case GDK_KEY_F1: return VirtualKey::kF1; + case GDK_KEY_F2: + return VirtualKey::kF2; case GDK_KEY_F3: return VirtualKey::kF3; case GDK_KEY_F4: @@ -494,6 +498,14 @@ VirtualKey GTKWindow::TranslateVirtualKey(guint keyval) { return VirtualKey::kF5; case GDK_KEY_F6: return VirtualKey::kF6; + case GDK_KEY_F7: + return VirtualKey::kF7; + case GDK_KEY_F8: + return VirtualKey::kF8; + case GDK_KEY_F9: + return VirtualKey::kF9; + case GDK_KEY_F10: + return VirtualKey::kF10; case GDK_KEY_F11: return VirtualKey::kF11; case GDK_KEY_F12: From 37fabb93462d5f90c13aa9ab71f1b6df349912bf Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sat, 27 Sep 2025 18:09:50 +0900 Subject: [PATCH 09/31] [GPU] Ensure vsync works correctly on Linux. Linux scheduler quantum is 4-10ms so requesting 15ms sleep ends up sleeping 16-19ms and overshoots our target causing games to run at 57fps --- src/xenia/gpu/graphics_system.cc | 39 +++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/xenia/gpu/graphics_system.cc b/src/xenia/gpu/graphics_system.cc index 30588aca9..6be717f52 100644 --- a/src/xenia/gpu/graphics_system.cc +++ b/src/xenia/gpu/graphics_system.cc @@ -164,13 +164,20 @@ X_STATUS GraphicsSystem::Setup(cpu::Processor* processor, normalized_framerate_limit)) : 1.0; uint64_t last_frame_time = Clock::QueryGuestTickCount(); - // Sleep for 90% of the vblank duration, spin for 10% + // Sleep for 90% of the vblank duration on Windows, spin for 10% + // Linux uses full sleep duration due to scheduler quantum issues +#if XE_PLATFORM_WIN32 constexpr double duration_scalar = 0.90; +#endif +#if XE_PLATFORM_LINUX + constexpr double duration_scalar = 1.0; +#endif while (frame_limiter_worker_running_) { register_file()->values[XE_GPU_REG_D1MODE_V_COUNTER] += GetInternalDisplayResolution().second; +#if XE_PLATFORM_WIN32 if (cvars::vsync) { const uint64_t current_time = Clock::QueryGuestTickCount(); const uint64_t tick_freq = Clock::guest_tick_frequency(); @@ -181,18 +188,12 @@ X_STATUS GraphicsSystem::Setup(cpu::Processor* processor, if (elapsed_d >= vsync_duration_d) { last_frame_time = current_time; - // TODO(disjtqz): should recalculate the remaining time to a - // vblank after MarkVblank, no idea how long the guest code - // normally takes MarkVblank(); - if (cvars::vsync) { - const uint64_t estimated_nanoseconds = - static_cast( - (vsync_duration_d * 1000000.0) * - duration_scalar); // 1000 microseconds = 1 ms + const uint64_t estimated_nanoseconds = static_cast( + (vsync_duration_d * 1000000.0) * + duration_scalar); // 1000 microseconds = 1 ms - threading::NanoSleep(estimated_nanoseconds); - } + threading::NanoSleep(estimated_nanoseconds); } } @@ -210,6 +211,22 @@ X_STATUS GraphicsSystem::Setup(cpu::Processor* processor, xe::threading::Sleep(std::chrono::milliseconds(1)); } } +#endif +#if XE_PLATFORM_LINUX + // Linux: Use simplified timing logic to avoid oversleeping + MarkVblank(); + + if (cvars::vsync || normalized_framerate_limit > 0) { + uint64_t sleep_duration_ns = + static_cast(vsync_duration_d * 1000000.0); + if (!cvars::vsync && normalized_framerate_limit > 0) { + sleep_duration_ns = 1000000000 / normalized_framerate_limit; + } + threading::NanoSleep(sleep_duration_ns); + } else { + xe::threading::Sleep(std::chrono::milliseconds(1)); + } +#endif } return 0; }, From bc3585d0ef46835296f36a5e272d98cd55dc620f Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 29 Sep 2025 16:39:16 +0900 Subject: [PATCH 10/31] Keep threads from trying to suspend themselves on Linux Avoids certain sporadic lockups (usually on startup) --- .../kernel/xboxkrnl/xboxkrnl_threading.cc | 28 +++++++++++++++++++ src/xenia/kernel/xthread.cc | 16 +++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc index e8740e201..e6fa86ba1 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc @@ -10,6 +10,7 @@ #include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h" #include "xenia/base/atomic.h" #include "xenia/base/clock.h" +#include "xenia/base/platform.h" #include "xenia/cpu/processor.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_private.h" @@ -236,12 +237,39 @@ dword_result_t NtSuspendThread_entry(dword_t handle, if (thread->type() == XObject::Type::Thread) { auto current_pcr = context->TranslateVirtualGPR(context->r[13]); +#if XE_PLATFORM_WIN32 if (current_pcr->prcb_data.current_thread == thread->guest_object() || !thread->guest_object()->terminated) { result = thread->Suspend(&suspend_count); } else { return X_STATUS_THREAD_IS_TERMINATING; } +#elif XE_PLATFORM_LINUX + // On Linux, we need to handle self-suspension specially to avoid deadlock + if (!thread->guest_object()->terminated) { + bool is_self_suspend = + (current_pcr->prcb_data.current_thread == thread->guest_object()); + + if (is_self_suspend) { + // Self-suspension: just increment the suspend count and return + // The thread continues running - this matches Windows/Xbox behavior + auto guest_thread = thread->guest_object(); + suspend_count = guest_thread->suspend_count; + guest_thread->suspend_count++; + result = X_STATUS_SUCCESS; + XELOGD( + "Thread {:X} self-suspending (count: {}) - continuing execution", + thread->handle(), guest_thread->suspend_count); + } else { + // Normal suspension of another thread + result = thread->Suspend(&suspend_count); + } + } else { + return X_STATUS_THREAD_IS_TERMINATING; + } +#else +#error "Unsupported platform" +#endif } else { return X_STATUS_OBJECT_TYPE_MISMATCH; } diff --git a/src/xenia/kernel/xthread.cc b/src/xenia/kernel/xthread.cc index 88523c80a..4948eca55 100644 --- a/src/xenia/kernel/xthread.cc +++ b/src/xenia/kernel/xthread.cc @@ -11,6 +11,7 @@ #include "xenia/base/byte_stream.h" #include "xenia/base/logging.h" +#include "xenia/base/platform.h" #include "xenia/base/profiling.h" #include "xenia/base/threading.h" #include "xenia/cpu/processor.h" @@ -717,12 +718,27 @@ X_STATUS XThread::Resume(uint32_t* out_suspend_count) { if (out_suspend_count) { *out_suspend_count = previous_suspend_count; } + uint32_t unused_host_suspend_count = 0; +#if XE_PLATFORM_WIN32 if (thread_->Resume(&unused_host_suspend_count)) { return X_STATUS_SUCCESS; } else { return X_STATUS_UNSUCCESSFUL; } +#elif XE_PLATFORM_LINUX + // On Linux, only resume if suspend count is 0 + // Resume might fail if thread was self-suspended - this is expected + if (guest_thread->suspend_count == 0) { + if (!thread_->Resume(&unused_host_suspend_count)) { + XELOGD("Host thread resume skipped for thread {:X} (was self-suspended)", + handle()); + } + } + return X_STATUS_SUCCESS; +#else +#error "Unsupported platform" +#endif } X_STATUS XThread::Suspend(uint32_t* out_suspend_count) { From 28e48410c58c1e723c20d0e7415fea59797fe859 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:41:02 +0900 Subject: [PATCH 11/31] Fix potential buffer overflow in posix memory handling --- src/xenia/base/memory_posix.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/xenia/base/memory_posix.cc b/src/xenia/base/memory_posix.cc index c7e061ece..15c8a7d68 100644 --- a/src/xenia/base/memory_posix.cc +++ b/src/xenia/base/memory_posix.cc @@ -184,7 +184,8 @@ bool QueryProtect(void* base_address, size_t& length, PageAccess& access_out) { while (std::getline(memory_maps, maps_entry_string)) { std::stringstream entry_stream(maps_entry_string); uintptr_t map_region_begin, map_region_end; - char separator, protection[4]; + char separator; + char protection[5]; // 4 chars (e.g., "r-xp") + null terminator entry_stream >> std::hex >> map_region_begin >> separator >> map_region_end >> protection; @@ -199,7 +200,7 @@ bool QueryProtect(void* base_address, size_t& length, PageAccess& access_out) { while (std::getline(memory_maps, maps_entry_string)) { std::stringstream next_entry_stream(maps_entry_string); uintptr_t next_map_region_begin, next_map_region_end; - char next_protection[4]; + char next_protection[5]; // 4 chars (e.g., "r-xp") + null terminator next_entry_stream >> std::hex >> next_map_region_begin >> separator >> next_map_region_end >> next_protection; From 32b8c341eef3b84fd1965b3398eb2dac7c16b316 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sun, 12 Oct 2025 20:30:29 +0900 Subject: [PATCH 12/31] [Kernel] ReleaseSemaphore for Posix to match Windows semantics --- src/xenia/base/threading_posix.cc | 15 ++++++++------- src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc | 17 +++++++++++++++-- src/xenia/kernel/xsemaphore.cc | 10 +++++++--- src/xenia/kernel/xsemaphore.h | 3 ++- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/xenia/base/threading_posix.cc b/src/xenia/base/threading_posix.cc index c8549ecfb..a2c634d08 100644 --- a/src/xenia/base/threading_posix.cc +++ b/src/xenia/base/threading_posix.cc @@ -320,14 +320,15 @@ class PosixCondition final : public PosixConditionBase { bool Signal() override { return Release(1, nullptr); } bool Release(uint32_t release_count, int* out_previous_count) { - if (maximum_count_ - count_ >= release_count) { - auto lock = std::unique_lock(mutex_); - if (out_previous_count) *out_previous_count = count_; - count_ += release_count; - cond_.notify_all(); - return true; + auto lock = std::unique_lock(mutex_); + // Validate that releasing would not exceed the maximum count + if (count_ + release_count > maximum_count_) { + return false; } - return false; + if (out_previous_count) *out_previous_count = count_; + count_ += release_count; + cond_.notify_all(); + return true; } private: diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc index e6fa86ba1..fba756fee 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc @@ -718,7 +718,10 @@ uint32_t xeKeReleaseSemaphore(X_KSEMAPHORE* semaphore_ptr, uint32_t increment, // TODO(benvanik): increment thread priority? // TODO(benvanik): wait? - return sem->ReleaseSemaphore(adjustment); + int32_t previous_count = 0; + [[maybe_unused]] bool success = + sem->ReleaseSemaphore(adjustment, &previous_count); + return static_cast(previous_count); } dword_result_t KeReleaseSemaphore_entry(pointer_t semaphore_ptr, @@ -777,7 +780,17 @@ dword_result_t NtReleaseSemaphore_entry(dword_t sem_handle, auto sem = kernel_state()->object_table()->LookupObject(sem_handle); if (sem) { - previous_count = sem->ReleaseSemaphore((int32_t)release_count); + bool success = + sem->ReleaseSemaphore((int32_t)release_count, &previous_count); + if (!success) { + // Releasing would exceed the semaphore's maximum count + // Windows returns STATUS_SEMAPHORE_LIMIT_EXCEEDED (0x0000012B) + XELOGW( + "NtReleaseSemaphore: release_count={} would exceed maximum (current " + "count={})", + uint32_t(release_count), previous_count); + result = 0x0000012B; + } } else { result = X_STATUS_INVALID_HANDLE; } diff --git a/src/xenia/kernel/xsemaphore.cc b/src/xenia/kernel/xsemaphore.cc index 34b960b25..f04d0826e 100644 --- a/src/xenia/kernel/xsemaphore.cc +++ b/src/xenia/kernel/xsemaphore.cc @@ -40,10 +40,14 @@ bool XSemaphore::InitializeNative(void* native_ptr, X_DISPATCH_HEADER* header) { return !!semaphore_; } -int32_t XSemaphore::ReleaseSemaphore(int32_t release_count) { +bool XSemaphore::ReleaseSemaphore(int32_t release_count, + int32_t* out_previous_count) { int32_t previous_count = 0; - semaphore_->Release(release_count, &previous_count); - return previous_count; + bool success = semaphore_->Release(release_count, &previous_count); + if (out_previous_count) { + *out_previous_count = previous_count; + } + return success; } bool XSemaphore::Save(ByteStream* stream) { diff --git a/src/xenia/kernel/xsemaphore.h b/src/xenia/kernel/xsemaphore.h index 77278af9f..2135fdec3 100644 --- a/src/xenia/kernel/xsemaphore.h +++ b/src/xenia/kernel/xsemaphore.h @@ -29,7 +29,8 @@ class XSemaphore : public XObject { [[nodiscard]] bool InitializeNative(void* native_ptr, X_DISPATCH_HEADER* header); - int32_t ReleaseSemaphore(int32_t release_count); + [[nodiscard]] bool ReleaseSemaphore(int32_t release_count, + int32_t* out_previous_count); bool Save(ByteStream* stream) override; static object_ref Restore(KernelState* kernel_state, From 81dd887e744ceb1eab95eb21604e1fc78ad9c4b1 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sun, 12 Oct 2025 21:00:11 +0900 Subject: [PATCH 13/31] [Kernel] Thread suspend for posix to match windows semantics --- src/xenia/base/threading_posix.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/xenia/base/threading_posix.cc b/src/xenia/base/threading_posix.cc index a2c634d08..862c66911 100644 --- a/src/xenia/base/threading_posix.cc +++ b/src/xenia/base/threading_posix.cc @@ -703,11 +703,17 @@ class PosixCondition final : public PosixConditionBase { } WaitStarted(); std::unique_lock lock(state_mutex_); - if (state_ != State::kSuspended) return false; + // Check if thread has any suspend count (Windows allows resume even if + // running) + if (suspend_count_ == 0) return false; if (out_previous_suspend_count) { *out_previous_suspend_count = suspend_count_; } --suspend_count_; + // If suspend count reaches 0, transition to running + if (suspend_count_ == 0 && state_ == State::kSuspended) { + state_ = State::kRunning; + } state_signal_.notify_all(); return true; } From 137baa2b9f6f67f03d3217cb35a41efcedc8a3ef Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 13 Oct 2025 02:33:21 +0900 Subject: [PATCH 14/31] [Kernel] Ensure file mutex is initialized --- src/xenia/kernel/xfile.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xenia/kernel/xfile.cc b/src/xenia/kernel/xfile.cc index a419b466c..e5a4ff24b 100644 --- a/src/xenia/kernel/xfile.cc +++ b/src/xenia/kernel/xfile.cc @@ -24,7 +24,7 @@ XFile::XFile(KernelState* kernel_state, vfs::File* file, bool synchronous) assert_not_null(async_event_); } -XFile::XFile() : XObject(kObjectType) { +XFile::XFile() : XObject(kObjectType), completion_port_lock_() { async_event_ = threading::Event::CreateAutoResetEvent(false); assert_not_null(async_event_); } From c9eba5daf88682349f8df9b16627b1577ab2fc34 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 13 Oct 2025 14:53:59 +0900 Subject: [PATCH 15/31] [Threading] Signal handler / self-suspension improvements --- src/xenia/base/threading_posix.cc | 41 +++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/xenia/base/threading_posix.cc b/src/xenia/base/threading_posix.cc index 862c66911..252bb767f 100644 --- a/src/xenia/base/threading_posix.cc +++ b/src/xenia/base/threading_posix.cc @@ -111,19 +111,25 @@ SignalType GetSystemSignalType(int num) { return static_cast(num - SIGRTMIN); } -thread_local std::array(SignalType::k_Count)> +std::array, static_cast(SignalType::k_Count)> signal_handler_installed = {}; static void signal_handler(int signal, siginfo_t* info, void* context); void install_signal_handler(SignalType type) { - if (signal_handler_installed[static_cast(type)]) return; + bool expected = false; + if (!signal_handler_installed[static_cast(type)] + .compare_exchange_strong(expected, true)) { + return; // Already installed + } struct sigaction action{}; - action.sa_flags = SA_SIGINFO; + action.sa_flags = SA_SIGINFO | SA_RESTART; action.sa_sigaction = signal_handler; sigemptyset(&action.sa_mask); - if (sigaction(GetSystemSignal(type), &action, nullptr) == -1) - signal_handler_installed[static_cast(type)] = true; + if (sigaction(GetSystemSignal(type), &action, nullptr) != 0) { + // Failed to install, reset the flag + signal_handler_installed[static_cast(type)] = false; + } } // TODO(dougvj) @@ -705,7 +711,9 @@ class PosixCondition final : public PosixConditionBase { std::unique_lock lock(state_mutex_); // Check if thread has any suspend count (Windows allows resume even if // running) - if (suspend_count_ == 0) return false; + if (suspend_count_ == 0) { + return false; + } if (out_previous_suspend_count) { *out_previous_suspend_count = suspend_count_; } @@ -723,6 +731,10 @@ class PosixCondition final : public PosixConditionBase { *out_previous_suspend_count = 0; } WaitStarted(); + + // Check if we're trying to suspend ourselves + bool is_current_thread = pthread_self() == thread_; + { if (out_previous_suspend_count) { *out_previous_suspend_count = suspend_count_; @@ -730,6 +742,15 @@ class PosixCondition final : public PosixConditionBase { state_ = State::kSuspended; ++suspend_count_; } + + if (is_current_thread) { + // Self-suspension: Instead of sending a signal, directly call + // WaitSuspended This avoids the signal handler complexity for the + // self-suspend case + WaitSuspended(); + return true; + } + int result = pthread_kill(thread_, GetSystemSignal(SignalType::kThreadSuspend)); return result == 0; @@ -1191,10 +1212,14 @@ void set_name(const std::string_view name) { #endif } -static void signal_handler(int signal, siginfo_t* info, void* /*context*/) { +static void signal_handler(int signal, siginfo_t* info, void* context) { switch (GetSystemSignalType(signal)) { case SignalType::kThreadSuspend: { - assert_not_null(current_thread_); + if (!current_thread_) { + // current_thread_ is NULL - this can happen if the signal arrives + // before the thread has initialized or after it has exited + return; + } current_thread_->WaitSuspended(); } break; case SignalType::kThreadUserCallback: { From 1ae82023eaac7f50713e199bfca4db7b4ce0a9a4 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 13 Oct 2025 17:46:22 +0900 Subject: [PATCH 16/31] [Posix Threads] Refactored condition variables from static to per-instance members and replaced deadlock-prone predicate-based WaitMultiple with polling using try_lock. --- src/xenia/base/threading_posix.cc | 143 ++++++++++++++++++++---------- 1 file changed, 96 insertions(+), 47 deletions(-) diff --git a/src/xenia/base/threading_posix.cc b/src/xenia/base/threading_posix.cc index 252bb767f..4258ecab1 100644 --- a/src/xenia/base/threading_posix.cc +++ b/src/xenia/base/threading_posix.cc @@ -222,64 +222,111 @@ class PosixConditionBase { std::chrono::milliseconds timeout) { assert_true(!handles.empty()); - // Construct a condition for all or any depending on wait_all - std::function predicate; - { - using iter_t = std::vector::const_iterator; - const auto predicate_inner = [](auto h) { return h->signaled(); }; - const auto operation = - wait_all ? std::all_of - : std::any_of; - predicate = [&handles, operation, predicate_inner] { - return operation(handles.cbegin(), handles.cend(), predicate_inner); - }; + // For single handle, just use the normal Wait path + if (handles.size() == 1) { + auto result = handles[0]->Wait(timeout); + return std::make_pair(result, 0); } - // TODO(bwrsandman, Triang3l) This is controversial, see issue #1677 - // This will probably cause a deadlock on the next thread doing any waiting - // if the thread is suspended between locking and waiting - std::unique_lock lock(mutex_); + // For multiple handles, we need to poll since we can't wait on multiple + // condition variables simultaneously. This is a limitation of the POSIX + // condition variable API. + auto start_time = std::chrono::steady_clock::now(); + auto end_time = (timeout == std::chrono::milliseconds::max()) + ? std::chrono::steady_clock::time_point::max() + : start_time + timeout; - bool wait_success = true; - // If the timeout is infinite, wait without timeout. - // The predicate will be checked before beginning the wait - if (timeout == std::chrono::milliseconds::max()) { - cond_.wait(lock, predicate); - } else { - // Wait with timeout. - wait_success = cond_.wait_for(lock, timeout, predicate); - } - if (wait_success) { - auto first_signaled = std::numeric_limits::max(); - for (auto i = 0u; i < handles.size(); ++i) { - if (handles[i]->signaled()) { - if (first_signaled > i) { - first_signaled = i; - } - handles[i]->post_execution(); - if (!wait_all) break; + while (true) { + // Check all handles to see if any/all are signaled + // Use try_lock to avoid deadlocks from lock ordering issues + size_t first_signaled = std::numeric_limits::max(); + bool condition_met = false; + + // Try to acquire all locks without blocking + std::vector> locks; + locks.reserve(handles.size()); + bool all_locked = true; + + for (size_t i = 0; i < handles.size(); ++i) { + locks.emplace_back(handles[i]->mutex_, std::try_to_lock); + if (!locks.back().owns_lock()) { + all_locked = false; + break; } } - assert_true(std::numeric_limits::max() != first_signaled); - return std::make_pair(WaitResult::kSuccess, first_signaled); + + // If we couldn't acquire all locks, release what we have and retry + if (!all_locked) { + locks.clear(); + std::this_thread::yield(); + continue; + } + + // Now we have all locks, check the condition + if (wait_all) { + // For wait_all, check if ALL are signaled + bool all_signaled = true; + for (size_t i = 0; i < handles.size(); ++i) { + if (!handles[i]->signaled()) { + all_signaled = false; + break; + } + if (first_signaled == std::numeric_limits::max()) { + first_signaled = i; + } + } + condition_met = all_signaled; + } else { + // For wait_any, check if ANY is signaled + for (size_t i = 0; i < handles.size(); ++i) { + if (handles[i]->signaled()) { + first_signaled = i; + condition_met = true; + break; + } + } + } + + if (condition_met) { + // Execute post_execution for the signaled handle(s) + if (wait_all) { + for (size_t i = 0; i < handles.size(); ++i) { + handles[i]->post_execution(); + } + } else { + handles[first_signaled]->post_execution(); + } + return std::make_pair(WaitResult::kSuccess, first_signaled); + } + + // Release locks before sleeping + locks.clear(); + + // Check timeout + auto now = std::chrono::steady_clock::now(); + if (now >= end_time) { + return std::make_pair(WaitResult::kTimeout, 0); + } + + // Sleep for a short time before polling again + auto remaining = + std::chrono::duration_cast(end_time - now); + auto sleep_time = std::min(remaining, std::chrono::milliseconds(1)); + std::this_thread::sleep_for(sleep_time); } - return std::make_pair(WaitResult::kTimeout, 0); } [[nodiscard]] virtual void* native_handle() const { - return cond_.native_handle(); + return const_cast(cond_).native_handle(); } protected: [[nodiscard]] inline virtual bool signaled() const = 0; inline virtual void post_execution() = 0; - static std::condition_variable cond_; - static std::mutex mutex_; + std::condition_variable cond_; + std::mutex mutex_; }; -std::condition_variable PosixConditionBase::cond_; -std::mutex PosixConditionBase::mutex_; - // There really is no native POSIX handle for a single wait/signal construct // pthreads is at a lower level with more handles for such a mechanism. // This simple wrapper class functions as our handle and uses conditional @@ -373,7 +420,7 @@ class PosixCondition final : public PosixConditionBase { } [[nodiscard]] void* native_handle() const override { - return mutex_.native_handle(); + return const_cast(mutex_).native_handle(); } private: @@ -1152,10 +1199,12 @@ void* PosixCondition::ThreadStartRoutine(void* parameter) { thread->handle_.state_ = State::kFinished; } - std::unique_lock lock(mutex_); - thread->handle_.exit_code_ = 0; - thread->handle_.signaled_ = true; - cond_.notify_all(); + { + std::unique_lock lock(thread->handle_.mutex_); + thread->handle_.exit_code_ = 0; + thread->handle_.signaled_ = true; + thread->handle_.cond_.notify_all(); + } current_thread_ = nullptr; return nullptr; From 174d2d42059e892b25de39bdc4808a194c1346a8 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:04:36 +0900 Subject: [PATCH 17/31] [Posix Threading] Robust mutexes --- src/xenia/base/threading_posix.cc | 43 ++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/xenia/base/threading_posix.cc b/src/xenia/base/threading_posix.cc index 4258ecab1..fbbff7bb3 100644 --- a/src/xenia/base/threading_posix.cc +++ b/src/xenia/base/threading_posix.cc @@ -193,13 +193,38 @@ bool SetTlsValue(TlsHandle handle, uintptr_t value) { class PosixConditionBase { public: + PosixConditionBase() { + // Initialize as robust mutex to handle thread termination gracefully + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST); + + // Get the native handle and set it as robust + auto native_mutex = static_cast(mutex_.native_handle()); + pthread_mutex_destroy(native_mutex); // Destroy default mutex + pthread_mutex_init(native_mutex, &attr); // Reinit as robust + pthread_mutexattr_destroy(&attr); + } + virtual ~PosixConditionBase() = default; virtual bool Signal() = 0; WaitResult Wait(std::chrono::milliseconds timeout) { bool executed; auto predicate = [this] { return this->signaled(); }; - auto lock = std::unique_lock(mutex_); + + // Handle robust mutex locking + auto native_mutex = static_cast(mutex_.native_handle()); + int lock_result = pthread_mutex_lock(native_mutex); + if (lock_result == EOWNERDEAD) { + // Recover from dead owner + pthread_mutex_consistent(native_mutex); + } else if (lock_result != 0) { + return WaitResult::kFailed; + } + + std::unique_lock lock(mutex_, std::adopt_lock); + if (predicate()) { executed = true; } else { @@ -248,8 +273,20 @@ class PosixConditionBase { bool all_locked = true; for (size_t i = 0; i < handles.size(); ++i) { - locks.emplace_back(handles[i]->mutex_, std::try_to_lock); - if (!locks.back().owns_lock()) { + // Try to lock, handling robust mutex EOWNERDEAD case + auto native_mutex = + static_cast(handles[i]->mutex_.native_handle()); + int result = pthread_mutex_trylock(native_mutex); + + if (result == 0 || result == EOWNERDEAD) { + // Successfully acquired lock or recovered from dead owner + if (result == EOWNERDEAD) { + // Make mutex consistent after previous owner died + pthread_mutex_consistent(native_mutex); + } + locks.emplace_back(handles[i]->mutex_, std::adopt_lock); + } else { + // Couldn't acquire lock all_locked = false; break; } From ae1706d1dca7dcff2a8b8d5433b315e2f0ccab55 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:48:11 +0900 Subject: [PATCH 18/31] [Threads] Ensure proper RTTI/vtable emission for PosixWaitHandle --- src/xenia/base/threading_posix.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/xenia/base/threading_posix.cc b/src/xenia/base/threading_posix.cc index fbbff7bb3..b768c6872 100644 --- a/src/xenia/base/threading_posix.cc +++ b/src/xenia/base/threading_posix.cc @@ -924,10 +924,13 @@ class PosixCondition final : public PosixConditionBase { class PosixWaitHandle { public: - virtual ~PosixWaitHandle() = default; + virtual ~PosixWaitHandle(); virtual PosixConditionBase& condition() = 0; }; +// Out-of-line destructor to ensure proper RTTI/vtable emission +PosixWaitHandle::~PosixWaitHandle() = default; + // This wraps a condition object as our handle because posix has no single // native handle for higher level concurrency constructs such as semaphores template From 15483b02af0965c3057225668c8fc974f9c86227 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:41:32 +0900 Subject: [PATCH 19/31] [Memory] Linux cleanup shm files on exit Prevents running out of /dev/shm space and crashing the whole system. --- src/xenia/base/memory_posix.cc | 46 +++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/xenia/base/memory_posix.cc b/src/xenia/base/memory_posix.cc index 15c8a7d68..bb14fc583 100644 --- a/src/xenia/base/memory_posix.cc +++ b/src/xenia/base/memory_posix.cc @@ -12,7 +12,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -108,6 +110,29 @@ struct MappedFileRange { std::vector mapped_file_ranges; std::mutex g_mapped_file_ranges_mutex; +// Track shm file names for cleanup on exit +std::vector g_shm_file_names; +std::mutex g_shm_file_names_mutex; +static bool g_cleanup_handlers_installed = false; + +#if !XE_PLATFORM_ANDROID +static void CleanupAtExit() { + for (const auto& name : g_shm_file_names) { + shm_unlink(name.c_str()); + } +} + +static void InstallCleanupHandlers() { + if (g_cleanup_handlers_installed) { + return; + } + g_cleanup_handlers_installed = true; + + std::atexit(CleanupAtExit); + std::at_quick_exit(CleanupAtExit); +} +#endif // !XE_PLATFORM_ANDROID + void* AllocFixed(void* base_address, size_t length, AllocationType allocation_type, PageAccess access) { // mmap does not support reserve / commit, so ignore allocation_type. @@ -271,7 +296,17 @@ FileMappingHandle CreateFileMappingHandle(const std::filesystem::path& path, if (ret < 0) { return kFileMappingHandleInvalid; } - ftruncate64(ret, length); + if (ftruncate64(ret, length) < 0) { + close(ret); + shm_unlink(full_path.c_str()); + return kFileMappingHandleInvalid; + } + // Track for cleanup on abnormal exit and install cleanup handlers + { + std::lock_guard guard(g_shm_file_names_mutex); + g_shm_file_names.push_back(full_path.string()); + } + InstallCleanupHandlers(); return ret; #endif } @@ -282,6 +317,15 @@ void CloseFileMappingHandle(FileMappingHandle handle, #if !XE_PLATFORM_ANDROID auto full_path = "/" / path; shm_unlink(full_path.c_str()); + // Remove from tracking + { + std::lock_guard guard(g_shm_file_names_mutex); + auto it = std::find(g_shm_file_names.begin(), g_shm_file_names.end(), + full_path.string()); + if (it != g_shm_file_names.end()) { + g_shm_file_names.erase(it); + } + } #endif } From 92b07391df8c5bcc3cdccfe9f5a4c815f6d025fc Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:54:08 +0900 Subject: [PATCH 20/31] [Linux/Threading] change self-suspend to wait to be resumed --- src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc index fba756fee..f284f88bc 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc @@ -251,15 +251,19 @@ dword_result_t NtSuspendThread_entry(dword_t handle, (current_pcr->prcb_data.current_thread == thread->guest_object()); if (is_self_suspend) { - // Self-suspension: just increment the suspend count and return - // The thread continues running - this matches Windows/Xbox behavior + // Self-suspension: increment the suspend count and wait until + // another thread resumes us (decrements count to 0) auto guest_thread = thread->guest_object(); suspend_count = guest_thread->suspend_count; guest_thread->suspend_count++; result = X_STATUS_SUCCESS; - XELOGD( - "Thread {:X} self-suspending (count: {}) - continuing execution", - thread->handle(), guest_thread->suspend_count); + XELOGD("Thread {:X} self-suspending (count: {}) - waiting for resume", + thread->handle(), guest_thread->suspend_count); + // Wait until suspend_count reaches 0 (resumed by another thread) + while (guest_thread->suspend_count > 0) { + xe::threading::Sleep(std::chrono::microseconds(100)); + } + XELOGD("Thread {:X} resumed", thread->handle()); } else { // Normal suspension of another thread result = thread->Suspend(&suspend_count); From 52297ea8f8dbb09eaf96beaa5ca5bafa8119ce33 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:33:18 +0900 Subject: [PATCH 21/31] [Linux] Implement fast mutex similar to Windows version --- src/xenia/base/mutex.cc | 177 ++++++++++++++++++++++++++++++++++++++++ src/xenia/base/mutex.h | 80 ++++++++++++++++-- 2 files changed, 252 insertions(+), 5 deletions(-) diff --git a/src/xenia/base/mutex.cc b/src/xenia/base/mutex.cc index 5f62ae39b..c334a8b0c 100644 --- a/src/xenia/base/mutex.cc +++ b/src/xenia/base/mutex.cc @@ -10,6 +10,10 @@ #include "xenia/base/mutex.h" #if XE_PLATFORM_WIN32 == 1 #include "xenia/base/platform_win.h" +#elif XE_PLATFORM_LINUX == 1 +#include +#include +#include #endif namespace xe { @@ -70,6 +74,179 @@ bool xe_fast_mutex::try_lock() { } return false; } +#elif XE_PLATFORM_LINUX == 1 && XE_ENABLE_FAST_LINUX_MUTEX == 1 + +namespace { + +inline int futex_wait(std::atomic* addr, uint32_t expected) { + return syscall(SYS_futex, addr, FUTEX_WAIT_PRIVATE, expected, nullptr, + nullptr, 0); +} + +inline int futex_wake(std::atomic* addr, int count) { + return syscall(SYS_futex, addr, FUTEX_WAKE_PRIVATE, count, nullptr, nullptr, + 0); +} + +inline pid_t gettid() { return static_cast(syscall(SYS_gettid)); } + +} // namespace + +// xe_global_mutex implementation (recursive) +void xe_global_mutex::lock() { + pid_t self = gettid(); + + // Fast path: check if we already own it (recursive lock) + if (owner_.load(std::memory_order_relaxed) == self) { + ++recursion_count_; + return; + } + + // Try to acquire with a simple CAS first (uncontended case) + uint32_t expected = 0; + if (XE_LIKELY(state_.compare_exchange_strong( + expected, 1, std::memory_order_acquire, std::memory_order_relaxed))) { + owner_.store(self, std::memory_order_relaxed); + recursion_count_ = 1; + return; + } + + lock_slow(); +} + +void xe_global_mutex::lock_slow() { + pid_t self = gettid(); + + // Spin phase + for (int i = 0; i < XE_LINUX_MUTEX_SPINCOUNT; ++i) { +#if XE_ARCH_AMD64 == 1 + _mm_pause(); +#endif + uint32_t expected = 0; + if (state_.compare_exchange_strong(expected, 1, std::memory_order_acquire, + std::memory_order_relaxed)) { + owner_.store(self, std::memory_order_relaxed); + recursion_count_ = 1; + return; + } + } + + // Slow path: use futex + while (true) { + // Mark as contended (state = 2) and wait + uint32_t state = state_.exchange(2, std::memory_order_acquire); + if (state == 0) { + // We got the lock while marking contended + owner_.store(self, std::memory_order_relaxed); + recursion_count_ = 1; + return; + } + + // Wait on futex + futex_wait(&state_, 2); + + // Try to acquire after wakeup + uint32_t expected = 0; + if (state_.compare_exchange_strong(expected, 2, std::memory_order_acquire, + std::memory_order_relaxed)) { + owner_.store(self, std::memory_order_relaxed); + recursion_count_ = 1; + return; + } + } +} + +void xe_global_mutex::unlock() { + if (--recursion_count_ > 0) { + return; // Still have recursive locks + } + + owner_.store(0, std::memory_order_relaxed); + + // If state was 2 (contended), we need to wake a waiter + if (state_.exchange(0, std::memory_order_release) == 2) { + futex_wake(&state_, 1); + } +} + +bool xe_global_mutex::try_lock() { + pid_t self = gettid(); + + // Check for recursive lock + if (owner_.load(std::memory_order_relaxed) == self) { + ++recursion_count_; + return true; + } + + uint32_t expected = 0; + if (state_.compare_exchange_strong(expected, 1, std::memory_order_acquire, + std::memory_order_relaxed)) { + owner_.store(self, std::memory_order_relaxed); + recursion_count_ = 1; + return true; + } + return false; +} + +// xe_fast_mutex implementation (non-recursive) +void xe_fast_mutex::lock() { + // Fast path: uncontended + uint32_t expected = 0; + if (XE_LIKELY(state_.compare_exchange_strong( + expected, 1, std::memory_order_acquire, std::memory_order_relaxed))) { + return; + } + + lock_slow(); +} + +void xe_fast_mutex::lock_slow() { + // Spin phase + for (int i = 0; i < XE_LINUX_MUTEX_SPINCOUNT; ++i) { +#if XE_ARCH_AMD64 == 1 + _mm_pause(); +#endif + uint32_t expected = 0; + if (state_.compare_exchange_strong(expected, 1, std::memory_order_acquire, + std::memory_order_relaxed)) { + return; + } + } + + // Slow path: use futex + while (true) { + // Mark as contended (state = 2) and wait + uint32_t state = state_.exchange(2, std::memory_order_acquire); + if (state == 0) { + // We got the lock while marking contended + return; + } + + // Wait on futex + futex_wait(&state_, 2); + + // Try to acquire after wakeup + uint32_t expected = 0; + if (state_.compare_exchange_strong(expected, 2, std::memory_order_acquire, + std::memory_order_relaxed)) { + return; + } + } +} + +void xe_fast_mutex::unlock() { + // If state was 2 (contended), we need to wake a waiter + if (state_.exchange(0, std::memory_order_release) == 2) { + futex_wake(&state_, 1); + } +} + +bool xe_fast_mutex::try_lock() { + uint32_t expected = 0; + return state_.compare_exchange_strong(expected, 1, std::memory_order_acquire, + std::memory_order_relaxed); +} + #endif global_mutex_type& global_critical_region::mutex() { static global_mutex_type global_mutex; diff --git a/src/xenia/base/mutex.h b/src/xenia/base/mutex.h index 665f38f97..94aa36e59 100644 --- a/src/xenia/base/mutex.h +++ b/src/xenia/base/mutex.h @@ -9,6 +9,7 @@ #ifndef XENIA_BASE_MUTEX_H_ #define XENIA_BASE_MUTEX_H_ +#include #include #include #include "platform.h" @@ -19,6 +20,7 @@ #endif #include "memory.h" #define XE_ENABLE_FAST_WIN32_MUTEX 1 +#define XE_ENABLE_FAST_LINUX_MUTEX 1 namespace xe { #if XE_PLATFORM_WIN32 == 1 && XE_ENABLE_FAST_WIN32_MUTEX == 1 @@ -80,6 +82,78 @@ class xe_unlikely_mutex { void unlock() { mut.exchange(0); } bool try_lock() { return _tryget(); } }; +using xe_mutex = xe_fast_mutex; +#elif XE_PLATFORM_LINUX == 1 && XE_ENABLE_FAST_LINUX_MUTEX == 1 + +#define XE_LINUX_MUTEX_SPINCOUNT 128 + +// Fast recursive mutex for Linux using futex +// Mimics Windows CRITICAL_SECTION behavior: spin before blocking +class alignas(4096) xe_global_mutex { + std::atomic state_{0}; // 0 = unlocked, 1 = locked, 2 = contended + std::atomic owner_{0}; + uint32_t recursion_count_{0}; + + void lock_slow(); + + public: + xe_global_mutex() = default; + ~xe_global_mutex() = default; + + void lock(); + void unlock(); + bool try_lock(); +}; +using global_mutex_type = xe_global_mutex; + +// Fast non-recursive mutex for Linux using futex +class alignas(64) xe_fast_mutex { + std::atomic state_{0}; // 0 = unlocked, 1 = locked, 2 = contended + + void lock_slow(); + + public: + xe_fast_mutex() = default; + ~xe_fast_mutex() = default; + + void lock(); + void unlock(); + bool try_lock(); +}; + +// xe_unlikely_mutex remains a simple spinlock for Linux too +class xe_unlikely_mutex { + std::atomic mut{0}; + bool _tryget() { + uint32_t lock_expected = 0; + return mut.compare_exchange_strong( + lock_expected, 1, std::memory_order_acquire, std::memory_order_relaxed); + } + + public: + xe_unlikely_mutex() = default; + ~xe_unlikely_mutex() = default; + + void lock() { + if (XE_LIKELY(_tryget())) { + return; + } + // Spin a bit before yielding + for (int i = 0; i < XE_LINUX_MUTEX_SPINCOUNT; ++i) { +#if XE_ARCH_AMD64 == 1 + _mm_pause(); +#endif + if (_tryget()) return; + } + // Fall back to yielding + while (!_tryget()) { + std::this_thread::yield(); + } + } + void unlock() { mut.store(0, std::memory_order_release); } + bool try_lock() { return _tryget(); } +}; + using xe_mutex = xe_fast_mutex; #else using global_mutex_type = std::recursive_mutex; @@ -150,11 +224,7 @@ class global_critical_region { return global_unique_lock_type(mutex()); } - static inline void PrepareToAcquire() { -#if XE_PLATFORM_WIN32 == 1 - swcache::PrefetchW(&mutex()); -#endif - } + static inline void PrepareToAcquire() { swcache::PrefetchW(&mutex()); } // Acquires a deferred lock on the global critical section. static inline global_unique_lock_type AcquireDeferred() { From b7add3e0ec49df1bbd2ed65972ad5393ca026587 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:08:36 +0900 Subject: [PATCH 22/31] [CPU/PPC] Add Linux workaround for fdiv instruction format On Linux, these instructions appear to use A format (FRA, FRB, FRT) instead of the standard X format (RA, RB, RT). Need to verify if this is needed on Windows or is Linux specific. --- src/xenia/cpu/ppc/ppc_emit_fpu.cc | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/xenia/cpu/ppc/ppc_emit_fpu.cc b/src/xenia/cpu/ppc/ppc_emit_fpu.cc index 286d61b5f..46f62d52a 100644 --- a/src/xenia/cpu/ppc/ppc_emit_fpu.cc +++ b/src/xenia/cpu/ppc/ppc_emit_fpu.cc @@ -54,18 +54,35 @@ int InstrEmit_faddsx(PPCHIRBuilder& f, const InstrData& i) { int InstrEmit_fdivx(PPCHIRBuilder& f, const InstrData& i) { // frD <- frA / frB +#if XE_PLATFORM_LINUX + // TODO(has207): verify if this is needed on Windows + // On Linux, fdiv needs to use A format fields instead of X format Value* v = f.Div(f.LoadFPR(i.A.FRA), f.LoadFPR(i.A.FRB)); f.StoreFPR(i.A.FRT, v); f.UpdateFPSCR(v, i.A.Rc); +#else + Value* v = f.Div(f.LoadFPR(i.X.RA), f.LoadFPR(i.X.RB)); + f.StoreFPR(i.X.RT, v); + f.UpdateFPSCR(v, i.X.Rc); +#endif return 0; } int InstrEmit_fdivsx(PPCHIRBuilder& f, const InstrData& i) { // frD <- frA / frB +#if XE_PLATFORM_LINUX + // TODO(has207): verify if this is needed on Windows + // On Linux, fdivs needs to use A format fields instead of X format Value* v = f.Div(f.LoadFPR(i.A.FRA), f.LoadFPR(i.A.FRB)); v = f.ToSingle(v); f.StoreFPR(i.A.FRT, v); f.UpdateFPSCR(v, i.A.Rc); +#else + Value* v = f.Div(f.LoadFPR(i.X.RA), f.LoadFPR(i.X.RB)); + v = f.ToSingle(v); + f.StoreFPR(i.X.RT, v); + f.UpdateFPSCR(v, i.X.Rc); +#endif return 0; } From bf6e1a81eb22ee0866978cd812f2adf3a83a1593 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sun, 15 Feb 2026 14:37:28 +0900 Subject: [PATCH 23/31] [Posix] Fix DeallocFixed crashes / memory leaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BaseHeap::Dispose() walks the page table and calls DeallocFixed(addr, 0, kRelease) for every allocated page region. These addresses are within file-backed view mappings (i.e. guest memory). On Windows, VirtualFree on file-mapped pages silently returns FALSE. On POSIX, the code hit assert_always() and crashed. Changed to return false to match Windows behavior. Memory leaks from munmap(addr, 0): Callers passed length=0 to DeallocFixed(kRelease). On Windows this works because VirtualFree(addr, 0, MEM_RELEASE) means "release the entire region" — Windows ignores the length parameter for MEM_RELEASE and always frees the whole allocation. On POSIX, munmap(addr, 0) fails with EINVAL, so the memory was silently leaked every time. --- src/xenia/base/memory_posix.cc | 2 +- src/xenia/cpu/backend/x64/x64_code_cache.cc | 2 +- src/xenia/cpu/backend/x64/x64_emitter.cc | 3 ++- src/xenia/cpu/thread_state.cc | 7 ++++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/xenia/base/memory_posix.cc b/src/xenia/base/memory_posix.cc index bb14fc583..15a9ab858 100644 --- a/src/xenia/base/memory_posix.cc +++ b/src/xenia/base/memory_posix.cc @@ -171,7 +171,7 @@ bool DeallocFixed(void* base_address, size_t length, case DeallocationType::kDecommit: return Protect(base_address, length, PageAccess::kNoAccess); case DeallocationType::kRelease: - assert_always("Error: Tried to release mapped memory!"); + return false; default: assert_unhandled_case(deallocation_type); } diff --git a/src/xenia/cpu/backend/x64/x64_code_cache.cc b/src/xenia/cpu/backend/x64/x64_code_cache.cc index b63127c6f..c386d15ca 100644 --- a/src/xenia/cpu/backend/x64/x64_code_cache.cc +++ b/src/xenia/cpu/backend/x64/x64_code_cache.cc @@ -38,7 +38,7 @@ X64CodeCache::X64CodeCache() = default; X64CodeCache::~X64CodeCache() { if (indirection_table_base_) { - xe::memory::DeallocFixed(indirection_table_base_, 0, + xe::memory::DeallocFixed(indirection_table_base_, kIndirectionTableSize, xe::memory::DeallocationType::kRelease); } diff --git a/src/xenia/cpu/backend/x64/x64_emitter.cc b/src/xenia/cpu/backend/x64/x64_emitter.cc index 5262d0a4f..51e9847fd 100644 --- a/src/xenia/cpu/backend/x64/x64_emitter.cc +++ b/src/xenia/cpu/backend/x64/x64_emitter.cc @@ -1291,7 +1291,8 @@ uintptr_t X64Emitter::PlaceConstData() { } void X64Emitter::FreeConstData(uintptr_t data) { - memory::DeallocFixed(reinterpret_cast(data), 0, + memory::DeallocFixed(reinterpret_cast(data), + xe::round_up(kConstDataSize, memory::page_size()), memory::DeallocationType::kRelease); } diff --git a/src/xenia/cpu/thread_state.cc b/src/xenia/cpu/thread_state.cc index bc1315887..fc16b5a1c 100644 --- a/src/xenia/cpu/thread_state.cc +++ b/src/xenia/cpu/thread_state.cc @@ -56,9 +56,10 @@ static void* AllocateContext() { } static void FreeContext(void* ctx) { - char* true_start_of_ctx = &reinterpret_cast( - ctx)[-static_cast(xe::memory::allocation_granularity())]; - memory::DeallocFixed(true_start_of_ctx, 0, + size_t granularity = xe::memory::allocation_granularity(); + char* true_start_of_ctx = + &reinterpret_cast(ctx)[-static_cast(granularity)]; + memory::DeallocFixed(true_start_of_ctx, granularity + sizeof(ppc::PPCContext), memory::DeallocationType::kRelease); } From 7e0254b36c8bbab4c8286d4a3f47bf0d4bc15550 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Fri, 17 Oct 2025 10:15:46 +0900 Subject: [PATCH 24/31] [CPU] Check for null when returning machine code ptr --- src/xenia/cpu/function.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xenia/cpu/function.cc b/src/xenia/cpu/function.cc index ecbb239d6..c2dcf5264 100644 --- a/src/xenia/cpu/function.cc +++ b/src/xenia/cpu/function.cc @@ -122,7 +122,6 @@ uint32_t GuestFunction::MapMachineCodeToGuestAddress( uintptr_t host_address) const { void* code = machine_code(); if (!code) { - assert_always("MapMachineCodeToGuestAddress called with no compiled code"); return address(); } auto entry = LookupMachineCodeOffset( From f41ea1296b4cb51f314a048db0b821829a631dc1 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:35:07 +0900 Subject: [PATCH 25/31] [x64/Debug] Fix TrapDebugPrint to properly terminate string --- src/xenia/cpu/backend/x64/x64_emitter.cc | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/xenia/cpu/backend/x64/x64_emitter.cc b/src/xenia/cpu/backend/x64/x64_emitter.cc index 51e9847fd..9b400c58f 100644 --- a/src/xenia/cpu/backend/x64/x64_emitter.cc +++ b/src/xenia/cpu/backend/x64/x64_emitter.cc @@ -437,15 +437,22 @@ uint64_t TrapDebugPrint(void* raw_context, uint64_t address) { auto thread_state = reinterpret_cast(raw_context)->thread_state; uint32_t str_ptr = uint32_t(thread_state->context()->r[3]); - // uint16_t str_len = uint16_t(thread_state->context()->r[4]); + uint32_t str_length = uint32_t(thread_state->context()->r[4]); + auto str = thread_state->memory()->TranslateVirtual(str_ptr); - // TODO(benvanik): truncate to length? - XELOGD("(DebugPrint) {}", str); + + // Allocate temporary buffer and null-terminate to respect length parameter + char* string_tmp = new char[str_length + 1]; + std::memcpy(string_tmp, str, str_length); + string_tmp[str_length] = 0; + + XELOGD("(DebugPrint) {}", string_tmp); if (cvars::debugprint_trap_log) { - debugging::DebugPrint("(DebugPrint) {}", str); + debugging::DebugPrint("(DebugPrint) {}", string_tmp); } + delete[] string_tmp; return 0; } From 341a735142c67cd088cef7a0021c8fc24ab36284 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:49:56 +0900 Subject: [PATCH 26/31] [x64] Fix vector mask issue and add missing tests --- src/xenia/cpu/backend/x64/x64_seq_vector.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/xenia/cpu/backend/x64/x64_seq_vector.cc b/src/xenia/cpu/backend/x64/x64_seq_vector.cc index 91a659cae..a1436f175 100644 --- a/src/xenia/cpu/backend/x64/x64_seq_vector.cc +++ b/src/xenia/cpu/backend/x64/x64_seq_vector.cc @@ -934,6 +934,7 @@ struct VECTOR_SHL_V128 e.vpmovzxbd(e.ymm2, i.src2); e.vpmovzxbd(e.ymm3, e.xmm3); + // Mask shift counts to 3 bits (0-7) for byte shifts e.vpbroadcastd(e.ymm4, e.GetXmmConstPtr(XMMXOPByteShiftMask)); e.vpand(e.ymm2, e.ymm2, e.ymm4); e.vpand(e.ymm3, e.ymm3, e.ymm4); @@ -1032,7 +1033,8 @@ struct VECTOR_SHL_V128 e.L(looper); e.movzx(e.ecx, e.byte[e.rsp + stack_offset_src2 + e.rdx]); - e.and_(e.ecx, 7); + e.and_(e.cl, 7); // Mask shift count to 3 bits (0-7) for byte shifts + e.shl(e.byte[e.rsp + stack_offset_src1 + e.rdx], e.cl); if (e.IsFeatureEnabled(kX64FlagsIndependentVars)) { From 5ac07f686d3143798dd91f980d1d30b14817fe2e Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 29 Sep 2025 16:19:26 +0900 Subject: [PATCH 27/31] [Xboxkrnl] Remove devkit memory access assert and replace with warning --- src/xenia/kernel/xboxkrnl/xboxkrnl_memory.cc | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_memory.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_memory.cc index 9ac3aea94..aac21eba2 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_memory.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_memory.cc @@ -76,7 +76,14 @@ dword_result_t NtAllocateVirtualMemory_entry(lpdword_t base_addr_ptr, assert_not_null(region_size_ptr); // Set to TRUE when allocation is from devkit memory area. - assert_true(debug_memory == 0); + // We don't support separate devkit memory, so just ignore this flag. + if (debug_memory) { + XELOGW( + "Game is attempting to allocate devkit debug memory (base: {:08X}, " + "size: {:08X}). Ignoring debug flag and using normal allocation.", + base_addr_ptr ? uint32_t(*base_addr_ptr) : 0, + region_size_ptr ? uint32_t(*region_size_ptr) : 0); + } // This allocates memory from the kernel heap, which is initialized on startup // and shared by both the kernel implementation and user code. From a00fd0eb12d0e4f8e1997c3eca06db1f91718229 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:11:36 +0900 Subject: [PATCH 28/31] [Kernel] Remove some log spam --- src/xenia/kernel/util/object_table.cc | 4 ++-- src/xenia/kernel/xboxkrnl/xboxkrnl_error.cc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xenia/kernel/util/object_table.cc b/src/xenia/kernel/util/object_table.cc index e9e8ad9b4..fa06cb4c1 100644 --- a/src/xenia/kernel/util/object_table.cc +++ b/src/xenia/kernel/util/object_table.cc @@ -141,7 +141,7 @@ X_STATUS ObjectTable::AddHandle(XObject* object, X_HANDLE* out_handle) { // Retain so long as the object is in the table. object->Retain(); - XELOGI("Added handle:{:08X} for {}", handle, typeid(*object).name()); + XELOGD("Added handle:{:08X} for {}", handle, typeid(*object).name()); } } @@ -228,7 +228,7 @@ X_STATUS ObjectTable::RemoveHandle(X_HANDLE handle) { object->handles().erase(handle_entry); } - XELOGI("Removed handle:{:08X} for {}", handle, typeid(*object).name()); + XELOGD("Removed handle:{:08X} for {}", handle, typeid(*object).name()); // Remove object name from mapping to prevent naming collision. if (!object->name().empty()) { diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_error.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_error.cc index 3a7e0e0a8..c2e408f42 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_error.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_error.cc @@ -991,7 +991,7 @@ uint32_t xeRtlNtStatusToDosError(uint32_t source_status) { if (!result) { break; } - XELOGI("xeRtlNtStatusToDosError {:X} => {:X}", status, result); + XELOGD("xeRtlNtStatusToDosError {:X} => {:X}", status, result); return result; } ++error_table; From 6203d382e2933a0b286a459f9f5599d2e7f877fb Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:34:37 +0900 Subject: [PATCH 29/31] [Kernel] Clear object handles before release in ObjectTable::Reset During bulk teardown, objects still had stale handle entries when Release() triggered their destructors, hitting the handles_.empty() assertion. --- src/xenia/kernel/util/object_table.cc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/xenia/kernel/util/object_table.cc b/src/xenia/kernel/util/object_table.cc index fa06cb4c1..4e6dc1b65 100644 --- a/src/xenia/kernel/util/object_table.cc +++ b/src/xenia/kernel/util/object_table.cc @@ -25,16 +25,21 @@ ObjectTable::~ObjectTable() { Reset(); } void ObjectTable::Reset() { auto global_lock = global_critical_region_.Acquire(); - // Release all objects. + // Release all objects, clearing their handles first so destructors + // don't assert on non-empty handles_. for (uint32_t n = 0; n < table_capacity_; n++) { ObjectTableEntry& entry = table_[n]; if (entry.object) { + entry.object->handles().clear(); + entry.handle_ref_count = 0; entry.object->Release(); } } for (uint32_t n = 0; n < host_table_capacity_; n++) { ObjectTableEntry& entry = host_table_[n]; if (entry.object) { + entry.object->handles().clear(); + entry.handle_ref_count = 0; entry.object->Release(); } } From 9371e73d928230a0e76f2ad7f4552a166b5f3d3a Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:39:43 +0900 Subject: [PATCH 30/31] [APU] Fix audio crashes during shutdown --- src/xenia/apu/audio_system.cc | 19 +++++++++++++++++++ src/xenia/apu/xaudio2/xaudio2_audio_driver.cc | 8 ++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/xenia/apu/audio_system.cc b/src/xenia/apu/audio_system.cc index f3cc77a52..e5c4fd3c9 100644 --- a/src/xenia/apu/audio_system.cc +++ b/src/xenia/apu/audio_system.cc @@ -178,6 +178,25 @@ void AudioSystem::Shutdown() { worker_thread_->Wait(0, 0, 0, nullptr); worker_thread_.reset(); } + + // Unregister all active clients to shut down their audio drivers before + // the semaphores are destroyed with this AudioSystem. + { + auto global_lock = global_critical_region_.Acquire(); + for (size_t i = 0; i < kMaximumClientCount; ++i) { + if (clients_[i].in_use) { + DestroyDriver(clients_[i].driver); + if (clients_[i].wrapped_callback_arg) { + memory()->SystemHeapFree(clients_[i].wrapped_callback_arg); + } + clients_[i].driver = nullptr; + clients_[i].callback = 0; + clients_[i].callback_arg = 0; + clients_[i].wrapped_callback_arg = 0; + clients_[i].in_use = false; + } + } + } } X_STATUS AudioSystem::RegisterClient(uint32_t callback, uint32_t callback_arg, diff --git a/src/xenia/apu/xaudio2/xaudio2_audio_driver.cc b/src/xenia/apu/xaudio2/xaudio2_audio_driver.cc index 7b254808b..f9c199224 100644 --- a/src/xenia/apu/xaudio2/xaudio2_audio_driver.cc +++ b/src/xenia/apu/xaudio2/xaudio2_audio_driver.cc @@ -308,8 +308,13 @@ void XAudio2AudioDriver::Shutdown() { template void XAudio2AudioDriver::ShutdownObjects(Objects& objects) { + // Stop the engine first to ensure no callbacks are in-flight before + // destroying voices and the callback object. + if (objects.audio) { + objects.audio->StopEngine(); + } + if (objects.pcm_voice) { - objects.pcm_voice->Stop(); objects.pcm_voice->DestroyVoice(); objects.pcm_voice = nullptr; } @@ -320,7 +325,6 @@ void XAudio2AudioDriver::ShutdownObjects(Objects& objects) { } if (objects.audio) { - objects.audio->StopEngine(); objects.audio->Release(); objects.audio = nullptr; } From b0a387c6ea6f1f60eed4aa9993c91c539230f26f Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:51:38 +0900 Subject: [PATCH 31/31] [XboxKrnl] Don't crash the host on guest trap --- src/xenia/kernel/xboxkrnl/xboxkrnl_debug.cc | 43 ++++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_debug.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_debug.cc index 309dee985..fdfa33cf4 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_debug.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_debug.cc @@ -9,10 +9,15 @@ #include "xenia/base/debugging.h" #include "xenia/base/logging.h" +#include "xenia/emulator.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_private.h" #include "xenia/kernel/xthread.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" namespace xe { namespace kernel { @@ -147,13 +152,39 @@ DECLARE_XBOXKRNL_EXPORT2(RtlRaiseException, kDebug, kStub, kImportant); void KeBugCheckEx_entry(dword_t code, dword_t param1, dword_t param2, dword_t param3, dword_t param4) { - XELOGD("*** STOP: 0x{:08X} (0x{:08X}, 0x{:08X}, 0x{:08X}, 0x{:08X})", - static_cast(code), static_cast(param1), - static_cast(param2), static_cast(param3), - static_cast(param4)); + auto msg = + fmt::format("*** STOP: 0x{:08X} (0x{:08X}, 0x{:08X}, 0x{:08X}, 0x{:08X})", + static_cast(code), static_cast(param1), + static_cast(param2), static_cast(param3), + static_cast(param4)); + XELOGE("{}", msg); fflush(stdout); - xe::debugging::Break(); - assert_always(); + + if (xe::debugging::IsDebuggerAttached()) { + xe::debugging::Break(); + } + + // Show crash dialog and suspend the guest thread instead of killing the + // host process. + auto current_thread = kernel::XThread::GetCurrentThread(); + const auto* emulator = kernel_state()->emulator(); + auto* display_window = emulator->display_window(); + auto* imgui_drawer = emulator->imgui_drawer(); + if (display_window && imgui_drawer) { + auto dlg_msg = fmt::format( + "The guest kernel has crashed (KeBugCheck).\n\n{}\n\n" + "The faulting thread has been suspended.", + msg); + display_window->app_context().CallInUIThreadSynchronous( + [imgui_drawer, &dlg_msg]() { + xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer, + "Guest Kernel Crash", dlg_msg); + }); + } + + if (current_thread) { + current_thread->Suspend(nullptr); + } } DECLARE_XBOXKRNL_EXPORT2(KeBugCheckEx, kDebug, kStub, kImportant);