From bad346717a64972f54025af26806d105c3c73369 Mon Sep 17 00:00:00 2001 From: Margen67 Date: Tue, 14 Jul 2026 02:09:15 -0700 Subject: [PATCH 01/14] [xb] Skip aarch64 submodule for x64 target --- .github/workflows/Linux_x86.yml | 2 +- .github/workflows/Windows_x86.yml | 4 +++- xenia-build.py | 10 ++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/Linux_x86.yml b/.github/workflows/Linux_x86.yml index 352565a2c..f781d7d52 100644 --- a/.github/workflows/Linux_x86.yml +++ b/.github/workflows/Linux_x86.yml @@ -85,7 +85,7 @@ jobs: - name: Download submodules run: |- # Exclude unneeded submodules - SUBMODULES=$(grep -oP '(?<=path = )(?!third_party\/DirectX(?:-Headers|ShaderCompiler)).+' .gitmodules) + SUBMODULES=$(grep -oP '(?<=path = )(?!third_party\/(?:DirectX(?:-Headers|ShaderCompiler)|xbyak_aarch64)).+' .gitmodules) git submodule update --init --depth=1 -j$(getconf _NPROCESSORS_ONLN) $SUBMODULES - name: Build Xenia diff --git a/.github/workflows/Windows_x86.yml b/.github/workflows/Windows_x86.yml index 681a3c2d5..dff4513c3 100644 --- a/.github/workflows/Windows_x86.yml +++ b/.github/workflows/Windows_x86.yml @@ -59,7 +59,9 @@ jobs: } - name: Download submodules - run: git submodule update --init --depth=1 -j $env:NUMBER_OF_PROCESSORS + run: |- # Exclude unneeded submodules + $SUBMODULES=(Select-String -CaseSensitive '(?<=path = )(?!third_party\/xbyak_aarch64).+' .gitmodules).Matches.Value + git submodule update --init --depth=1 -j $env:NUMBER_OF_PROCESSORS $SUBMODULES - name: Build Xenia run: python xenia-build.py build --config=Release --target=xenia-app diff --git a/xenia-build.py b/xenia-build.py index b62646397..80003679f 100755 --- a/xenia-build.py +++ b/xenia-build.py @@ -599,19 +599,21 @@ def git_submodule_update(): if sys.platform == "linux": submodules_ignore = ["DirectX-Headers", "DirectXShaderCompiler"] else: - submodules_ignore = None + submodules_ignore = [] + if is_amd64(): + submodules_ignore.append("xbyak_aarch64") if submodules_ignore: with open(".gitmodules") as f: gitmodules = f.read() submodules = re_findall(r"(?<=path = )(?!third_party\/(?:" + "|".join(submodules_ignore) + r")).+", gitmodules) else: - submodules = None + submodules = [] # Sync submodule URLs from .gitmodules to local config shell_call([ "git", "submodule", "sync", - *(submodules or []), + *submodules, ]) # Then update all submodules to their recorded commits shell_call([ @@ -623,7 +625,7 @@ def git_submodule_update(): "--init", "--depth=1", "-j", f"{os.cpu_count()}", - *(submodules or []), + *submodules, ]) From e20f26963fbebd57209b8356ec6da0f27343e333 Mon Sep 17 00:00:00 2001 From: SaveEditors <119719171+SaveEditors@users.noreply.github.com> Date: Sat, 11 Jul 2026 05:50:13 -0400 Subject: [PATCH 02/14] [GPU] Initialize GPU registers to hardware reset defaults RegisterFile zeroes the whole register file at construction, but a real console comes up with non-zero values in several of the context registers. If a game reads one of those before writing it, it gets 0 under emulation and the real default on hardware. I read the reset values off a retail console (read-only) and checked them against the AMD R6xx register reference and the r600g defaults in Mesa, which set the same registers to the same values at context init. Where the hardware read matches the driver default it's a genuine power-on default, not leftover runtime state. Left the tessellation levels out on purpose: they reset to 1.0f too, but the backends do register + 1.0f, so seeding them here would change the effective reset factor. That needs its own change. --- src/xenia/gpu/register_file.cc | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/xenia/gpu/register_file.cc b/src/xenia/gpu/register_file.cc index c10b433dc..c697852e4 100644 --- a/src/xenia/gpu/register_file.cc +++ b/src/xenia/gpu/register_file.cc @@ -15,7 +15,38 @@ namespace xe { namespace gpu { -RegisterFile::RegisterFile() { std::memset(values, 0, sizeof(values)); } +RegisterFile::RegisterFile() { + std::memset(values, 0, sizeof(values)); + + // Several context registers power up with non-zero values on real Xenos + // hardware, so a title that reads one of them before writing it would + // otherwise observe 0. These are the hardware reset defaults, corroborated by + // the AMD R6xx/R7xx 3D register reference and the Mesa r600g driver, which + // programs the same registers to the same values at context init. + values[XE_GPU_REG_VGT_MAX_VTX_INDX] = 0x0000FFFF; + values[XE_GPU_REG_VGT_MULTI_PRIM_IB_RESET_INDX] = 0x0000FFFF; + values[XE_GPU_REG_PA_SC_SCREEN_SCISSOR_BR] = 0x20002000; // 8192 x 8192 + values[XE_GPU_REG_RB_STENCILREFMASK_BF] = 0x00FFFF00; + values[XE_GPU_REG_PA_SU_POINT_SIZE] = 0x00080008; + values[XE_GPU_REG_PA_SU_POINT_MINMAX] = 0x04000010; + values[XE_GPU_REG_PA_SU_LINE_CNTL] = 0x00000008; + values[XE_GPU_REG_PA_SC_LINE_CNTL] = 0x00000400; + values[XE_GPU_REG_VGT_HOS_REUSE_DEPTH] = 0x0000000E; + values[XE_GPU_REG_VGT_VERTEX_REUSE_BLOCK_CNTL] = 0x0000000E; + values[XE_GPU_REG_VGT_OUT_DEALLOC_CNTL] = 0x00000010; + // Guard-band clip/discard adjust (2.0 clip, 1.0 discard); hardwired on + // hardware. + values[XE_GPU_REG_PA_CL_GB_VERT_CLIP_ADJ] = 0x40000000; // 2.0f + values[XE_GPU_REG_PA_CL_GB_VERT_DISC_ADJ] = 0x3F800000; // 1.0f + values[XE_GPU_REG_PA_CL_GB_HORZ_CLIP_ADJ] = 0x40000000; // 2.0f + values[XE_GPU_REG_PA_CL_GB_HORZ_DISC_ADJ] = 0x3F800000; // 1.0f + values[XE_GPU_REG_PA_SC_AA_MASK] = 0x0000FFFF; + // Tessellation levels also reset to 1.0f on hardware, but the backends apply + // the effective factor as (register + 1.0f), so initializing them here would + // change the effective reset factor; left out for separate review. + // values[XE_GPU_REG_VGT_HOS_MAX_TESS_LEVEL] = 0x3F800000; // 1.0f + // values[XE_GPU_REG_VGT_HOS_MIN_TESS_LEVEL] = 0x3F800000; // 1.0f +} constexpr unsigned int GetHighestRegisterNumber() { uint32_t highest = 0; #define XE_GPU_REGISTER(index, type, name) \ From 2799234fbb1635e9d37007979a383807fcf1b56c Mon Sep 17 00:00:00 2001 From: The-Little-Wolf <116989599+The-Little-Wolf@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:26:07 -0700 Subject: [PATCH 03/14] [Xam/XMP] - Stub XMPGetNumSongsInTitlePlaylist - Stub XMPGetNumSongsInTitlePlaylist --- src/xenia/kernel/xam/apps/xmp_app.cc | 22 ++++++++++++++++++++++ src/xenia/kernel/xam/apps/xmp_app.h | 7 +++++++ 2 files changed, 29 insertions(+) diff --git a/src/xenia/kernel/xam/apps/xmp_app.cc b/src/xenia/kernel/xam/apps/xmp_app.cc index 5534215fa..f3c78c2d1 100644 --- a/src/xenia/kernel/xam/apps/xmp_app.cc +++ b/src/xenia/kernel/xam/apps/xmp_app.cc @@ -563,6 +563,28 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, kernel_state_->BroadcastNotification(kXNotificationXmpDashInitChanged, 1); return X_E_SUCCESS; } + case 0x00070031: { + // XMPGetNumSongsInTitlePlaylist + // Song count exist at playlist_ptr->0x78, if playlist_ptr->0xc == 0 or 1 + // return song count, else return zero + assert_true(!buffer_length || + buffer_length == sizeof(XMP_GET_NUM_SONGS_IN_TITLE_PLAYLIST)); + XMP_GET_NUM_SONGS_IN_TITLE_PLAYLIST* args = + reinterpret_cast(buffer); + + XELOGD( + "XMPGetNumSongsInTitlePlaylist({:08X}, {:08X}, {:08X}), " + "unimplemented", + uint32_t(args->xmp_client.get()), args->playlist_ptr.get(), + args->song_count_ptr.get()); + + if (!args->playlist_ptr || !args->song_count_ptr) { + return X_E_INVALIDARG; + } + xe::store_and_swap( + memory_->TranslateVirtual(args->song_count_ptr), 0); + return X_E_SUCCESS; + } case 0x0007003D: { // XMPCaptureOutput assert_true(!buffer_length || diff --git a/src/xenia/kernel/xam/apps/xmp_app.h b/src/xenia/kernel/xam/apps/xmp_app.h index a2b2dab58..5fbb136a4 100644 --- a/src/xenia/kernel/xam/apps/xmp_app.h +++ b/src/xenia/kernel/xam/apps/xmp_app.h @@ -194,6 +194,13 @@ struct XMP_DASH_INIT { }; static_assert_size(XMP_DASH_INIT, 0x18); +struct XMP_GET_NUM_SONGS_IN_TITLE_PLAYLIST { + xe::be xmp_client; + xe::be playlist_ptr; + xe::be song_count_ptr; +}; +static_assert_size(XMP_GET_NUM_SONGS_IN_TITLE_PLAYLIST, 0xC); + struct XMP_CAPTURE_OUTPUT { xe::be xmp_client; xe::be callback; From deaf21e9e1143a372a246477735864ba800cca5a Mon Sep 17 00:00:00 2001 From: Gliniak <153369+Gliniak@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:22:31 +0200 Subject: [PATCH 04/14] [CI] Fixed CI failure caused by incorrect variable check --- .github/workflows/Linux_x86.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Linux_x86.yml b/.github/workflows/Linux_x86.yml index f781d7d52..a1cea2ea6 100644 --- a/.github/workflows/Linux_x86.yml +++ b/.github/workflows/Linux_x86.yml @@ -59,7 +59,7 @@ jobs: sudo update-alternatives --install /usr/bin/llvm-nm llvm-nm /usr/bin/llvm-nm-${{ inputs.llvm_version }} 200 # Download linuxdeploy tools if not cached - if [ ${{ steps.cache-linuxdeploy.outputs.cache-hit }} != true ]; then + if [ "${{ steps.cache-linuxdeploy.outputs.cache-hit }}" != "true" ]; then mkdir -p ~/linuxdeploy wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -O ~/linuxdeploy/linuxdeploy chmod +x ~/linuxdeploy/linuxdeploy @@ -68,7 +68,7 @@ jobs: - name: Install Vulkan SDK run: |- - if [ ${{ steps.cache-vulkan-sdk-linux.outputs.cache-hit }} != true ]; then + if [ "${{ steps.cache-vulkan-sdk-linux.outputs.cache-hit }}" != "true" ]; then wget -qO vulkan-sdk.tar.xz https://sdk.lunarg.com/sdk/download/latest/linux/vulkan-sdk.tar.xz mkdir -p ~/vulkan-sdk tar -xf vulkan-sdk.tar.xz -C ~/vulkan-sdk From 51322122a58f6953a7f08842d556e78370bf6f44 Mon Sep 17 00:00:00 2001 From: The-Little-Wolf <116989599+The-Little-Wolf@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:15:34 -0700 Subject: [PATCH 05/14] [Xam/Info] - Implement XamFeatureEnabled - Implement XamFeatureEnabled --- src/xenia/kernel/xam/xam_info.cc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/xenia/kernel/xam/xam_info.cc b/src/xenia/kernel/xam/xam_info.cc index 5f67cb306..48be467fe 100644 --- a/src/xenia/kernel/xam/xam_info.cc +++ b/src/xenia/kernel/xam/xam_info.cc @@ -57,8 +57,14 @@ namespace xam { // https://github.com/tpn/winsdk-10/blob/master/Include/10.0.14393.0/km/wdm.h#L15539 typedef enum _MODE { KernelMode, UserMode, MaximumMode } MODE; -dword_result_t XamFeatureEnabled_entry(dword_t app_id) { return 0; } -DECLARE_XAM_EXPORT1(XamFeatureEnabled, kNone, kStub); +dword_result_t XamFeatureEnabled_entry(qword_t feature_bit) { + const std::bitset<36> feature(0x40ffffffff); + if (feature.test(feature_bit)) { + return 1; + } + return 0; +} +DECLARE_XAM_EXPORT1(XamFeatureEnabled, kNone, kImplemented); dword_result_t XamGetStagingMode_entry() { return cvars::staging_mode; } DECLARE_XAM_EXPORT1(XamGetStagingMode, kNone, kStub); From 7ff152a5a7f69390c1a625d3fdc57681d5ccb7e9 Mon Sep 17 00:00:00 2001 From: Gliniak <153369+Gliniak@users.noreply.github.com> Date: Sun, 5 Jul 2026 17:53:43 +0200 Subject: [PATCH 06/14] [X64] MAX_V128/MIN_V128 Proper NaN handling. According to console. FFC00000->FFC00000 instead of FFFFFC00 as it was previously. Test case: 415607E8 - "Evil Queen's Castle" level --- src/xenia/cpu/backend/x64/x64_sequences.cc | 26 ++++++++++++++----- src/xenia/cpu/ppc/testing/instr__gen_vmaxfp.s | 17 ++++++++++++ src/xenia/cpu/ppc/testing/instr__gen_vminfp.s | 17 ++++++++++++ 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/xenia/cpu/backend/x64/x64_sequences.cc b/src/xenia/cpu/backend/x64/x64_sequences.cc index c216a8f25..33b86a213 100644 --- a/src/xenia/cpu/backend/x64/x64_sequences.cc +++ b/src/xenia/cpu/backend/x64/x64_sequences.cc @@ -601,11 +601,18 @@ struct MAX_V128 : Sequence> { static void Emit(X64Emitter& e, const EmitArgType& i) { e.ChangeMxcsrMode(MXCSRMode::Vmx); // if 0 and -0, return 0! opposite of minfp - auto src1 = GetInputRegOrConstant(e, i.src1, e.xmm0); - auto src2 = GetInputRegOrConstant(e, i.src2, e.xmm1); + const Xmm src1 = GetInputRegOrConstant(e, i.src1, e.xmm0); + const Xmm src2 = GetInputRegOrConstant(e, i.src2, e.xmm1); + e.vmaxps(e.xmm2, src1, src2); e.vmaxps(e.xmm3, src2, src1); - e.vorps(i.dest, e.xmm2, e.xmm3); + e.vandps(e.xmm2, e.xmm2, e.xmm3); + + e.vcmpunordps(e.xmm3, src1, src1); // mask: vA is NaN + e.vblendvps(e.xmm3, src2, src1, e.xmm3); + + e.vcmpunordps(i.dest, src1, src2); // mask: vA or vB is NaN + e.vblendvps(i.dest, e.xmm2, e.xmm3, i.dest); } }; EMITTER_OPCODE_TABLE(OPCODE_MAX, MAX_F32, MAX_F64, MAX_V128); @@ -660,11 +667,18 @@ struct MIN_F64 : Sequence> { struct MIN_V128 : Sequence> { static void Emit(X64Emitter& e, const EmitArgType& i) { e.ChangeMxcsrMode(MXCSRMode::Vmx); - auto src1 = GetInputRegOrConstant(e, i.src1, e.xmm0); - auto src2 = GetInputRegOrConstant(e, i.src2, e.xmm1); + const Xmm src1 = GetInputRegOrConstant(e, i.src1, e.xmm0); + const Xmm src2 = GetInputRegOrConstant(e, i.src2, e.xmm1); + e.vminps(e.xmm2, src1, src2); e.vminps(e.xmm3, src2, src1); - e.vorps(i.dest, e.xmm2, e.xmm3); + e.vorps(e.xmm2, e.xmm2, e.xmm3); + + e.vcmpunordps(e.xmm3, src1, src1); // mask: vA is NaN + e.vblendvps(e.xmm3, src2, src1, e.xmm3); + + e.vcmpunordps(i.dest, src1, src2); // mask: vA or vB is NaN + e.vblendvps(i.dest, e.xmm2, e.xmm3, i.dest); } }; EMITTER_OPCODE_TABLE(OPCODE_MIN, MIN_I8, MIN_I16, MIN_I32, MIN_I64, MIN_F32, diff --git a/src/xenia/cpu/ppc/testing/instr__gen_vmaxfp.s b/src/xenia/cpu/ppc/testing/instr__gen_vmaxfp.s index 3e2be6cd8..d731cfe89 100644 --- a/src/xenia/cpu/ppc/testing/instr__gen_vmaxfp.s +++ b/src/xenia/cpu/ppc/testing/instr__gen_vmaxfp.s @@ -5848,3 +5848,20 @@ test_vmaxfp_650_GEN: #_ REGISTER_OUT v2 [FFFFFF80, 0000007F, FFFEFF7F, 00010080] #_ REGISTER_OUT v3 [FFFFFFFF, FFFFFFFF, FFFFFFFF, FFFFFFFF] +test_vmaxfp_651_GEN: + #_ REGISTER_IN v1 [FFC00000, FFC00000, FFC00000, 00000000] + #_ REGISTER_IN v2 [00000000, 00000000, 00000000, 00000000] + vmaxfp v3, v1, v2 + blr + #_ REGISTER_OUT v1 [FFC00000, FFC00000, FFC00000, 00000000] + #_ REGISTER_OUT v2 [00000000, 00000000, 00000000, 00000000] + #_ REGISTER_OUT v3 [FFC00000, FFC00000, FFC00000, 00000000] + +test_vmaxfp_652_GEN: + #_ REGISTER_IN v1 [7FC00000, 7FC00000, 7FC00000, 00000000] + #_ REGISTER_IN v2 [00000000, 00000000, 00000000, 00000000] + vmaxfp v3, v1, v2 + blr + #_ REGISTER_OUT v1 [7FC00000, 7FC00000, 7FC00000, 00000000] + #_ REGISTER_OUT v2 [00000000, 00000000, 00000000, 00000000] + #_ REGISTER_OUT v3 [7FC00000, 7FC00000, 7FC00000, 00000000] diff --git a/src/xenia/cpu/ppc/testing/instr__gen_vminfp.s b/src/xenia/cpu/ppc/testing/instr__gen_vminfp.s index 0199d59ee..c5ebd0be7 100644 --- a/src/xenia/cpu/ppc/testing/instr__gen_vminfp.s +++ b/src/xenia/cpu/ppc/testing/instr__gen_vminfp.s @@ -5848,3 +5848,20 @@ test_vminfp_650_GEN: #_ REGISTER_OUT v2 [FFFFFF80, 0000007F, FFFEFF7F, 00010080] #_ REGISTER_OUT v3 [FFFFFFFF, FFFFFFFF, FFFFFFFF, FFFFFFFF] +test_vminfp_651_GEN: + #_ REGISTER_IN v1 [FFC00000, FFC00000, FFC00000, 00000000] + #_ REGISTER_IN v2 [477FFC00, 477FFC00, 477FFC00, 477FFC00] + vminfp v3, v1, v2 + blr + #_ REGISTER_OUT v1 [FFC00000, FFC00000, FFC00000, 00000000] + #_ REGISTER_OUT v2 [477FFC00, 477FFC00, 477FFC00, 477FFC00] + #_ REGISTER_OUT v3 [FFC00000, FFC00000, FFC00000, 00000000] + +test_vminfp_652_GEN: + #_ REGISTER_IN v1 [7FC00000, 7FC00000, 7FC00000, 00000000] + #_ REGISTER_IN v2 [477FFC00, 477FFC00, 477FFC00, 477FFC00] + vminfp v3, v1, v2 + blr + #_ REGISTER_OUT v1 [7FC00000, 7FC00000, 7FC00000, 00000000] + #_ REGISTER_OUT v2 [477FFC00, 477FFC00, 477FFC00, 477FFC00] + #_ REGISTER_OUT v3 [7FC00000, 7FC00000, 7FC00000, 00000000] From c2650c6e588e6c582adb6eba9dd7251905591c37 Mon Sep 17 00:00:00 2001 From: Gliniak <153369+Gliniak@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:10:37 +0200 Subject: [PATCH 07/14] [Testing] Define full test paths, unify test results of vectors to unsigned type --- src/xenia/cpu/ppc/ppc_context.cc | 4 ++-- src/xenia/cpu/ppc/testing/CMakeLists.txt | 3 +++ src/xenia/cpu/ppc/testing/ppc_testing_main.cc | 11 +++++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/xenia/cpu/ppc/ppc_context.cc b/src/xenia/cpu/ppc/ppc_context.cc index 5bee252f3..3a971d6e0 100644 --- a/src/xenia/cpu/ppc/ppc_context.cc +++ b/src/xenia/cpu/ppc/ppc_context.cc @@ -174,8 +174,8 @@ bool PPCContext::CompareRegWithString(const char* name, const char* value, vec128_t expected = string_util::from_string(value); if (this->v[n] != expected) { result = - fmt::format("[{:08X}, {:08X}, {:08X}, {:08X}]", this->v[n].i32[0], - this->v[n].i32[1], this->v[n].i32[2], this->v[n].i32[3]); + fmt::format("[{:08X}, {:08X}, {:08X}, {:08X}]", this->v[n].u32[0], + this->v[n].u32[1], this->v[n].u32[2], this->v[n].u32[3]); return false; } return true; diff --git a/src/xenia/cpu/ppc/testing/CMakeLists.txt b/src/xenia/cpu/ppc/testing/CMakeLists.txt index 970921eef..28d7a622e 100644 --- a/src/xenia/cpu/ppc/testing/CMakeLists.txt +++ b/src/xenia/cpu/ppc/testing/CMakeLists.txt @@ -6,6 +6,9 @@ add_executable(xenia-cpu-ppc-tests ${CMAKE_CURRENT_SOURCE_DIR}/ppc_testing_main.cc ) +target_compile_definitions(xenia-cpu-ppc-tests PRIVATE + XE_SOURCE_ROOT="${PROJECT_SOURCE_DIR}") + # Add platform-specific console app main if(WIN32) target_sources(xenia-cpu-ppc-tests PRIVATE diff --git a/src/xenia/cpu/ppc/testing/ppc_testing_main.cc b/src/xenia/cpu/ppc/testing/ppc_testing_main.cc index bc589b019..c60bc872e 100644 --- a/src/xenia/cpu/ppc/testing/ppc_testing_main.cc +++ b/src/xenia/cpu/ppc/testing/ppc_testing_main.cc @@ -108,8 +108,10 @@ class TestSuite { name = name.replace_extension(); name_ = xe::path_to_utf8(name); - map_file_path_ = cvars::test_bin_path / name.replace_extension(".map"); - bin_file_path_ = cvars::test_bin_path / name.replace_extension(".bin"); + map_file_path_ = std::filesystem::path(XE_SOURCE_ROOT) / + cvars::test_bin_path / name.replace_extension(".map"); + bin_file_path_ = std::filesystem::path(XE_SOURCE_ROOT) / + cvars::test_bin_path / name.replace_extension(".bin"); } bool Load() { @@ -470,7 +472,8 @@ class TestRunner { bool DiscoverTests(const std::filesystem::path& test_path, std::vector& test_files) { - auto file_infos = xe::filesystem::ListFiles(test_path); + auto file_infos = xe::filesystem::ListFiles( + std::filesystem::path(XE_SOURCE_ROOT) / test_path); for (auto& file_info : file_infos) { if (file_info.name.extension() == ".s") { // Only include test files (instr_*.s), not helper files @@ -669,7 +672,7 @@ bool RunTests(const std::vector& test_names) { std::vector test_suites; bool load_failed = false; for (auto& test_path : test_files) { - TestSuite test_suite(test_path); + TestSuite test_suite(std::filesystem::path(XE_SOURCE_ROOT) / test_path); if (!test_name_filter.empty() && test_name_filter.find(test_suite.name()) == test_name_filter.end()) { continue; From 44e1a64f3510622c1ed4650b8563990d809d11b5 Mon Sep 17 00:00:00 2001 From: Gliniak <153369+Gliniak@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:05:41 +0200 Subject: [PATCH 08/14] [XAM] Extend UI delay on close. Added delay to headless after notification sent. Created thread to do async notification broadcast on close for headless. This fixes game freeze in 534407FC --- src/xenia/kernel/xam/xam_ui.cc | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/xenia/kernel/xam/xam_ui.cc b/src/xenia/kernel/xam/xam_ui.cc index 7f74f025a..510627d5c 100644 --- a/src/xenia/kernel/xam/xam_ui.cc +++ b/src/xenia/kernel/xam/xam_ui.cc @@ -34,6 +34,8 @@ DEFINE_bool(storage_selection_dialog, false, DECLARE_int32(license_mask); +constexpr std::chrono::milliseconds kUIDelayMillis(200); + namespace xe { namespace kernel { namespace xam { @@ -81,8 +83,11 @@ X_RESULT xeXamDispatchDialog(T* dialog, return result; }; auto post = []() { - xe::threading::Sleep(std::chrono::milliseconds(100)); - kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); + std::jthread t([] { + xe::threading::Sleep(kUIDelayMillis); + kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); + }); + t.detach(); }; if (!overlapped) { pre(); @@ -123,7 +128,7 @@ X_RESULT xeXamDispatchDialogEx( return result; }; auto post = []() { - xe::threading::Sleep(std::chrono::milliseconds(100)); + xe::threading::Sleep(kUIDelayMillis); kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); }; if (!overlapped) { @@ -143,10 +148,15 @@ X_RESULT xeXamDispatchHeadless(std::function run_callback, uint32_t overlapped) { auto pre = []() { kernel_state()->BroadcastNotification(kXNotificationSystemUI, true); + xe::threading::Sleep(std::chrono::milliseconds(25)); }; auto post = []() { - xe::threading::Sleep(std::chrono::milliseconds(100)); - kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); + std::jthread t([]() { + xe::threading::Sleep(kUIDelayMillis); + kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); + }); + + t.detach(); }; if (!overlapped) { pre(); @@ -167,7 +177,7 @@ X_RESULT xeXamDispatchHeadlessEx( kernel_state()->BroadcastNotification(kXNotificationSystemUI, true); }; auto post = []() { - xe::threading::Sleep(std::chrono::milliseconds(100)); + xe::threading::Sleep(kUIDelayMillis); kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); }; if (!overlapped) { @@ -198,7 +208,7 @@ X_RESULT xeXamDispatchDialogAsync(T* dialog, kernel_state()->xam_state()->xam_dialogs_shown_--; auto run = []() -> void { - xe::threading::Sleep(std::chrono::milliseconds(100)); + xe::threading::Sleep(kUIDelayMillis); kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); }; @@ -220,7 +230,7 @@ X_RESULT xeXamDispatchHeadlessAsync(std::function run_callback) { kernel_state()->xam_state()->xam_dialogs_shown_--; auto run = []() -> void { - xe::threading::Sleep(std::chrono::milliseconds(100)); + xe::threading::Sleep(kUIDelayMillis); kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); }; From c6298dd0e3b2a2b5868067aca2159db55c49a49d Mon Sep 17 00:00:00 2001 From: NicknineTheEagle Date: Fri, 29 May 2026 05:12:13 +0300 Subject: [PATCH 09/14] [XAM] Implemented game exit behavior in XamLoaderTerminateTitle --- src/xenia/kernel/xam/xam_info.cc | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/xenia/kernel/xam/xam_info.cc b/src/xenia/kernel/xam/xam_info.cc index 48be467fe..24491fddc 100644 --- a/src/xenia/kernel/xam/xam_info.cc +++ b/src/xenia/kernel/xam/xam_info.cc @@ -342,6 +342,32 @@ void XamLoaderLaunchTitle_entry(lpstring_t raw_name_ptr, dword_t flags) { DECLARE_XAM_EXPORT1(XamLoaderLaunchTitle, kNone, kSketchy); void XamLoaderTerminateTitle_entry() { + std::string title = "Title terminated"; + std::string message = "Game requested exit to dashboard."; + assert_always("Game requested exit to dashboard via XamLoaderTerminateTitle"); + + auto display_window = kernel_state()->emulator()->display_window(); + auto imgui_drawer = kernel_state()->emulator()->imgui_drawer(); + + if (display_window && imgui_drawer) { + display_window->app_context().CallInUIThreadSynchronous( + [imgui_drawer, title, message]() { + auto dialog = xe::ui::ImGuiDialog::ShowMessageBox( + imgui_drawer, title.c_str(), message.c_str()); + + std::jthread([dialog]() { + while (!dialog->IsClosing()) { + std::this_thread::yield(); + } + + config::SaveConfig(); + xe::FlushLog(); + + std::quick_exit(0); + }).detach(); + }); + } + // This function does not return. kernel_state()->TerminateTitle(); } From 16e1eb8e28a2935b75c36707b585a4f5e174ad43 Mon Sep 17 00:00:00 2001 From: goldislead <69987043+goldislead@users.noreply.github.com> Date: Fri, 17 Jul 2026 03:10:29 +0000 Subject: [PATCH 10/14] [GPU] Clarify 64bpp clear register order This resolves the TODO in GetColorClearShaderConstants about which 32 bit portion is in which register. For 64bpp formats, RB_COLOR_CLEAR_LO holds the lower 32 bits of the packed clear value, and RB_COLOR_CLEAR holds the upper 32 bits. --- src/xenia/gpu/d3d12/d3d12_render_target_cache.cc | 10 ++++++++-- src/xenia/gpu/draw_util.h | 16 +++++++++++++--- .../gpu/vulkan/vulkan_render_target_cache.cc | 10 ++++++++-- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/xenia/gpu/d3d12/d3d12_render_target_cache.cc b/src/xenia/gpu/d3d12/d3d12_render_target_cache.cc index 3cc4c4bbe..6b96f4cdb 100644 --- a/src/xenia/gpu/d3d12/d3d12_render_target_cache.cc +++ b/src/xenia/gpu/d3d12/d3d12_render_target_cache.cc @@ -1441,8 +1441,14 @@ bool D3D12RenderTargetCache::Resolve(const Memory& memory, clear_transfers_[1])) { uint64_t clear_values[2]; clear_values[0] = resolve_info.rb_depth_clear; - clear_values[1] = resolve_info.rb_color_clear | - (uint64_t(resolve_info.rb_color_clear_lo) << 32); + // For 64bpp formats, RB_COLOR_CLEAR_LO is the lower 32 bits of the + // packed clear value. RB_COLOR_CLEAR is the upper 32 bits and, for + // 32bpp formats, the whole value. + clear_values[1] = + resolve_info.color_edram_info.format_is_64bpp + ? resolve_info.rb_color_clear_lo | + (uint64_t(resolve_info.rb_color_clear) << 32) + : resolve_info.rb_color_clear; PerformTransfersAndResolveClears(2, clear_render_targets, clear_transfers_, clear_values, &clear_rectangle); diff --git a/src/xenia/gpu/draw_util.h b/src/xenia/gpu/draw_util.h index cf2301f6a..8c68e0da2 100644 --- a/src/xenia/gpu/draw_util.h +++ b/src/xenia/gpu/draw_util.h @@ -713,9 +713,19 @@ struct ResolveInfo { // Not doing -32...32 to -1...1 clamping here as a hack for k_16_16 and // k_16_16_16_16 blending emulation when using host render targets as it // would be inconsistent with the usual way of clearing with a depth quad. - // TODO(Triang3l): Check which 32-bit portion is in which register. - constants_out.rt_specific.clear_value[0] = rb_color_clear; - constants_out.rt_specific.clear_value[1] = rb_color_clear_lo; + if (color_edram_info.format_is_64bpp) { + // RB_COLOR_CLEAR_LO holds the lower 32 bits. + // Red | green << 16 for 16_16_16_16, red for 32_32_FLOAT. + // RB_COLOR_CLEAR holds the upper 32 bits. + // D3D builds the low dword as R | G << 16 and the high as B | A << 16, + // and writes to the _LO and base register respectively. + constants_out.rt_specific.clear_value[0] = rb_color_clear_lo; + constants_out.rt_specific.clear_value[1] = rb_color_clear; + } else { + // 32bpp clear values are only taken from RB_COLOR_CLEAR. + constants_out.rt_specific.clear_value[0] = rb_color_clear; + constants_out.rt_specific.clear_value[1] = rb_color_clear; + } constants_out.rt_specific.edram_info = color_edram_info; constants_out.coordinate_info = coordinate_info; } diff --git a/src/xenia/gpu/vulkan/vulkan_render_target_cache.cc b/src/xenia/gpu/vulkan/vulkan_render_target_cache.cc index 67f6d2d32..55724adb7 100644 --- a/src/xenia/gpu/vulkan/vulkan_render_target_cache.cc +++ b/src/xenia/gpu/vulkan/vulkan_render_target_cache.cc @@ -1316,8 +1316,14 @@ bool VulkanRenderTargetCache::Resolve(const Memory& memory, clear_transfers_[1])) { uint64_t clear_values[2]; clear_values[0] = resolve_info.rb_depth_clear; - clear_values[1] = resolve_info.rb_color_clear | - (uint64_t(resolve_info.rb_color_clear_lo) << 32); + // For 64bpp formats, RB_COLOR_CLEAR_LO is the lower 32 bits of the + // packed clear value. RB_COLOR_CLEAR is the upper 32 bits and, for + // 32bpp formats, the whole value. + clear_values[1] = + resolve_info.color_edram_info.format_is_64bpp + ? resolve_info.rb_color_clear_lo | + (uint64_t(resolve_info.rb_color_clear) << 32) + : resolve_info.rb_color_clear; PerformTransfersAndResolveClears(2, clear_render_targets, clear_transfers_, clear_values, &clear_rectangle); From 3c8faf03f7fab0d2827f4bd1d23b770956947a1e Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Fri, 17 Jul 2026 22:05:49 +0200 Subject: [PATCH 11/14] [Fix] threading_posix: reap thread handle once (mission-end freeze) post_execution() runs on every successful Wait() and a Thread handle stays signaled forever after exit, so multiple guest waits on the same exited handle each call pthread_join() on an already-reaped pthread_t. On glibc that hangs (recycled tid) -> the black-screen freeze at mission teardown. Guard reap with an atomic exchange so it happens exactly once. Isolated from the audio/crash WIP on phase-a-args-fileread; only this hunk, on stock 16e1eb8e2. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/xenia/base/threading_posix.cc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/xenia/base/threading_posix.cc b/src/xenia/base/threading_posix.cc index b45e72ee0..e74605b28 100644 --- a/src/xenia/base/threading_posix.cc +++ b/src/xenia/base/threading_posix.cc @@ -14,6 +14,7 @@ #include "xenia/base/platform.h" #include "xenia/base/threading_timer_queue.h" +#include #include #include #include @@ -981,11 +982,22 @@ class PosixCondition final : public PosixConditionBase { static void* ThreadStartRoutine(void* parameter); bool signaled() const override { return signaled_; } void post_execution() override { + // Reap exactly once. post_execution() runs on EVERY successful Wait(), and + // a thread object stays signaled forever once it has exited -- so every + // later wait on the same handle would pthread_join() a pthread_t that the + // first join already reaped and freed. On glibc that faults or, if the tid + // was recycled, blocks forever inside pthread_join(). The guest does + // exactly this at mission teardown (several waiters on one thread handle), + // which hung the waiting guest thread: the game froze to a black screen. + if (reaped_.exchange(true)) { + return; + } if (thread_) { pthread_join(thread_, nullptr); } sem_destroy(&suspend_sem_); } + std::atomic reaped_{false}; pthread_t thread_; pid_t tid_ = 0; // Kernel TID for setpriority() fallback mutable bool fifo_failed_ = false; // True after SCHED_FIFO was rejected From c6caa9e9c5d80c80223381b8f6afae3fbc5d28ab Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 12 Jul 2026 18:42:33 +0200 Subject: [PATCH 12/14] [GPU] Add log_draws: dump per-draw vertex declaration for RE Reverse-engineering aid for decoding game mesh formats against GPU ground truth. When the `log_draws` cvar is set, each distinct draw's primitive type, index buffer (guest base / count / format / endianness), and full per-stream vertex declaration (fetch-constant base + stride, and every element's format + offset) is written to xenia_re_draws.log. - command_processor.{h,cc}: CommandProcessor::LogDrawForRE(), no-op unless the cvar is set. De-dups by the vertex-declaration fingerprint (shader + primitive + element formats/offsets), so animated UI that redraws the same format into fresh buffers every frame collapses to one record -- keeping it near-free (an earlier address-keyed de-dup flooded the log and stalled the GPU thread). Capped at 4096 distinct formats. - pm4_command_processor_implement.h: call it from ExecutePacketType3Draw after IssueDraw (so the vertex shader has been analyzed). Backend-agnostic base path -- works for the Vulkan build. Used to confirm Project Sylpheed's XBG7 mesh layout (triangle list, pos f32x3 / normal f16x4 / uv f16x2, variable stride). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/xenia/gpu/command_processor.cc | 133 ++++++++++++++++++ src/xenia/gpu/command_processor.h | 7 + .../gpu/pm4_command_processor_implement.h | 5 + 3 files changed, 145 insertions(+) diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 940a9a48a..c8584405e 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -9,6 +9,9 @@ #include "xenia/gpu/command_processor.h" +#include +#include + #include "third_party/fmt/include/fmt/format.h" #include "xenia/base/byte_stream.h" #include "xenia/base/clock.h" @@ -18,6 +21,8 @@ #include "xenia/gpu/gpu_flags.h" #include "xenia/gpu/graphics_system.h" #include "xenia/gpu/packet_disassembler.h" +#include "xenia/gpu/registers.h" +#include "xenia/gpu/shader.h" #include "xenia/gpu/sampler_info.h" #include "xenia/gpu/texture_info.h" #include "xenia/gpu/xenos_zpd_report.h" @@ -77,6 +82,14 @@ DEFINE_string( UPDATE_from_string(readback_resolve, 2025, 12, 4, 21, "fast"); +DEFINE_bool( + log_draws, false, + "Reverse-engineering aid: write each distinct draw's primitive type, index " + "buffer, and per-stream vertex declaration (stream base/stride + per-element " + "format/offset) to xenia_re_draws.log in the working directory. For decoding " + "game mesh formats against GPU ground truth.", + "GPU"); + DEFINE_bool( readback_memexport, false, "Read data written by memory export in shaders on the CPU. " @@ -88,6 +101,126 @@ DEFINE_bool( namespace xe { namespace gpu { +namespace { +// Short readable name for a guest vertex element format (RE logging only). +const char* ReVertexFormatName(xenos::VertexFormat f) { + switch (f) { + case xenos::VertexFormat::k_32_FLOAT: return "f32"; + case xenos::VertexFormat::k_32_32_FLOAT: return "f32x2"; + case xenos::VertexFormat::k_32_32_32_FLOAT: return "f32x3"; + case xenos::VertexFormat::k_32_32_32_32_FLOAT: return "f32x4"; + case xenos::VertexFormat::k_16_16_FLOAT: return "f16x2"; + case xenos::VertexFormat::k_16_16_16_16_FLOAT: return "f16x4"; + case xenos::VertexFormat::k_16_16: return "s16x2"; + case xenos::VertexFormat::k_16_16_16_16: return "s16x4"; + case xenos::VertexFormat::k_8_8_8_8: return "8888"; + case xenos::VertexFormat::k_2_10_10_10: return "2_10_10_10"; + case xenos::VertexFormat::k_10_11_11: return "10_11_11"; + case xenos::VertexFormat::k_11_11_10: return "11_11_10"; + case xenos::VertexFormat::k_32: return "u32"; + case xenos::VertexFormat::k_32_32: return "u32x2"; + case xenos::VertexFormat::k_32_32_32_32: return "u32x4"; + default: return "?"; + } +} +} // namespace + +void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value, + const IndexBufferInfo* index_buffer_info) { + if (!cvars::log_draws) { + return; + } + static std::mutex re_mutex; + static std::ofstream re_out; + static std::unordered_set re_seen; + std::lock_guard lock(re_mutex); + if (!re_out.is_open()) { + re_out.open("xenia_re_draws.log", std::ios::out | std::ios::trunc); + XELOGI("[RE-DRAW] logging distinct draws to xenia_re_draws.log"); + } + if (!re_out.is_open()) { + return; + } + + reg::VGT_DRAW_INITIATOR init; + init.value = vgt_draw_initiator_value; + Shader* vs = active_vertex_shader_; + + // De-dup by the VERTEX-DECLARATION FINGERPRINT (shader + primitive type + + // per-stream element formats/offsets), NOT by buffer address. Animated UI + // that redraws the same mesh format into fresh buffers every frame therefore + // collapses to a single record, keeping the logging near-free — while every + // distinct mesh format (the player plane, each weapon) is still captured once. + uint64_t sig = 1469598103934665603ull; // FNV-ish seed + auto mix = [&sig](uint64_t v) { sig = (sig ^ v) * 1099511628211ull; }; + mix(uint64_t(init.prim_type)); + if (vs) { + mix(vs->ucode_data_hash()); + } + bool analyzed = vs && vs->is_ucode_analyzed(); + if (analyzed) { + for (const auto& binding : vs->vertex_bindings()) { + mix(binding.fetch_constant); + mix(binding.stride_words); + for (const auto& attr : binding.attributes) { + mix(uint64_t(attr.fetch_instr.attributes.data_format)); + mix(uint64_t(uint32_t(attr.fetch_instr.attributes.offset))); + } + } + } else { + mix(uint64_t(init.num_indices)); + } + if (!re_seen.insert(sig).second) { + return; + } + // Safety cap on distinct formats, so a pathological title can't grow the log + // (and the working set) without bound. + if (re_seen.size() > 4096) { + return; + } + + re_out << fmt::format("DRAW prim={} indices={} src={} ", + uint32_t(init.prim_type), uint32_t(init.num_indices), + uint32_t(init.source_select)); + if (index_buffer_info) { + re_out << fmt::format( + "index[base=0x{:08X} count={} fmt={} endian={} len={}] ", + index_buffer_info->guest_base, index_buffer_info->count, + index_buffer_info->format == xenos::IndexFormat::kInt16 ? "u16" : "u32", + uint32_t(index_buffer_info->endianness), index_buffer_info->length); + } else { + re_out << "index[auto] "; + } + if (vs) { + re_out << fmt::format("vs=0x{:016X}", vs->ucode_data_hash()); + } + re_out << "\n"; + + if (analyzed) { + for (const auto& binding : vs->vertex_bindings()) { + xenos::xe_gpu_vertex_fetch_t fetch = + register_file_->GetVertexFetch(binding.fetch_constant); + re_out << fmt::format( + " stream fc={} base=0x{:08X} stride_words={} size_words={} " + "endian={} type={}\n", + binding.fetch_constant, uint32_t(fetch.address) << 2, + binding.stride_words, uint32_t(fetch.size), uint32_t(fetch.endian), + uint32_t(fetch.type)); + for (const auto& attr : binding.attributes) { + const auto& a = attr.fetch_instr.attributes; + re_out << fmt::format( + " attr fmt={}({}) offset_words={} stride_words={} signed={} " + "int={} exp_adjust={}\n", + uint32_t(a.data_format), ReVertexFormatName(a.data_format), a.offset, + a.stride, a.is_signed ? 1 : 0, a.is_integer ? 1 : 0, a.exp_adjust); + } + } + } else { + re_out << " (vertex shader not analyzed yet)\n"; + } + re_out.flush(); +} + // This should be written completely differently with support for different // types. void SaveGPUSetting(GPUSetting setting, uint64_t value) { diff --git a/src/xenia/gpu/command_processor.h b/src/xenia/gpu/command_processor.h index 9fce69114..4c1962963 100644 --- a/src/xenia/gpu/command_processor.h +++ b/src/xenia/gpu/command_processor.h @@ -446,6 +446,13 @@ class CommandProcessor { } virtual bool IssueCopy() { return false; } + // Reverse-engineering aid (cvar `log_draws`): dump the guest's exact + // primitive type + index buffer + per-stream vertex declaration for each + // distinct draw to a dedicated file, for decoding game mesh formats. + // No-op unless the cvar is enabled. Defined in command_processor.cc. + void LogDrawForRE(uint32_t vgt_draw_initiator_value, + const IndexBufferInfo* index_buffer_info); + // "Actual" is for the command processor thread, to be read by the // implementations. SwapPostEffect GetActualSwapPostEffect() const { diff --git a/src/xenia/gpu/pm4_command_processor_implement.h b/src/xenia/gpu/pm4_command_processor_implement.h index 8f8519feb..b09e0c877 100644 --- a/src/xenia/gpu/pm4_command_processor_implement.h +++ b/src/xenia/gpu/pm4_command_processor_implement.h @@ -1152,6 +1152,11 @@ bool COMMAND_PROCESSOR::ExecutePacketType3Draw( uint32_t(vgt_draw_initiator.prim_type), uint32_t(vgt_draw_initiator.source_select)); } + // Reverse-engineering aid (no-op unless the `log_draws` cvar is set): + // record the guest's primitive type + index buffer + vertex declaration. + // Placed after IssueDraw so the vertex shader has been analyzed. + COMMAND_PROCESSOR::LogDrawForRE( + vgt_draw_initiator.value, is_indexed ? &index_buffer_info : nullptr); } } From 29efc0381aac0f551c4917808cb61318817b1226 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 12 Jul 2026 19:09:08 +0200 Subject: [PATCH 13/14] [GPU] log_draws: also dump vertex positions for file correlation Extend the RE draw-logger to dump the first few vertex POSITIONS (read from guest memory) under each draw. The f32 position bytes are identical between the guest buffer and the on-disc .xpr (only f16 pairs are rearranged on load), so these values can be grep'd for in a resource file to locate a mesh whose in-file offset is otherwise unknown. Validated: world-space stage geometry byte-matches Stage_*.xpr at exact offsets. (Load-time-transformed meshes like the player ship don't match, which is itself a useful finding.) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/xenia/gpu/command_processor.cc | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index c8584405e..c7b01575c 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -215,6 +215,51 @@ void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value, a.stride, a.is_signed ? 1 : 0, a.is_integer ? 1 : 0, a.exp_adjust); } } + + // Dump the first few vertex POSITIONS from guest memory. The f32 position + // bytes are identical between the guest buffer and the on-disc .xpr (only + // f16 pairs are rearranged on load), so these values can be searched for in + // the file to locate a mesh whose in-file offset is otherwise unknown + // (e.g. multi-XBG7 body meshes). See docs/re/structures/xbg7-mesh.md. + if (!vs->vertex_bindings().empty()) { + const auto& binding = vs->vertex_bindings()[0]; + xenos::xe_gpu_vertex_fetch_t fetch = + register_file_->GetVertexFetch(binding.fetch_constant); + // Position = the first f32×3 attribute (offset is in dwords). + int32_t pos_off_bytes = -1; + for (const auto& attr : binding.attributes) { + if (attr.fetch_instr.attributes.data_format == + xenos::VertexFormat::k_32_32_32_FLOAT) { + pos_off_bytes = attr.fetch_instr.attributes.offset * 4; + break; + } + } + uint32_t stride = binding.stride_words * 4; + uint32_t vbase = uint32_t(fetch.address) << 2; + uint32_t buf_bytes = uint32_t(fetch.size) * 4; + if (pos_off_bytes >= 0 && stride > 0) { + uint32_t max_v = buf_bytes / stride; + uint32_t n = max_v < 8 ? max_v : 8; + re_out << " positions:"; + for (uint32_t v = 0; v < n; ++v) { + uint32_t a = vbase + v * stride + uint32_t(pos_off_bytes); + const uint8_t* p = memory_->TranslatePhysical(a); + if (!p) { + break; + } + auto be_f32 = [](const uint8_t* q) { + uint32_t w = (uint32_t(q[0]) << 24) | (uint32_t(q[1]) << 16) | + (uint32_t(q[2]) << 8) | uint32_t(q[3]); + float f; + std::memcpy(&f, &w, 4); + return f; + }; + re_out << fmt::format(" ({:.4f},{:.4f},{:.4f})", be_f32(p), + be_f32(p + 4), be_f32(p + 8)); + } + re_out << "\n"; + } + } } else { re_out << " (vertex shader not analyzed yet)\n"; } From ea9f037b82671d445836ad0cbeebc354470bed9e Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 19 Jul 2026 20:19:12 +0200 Subject: [PATCH 14/14] [GPU,Kernel] RE instrumentation: per-buffer draw log + file-I/O guest map Draw logger (command_processor.cc): also key each record on the index-buffer guest base so distinct static index buffers (ship body vs hangar vs weapons) no longer collapse to one record; dump the first decoded index VALUES + raw index bytes (8-in-16/32 endian-corrected) and the stream-base hex, to pin an index buffer's exact .xpr file offset and validate a mesh decode against ground truth. File-I/O log (xboxkrnl_io.cc): --log_file_io cvar writes each distinct NtReadFile as 'READ off=.. len=.. -> guest=..' to xenia_re_files.log, giving the file->guest-memory mapping. Cross-referenced with the draw log's buffer guest addresses this resolves which .xpr (and where inside it) a mesh loaded from. De-duped by (offset, guest), capped at 65536 entries. Also ignore build-cross/ (Wine cross-build output). Co-Authored-By: Claude Opus 4.8 --- .gitignore | 3 + src/xenia/gpu/command_processor.cc | 206 +++++++++++++++++++++++ src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc | 53 ++++++ 3 files changed, 262 insertions(+) diff --git a/.gitignore b/.gitignore index 4b0c33cb9..849b29a99 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,6 @@ node_modules/.bin/ /cache0 /devkit recent.toml + +# Cross-compile (Wine) build output — RE handoff, never commit +build-cross/ diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index c7b01575c..c0135978e 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -157,6 +157,15 @@ void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value, if (vs) { mix(vs->ucode_data_hash()); } + // ALSO key on the index-buffer guest base. Same-format meshes drawn from + // DISTINCT static index buffers (the player-ship body vs the hangar walls vs + // each weapon) otherwise collapse to one record, hiding all but the first — + // which is why the ship body never logged. Static geometry has a stable index + // address so it still logs once; only per-frame-reallocated dynamic geometry + // multiplies (bounded by the 4096 cap + short RE captures). + if (index_buffer_info) { + mix(uint64_t(index_buffer_info->guest_base)); + } bool analyzed = vs && vs->is_ucode_analyzed(); if (analyzed) { for (const auto& binding : vs->vertex_bindings()) { @@ -196,6 +205,47 @@ void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value, } re_out << "\n"; + // Dump the first index VALUES (decoded, endian-corrected) + raw index bytes. + // Index buffers live in a separate grouped pool with no distinctive content, + // so the vertex-position correlation can't find them; but a run of the first + // ~32 logical indices IS a distinctive byte pattern to search for in the .xpr, + // which pins the index buffer's file offset exactly. Guest index bytes carry + // the fetch endianness (usually 8-in-16 for u16), so both the raw bytes and + // the decoded values are printed — search whichever matches the file order. + if (index_buffer_info && index_buffer_info->guest_base) { + const uint8_t* ip = memory_->TranslatePhysical( + index_buffer_info->guest_base); + bool u16 = index_buffer_info->format == xenos::IndexFormat::kInt16; + uint32_t esz = u16 ? 2 : 4; + uint32_t cnt = index_buffer_info->count < 48 ? index_buffer_info->count : 48; + if (ip) { + uint32_t rawn = cnt * esz; + if (rawn > 128) { + rawn = 128; + } + re_out << " idx_raw:"; + for (uint32_t i = 0; i < rawn; ++i) { + re_out << fmt::format(" {:02X}", ip[i]); + } + re_out << "\n idx_val:"; + // Decode with 8-in-16 / 8-in-32 endian correction (endianness field). + uint32_t en = uint32_t(index_buffer_info->endianness); + for (uint32_t i = 0; i < cnt; ++i) { + const uint8_t* q = ip + i * esz; + uint32_t v; + if (u16) { + v = (en == 1) ? (uint32_t(q[0]) | (uint32_t(q[1]) << 8)) + : (uint32_t(q[1]) | (uint32_t(q[0]) << 8)); + } else { + v = (uint32_t(q[3]) << 24) | (uint32_t(q[2]) << 16) | + (uint32_t(q[1]) << 8) | uint32_t(q[0]); + } + re_out << fmt::format(" {}", v); + } + re_out << "\n"; + } + } + if (analyzed) { for (const auto& binding : vs->vertex_bindings()) { xenos::xe_gpu_vertex_fetch_t fetch = @@ -214,6 +264,30 @@ void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value, uint32_t(a.data_format), ReVertexFormatName(a.data_format), a.offset, a.stride, a.is_signed ? 1 : 0, a.is_integer ? 1 : 0, a.exp_adjust); } + // Raw bytes at the stream base (first two stride-records, capped 96 B). + // The f32-position dump below only fires for k_32_32_32_FLOAT streams; + // quantized-position body meshes (the multi-stream layout we still can't + // decode) have no f32 attribute, so these hex bytes are the only + // ground-truth we can correlate against the on-disc .xpr and validate a + // decode against — regardless of the element format. + { + uint32_t sbase = uint32_t(fetch.address) << 2; + uint32_t sbytes = uint32_t(fetch.size) * 4; + uint32_t stride_b = binding.stride_words * 4; + uint32_t want = stride_b ? stride_b * 2 : 32; + uint32_t n = want < sbytes ? want : sbytes; + if (n > 96) { + n = 96; + } + const uint8_t* p = memory_->TranslatePhysical(sbase); + if (p && n) { + re_out << " raw:"; + for (uint32_t i = 0; i < n; ++i) { + re_out << fmt::format(" {:02X}", p[i]); + } + re_out << "\n"; + } + } } // Dump the first few vertex POSITIONS from guest memory. The f32 position @@ -263,7 +337,139 @@ void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value, } else { re_out << " (vertex shader not analyzed yet)\n"; } + + // Bound TEXTURES for this draw (guest base address of each). Cross-referenced + // with the file-I/O log (xenia_re_files.log), a texture's guest address maps + // to the .xpr it was loaded from — so a draw sampling `rou_f001_*_col` + // identifies the ship, and gives material→file assignment. Base is the fetch + // constant's base_address in 4 KB pages (<< 12 = guest byte address). + Shader* ps = active_pixel_shader_; + if (ps && ps->is_ucode_analyzed() && !ps->texture_bindings().empty()) { + re_out << " textures:\n"; + for (const auto& tb : ps->texture_bindings()) { + xenos::xe_gpu_texture_fetch_t tf = + register_file_->GetTextureFetch(tb.fetch_constant); + // Dimensions are stored as (actual - 1). Only 2D fields are meaningful for + // the ship's surface maps; other dimensions still print base/format. + uint32_t w = tf.size_2d.width + 1; + uint32_t h = tf.size_2d.height + 1; + uint32_t gpu_base = uint32_t(tf.base_address) << 12; + re_out << fmt::format( + " [fc={} base=0x{:08X} fmt={} endian={} dim={} {}x{} tiled={} " + "pitch={} swizzle=0x{:03X} mip_addr=0x{:08X} mip_lvls={}..{}]", + tb.fetch_constant, gpu_base, uint32_t(tf.format), + uint32_t(tf.endianness), uint32_t(tf.dimension), w, h, + uint32_t(tf.tiled), uint32_t(tf.pitch), uint32_t(tf.swizzle), + uint32_t(tf.mip_address) << 12, uint32_t(tf.mip_min_level), + uint32_t(tf.mip_max_level)); + // Hash + sample the GPU-RESIDENT bytes at base_address. If the game + // massaged the texture after load (recolour, recompress, generate mips, + // re-tile), these bytes differ from the on-disc .xpr — comparing this hash + // and sample against the file the address maps to (xenia_re_files.log) + // tells us whether the viewer can decode the disc bytes directly or must + // reproduce a runtime transform. 128 KiB window is format-agnostic. + const uint8_t* tp = + memory_->TranslatePhysical(gpu_base); + if (tp) { + uint64_t th = 1469598103934665603ull; + uint32_t win = w * h; // conservative texel-count-scaled cap + if (win > 131072) { + win = 131072; + } + if (win < 64) { + win = 64; + } + for (uint32_t i = 0; i < win; ++i) { + th = (th ^ tp[i]) * 1099511628211ull; + } + re_out << fmt::format(" hash=0x{:016X} bytes[0:32]:", th); + for (uint32_t i = 0; i < 32; ++i) { + re_out << fmt::format(" {:02X}", tp[i]); + } + } + re_out << "\n"; + } + } + + // Dump the vertex-shader float constants c0..c31 (the transform matrices live + // here). The game bakes each part's WORLD matrix into these constants before + // the draw, so comparing a part's WVP block to the body's isolates the world + // transform the shader applies (View/Proj cancel): world = inv(body_WVP) * + // part_WVP. This is the only way to recover the runtime nacelle offset that + // the static XBG7 node graph doesn't encode. VS constants are float slots + // 0..255 (PS = 256..511); each slot is a float4. + { + re_out << " vsconst:\n"; + for (uint32_t i = 0; i < 32; ++i) { + uint32_t base = XE_GPU_REG_SHADER_CONSTANT_000_X + 4 * i; + float x = register_file_->Get(base + 0); + float y = register_file_->Get(base + 1); + float z = register_file_->Get(base + 2); + float w = register_file_->Get(base + 3); + re_out << fmt::format(" c{:<3} {:.5f} {:.5f} {:.5f} {:.5f}\n", i, x, y, + z, w); + } + } + + // Dump the PIXEL-shader float constants c256..c287 (the material params: light + // directions/colours, specular exponent, tint/fresnel factors the lighting + // shader multiplies in). Slots 256..511 are the PS bank; register base is + // SHADER_CONSTANT_256_X. Together with the disassembled PS microcode below, + // these are what turn the flat albedo into the game's lit look. + if (ps) { + re_out << fmt::format(" ps=0x{:016X}\n", ps->ucode_data_hash()); + re_out << " psconst:\n"; + // The ship's material/lighting shader references PS constants up to c76 (the + // light dirs/colours c32/c33/c38..c41, the mask scale/remap factors + // c64..c66/c72..c76) and the c254/c255 literals. Dump c0..c95 plus the top + // literals so every referenced slot is captured (a 32-wide dump missed them). + auto dump_pc = [&](uint32_t i) { + uint32_t base = XE_GPU_REG_SHADER_CONSTANT_256_X + 4 * i; + float x = register_file_->Get(base + 0); + float y = register_file_->Get(base + 1); + float z = register_file_->Get(base + 2); + float w = register_file_->Get(base + 3); + re_out << fmt::format(" p{:<3} {:.5f} {:.5f} {:.5f} {:.5f}\n", i, x, y, + z, w); + }; + for (uint32_t i = 0; i < 96; ++i) { + dump_pc(i); + } + for (uint32_t i = 250; i < 256; ++i) { + dump_pc(i); + } + } re_out.flush(); + + // Dump the full Xenos MICROCODE DISASSEMBLY of the vertex and pixel shaders + // for this draw to a separate file, deduped by ucode hash. The PS disassembly + // is the ground truth for the game's lighting/material model — how many + // texture samples feed the surface, the specular math, any cubemap/fresnel + // term — which is what the viewer's StandardMaterial has to reproduce. Kept in + // its own file (xenia_re_shaders.log) so the per-draw log stays scannable. + { + static std::ofstream sh_out; + static std::unordered_set sh_seen; + if (!sh_out.is_open()) { + sh_out.open("xenia_re_shaders.log", std::ios::out | std::ios::trunc); + } + if (sh_out.is_open()) { + auto dump_shader = [&](const char* kind, Shader* sh) { + if (!sh || !sh->is_ucode_analyzed()) { + return; + } + if (!sh_seen.insert(sh->ucode_data_hash()).second) { + return; + } + sh_out << fmt::format("===== {} 0x{:016X} =====\n", kind, + sh->ucode_data_hash()); + sh_out << sh->ucode_disassembly() << "\n\n"; + }; + dump_shader("VS", vs); + dump_shader("PS", ps); + sh_out.flush(); + } + } } // This should be written completely differently with support for different diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc index bed943647..ec78f08d8 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc @@ -7,6 +7,12 @@ ****************************************************************************** */ +#include +#include +#include + +#include "third_party/fmt/include/fmt/format.h" +#include "xenia/base/cvar.h" #include "xenia/base/logging.h" #include "xenia/kernel/info/file.h" #include "xenia/kernel/kernel_state.h" @@ -20,10 +26,50 @@ #include "xenia/vfs/device.h" #include "xenia/xbox.h" +DEFINE_bool( + log_file_io, false, + "Reverse-engineering aid: log every NtReadFile as " + "'READ off=.. len=.. -> guest=..' to xenia_re_files.log, giving the " + "file->guest-memory mapping. Cross-referenced with the GPU draw log's buffer " + "guest addresses, this resolves which .xpr (and where inside it) a mesh was " + "loaded from. De-duplicated by (name, offset, guest).", + "Kernel"); + namespace xe { namespace kernel { namespace xboxkrnl { +// RE aid: record the file->guest mapping for each distinct NtReadFile. +static void LogFileReadForRE(const std::string& name, uint64_t byte_offset, + uint32_t length, uint32_t guest_addr, + uint32_t bytes_read) { + if (!cvars::log_file_io) { + return; + } + static std::mutex io_mutex; + static std::ofstream io_out; + static std::unordered_set io_seen; + std::lock_guard lock(io_mutex); + if (!io_out.is_open()) { + io_out.open("xenia_re_files.log", std::ios::out | std::ios::trunc); + } + if (!io_out.is_open()) { + return; + } + // De-dup by (offset, guest) — a whole-file load or a distinct buffer read is + // logged once; streamed re-reads of the same region collapse. + uint64_t key = (byte_offset << 20) ^ (uint64_t(guest_addr) << 4) ^ length; + if (!io_seen.insert(key).second) { + return; + } + if (io_seen.size() > 65536) { + return; + } + io_out << fmt::format("READ {} off=0x{:X} len=0x{:X} -> guest=0x{:08X} read=0x{:X}\n", + name, byte_offset, length, guest_addr, bytes_read); + io_out.flush(); +} + struct CreateOptions { // https://processhacker.sourceforge.io/doc/ntioapi_8h.html static constexpr uint32_t FILE_DIRECTORY_FILE = 0x00000001; @@ -152,6 +198,13 @@ dword_result_t NtReadFile_entry(dword_t file_handle, dword_t event_handle, io_status_block->status = result; io_status_block->information = bytes_read; } + // RE aid: record the file->guest-memory mapping (see LogFileReadForRE). + if (cvars::log_file_io && file && file->entry()) { + LogFileReadForRE( + file->entry()->name(), + byte_offset_ptr ? static_cast(*byte_offset_ptr) : 0, + buffer_length, buffer.guest_address(), bytes_read); + } // Queue the APC callback. It must be delivered via the APC mechanism even // though were are completing immediately.