diff --git a/.github/workflows/Linux_x86.yml b/.github/workflows/Linux_x86.yml index 352565a2c..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 @@ -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/.gitignore b/.gitignore index 0407b8326..d687f54e0 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,6 @@ node_modules/.bin/ /cache0 /devkit recent.toml + +# Cross-compile (Wine) build output — RE handoff, never commit +build-cross/ diff --git a/src/xenia/base/threading_posix.cc b/src/xenia/base/threading_posix.cc index 60108c6d2..38cb2cb4c 100644 --- a/src/xenia/base/threading_posix.cc +++ b/src/xenia/base/threading_posix.cc @@ -985,10 +985,11 @@ class PosixCondition final : public PosixConditionBase { // 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, faulting inside pthread_join. The - // guest does exactly this at mission teardown (several waiters on one - // thread handle), which crashed the waiting guest thread in a permanent - // fault loop: audio died and the game froze. + // 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 crashed or hung the waiting guest thread: audio died and the game + // froze to a black screen. if (reaped_.exchange(true)) { return; } 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/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/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] 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; diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 89d6bbb6b..245543515 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -359,6 +359,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()) { @@ -398,6 +407,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 = @@ -416,6 +466,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 @@ -465,7 +539,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/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/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) \ 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); 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; diff --git a/src/xenia/kernel/xam/xam_info.cc b/src/xenia/kernel/xam/xam_info.cc index 5f67cb306..24491fddc 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); @@ -336,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(); } 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); }; diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc index c0adfecc2..cffa84ee1 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/cpu/thread_state.h" #include "xenia/kernel/info/file.h" @@ -22,10 +28,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; @@ -154,6 +200,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. 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, ])