From 11ad6b0a87d80a4a7ac8478ac620b14907a69c00 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:00:26 +0900 Subject: [PATCH 01/25] [Memory] Add alignment guards to PhysicalHeap allocations Add alignment checks and parent release on failure in Alloc, AllocFixed, and AllocRange to prevent misaligned addresses reaching BaseHeap. Fixes #954 --- src/xenia/base/testing/CMakeLists.txt | 2 +- src/xenia/base/testing/physical_heap_test.cc | 211 +++++++++++++++++++ src/xenia/memory.cc | 31 +++ 3 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 src/xenia/base/testing/physical_heap_test.cc diff --git a/src/xenia/base/testing/CMakeLists.txt b/src/xenia/base/testing/CMakeLists.txt index 674b6d148..62de6a077 100644 --- a/src/xenia/base/testing/CMakeLists.txt +++ b/src/xenia/base/testing/CMakeLists.txt @@ -1,3 +1,3 @@ xe_test_suite(xenia-base-tests ${CMAKE_CURRENT_SOURCE_DIR} - LINKS fmt xenia-base + LINKS fmt xenia-base xenia-core xenia-cpu ) diff --git a/src/xenia/base/testing/physical_heap_test.cc b/src/xenia/base/testing/physical_heap_test.cc new file mode 100644 index 000000000..afd9aee79 --- /dev/null +++ b/src/xenia/base/testing/physical_heap_test.cc @@ -0,0 +1,211 @@ +/** + ****************************************************************************** + * 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/memory.h" + +#include "third_party/catch/include/catch.hpp" + +#include "xenia/base/memory.h" + +namespace xe { +namespace test { + +// All tests use kMemoryAllocationReserve which only touches the page table, +// not host memory. This lets us pass nullptr for membase and Memory*. + +TEST_CASE("PhysicalHeap::GetPhysicalAddress", "[memory]") { + VirtualHeap parent; + parent.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0x00000000, + 0x20000000, 4096); + + SECTION("heap with no offset returns heap-relative address") { + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xA0000000, + 0x20000000, 64 * 1024, &parent); + + REQUIRE(heap.host_address_offset() == 0); + REQUIRE(heap.GetPhysicalAddress(0xA0000000) == 0); + REQUIRE(heap.GetPhysicalAddress(0xA0010000) == 0x10000); + REQUIRE(heap.GetPhysicalAddress(0xA1000000) == 0x1000000); + } + + SECTION("0xE0000000 heap always has 0x1000 physical offset") { + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xE0000000, + 0x1FD00000, 4096, &parent); + + // The 0x1000 physical offset is baked into the view mapping + // (map_info target_address), not derived from host_address_offset. + REQUIRE(heap.GetPhysicalAddress(0xE0000000) == 0x1000); + REQUIRE(heap.GetPhysicalAddress(0xE0001000) == 0x2000); + REQUIRE(heap.GetPhysicalAddress(0xE0010000) == 0x11000); + } +} + +TEST_CASE("PhysicalHeap::Alloc alignment", "[memory]") { + VirtualHeap parent; + parent.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0x00000000, + 0x20000000, 4096); + + SECTION("returned address is page-aligned") { + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xA0000000, + 0x20000000, 64 * 1024, &parent); + + uint32_t addr = 0; + bool ok = heap.Alloc(0x10000, 0x10000, kMemoryAllocationReserve, + kMemoryProtectRead, false, &addr); + REQUIRE(ok); + REQUIRE(addr != 0); + REQUIRE(addr % 0x10000 == 0); + REQUIRE(addr >= 0xA0000000); + REQUIRE(addr < 0xC0000000); + } + + SECTION("multiple allocations with different alignments") { + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xA0000000, + 0x20000000, 64 * 1024, &parent); + + for (uint32_t alignment : {0x10000u, 0x20000u, 0x40000u, 0x100000u}) { + uint32_t addr = 0; + bool ok = heap.Alloc(alignment, alignment, kMemoryAllocationReserve, + kMemoryProtectRead, false, &addr); + REQUIRE(ok); + REQUIRE(addr % alignment == 0); + } + } +} + +TEST_CASE("PhysicalHeap::AllocRange alignment", "[memory]") { + VirtualHeap parent; + parent.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0x00000000, + 0x20000000, 4096); + + SECTION("returned address respects alignment within range") { + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xA0000000, + 0x20000000, 64 * 1024, &parent); + + uint32_t addr = 0; + bool ok = heap.AllocRange(0xA0000000, 0xBFFFFFFF, 0x10000, 0x10000, + kMemoryAllocationReserve, kMemoryProtectRead, + false, &addr); + REQUIRE(ok); + REQUIRE(addr % 0x10000 == 0); + REQUIRE(addr >= 0xA0000000); + REQUIRE(addr <= 0xBFFFFFFF); + } + + SECTION("large alignment preserved through translation") { + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xC0000000, + 0x20000000, 16 * 1024 * 1024, &parent); + + uint32_t addr = 0; + bool ok = heap.AllocRange(0xC0000000, 0xDFFFFFFF, 0x1000000, 0x1000000, + kMemoryAllocationReserve, kMemoryProtectRead, + false, &addr); + REQUIRE(ok); + REQUIRE(addr % 0x1000000 == 0); + } +} + +TEST_CASE("PhysicalHeap::AllocFixed alignment", "[memory]") { + VirtualHeap parent; + parent.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0x00000000, + 0x20000000, 4096); + + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xA0000000, + 0x20000000, 64 * 1024, &parent); + + // AllocFixed at a specific aligned address must succeed + bool ok = heap.AllocFixed(0xA0100000, 0x10000, 0x10000, + kMemoryAllocationReserve, kMemoryProtectRead); + REQUIRE(ok); +} + +TEST_CASE("PhysicalHeap vE0000000 alignment", "[memory]") { + VirtualHeap parent; + parent.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0x00000000, + 0x20000000, 4096); + + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xE0000000, + 0x1FD00000, 4096, &parent); + + // The 0xE0000000 heap always has a 0x1000 physical offset, so the + // translation offset is 0xE0000000 - 0x1000 = 0xDFFFF000, which is + // 4KB-aligned but not 64KB-aligned. This is true on all platforms. + uint32_t physical_base = heap.GetPhysicalAddress(heap.heap_base()); + REQUIRE(physical_base == 0x1000); + + SECTION("page-size allocation preserves alignment") { + uint32_t addr = 0; + bool ok = heap.Alloc(0x1000, 0x1000, kMemoryAllocationReserve, + kMemoryProtectRead, false, &addr); + REQUIRE(ok); + REQUIRE(addr % 0x1000 == 0); + REQUIRE(addr >= 0xE0000000); + } + + SECTION("translation offset is 4KB-aligned") { + uint32_t translation_offset = heap.heap_base() - physical_base; + REQUIRE(translation_offset % heap.page_size() == 0); + } + + SECTION("alloc with alignment larger than page_size is rejected") { + // The translation offset (0xDFFFF000) is only 4KB-aligned, so a + // 64KB alignment request produces a misaligned translated address. + // + // Without the fix: BaseHeap::AllocFixed receives a misaligned address, + // hitting assert_true in debug or silently corrupting in release. + // With the fix: PhysicalHeap::Alloc detects the misalignment and + // returns false cleanly. + uint32_t alignment = 0x10000; // 64KB + uint32_t addr = 0; + bool ok = heap.Alloc(0x10000, alignment, kMemoryAllocationReserve, + kMemoryProtectRead, false, &addr); + REQUIRE_FALSE(ok); + } +} + +TEST_CASE("PhysicalHeap vE0000000 AllocRange alignment", "[memory]") { + VirtualHeap parent; + parent.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0x00000000, + 0x20000000, 4096); + + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xE0000000, + 0x1FD00000, 4096, &parent); + + SECTION("page-aligned AllocRange succeeds") { + uint32_t addr = 0; + bool ok = heap.AllocRange(0xE0000000, 0xFFFCFFFF, 0x1000, 0x1000, + kMemoryAllocationReserve, kMemoryProtectRead, + false, &addr); + REQUIRE(ok); + REQUIRE(addr % 0x1000 == 0); + } + + SECTION("AllocRange rejects misaligned translation") { + // Same scenario as Alloc: 64KB alignment on a heap whose translation + // offset (0xDFFFF000) is not 64KB-aligned. + uint32_t alignment = 0x10000; + uint32_t addr = 0; + bool ok = heap.AllocRange(0xE0000000, 0xFFFCFFFF, 0x10000, alignment, + kMemoryAllocationReserve, kMemoryProtectRead, + false, &addr); + REQUIRE_FALSE(ok); + } +} + +} // namespace test +} // namespace xe diff --git a/src/xenia/memory.cc b/src/xenia/memory.cc index a67dc56f3..d3d101784 100644 --- a/src/xenia/memory.cc +++ b/src/xenia/memory.cc @@ -1645,6 +1645,13 @@ void PhysicalHeap::Initialize(Memory* memory, uint8_t* membase, BaseHeap::Initialize(memory, membase, heap_type, heap_base, heap_size, page_size, host_address_offset); parent_heap_ = parent_heap; + + // The physical base offset (host_address_offset) must be a multiple of + // page_size. Otherwise, aligned parent allocations become misaligned after + // translation back to virtual addresses (parent_address + heap_base_ - + // GetPhysicalAddress(heap_base_) loses alignment). + xenia_assert(host_address_offset % page_size == 0); + system_page_size_ = uint32_t(xe::memory::page_size()); xenia_assert(xe::is_pow2(system_page_size_)); system_page_shift_ = xe::log2_floor(system_page_size_); @@ -1685,6 +1692,14 @@ bool PhysicalHeap::Alloc(uint32_t size, uint32_t alignment, // Given the address we've reserved in the parent heap, pin that here. // Shouldn't be possible for it to be allocated already. uint32_t address = heap_base_ + parent_address - parent_heap_start; + if (address % alignment != 0) { + XELOGE( + "PhysicalHeap::Alloc translated address {:08X} misaligned " + "(alignment {:08X}, physical base offset {:08X})", + address, alignment, parent_heap_start); + parent_heap_->Release(parent_address); + return false; + } if (!BaseHeap::AllocFixed(address, size, alignment, allocation_type, protect)) { XELOGE( @@ -1721,6 +1736,14 @@ bool PhysicalHeap::AllocFixed(uint32_t base_address, uint32_t size, // Shouldn't be possible for it to be allocated already. uint32_t address = heap_base_ + parent_base_address - GetPhysicalAddress(heap_base_); + if (address % alignment != 0) { + XELOGE( + "PhysicalHeap::AllocFixed translated address {:08X} misaligned " + "(alignment {:08X}, physical base offset {:08X})", + address, alignment, GetPhysicalAddress(heap_base_)); + parent_heap_->Release(parent_base_address); + return false; + } if (!BaseHeap::AllocFixed(address, size, alignment, allocation_type, protect)) { XELOGE( @@ -1762,6 +1785,14 @@ bool PhysicalHeap::AllocRange(uint32_t low_address, uint32_t high_address, // Shouldn't be possible for it to be allocated already. uint32_t address = heap_base_ + parent_address - GetPhysicalAddress(heap_base_); + if (address % alignment != 0) { + XELOGE( + "PhysicalHeap::AllocRange translated address {:08X} misaligned " + "(alignment {:08X}, physical base offset {:08X})", + address, alignment, GetPhysicalAddress(heap_base_)); + parent_heap_->Release(parent_address); + return false; + } if (!BaseHeap::AllocFixed(address, size, alignment, allocation_type, protect)) { XELOGE( From c383d049ecd8e89fad4a72567ad66151d7572e1c Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Mon, 30 Mar 2026 21:58:35 -0700 Subject: [PATCH 02/25] [CPU] Implement `AND`+`NOT` folding into `AND_NOT` Detect dependent `AND` and `NOT` IR sequences and combine them into a singular `AND_NOT` opcode. The later dead-code-elimination-pass will get rid of the left-over `NOT` opcode if nothing else uses it. This gets quite a good amount of hits in some of the titles I've tested. Also updates unit tests with additional data-types and ensures that `And(..., Not())` returns the same result as `AndNot(...)` --- .../compiler/passes/simplification_pass.cc | 36 ++++ .../cpu/compiler/passes/simplification_pass.h | 1 + src/xenia/cpu/testing/opcode_coverage_test.cc | 171 ++++++++++++++++++ 3 files changed, 208 insertions(+) diff --git a/src/xenia/cpu/compiler/passes/simplification_pass.cc b/src/xenia/cpu/compiler/passes/simplification_pass.cc index d7fdf246a..2779f682a 100644 --- a/src/xenia/cpu/compiler/passes/simplification_pass.cc +++ b/src/xenia/cpu/compiler/passes/simplification_pass.cc @@ -568,6 +568,10 @@ bool SimplificationPass::TryHandleANDROLORSHLSeq(hir::Instr* i, bool SimplificationPass::CheckAnd(hir::Instr* i, hir::HIRBuilder* builder) { retry_and_simplification: + if (SimplifyAndNot(i, builder)) { + return true; + } + auto [constant_value, variable_value] = i->BinaryValueArrangeAsConstAndVar(); if (!constant_value) { // added this for srawi @@ -1247,6 +1251,38 @@ bool SimplificationPass::SimplifyAddArith(hir::Instr* i, return false; } +bool SimplificationPass::SimplifyAndNot(hir::Instr* i, + hir::HIRBuilder* builder) { + // check if either of the 2 AND operands has just used NOT and fold into + // an AND_NOT opcode + Value* src1 = i->src1.value; + Value* src2 = i->src2.value; + + Instr* def1 = src1->def; + Instr* def2 = src2->def; + if (!def1 || !def2) return false; + + // Bypass the NOT from an incoming operand and combine it into AND_NOT. + // If the original NOT does not have any further uses, then the + // dead-code-elimination pass will delete it. Otherwise, if it still has uses, + // then there will still be a NOT operation. + if (def2->opcode == &OPCODE_NOT_info) { + // Fold src2's NOT into AND_NOT + i->Replace(&OPCODE_AND_NOT_info, 0); + i->set_src1(src1); + i->set_src2(def2->src1.value); + return true; + } else if (def1->opcode == &OPCODE_NOT_info) { + // Swap operands and fold src1's NOT into AND_NOT + i->Replace(&OPCODE_AND_NOT_info, 0); + i->set_src1(src2); + i->set_src2(def1->src1.value); + return true; + } + + return false; +} + bool SimplificationPass::SimplifySubArith(hir::Instr* i, hir::HIRBuilder* builder) { /* diff --git a/src/xenia/cpu/compiler/passes/simplification_pass.h b/src/xenia/cpu/compiler/passes/simplification_pass.h index 1e69951e5..d9c8fc092 100644 --- a/src/xenia/cpu/compiler/passes/simplification_pass.h +++ b/src/xenia/cpu/compiler/passes/simplification_pass.h @@ -42,6 +42,7 @@ class SimplificationPass : public ConditionalGroupSubpass { bool SimplifyAddWithSHL(hir::Instr* i, hir::HIRBuilder* builder); bool SimplifyAddToSelf(hir::Instr* i, hir::HIRBuilder* builder); bool SimplifyAddArith(hir::Instr* i, hir::HIRBuilder* builder); + bool SimplifyAndNot(hir::Instr* i, hir::HIRBuilder* builder); bool SimplifySubArith(hir::Instr* i, hir::HIRBuilder* builder); bool SimplifySHLArith(hir::Instr* i, hir::HIRBuilder* builder); // handle either or or xor with 0 diff --git a/src/xenia/cpu/testing/opcode_coverage_test.cc b/src/xenia/cpu/testing/opcode_coverage_test.cc index db1ece2ea..c70b051ab 100644 --- a/src/xenia/cpu/testing/opcode_coverage_test.cc +++ b/src/xenia/cpu/testing/opcode_coverage_test.cc @@ -491,8 +491,100 @@ TEST_CASE("ATOMIC_COMPARE_EXCHANGE_I32", "[atomic]") { // ============================================================================ // AND_NOT — bitwise AND with complement of second operand // ============================================================================ +TEST_CASE("AND_NOT_I8", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 2, + b.ZeroExtend(b.And(b.Truncate(LoadGPR(b, 4), INT8_TYPE), + b.Not(b.Truncate(LoadGPR(b, 5), INT8_TYPE))), + INT64_TYPE)); + StoreGPR(b, 3, + b.ZeroExtend(b.AndNot(b.Truncate(LoadGPR(b, 4), INT8_TYPE), + b.Truncate(LoadGPR(b, 5), INT8_TYPE)), + INT64_TYPE)); + b.Return(); + }); + // result = src1 & ~src2 + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFF; + ctx->r[5] = 0x0F; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0xF0); + }); + // All bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xAA; + ctx->r[5] = 0xFF; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0x00); + }); + // No bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x12; + ctx->r[5] = 0x00; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0x12); + }); +} + +TEST_CASE("AND_NOT_I16", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 2, + b.ZeroExtend(b.And(b.Truncate(LoadGPR(b, 4), INT16_TYPE), + b.Not(b.Truncate(LoadGPR(b, 5), INT16_TYPE))), + INT64_TYPE)); + StoreGPR(b, 3, + b.ZeroExtend(b.AndNot(b.Truncate(LoadGPR(b, 4), INT16_TYPE), + b.Truncate(LoadGPR(b, 5), INT16_TYPE)), + INT64_TYPE)); + b.Return(); + }); + // result = src1 & ~src2 + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFF00; + ctx->r[5] = 0x0F0F; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0xF000); + }); + // All bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xAAAA; + ctx->r[5] = 0xFFFF; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0x0000); + }); + // No bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x1234; + ctx->r[5] = 0x0000; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0x1234); + }); +} + TEST_CASE("AND_NOT_I32", "[bitwise]") { TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 2, + b.ZeroExtend(b.And(b.Truncate(LoadGPR(b, 4), INT32_TYPE), + b.Not(b.Truncate(LoadGPR(b, 5), INT32_TYPE))), + INT64_TYPE)); StoreGPR(b, 3, b.ZeroExtend(b.AndNot(b.Truncate(LoadGPR(b, 4), INT32_TYPE), b.Truncate(LoadGPR(b, 5), INT32_TYPE)), @@ -506,6 +598,7 @@ TEST_CASE("AND_NOT_I32", "[bitwise]") { ctx->r[5] = 0x0F0F0F0F; }, [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); REQUIRE(static_cast(ctx->r[3]) == 0xF000F000); }); // All bits masked out. @@ -515,6 +608,7 @@ TEST_CASE("AND_NOT_I32", "[bitwise]") { ctx->r[5] = 0xFFFFFFFF; }, [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); REQUIRE(static_cast(ctx->r[3]) == 0x00000000); }); // No bits masked out. @@ -524,10 +618,87 @@ TEST_CASE("AND_NOT_I32", "[bitwise]") { ctx->r[5] = 0x00000000; }, [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); REQUIRE(static_cast(ctx->r[3]) == 0x12345678); }); } +TEST_CASE("AND_NOT_I64", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 2, b.And(LoadGPR(b, 4), b.Not(LoadGPR(b, 5)))); + StoreGPR(b, 3, b.AndNot(LoadGPR(b, 4), LoadGPR(b, 5))); + b.Return(); + }); + // result = src1 & ~src2 + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFF00FF00FF00FF00; + ctx->r[5] = 0x0F0F0F0F0F0F0F0F; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(ctx->r[3] == 0xF000F000F000F000); + }); + // All bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xAAAAAAAAAAAAAAAA; + ctx->r[5] = 0xFFFFFFFFFFFFFFFF; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(ctx->r[3] == 0x0000000000000000); + }); + // No bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x1234567812345678; + ctx->r[5] = 0x0000000000000000; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(ctx->r[3] == 0x1234567812345678); + }); +} + +TEST_CASE("AND_NOT_V128", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreVR(b, 2, b.And(LoadVR(b, 4), b.Not(LoadVR(b, 5)))); + StoreVR(b, 3, b.AndNot(LoadVR(b, 4), LoadVR(b, 5))); + b.Return(); + }); + // result = src1 & ~src2 + test.Run( + [](PPCContext* ctx) { + ctx->v[4] = vec128s(0xFF00); + ctx->v[5] = vec128s(0x0F0F); + }, + [](PPCContext* ctx) { + REQUIRE(ctx->v[2] == ctx->v[3]); + REQUIRE(ctx->v[3] == vec128s(0xF000)); + }); + // All bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->v[4] = vec128b(0xAA); + ctx->v[5] = vec128b(0xFF); + }, + [](PPCContext* ctx) { + REQUIRE(ctx->v[2] == ctx->v[3]); + REQUIRE(ctx->v[3] == vec128b(0x00)); + }); + // No bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->v[4] = vec128i(0x12345678); + ctx->v[5] = vec128i(0x00000000); + }, + [](PPCContext* ctx) { + REQUIRE(ctx->v[2] == ctx->v[3]); + REQUIRE(ctx->v[3] == vec128i(0x12345678)); + }); +} + // ============================================================================ // TRUNCATE — integer narrowing // ============================================================================ From 7e39a7018f865b80082fb7bd53ad881a20af162c Mon Sep 17 00:00:00 2001 From: Gliniak <153369+Gliniak@users.noreply.github.com> Date: Tue, 31 Mar 2026 20:43:49 +0200 Subject: [PATCH 03/25] [Memory] Added heap offset to alignment guards - This was causing page deallocation on proper allocations --- src/xenia/memory.cc | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/xenia/memory.cc b/src/xenia/memory.cc index d3d101784..1d4f15597 100644 --- a/src/xenia/memory.cc +++ b/src/xenia/memory.cc @@ -980,7 +980,7 @@ bool BaseHeap::AllocFixed(uint32_t base_address, uint32_t size, uint32_t protect) { alignment = xe::round_up(alignment, page_size_); size = xe::align(size, alignment); - assert_true(base_address % alignment == 0); + assert_true((base_address + host_address_offset_) % alignment == 0); uint32_t page_count = get_page_count(size, page_size_); uint32_t start_page_number = (base_address - heap_base_) / page_size_; uint32_t end_page_number = start_page_number + page_count - 1; @@ -1691,8 +1691,8 @@ bool PhysicalHeap::Alloc(uint32_t size, uint32_t alignment, // Given the address we've reserved in the parent heap, pin that here. // Shouldn't be possible for it to be allocated already. - uint32_t address = heap_base_ + parent_address - parent_heap_start; - if (address % alignment != 0) { + const uint32_t address = heap_base_ + parent_address - parent_heap_start; + if ((address + host_address_offset_) % alignment != 0) { XELOGE( "PhysicalHeap::Alloc translated address {:08X} misaligned " "(alignment {:08X}, physical base offset {:08X})", @@ -1734,9 +1734,9 @@ bool PhysicalHeap::AllocFixed(uint32_t base_address, uint32_t size, // Given the address we've reserved in the parent heap, pin that here. // Shouldn't be possible for it to be allocated already. - uint32_t address = + const uint32_t address = heap_base_ + parent_base_address - GetPhysicalAddress(heap_base_); - if (address % alignment != 0) { + if ((address + host_address_offset_) % alignment != 0) { XELOGE( "PhysicalHeap::AllocFixed translated address {:08X} misaligned " "(alignment {:08X}, physical base offset {:08X})", @@ -1780,12 +1780,11 @@ bool PhysicalHeap::AllocRange(uint32_t low_address, uint32_t high_address, "PhysicalHeap::Alloc unable to alloc physical memory in parent heap"); return false; } - // Given the address we've reserved in the parent heap, pin that here. // Shouldn't be possible for it to be allocated already. - uint32_t address = + const uint32_t address = heap_base_ + parent_address - GetPhysicalAddress(heap_base_); - if (address % alignment != 0) { + if ((address + host_address_offset_) % alignment != 0) { XELOGE( "PhysicalHeap::AllocRange translated address {:08X} misaligned " "(alignment {:08X}, physical base offset {:08X})", From 9c00ce93661566c3ed25967fe0f0e9197e9d48d2 Mon Sep 17 00:00:00 2001 From: goldislead <69987043+goldislead@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:17:43 -0700 Subject: [PATCH 04/25] [GPU] EVENT_WRITE_ZPD batched sample accumulation cvar --- src/xenia/gpu/gpu_flags.cc | 5 +++++ src/xenia/gpu/gpu_flags.h | 2 ++ src/xenia/gpu/pm4_command_processor_implement.h | 15 ++++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/xenia/gpu/gpu_flags.cc b/src/xenia/gpu/gpu_flags.cc index 8f99e4e06..9ed9335fa 100644 --- a/src/xenia/gpu/gpu_flags.cc +++ b/src/xenia/gpu/gpu_flags.cc @@ -59,6 +59,11 @@ DEFINE_bool( "when MSAA is used with fullscreen passes.", "GPU"); +DEFINE_bool(query_occlusion_batched, false, + "Increment the sample count total by a fixed amount on every " + "EVENT_WRITE_ZPD. This provides an approximate batched occlusion " + "query implementation for many titles.", + "GPU"); DEFINE_int32(query_occlusion_sample_lower_threshold, 80, "If set to -1 no sample counts are written, games may hang. Else, " "the sample count of every tile will be incremented on every " diff --git a/src/xenia/gpu/gpu_flags.h b/src/xenia/gpu/gpu_flags.h index 5dbc34f09..1778371eb 100644 --- a/src/xenia/gpu/gpu_flags.h +++ b/src/xenia/gpu/gpu_flags.h @@ -26,6 +26,8 @@ DECLARE_bool(non_seamless_cube_map); DECLARE_bool(half_pixel_offset); +DECLARE_bool(query_occlusion_batched); + DECLARE_int32(query_occlusion_sample_lower_threshold); DECLARE_int32(query_occlusion_sample_upper_threshold); diff --git a/src/xenia/gpu/pm4_command_processor_implement.h b/src/xenia/gpu/pm4_command_processor_implement.h index 0072ecc7e..1302e13ab 100644 --- a/src/xenia/gpu/pm4_command_processor_implement.h +++ b/src/xenia/gpu/pm4_command_processor_implement.h @@ -955,6 +955,7 @@ bool COMMAND_PROCESSOR::ExecutePacketType3_EVENT_WRITE_EXT( } static uint32_t samples = cvars::query_occlusion_sample_upper_threshold; +static uint32_t batched_samples = 0; XE_NOINLINE bool COMMAND_PROCESSOR::ExecutePacketType3_EVENT_WRITE_ZPD( @@ -982,7 +983,19 @@ bool COMMAND_PROCESSOR::ExecutePacketType3_EVENT_WRITE_ZPD( bool is_end_via_z_fail = pSampleCounts->ZFail_A == kQueryFinished && pSampleCounts->ZFail_B == kQueryFinished; std::memset(pSampleCounts, 0, sizeof(xe_gpu_depth_sample_counts)); - if (is_end_via_z_pass || is_end_via_z_fail) { + + // Titles that use QueryBatch (4D5309B1, 4E4D0801) don't use an END result for + // every query. Each ISSUE snapshots the sample count into a new slot and + // recovers the total by looking at differences between slots. + // END detection isn't sufficient here. + if (cvars::query_occlusion_batched) { + // Mimic batched behavior by reporting a running count and writing it on + // every event. + const uint32_t step = std::max(uint32_t(1), samples); + batched_samples = std::min(batched_samples, UINT32_MAX - step) + step; + pSampleCounts->ZPass_A = batched_samples; + pSampleCounts->Total_A = batched_samples; + } else if (is_end_via_z_pass || is_end_via_z_fail) { pSampleCounts->ZPass_A = samples; pSampleCounts->Total_A = samples; } From 8a49c0380f1e4dd47538a8d0e3da9cc454786428 Mon Sep 17 00:00:00 2001 From: goldislead <69987043+goldislead@users.noreply.github.com> Date: Sat, 4 Apr 2026 12:26:34 -0700 Subject: [PATCH 05/25] [GPU] EVENT_WRITE_ZPD relaxed END detection Fixes culling flicker in 555307D5. --- src/xenia/gpu/pm4_command_processor_implement.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xenia/gpu/pm4_command_processor_implement.h b/src/xenia/gpu/pm4_command_processor_implement.h index 1302e13ab..62b6b07cc 100644 --- a/src/xenia/gpu/pm4_command_processor_implement.h +++ b/src/xenia/gpu/pm4_command_processor_implement.h @@ -977,10 +977,10 @@ bool COMMAND_PROCESSOR::ExecutePacketType3_EVENT_WRITE_ZPD( register_file_->values[XE_GPU_REG_RB_SAMPLE_COUNT_ADDR]); // 0xFFFFFEED is written to this two locations by D3D only on D3DISSUE_END // and used to detect a finished query. - bool is_end_via_z_pass = pSampleCounts->ZPass_A == kQueryFinished && + bool is_end_via_z_pass = pSampleCounts->ZPass_A == kQueryFinished || pSampleCounts->ZPass_B == kQueryFinished; // Older versions of D3D also checks for ZFail (4D5307D5). - bool is_end_via_z_fail = pSampleCounts->ZFail_A == kQueryFinished && + bool is_end_via_z_fail = pSampleCounts->ZFail_A == kQueryFinished || pSampleCounts->ZFail_B == kQueryFinished; std::memset(pSampleCounts, 0, sizeof(xe_gpu_depth_sample_counts)); From ade7e610bb2ea7da1cc18a7940285d21289a4e83 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 6 Apr 2026 09:06:53 +0900 Subject: [PATCH 06/25] [XMA] Fix stall detection false positive in Work loop Stall detection was triggering during multi-pass subframe consumption (e.g. stereo with subframe_decode_count < total subframes), breaking audio looping in games like Tomb Raider. Now only detects a stall when no subframes were pending, so Consume-only iterations aren't mistaken for no-progress cycles. Fixes Halo 4 without regressing Tomb Raider. --- src/xenia/apu/xma_context_new.cc | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/xenia/apu/xma_context_new.cc b/src/xenia/apu/xma_context_new.cc index 253206bff..e43fc50d0 100644 --- a/src/xenia/apu/xma_context_new.cc +++ b/src/xenia/apu/xma_context_new.cc @@ -176,6 +176,9 @@ bool XmaContextNew::Work() { data.output_buffer_valid, data.subframe_decode_count, data.output_buffer_padding); + const uint32_t pre_decode_offset = data.input_buffer_read_offset; + const uint8_t pre_remaining_subframes = current_frame_remaining_subframes_; + Decode(&data); Consume(&output_rb, &data); @@ -185,6 +188,21 @@ bool XmaContextNew::Work() { id(), data.IsAnyInputBufferValid(), data.error_status); break; } + + // If Decode didn't advance the read offset and produced no new frame, + // we can't make progress. Break to avoid spinning. + // Only check when there were no pending subframes — if we entered this + // iteration with subframes remaining, Decode() intentionally skipped + // (offset unchanged) while Consume() drained the frame. + if (pre_remaining_subframes == 0 && + data.input_buffer_read_offset == pre_decode_offset && + current_frame_remaining_subframes_ == 0) { + XELOGAPU( + "XmaContext {}: Decode stalled at offset {} (no progress), " + "waiting for next buffer", + id(), pre_decode_offset); + break; + } } data.output_buffer_write_offset = From 4acda223db8b814445deac9727d54a0b9e61f231 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 8 Apr 2026 00:05:29 +0900 Subject: [PATCH 07/25] [CPU] Remove ATOMIC_EXCHANGE opcode (dead code) --- src/xenia/cpu/backend/a64/a64_seq_memory.cc | 95 ------------------- src/xenia/cpu/backend/x64/x64_seq_memory.cc | 62 ------------ src/xenia/cpu/hir/hir_builder.cc | 11 --- src/xenia/cpu/hir/hir_builder.h | 1 - src/xenia/cpu/hir/opcodes.h | 1 - src/xenia/cpu/hir/opcodes.inl | 6 -- .../cpu/testing/backend_integration_test.cc | 39 -------- 7 files changed, 215 deletions(-) diff --git a/src/xenia/cpu/backend/a64/a64_seq_memory.cc b/src/xenia/cpu/backend/a64/a64_seq_memory.cc index 501fb74f3..47d04a71f 100644 --- a/src/xenia/cpu/backend/a64/a64_seq_memory.cc +++ b/src/xenia/cpu/backend/a64/a64_seq_memory.cc @@ -867,101 +867,6 @@ struct MEMSET_I64 EMITTER_OPCODE_TABLE(OPCODE_MEMSET, MEMSET_I64); // ============================================================================ -// OPCODE_ATOMIC_EXCHANGE -// ============================================================================ -// Note: src1 is a HOST address (not guest), matching the x64 backend. -struct ATOMIC_EXCHANGE_I8 - : Sequence> { - static void Emit(A64Emitter& e, const EmitArgType& i) { - // src1 is already a host address. - if (i.src1.is_constant) { - e.mov(e.x4, i.src1.constant()); - } else { - e.mov(e.x4, i.src1); - } - if (i.src2.is_constant) { - e.mov(e.w0, static_cast( - static_cast(i.src2.constant()) & 0xFF)); - } else { - e.and_(e.w0, i.src2, 0xFF); - } - - if (e.IsFeatureEnabled(kA64EmitLSE)) { - e.swpalb(e.w0, i.dest, ptr(e.x4)); - return; - } - - auto& retry = e.NewCachedLabel(); - e.L(retry); - e.ldaxrb(e.w1, ptr(e.x4)); - e.stlxrb(e.w2, e.w0, ptr(e.x4)); - e.cbnz(e.w2, retry); - e.mov(i.dest, e.w1); - } -}; -struct ATOMIC_EXCHANGE_I16 - : Sequence> { - static void Emit(A64Emitter& e, const EmitArgType& i) { - if (i.src1.is_constant) { - e.mov(e.x4, i.src1.constant()); - } else { - e.mov(e.x4, i.src1); - } - if (i.src2.is_constant) { - e.mov(e.w0, static_cast( - static_cast(i.src2.constant()) & 0xFFFF)); - } else { - e.and_(e.w0, i.src2, 0xFFFF); - } - - if (e.IsFeatureEnabled(kA64EmitLSE)) { - e.swpalh(e.w0, i.dest, ptr(e.x4)); - return; - } - - auto& retry = e.NewCachedLabel(); - e.L(retry); - e.ldaxrh(e.w1, ptr(e.x4)); - e.stlxrh(e.w2, e.w0, ptr(e.x4)); - e.cbnz(e.w2, retry); - e.mov(i.dest, e.w1); - } -}; -struct ATOMIC_EXCHANGE_I32 - : Sequence> { - static void Emit(A64Emitter& e, const EmitArgType& i) { - // src1 is a host address (not guest). - if (i.src1.is_constant) { - e.mov(e.x4, i.src1.constant()); - } else { - e.mov(e.x4, i.src1); - } - if (i.src2.is_constant) { - e.mov(e.w0, - static_cast(static_cast(i.src2.constant()))); - } else { - e.mov(e.w0, i.src2); - } - - if (e.IsFeatureEnabled(kA64EmitLSE)) { - e.swpal(e.w0, i.dest, ptr(e.x4)); - return; - } - - auto& retry = e.NewCachedLabel(); - e.L(retry); - e.ldaxr(e.w1, ptr(e.x4)); - e.stlxr(e.w2, e.w0, ptr(e.x4)); - e.cbnz(e.w2, retry); - e.mov(i.dest, e.w1); - } -}; -EMITTER_OPCODE_TABLE(OPCODE_ATOMIC_EXCHANGE, ATOMIC_EXCHANGE_I8, - ATOMIC_EXCHANGE_I16, ATOMIC_EXCHANGE_I32); - // ============================================================================ // OPCODE_ATOMIC_COMPARE_EXCHANGE // ============================================================================ diff --git a/src/xenia/cpu/backend/x64/x64_seq_memory.cc b/src/xenia/cpu/backend/x64/x64_seq_memory.cc index b543a57b8..ffccd4c31 100644 --- a/src/xenia/cpu/backend/x64/x64_seq_memory.cc +++ b/src/xenia/cpu/backend/x64/x64_seq_memory.cc @@ -295,68 +295,6 @@ RegExp ComputeMemoryAddressOffset(X64Emitter& e, const T& guest, } } -// ============================================================================ -// OPCODE_ATOMIC_EXCHANGE -// ============================================================================ -// Note that the address we use here is a real, host address! -// This is weird, and should be fixed. -template -void EmitAtomicExchangeXX(X64Emitter& e, const ARGS& i) { - if (i.dest == i.src1) { - e.mov(e.rax, i.src1); - if (i.dest != i.src2) { - if (i.src2.is_constant) { - e.mov(i.dest, i.src2.constant()); - } else { - e.mov(i.dest, i.src2); - } - } - e.lock(); - e.xchg(e.dword[e.rax], i.dest); - } else { - if (i.dest != i.src2) { - if (i.src2.is_constant) { - e.mov(i.dest, i.src2.constant()); - } else { - e.mov(i.dest, i.src2); - } - } - e.lock(); - e.xchg(e.dword[i.src1.reg()], i.dest); - } -} -struct ATOMIC_EXCHANGE_I8 - : Sequence> { - static void Emit(X64Emitter& e, const EmitArgType& i) { - EmitAtomicExchangeXX(e, i); - } -}; -struct ATOMIC_EXCHANGE_I16 - : Sequence> { - static void Emit(X64Emitter& e, const EmitArgType& i) { - EmitAtomicExchangeXX(e, i); - } -}; -struct ATOMIC_EXCHANGE_I32 - : Sequence> { - static void Emit(X64Emitter& e, const EmitArgType& i) { - EmitAtomicExchangeXX(e, i); - } -}; -struct ATOMIC_EXCHANGE_I64 - : Sequence> { - static void Emit(X64Emitter& e, const EmitArgType& i) { - EmitAtomicExchangeXX(e, i); - } -}; -EMITTER_OPCODE_TABLE(OPCODE_ATOMIC_EXCHANGE, ATOMIC_EXCHANGE_I8, - ATOMIC_EXCHANGE_I16, ATOMIC_EXCHANGE_I32, - ATOMIC_EXCHANGE_I64); - struct LVL_V128 : Sequence> { static void Emit(X64Emitter& e, const EmitArgType& i) { e.mov(e.edx, 0xf); diff --git a/src/xenia/cpu/hir/hir_builder.cc b/src/xenia/cpu/hir/hir_builder.cc index 44ac5d936..26bd58195 100644 --- a/src/xenia/cpu/hir/hir_builder.cc +++ b/src/xenia/cpu/hir/hir_builder.cc @@ -2225,17 +2225,6 @@ Value* HIRBuilder::Unpack(Value* value, uint32_t pack_flags) { return i->dest; } -Value* HIRBuilder::AtomicExchange(Value* address, Value* new_value) { - ASSERT_ADDRESS_TYPE(address); - ASSERT_INTEGER_TYPE(new_value); - Instr* i = - AppendInstr(OPCODE_ATOMIC_EXCHANGE_info, 0, AllocValue(new_value->type)); - i->set_src1(address); - i->set_src2(new_value); - i->src3.value = NULL; - return i->dest; -} - Value* HIRBuilder::AtomicCompareExchange(Value* address, Value* old_value, Value* new_value) { ASSERT_ADDRESS_TYPE(address); diff --git a/src/xenia/cpu/hir/hir_builder.h b/src/xenia/cpu/hir/hir_builder.h index 2621148ef..e8a80415a 100644 --- a/src/xenia/cpu/hir/hir_builder.h +++ b/src/xenia/cpu/hir/hir_builder.h @@ -297,7 +297,6 @@ class HIRBuilder { Value* Pack(Value* value1, Value* value2, uint32_t pack_flags = 0); Value* Unpack(Value* value, uint32_t pack_flags = 0); - Value* AtomicExchange(Value* address, Value* new_value); Value* AtomicCompareExchange(Value* address, Value* old_value, Value* new_value); Value* AtomicAdd(Value* address, Value* value); diff --git a/src/xenia/cpu/hir/opcodes.h b/src/xenia/cpu/hir/opcodes.h index 5a1bdc53b..43be8810a 100644 --- a/src/xenia/cpu/hir/opcodes.h +++ b/src/xenia/cpu/hir/opcodes.h @@ -282,7 +282,6 @@ enum Opcode { OPCODE_PACK, // break up into smaller operations and add a float16 convert // opcode OPCODE_UNPACK, - OPCODE_ATOMIC_EXCHANGE, OPCODE_ATOMIC_COMPARE_EXCHANGE, OPCODE_SET_ROUNDING_MODE, OPCODE_VECTOR_DENORMFLUSH, // converts denormals to signed zeros in a vector diff --git a/src/xenia/cpu/hir/opcodes.inl b/src/xenia/cpu/hir/opcodes.inl index c5c089e85..255b4fb80 100644 --- a/src/xenia/cpu/hir/opcodes.inl +++ b/src/xenia/cpu/hir/opcodes.inl @@ -650,12 +650,6 @@ DEFINE_OPCODE( OPCODE_SIG_V_V, 0) -DEFINE_OPCODE( - OPCODE_ATOMIC_EXCHANGE, - "atomic_exchange", - OPCODE_SIG_V_V_V, - OPCODE_FLAG_VOLATILE) - DEFINE_OPCODE( OPCODE_ATOMIC_COMPARE_EXCHANGE, "atomic_compare_exchange", diff --git a/src/xenia/cpu/testing/backend_integration_test.cc b/src/xenia/cpu/testing/backend_integration_test.cc index fdcf9414a..e907b0bf7 100644 --- a/src/xenia/cpu/testing/backend_integration_test.cc +++ b/src/xenia/cpu/testing/backend_integration_test.cc @@ -646,45 +646,6 @@ TEST_CASE("SET_NJM_OFF", "[backend]") { #endif } -// ============================================================================= -// Atomic Exchange I32 -// ============================================================================= -// Tests that AtomicExchange correctly swaps a value in memory and returns -// the old value. -// NOTE: OPCODE_ATOMIC_EXCHANGE uses a HOST address (not guest), per the -// x64 backend comment: "the address we use here is a real, host address!" -TEST_CASE("ATOMIC_EXCHANGE_I32", "[backend]") { - TestFunction test([](HIRBuilder& b) { - // r[4] holds the host address directly. - auto addr = LoadGPR(b, 4); - auto new_val = b.Truncate(LoadGPR(b, 5), hir::INT32_TYPE); - auto old_val = b.AtomicExchange(addr, new_val); - StoreGPR(b, 3, b.ZeroExtend(old_val, hir::INT64_TYPE)); - b.Return(); - }); - - // Allocate guest memory and compute the host pointer. - uint32_t guest_addr = test.memory->SystemHeapAlloc(4); - REQUIRE(guest_addr != 0); - auto* host_ptr = test.memory->TranslateVirtual(guest_addr); - - test.Run( - [&](PPCContext* ctx) { - *reinterpret_cast(host_ptr) = 0xAABBCCDD; - // Pass the HOST address in r[4]. - ctx->r[4] = reinterpret_cast(host_ptr); - ctx->r[5] = 0x11223344; - }, - [&](PPCContext* ctx) { - // r[3] should have the old value. - REQUIRE(static_cast(ctx->r[3]) == 0xAABBCCDD); - // Memory should now have the new value. - REQUIRE(*reinterpret_cast(host_ptr) == 0x11223344); - }); - - test.memory->SystemHeapFree(guest_addr); -} - // ============================================================================= // DOT_PRODUCT_3 — inline NEON dot product of first 3 vector elements // ============================================================================= From e23376afccc1a5147e7b0687d799cf8df41d4617 Mon Sep 17 00:00:00 2001 From: Adrian <78108584+AdrianCassar@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:40:57 +0100 Subject: [PATCH 08/25] [Kernel] Fixed NtSignalAndWaitForSingleObjectEx prototype --- src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc index 593a00e9c..6dec1c20e 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc @@ -1099,8 +1099,8 @@ DECLARE_XBOXKRNL_EXPORT3(NtWaitForMultipleObjectsEx, kThreading, kImplemented, dword_result_t NtSignalAndWaitForSingleObjectEx_entry(dword_t signal_handle, dword_t wait_handle, + dword_t wait_mode, dword_t alertable, - dword_t r6, lpqword_t timeout_ptr) { X_STATUS result = X_STATUS_SUCCESS; // pre-lock for these two handle lookups @@ -1113,9 +1113,9 @@ dword_result_t NtSignalAndWaitForSingleObjectEx_entry(dword_t signal_handle, global_critical_region::mutex().unlock(); if (signal_object && wait_object) { uint64_t timeout = timeout_ptr ? static_cast(*timeout_ptr) : 0u; - result = - XObject::SignalAndWait(signal_object.get(), wait_object.get(), 3, 1, - alertable, timeout_ptr ? &timeout : nullptr); + result = XObject::SignalAndWait(signal_object.get(), wait_object.get(), 3, + wait_mode, alertable, + timeout_ptr ? &timeout : nullptr); } else { result = X_STATUS_INVALID_HANDLE; } From 61c8eb07070887af566fdc972533365c7485fe24 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:14:39 +0900 Subject: [PATCH 09/25] [Threading] Improve same-CPU spinlock contention Spinlock acquire now checks if the lock holder shares the same guest CPU and yields more aggressively (Sleep(0)) when contending on the same Xenon HW thread, which should better approximate real kernel's implicit serialization. Child threads without an explicit affinity mask now inherit the parent's guest CPU assignment instead of round-robining, so the spinlock check correctly identifies parent-child co-location. --- .../kernel/xboxkrnl/xboxkrnl_threading.cc | 31 ++++++++++++++++--- src/xenia/kernel/xthread.cc | 16 ++++++---- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc index 6dec1c20e..83bb83e18 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc @@ -1138,11 +1138,34 @@ uint32_t xeKeKfAcquireSpinLock(PPCContext* ctx, X_KSPINLOCK* lock, PrefetchForCAS(lock); assert_true(lock->prcb_of_owner != static_cast(ctx->r[13])); + + uint32_t our_pcr = static_cast(ctx->r[13]); + uint8_t our_cpu = + ctx->TranslateVirtualGPR(our_pcr)->prcb_data.current_cpu; + // Lock. - while (!xe::atomic_cas(0, xe::byte_swap(static_cast(ctx->r[13])), - &lock->prcb_of_owner.value)) { - // Spin! - // TODO(benvanik): error on deadlock? + while ( + !xe::atomic_cas(0, xe::byte_swap(our_pcr), &lock->prcb_of_owner.value)) { + // On real hardware, threads sharing a Xenon HW thread are serialized by + // the kernel scheduler — the spinner would be preempted within one + // timeslice (~1ms) so the holder can make progress. In the naive + // host-thread model both threads run truly in parallel, so the spinner + // can burn its entire host quantum without giving the holder a chance. + // + // Check whether the lock holder is assigned to the same guest CPU as us. + // If so, yield the host thread aggressively (Sleep(0)) to force a host + // context switch and give the holder a chance to run and release. + // The relationship is stable — affinity doesn't change while a thread + // holds a spinlock — so one check per contention episode is sufficient. + uint32_t owner_pcr_be = lock->prcb_of_owner.value; + if (owner_pcr_be) { + uint32_t owner_pcr = xe::byte_swap(owner_pcr_be); + auto* owner_kpcr = ctx->TranslateVirtual(owner_pcr); + if (owner_kpcr->prcb_data.current_cpu == our_cpu) { + xe::threading::Sleep(std::chrono::milliseconds(0)); + continue; + } + } xe::threading::MaybeYield(); } diff --git a/src/xenia/kernel/xthread.cc b/src/xenia/kernel/xthread.cc index 19be6a6cd..f7ec64bcb 100644 --- a/src/xenia/kernel/xthread.cc +++ b/src/xenia/kernel/xthread.cc @@ -155,13 +155,17 @@ static uint8_t next_cpu = 0; static uint8_t GetFakeCpuNumber(uint8_t proc_mask) { // NOTE: proc_mask is logical processors, not physical processors or cores. if (!proc_mask) { - next_cpu = (next_cpu + 1) % 6; - return next_cpu; // is this reasonable? - // TODO(Triang3l): Does the following apply here? + // On Xbox 360, threads without an explicit processor assignment stay on + // the same hardware thread as the parent. Preserve this so that the + // guest CPU assignment reflects the game's intent — parent-child thread + // pairs that share a HW thread may rely on implicit serialization. // https://docs.microsoft.com/en-us/windows/win32/dxtecharts/coding-for-multiple-cores - // "On Xbox 360, you must explicitly assign software threads to a particular - // hardware thread by using XSetThreadProcessor. Otherwise, all child - // threads will stay on the same hardware thread as the parent." + XThread* parent = current_xthread_tls_; + if (parent) { + return parent->active_cpu(); + } + next_cpu = (next_cpu + 1) % 6; + return next_cpu; } assert_false(proc_mask & 0xC0); From b3d8a21b727fdab5ff77fb88b5152041e53de9b8 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Thu, 9 Apr 2026 22:33:08 +0900 Subject: [PATCH 10/25] [Threading] Add thread priority mapping with timer-driven quantum decay And default ignore_thread_priorities to false. Map Xenon's 0-31 priority range across all 5 host priority levels instead of collapsing 0-17 into kNormal. Use timer-driven quantum decay (~20ms period) matching Xenon's 60-quantum / 3-per-tick cycle to prevent starvation by gradually lowering effective priority for non-real-time threads (< 18), piggybacking on the existing 1ms timestamp timer. --- src/xenia/base/cvar.h | 2 +- src/xenia/kernel/kernel_state.cc | 12 +++ src/xenia/kernel/kernel_state.h | 1 + .../kernel/xboxkrnl/xboxkrnl_threading.cc | 38 +++++++- src/xenia/kernel/xobject.cc | 75 ++++++++++----- src/xenia/kernel/xthread.cc | 95 +++++++++++++++---- src/xenia/kernel/xthread.h | 15 ++- 7 files changed, 194 insertions(+), 44 deletions(-) diff --git a/src/xenia/base/cvar.h b/src/xenia/base/cvar.h index e174b3a70..c3a9f60b6 100644 --- a/src/xenia/base/cvar.h +++ b/src/xenia/base/cvar.h @@ -511,7 +511,7 @@ class IConfigVarUpdate { // If you're reviewing a pull request with a change here, check if 1) has been // done by the submitter before merging. static constexpr uint32_t kLastCommittedUpdateDate = - MakeConfigVarUpdateDate(2025, 12, 4, 21); + MakeConfigVarUpdateDate(2026, 4, 9, 12); virtual ~IConfigVarUpdate() = default; diff --git a/src/xenia/kernel/kernel_state.cc b/src/xenia/kernel/kernel_state.cc index 3bbad27be..32b138cc6 100644 --- a/src/xenia/kernel/kernel_state.cc +++ b/src/xenia/kernel/kernel_state.cc @@ -1151,6 +1151,18 @@ void KernelState::UpdateKeTimestampBundle() { xe::store_and_swap(&lpKeTimeStampBundle->system_time, Clock::QueryGuestSystemTime()); xe::store_and_swap(&lpKeTimeStampBundle->tick_count, uptime_ms); + + // Every 20 ticks (~20ms), decay priority on running guest threads. + // This simulates the Xenon decrementer-driven quantum expiration. + if (++quantum_timer_counter_ >= 20) { + quantum_timer_counter_ = 0; + auto global_lock = global_critical_region_.Acquire(); + for (auto& [id, thread] : threads_by_id_) { + if (thread->is_running()) { + thread->CheckQuantumAndDecay(); + } + } + } } uint32_t KernelState::GetKeTimestampBundle() { diff --git a/src/xenia/kernel/kernel_state.h b/src/xenia/kernel/kernel_state.h index 2e1a99a76..fb2bf33b8 100644 --- a/src/xenia/kernel/kernel_state.h +++ b/src/xenia/kernel/kernel_state.h @@ -384,6 +384,7 @@ class KernelState { BitMap tls_bitmap_; uint32_t ke_timestamp_bundle_ptr_ = 0; std::unique_ptr timestamp_timer_; + uint32_t quantum_timer_counter_ = 0; cpu::backend::GuestTrampolineGroup kernel_trampoline_group_; // fixed address referenced by dashboards. Data is currently unknown uint32_t strange_hardcoded_page_ = 0x8E038634 & (~0xFFFF); diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc index 83bb83e18..f524b49ab 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc @@ -1583,8 +1583,6 @@ DECLARE_XBOXKRNL_EXPORT2(KeInitializeDpc, kThreading, kImplemented, kSketchy); dword_result_t KeInsertQueueDpc_entry(pointer_t dpc, dword_t arg1, dword_t arg2) { - assert_always("DPC does not dispatch yet; going to hang!"); - uint32_t list_entry_ptr = dpc.guest_address() + 4; // Lock dispatcher. @@ -1602,9 +1600,43 @@ dword_result_t KeInsertQueueDpc_entry(pointer_t dpc, dword_t arg1, dpc_list->Insert(list_entry_ptr); + // Dispatch the DPC inline on the calling thread. On real hardware DPCs + // are deferred to DISPATCH_IRQL on the target processor, but DPC routines + // access per-CPU state via r13 (KPCR) so they must run on a thread whose + // KPCR is valid for the target CPU. The calling thread's KPCR satisfies + // this for the common case (desired_cpu_number == 0, meaning current CPU). + // Inline dispatch also avoids latency issues with shared work queues. + uint32_t routine = dpc->routine; + if (routine) { + auto thread = XThread::GetCurrentThread(); + if (thread) { + auto thread_state = thread->thread_state(); + auto ppc_context = thread_state->context(); + auto kpcr = ppc_context->TranslateVirtualGPR(ppc_context->r[13]); + + // If we're already inside a DPC (reentrant KeInsertQueueDpc from a DPC + // routine), skip the impersonation — we're already at DISPATCH_IRQL. + bool already_in_dpc = kpcr->prcb_data.dpc_active != 0; + + DPCImpersonationScope dpc_scope{}; + if (!already_in_dpc) { + kernel_state()->BeginDPCImpersonation(ppc_context, dpc_scope); + } + + uint64_t args[] = {dpc.guest_address(), (uint64_t)dpc->context, + (uint64_t)arg1, (uint64_t)arg2}; + kernel_state()->processor()->Execute(thread_state, routine, args, + xe::countof(args)); + + if (!already_in_dpc) { + kernel_state()->EndDPCImpersonation(ppc_context, dpc_scope); + } + } + } + return 1; } -DECLARE_XBOXKRNL_EXPORT2(KeInsertQueueDpc, kThreading, kStub, kSketchy); +DECLARE_XBOXKRNL_EXPORT2(KeInsertQueueDpc, kThreading, kImplemented, kSketchy); dword_result_t KeRemoveQueueDpc_entry(pointer_t dpc) { bool result = false; diff --git a/src/xenia/kernel/xobject.cc b/src/xenia/kernel/xobject.cc index 5d0148e29..83d2ec8ae 100644 --- a/src/xenia/kernel/xobject.cc +++ b/src/xenia/kernel/xobject.cc @@ -203,13 +203,21 @@ X_STATUS XObject::Wait(uint32_t wait_reason, uint32_t processor_mode, auto result = xe::threading::Wait(wait_handle, alertable ? true : false, timeout_ms); + switch (result) { case xe::threading::WaitResult::kSuccess: - WaitCallback(); - return X_STATUS_SUCCESS; - case xe::threading::WaitResult::kUserCallback: - // Or X_STATUS_ALERTED? + case xe::threading::WaitResult::kUserCallback: { + // Thread actually blocked and woke — reset priority to base. + auto current_thread = XThread::GetCurrentThread(); + if (current_thread) { + current_thread->ResetQuantum(); + } + if (result == xe::threading::WaitResult::kSuccess) { + WaitCallback(); + return X_STATUS_SUCCESS; + } return X_STATUS_USER_APC; + } case xe::threading::WaitResult::kTimeout: xe::threading::MaybeYield(); return X_STATUS_TIMEOUT; @@ -231,13 +239,20 @@ X_STATUS XObject::SignalAndWait(XObject* signal_object, XObject* wait_object, auto result = xe::threading::SignalAndWait( signal_object->GetWaitHandle(), wait_object->GetWaitHandle(), alertable ? true : false, timeout_ms); + switch (result) { case xe::threading::WaitResult::kSuccess: - wait_object->WaitCallback(); - return X_STATUS_SUCCESS; - case xe::threading::WaitResult::kUserCallback: - // Or X_STATUS_ALERTED? + case xe::threading::WaitResult::kUserCallback: { + auto current_thread = XThread::GetCurrentThread(); + if (current_thread) { + current_thread->ResetQuantum(); + } + if (result == xe::threading::WaitResult::kSuccess) { + wait_object->WaitCallback(); + return X_STATUS_SUCCESS; + } return X_STATUS_USER_APC; + } case xe::threading::WaitResult::kTimeout: xe::threading::MaybeYield(); return X_STATUS_TIMEOUT; @@ -264,25 +279,29 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects, TimeoutTicksToMs(*opt_timeout))) : std::chrono::milliseconds::max(); + X_STATUS status; if (wait_type) { auto result = xe::threading::WaitAny(wait_handles, count, alertable ? true : false, timeout_ms); switch (result.first) { case xe::threading::WaitResult::kSuccess: objects[result.second]->WaitCallback(); - - return X_STATUS(result.second); + status = X_STATUS(result.second); + break; case xe::threading::WaitResult::kUserCallback: - // Or X_STATUS_ALERTED? - return X_STATUS_USER_APC; + status = X_STATUS_USER_APC; + break; case xe::threading::WaitResult::kTimeout: xe::threading::MaybeYield(); - return X_STATUS_TIMEOUT; - default: + status = X_STATUS_TIMEOUT; + break; case xe::threading::WaitResult::kAbandoned: - return X_STATUS(X_STATUS_ABANDONED_WAIT_0 + result.second); + status = X_STATUS(X_STATUS_ABANDONED_WAIT_0 + result.second); + break; + default: case xe::threading::WaitResult::kFailed: - return X_STATUS_UNSUCCESSFUL; + status = X_STATUS_UNSUCCESSFUL; + break; } } else { auto result = xe::threading::WaitAll(wait_handles, count, @@ -292,20 +311,32 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects, for (uint32_t i = 0; i < count; i++) { objects[i]->WaitCallback(); } - - return X_STATUS_SUCCESS; + status = X_STATUS_SUCCESS; + break; case xe::threading::WaitResult::kUserCallback: - // Or X_STATUS_ALERTED? - return X_STATUS_USER_APC; + status = X_STATUS_USER_APC; + break; case xe::threading::WaitResult::kTimeout: xe::threading::MaybeYield(); - return X_STATUS_TIMEOUT; + status = X_STATUS_TIMEOUT; + break; default: case xe::threading::WaitResult::kAbandoned: case xe::threading::WaitResult::kFailed: - return X_STATUS_ABANDONED_WAIT_0; + status = X_STATUS_ABANDONED_WAIT_0; + break; } } + + // Only reset quantum if the thread actually blocked (not on timeout/failure). + if (status != X_STATUS_TIMEOUT && status != X_STATUS_UNSUCCESSFUL && + status != X_STATUS_ABANDONED_WAIT_0) { + auto current_thread = XThread::GetCurrentThread(); + if (current_thread) { + current_thread->ResetQuantum(); + } + } + return status; } uint8_t* XObject::CreateNative(uint32_t size) { diff --git a/src/xenia/kernel/xthread.cc b/src/xenia/kernel/xthread.cc index f7ec64bcb..49fd30d21 100644 --- a/src/xenia/kernel/xthread.cc +++ b/src/xenia/kernel/xthread.cc @@ -14,6 +14,7 @@ #endif #include "xenia/base/byte_stream.h" +#include "xenia/base/clock.h" #include "xenia/base/logging.h" #include "xenia/base/platform.h" #include "xenia/base/profiling.h" @@ -24,8 +25,9 @@ #include "xenia/kernel/user_module.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h" -DEFINE_bool(ignore_thread_priorities, true, +DEFINE_bool(ignore_thread_priorities, false, "Ignores game-specified thread priorities.", "Kernel"); +UPDATE_from_bool(ignore_thread_priorities, 2026, 4, 9, 12, true); DEFINE_bool(ignore_thread_affinities, true, "Ignores game-specified thread affinities.", "Kernel"); @@ -670,28 +672,87 @@ void XThread::RundownAPCs() { int32_t XThread::QueryPriority() { return thread_->priority(); } -void XThread::SetPriority(int32_t increment) { - if (is_guest_thread()) { - guest_object()->priority = static_cast(increment); - } - priority_ = increment; - int32_t target_priority = 0; - if (increment > 0x22) { - target_priority = xe::threading::ThreadPriority::kHighest; - } else if (increment > 0x11) { - target_priority = xe::threading::ThreadPriority::kAboveNormal; - } else if (increment < -0x22) { - target_priority = xe::threading::ThreadPriority::kLowest; - } else if (increment < -0x11) { - target_priority = xe::threading::ThreadPriority::kBelowNormal; +// Map Xenon's 0-31 priority range across the available host priority levels. +// Priority 18 (0x12) is the Xenon real-time threshold — threads at or above +// it don't get quantum decay on real hardware. +static int32_t GuestPriorityToHost(int32_t guest_priority) { + if (guest_priority >= 24) { + return xe::threading::ThreadPriority::kHighest; + } else if (guest_priority >= 17) { + return xe::threading::ThreadPriority::kAboveNormal; + } else if (guest_priority >= 10) { + return xe::threading::ThreadPriority::kNormal; + } else if (guest_priority >= 5) { + return xe::threading::ThreadPriority::kBelowNormal; } else { - target_priority = xe::threading::ThreadPriority::kNormal; + return xe::threading::ThreadPriority::kLowest; } +} + +void XThread::SetPriority(int32_t increment) { + // Clamp to valid Xenon priority range. Negative values can arrive via + // KeSetBasePriorityThread (signed offset from process base). + int32_t clamped = std::max(increment, 0); + if (is_guest_thread()) { + guest_object()->priority = static_cast(clamped); + } + priority_ = clamped; + base_priority_ = clamped; + quantum_start_ms_ = Clock::QueryHostUptimeMillis(); if (!cvars::ignore_thread_priorities) { - thread_->set_priority(target_priority); + thread_->set_priority(GuestPriorityToHost(clamped)); } } +void XThread::CheckQuantumAndDecay() { + if (cvars::ignore_thread_priorities) return; + // Real-time threads (priority >= 18) don't decay on Xenon. + if (priority_ >= 18) return; + + uint64_t now = Clock::QueryHostUptimeMillis(); + uint64_t elapsed = now - quantum_start_ms_; + // On Xenon, the clock interrupt fires every ~1ms and decrements the + // thread's quantum by 3. The process quantum is 60, so it takes ~20ms + // for quantum to expire. When it does, the scheduler decays the + // effective priority by exactly 1 and resets quantum. We approximate + // this by decaying 1 priority level per 20ms of elapsed wall-clock time. + constexpr uint64_t kQuantumPeriodMs = 20; + if (elapsed < kQuantumPeriodMs) return; + + // TODO(has207): The real kernel also subtracts the accumulated priority + // boost (boost_accumulator in X_KTHREAD) during decay: + // new_prio = priority - boost_accumulator - 1 + // We don't implement priority boosting on wait completion yet, so + // the boost accumulator is always effectively 0. When boost-on-wake + // is added, the accumulator needs to be drained here as well. + int32_t decay_steps = static_cast(elapsed / kQuantumPeriodMs); + int32_t new_priority = priority_ - decay_steps; + if (new_priority < base_priority_) { + new_priority = base_priority_; + } + if (new_priority != priority_) { + priority_ = new_priority; + if (is_guest_thread()) { + guest_object()->priority = static_cast(new_priority); + } + thread_->set_priority(GuestPriorityToHost(new_priority)); + } + quantum_start_ms_ = now; +} + +void XThread::ResetQuantum() { + if (cvars::ignore_thread_priorities) return; + if (priority_ != base_priority_) { + priority_ = base_priority_; + if (is_guest_thread()) { + guest_object()->priority = + static_cast(base_priority_); + } + thread_->set_priority(GuestPriorityToHost(base_priority_)); + } + quantum_start_ms_ = Clock::QueryHostUptimeMillis(); +} + void XThread::SetAffinity(uint32_t affinity) { SetActiveCpu(GetFakeCpuNumber(affinity)); } diff --git a/src/xenia/kernel/xthread.h b/src/xenia/kernel/xthread.h index dd0fa31b8..a2fc02020 100644 --- a/src/xenia/kernel/xthread.h +++ b/src/xenia/kernel/xthread.h @@ -422,6 +422,17 @@ class XThread : public XObject, public cpu::Thread { int32_t QueryPriority(); void SetPriority(int32_t increment); + // Called periodically (~20ms) by KernelState's timestamp timer to simulate + // the Xenon scheduler's quantum-based priority decay for non-real-time + // threads (priority < 18). Threads that run for longer than one quantum + // (~20ms) have their effective priority decayed toward the base, which + // causes them to drop into lower host priority buckets and prevents + // starvation. + void CheckQuantumAndDecay(); + // Resets effective priority back to base and restarts the quantum timer. + // Called when a thread wakes from a kernel wait. + void ResetQuantum(); + // Xbox thread IDs: // 0 - core 0, thread 0 - user // 1 - core 0, thread 1 - user @@ -491,7 +502,9 @@ class XThread : public XObject, public cpu::Thread { bool main_thread_ = false; // Entry-point thread bool running_ = false; - int32_t priority_ = 0; + int32_t priority_ = 0; // current effective priority (may be decayed) + int32_t base_priority_ = 0; // priority floor — decay never goes below this + uint64_t quantum_start_ms_ = 0; // host uptime (ms) when quantum last reset #if !XE_PLATFORM_WIN32 // Condition variable for thread self-suspension. From 65b74819aac85f188919b0a81902793585d0037a Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:05:05 +0900 Subject: [PATCH 11/25] [Threading] Implement priority boost on wake When a thread wakes from a kernel wait, the Xenon scheduler boosts its effective priority by the increment passed to the signaling call (KeSetEvent, KeReleaseSemaphore, KeReleaseMutant). The boost is clamped to the per-thread max_dynamic_priority cap, respects the guest boost_disabled flag, and is drained on the next quantum expiry. Guest KTHREAD priority fields are now initialized from parent process defaults, and the previously unknown fields involved have been renamed to match their identified purpose. --- src/xenia/kernel/kernel_state.cc | 11 +-- src/xenia/kernel/kernel_state.h | 13 +-- .../kernel/xboxkrnl/xboxkrnl_threading.cc | 3 +- src/xenia/kernel/xevent.cc | 2 + src/xenia/kernel/xmutant.cc | 2 + src/xenia/kernel/xobject.cc | 16 ++-- src/xenia/kernel/xobject.h | 8 ++ src/xenia/kernel/xthread.cc | 87 +++++++++++++++---- src/xenia/kernel/xthread.h | 32 ++++--- 9 files changed, 127 insertions(+), 47 deletions(-) diff --git a/src/xenia/kernel/kernel_state.cc b/src/xenia/kernel/kernel_state.cc index 32b138cc6..5b76f897a 100644 --- a/src/xenia/kernel/kernel_state.cc +++ b/src/xenia/kernel/kernel_state.cc @@ -1312,15 +1312,16 @@ void KernelState::EmulateCPInterruptDPC(uint32_t interrupt_callback, } void KernelState::InitializeProcess(X_KPROCESS* process, uint32_t type, - char unk_18, char unk_19, char unk_1A) { + char priority_class, char default_priority, + char max_dynamic_priority) { uint32_t guest_kprocess = memory()->HostToGuestVirtual(process); uint32_t thread_list_guest_ptr = guest_kprocess + offsetof(X_KPROCESS, thread_list); - process->unk_18 = unk_18; - process->unk_19 = unk_19; - process->unk_1A = unk_1A; + process->process_priority_class = priority_class; + process->default_thread_priority = default_priority; + process->max_dynamic_priority = max_dynamic_priority; util::XeInitializeListHead(&process->thread_list, thread_list_guest_ptr); process->quantum = 60; // doubt any guest code uses this ptr, which i think probably has something to @@ -1328,7 +1329,7 @@ void KernelState::InitializeProcess(X_KPROCESS* process, uint32_t type, process->clrdataa_masked_ptr = 0; // clrdataa_ & ~(1U << 31); process->thread_count = 0; - process->unk_1B = 0x06; + process->disable_quantum_decay = 0x06; process->kernel_stack_size = 16 * 1024; process->tls_slot_size = 0x80; diff --git a/src/xenia/kernel/kernel_state.h b/src/xenia/kernel/kernel_state.h index fb2bf33b8..0b5248943 100644 --- a/src/xenia/kernel/kernel_state.h +++ b/src/xenia/kernel/kernel_state.h @@ -66,10 +66,10 @@ struct X_KPROCESS { // so it sets this ptr to 0x1C0000 xe::be clrdataa_masked_ptr; xe::be thread_count; - uint8_t unk_18; - uint8_t unk_19; - uint8_t unk_1A; - uint8_t unk_1B; + uint8_t process_priority_class; + uint8_t default_thread_priority; + uint8_t max_dynamic_priority; + uint8_t disable_quantum_decay; xe::be kernel_stack_size; xe::be tls_static_data_address; xe::be tls_data_size; @@ -332,8 +332,9 @@ class KernelState { private: void LoadKernelModule(object_ref kernel_module); - void InitializeProcess(X_KPROCESS* process, uint32_t type, char unk_18, - char unk_19, char unk_1A); + void InitializeProcess(X_KPROCESS* process, uint32_t type, + char priority_class, char default_priority, + char max_dynamic_priority); void SetProcessTLSVars(X_KPROCESS* process, int num_slots, int tls_data_size, int tls_static_data_address); void InitializeKernelGuestGlobals(); diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc index f524b49ab..f4cfac6ee 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc @@ -707,8 +707,7 @@ uint32_t xeKeReleaseSemaphore(X_KSEMAPHORE* semaphore_ptr, uint32_t increment, return 0; } - // TODO(benvanik): increment thread priority? - // TODO(benvanik): wait? + sem->set_priority_increment(increment); int32_t previous_count = 0; [[maybe_unused]] bool success = diff --git a/src/xenia/kernel/xevent.cc b/src/xenia/kernel/xevent.cc index bf1176af8..b583bf732 100644 --- a/src/xenia/kernel/xevent.cc +++ b/src/xenia/kernel/xevent.cc @@ -58,11 +58,13 @@ void XEvent::InitializeNative(void* native_ptr, X_DISPATCH_HEADER* header) { } int32_t XEvent::Set(uint32_t priority_increment, bool wait) { + set_priority_increment(priority_increment); event_->Set(); return 1; } int32_t XEvent::Pulse(uint32_t priority_increment, bool wait) { + set_priority_increment(priority_increment); event_->Pulse(); return 1; } diff --git a/src/xenia/kernel/xmutant.cc b/src/xenia/kernel/xmutant.cc index 58e379730..42da93d67 100644 --- a/src/xenia/kernel/xmutant.cc +++ b/src/xenia/kernel/xmutant.cc @@ -45,6 +45,8 @@ X_STATUS XMutant::ReleaseMutant(uint32_t priority_increment, bool abandon, owning_thread_ = nullptr; } + set_priority_increment(priority_increment); + // TODO(benvanik): abandoning. assert_false(abandon); if (mutant_->Release()) { diff --git a/src/xenia/kernel/xobject.cc b/src/xenia/kernel/xobject.cc index 83d2ec8ae..3c501509c 100644 --- a/src/xenia/kernel/xobject.cc +++ b/src/xenia/kernel/xobject.cc @@ -207,10 +207,9 @@ X_STATUS XObject::Wait(uint32_t wait_reason, uint32_t processor_mode, switch (result) { case xe::threading::WaitResult::kSuccess: case xe::threading::WaitResult::kUserCallback: { - // Thread actually blocked and woke — reset priority to base. auto current_thread = XThread::GetCurrentThread(); if (current_thread) { - current_thread->ResetQuantum(); + current_thread->BoostOnWake(priority_increment()); } if (result == xe::threading::WaitResult::kSuccess) { WaitCallback(); @@ -245,7 +244,7 @@ X_STATUS XObject::SignalAndWait(XObject* signal_object, XObject* wait_object, case xe::threading::WaitResult::kUserCallback: { auto current_thread = XThread::GetCurrentThread(); if (current_thread) { - current_thread->ResetQuantum(); + current_thread->BoostOnWake(wait_object->priority_increment()); } if (result == xe::threading::WaitResult::kSuccess) { wait_object->WaitCallback(); @@ -280,12 +279,14 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects, : std::chrono::milliseconds::max(); X_STATUS status; + uint32_t boost_increment = 0; if (wait_type) { auto result = xe::threading::WaitAny(wait_handles, count, alertable ? true : false, timeout_ms); switch (result.first) { case xe::threading::WaitResult::kSuccess: objects[result.second]->WaitCallback(); + boost_increment = objects[result.second]->priority_increment(); status = X_STATUS(result.second); break; case xe::threading::WaitResult::kUserCallback: @@ -310,6 +311,10 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects, case xe::threading::WaitResult::kSuccess: for (uint32_t i = 0; i < count; i++) { objects[i]->WaitCallback(); + // Use the largest increment among the signaled objects. + if (objects[i]->priority_increment() > boost_increment) { + boost_increment = objects[i]->priority_increment(); + } } status = X_STATUS_SUCCESS; break; @@ -328,12 +333,13 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects, } } - // Only reset quantum if the thread actually blocked (not on timeout/failure). + // Apply priority boost if the thread actually blocked (not on + // timeout/failure). if (status != X_STATUS_TIMEOUT && status != X_STATUS_UNSUCCESSFUL && status != X_STATUS_ABANDONED_WAIT_0) { auto current_thread = XThread::GetCurrentThread(); if (current_thread) { - current_thread->ResetQuantum(); + current_thread->BoostOnWake(boost_increment); } } return status; diff --git a/src/xenia/kernel/xobject.h b/src/xenia/kernel/xobject.h index 77fbf0870..da67be4c0 100644 --- a/src/xenia/kernel/xobject.h +++ b/src/xenia/kernel/xobject.h @@ -226,6 +226,12 @@ class XObject { void* native_ptr, int32_t as_type = -1, bool already_locked = false); + // Priority increment stored by the most recent signal operation + // (KeSetEvent, KeReleaseSemaphore, etc.). Read by the waiter on wake + // to apply a priority boost matching real Xenon scheduler behavior. + uint32_t priority_increment() const { return priority_increment_; } + void set_priority_increment(uint32_t inc) { priority_increment_ = inc; } + protected: bool SaveObject(ByteStream* stream); bool RestoreObject(ByteStream* stream); @@ -253,6 +259,8 @@ class XObject { KernelState* kernel_state_; + uint32_t priority_increment_ = 0; + // Host objects are persisted through resets/etc. bool host_object_ = false; diff --git a/src/xenia/kernel/xthread.cc b/src/xenia/kernel/xthread.cc index 49fd30d21..cc7d90c2e 100644 --- a/src/xenia/kernel/xthread.cc +++ b/src/xenia/kernel/xthread.cc @@ -214,6 +214,19 @@ void XThread::InitializeGuestObject() { guest_thread->apc_lists[0].Initialize(memory()); guest_thread->apc_lists[1].Initialize(memory()); + guest_thread->process_priority_class = process->process_priority_class; + auto base_prio = process->default_thread_priority; + guest_thread->base_priority_copy = base_prio; + guest_thread->base_priority = base_prio; + guest_thread->priority = base_prio; + guest_thread->max_dynamic_priority = process->max_dynamic_priority; + guest_thread->quantum = process->quantum; + + // Sync the host-side priority tracking to match the guest defaults. + // Games may later override these via KeSetPriorityThread. + priority_ = base_prio; + base_priority_ = base_prio; + guest_thread->a_prcb_ptr = kpcrb; guest_thread->another_prcb_ptr = kpcrb; @@ -706,7 +719,7 @@ void XThread::SetPriority(int32_t increment) { void XThread::CheckQuantumAndDecay() { if (cvars::ignore_thread_priorities) return; - // Real-time threads (priority >= 18) don't decay on Xenon. + // Real-time threads (current priority >= 0x12) don't decay on Xenon. if (priority_ >= 18) return; uint64_t now = Clock::QueryHostUptimeMillis(); @@ -719,14 +732,15 @@ void XThread::CheckQuantumAndDecay() { constexpr uint64_t kQuantumPeriodMs = 20; if (elapsed < kQuantumPeriodMs) return; - // TODO(has207): The real kernel also subtracts the accumulated priority - // boost (boost_accumulator in X_KTHREAD) during decay: - // new_prio = priority - boost_accumulator - 1 - // We don't implement priority boosting on wait completion yet, so - // the boost accumulator is always effectively 0. When boost-on-wake - // is added, the accumulator needs to be drained here as well. int32_t decay_steps = static_cast(elapsed / kQuantumPeriodMs); - int32_t new_priority = priority_ - decay_steps; + // On the first decay step, drain the accumulated priority boost as well. + // The real kernel computes: new_prio = priority - boost_accumulator - 1 + // then zeroes the accumulator. Additional decay steps (if the timer + // callback was late) each subtract 1 more. + int32_t total_decay = boost_amount_ + decay_steps; + boost_amount_ = 0; + + int32_t new_priority = priority_ - total_decay; if (new_priority < base_priority_) { new_priority = base_priority_; } @@ -740,16 +754,57 @@ void XThread::CheckQuantumAndDecay() { quantum_start_ms_ = now; } -void XThread::ResetQuantum() { +void XThread::BoostOnWake(int32_t increment) { if (cvars::ignore_thread_priorities) return; - if (priority_ != base_priority_) { - priority_ = base_priority_; - if (is_guest_thread()) { - guest_object()->priority = - static_cast(base_priority_); - } - thread_->set_priority(GuestPriorityToHost(base_priority_)); + + // Real-time threads (priority >= 0x12) just get their quantum reset. + if (priority_ >= 18) { + boost_amount_ = 0; + quantum_start_ms_ = Clock::QueryHostUptimeMillis(); + return; } + + // Match the real kernel (xeEnqueueThreadPostWait): + // - Only apply boost if there is no pending decay (priority_decrement == 0) + // AND boost is not disabled on this thread. + // - Boosted priority = base + increment, clamped to max_priority_cap. + // - Only boost UP — never lower priority below its current value. + bool apply_boost = false; + if (increment > 0 && is_guest_thread()) { + auto* kthread = guest_object(); + if (kthread->priority_decrement == 0 && !kthread->boost_disabled) { + apply_boost = true; + } + } else if (increment > 0) { + // Host threads (non-guest): apply boost unconditionally. + apply_boost = true; + } + + if (apply_boost) { + int32_t boosted = base_priority_ + increment; + // Clamp to the per-thread max dynamic priority cap. + // For title threads this is 17 (just below real-time threshold). + int32_t max_cap = 17; + if (is_guest_thread()) { + uint8_t guest_cap = guest_object()->max_dynamic_priority; + if (guest_cap > 0) { + max_cap = guest_cap; + } + } + if (boosted > max_cap) { + boosted = max_cap; + } + // Only boost UP, never lower. + if (boosted > priority_) { + priority_ = boosted; + boost_amount_ = priority_ - base_priority_; + if (is_guest_thread()) { + guest_object()->priority = static_cast(priority_); + } + thread_->set_priority(GuestPriorityToHost(priority_)); + } + } + quantum_start_ms_ = Clock::QueryHostUptimeMillis(); } diff --git a/src/xenia/kernel/xthread.h b/src/xenia/kernel/xthread.h index a2fc02020..527e549c5 100644 --- a/src/xenia/kernel/xthread.h +++ b/src/xenia/kernel/xthread.h @@ -298,9 +298,9 @@ struct X_KTHREAD { uint8_t unk_A5[0xB]; // 0xA5 int32_t apc_disable_count; // 0xB0 xe::be quantum; // 0xB4 - uint8_t unk_B8; // 0xB8 - uint8_t unk_B9; // 0xB9 - uint8_t unk_BA; // 0xBA + uint8_t saturation_increment; // 0xB8 + uint8_t base_priority; // 0xB9 + uint8_t priority_decrement; // 0xBA uint8_t boost_disabled; // 0xBB uint8_t suspend_count; // 0xBC uint8_t was_preempted; // 0xBD @@ -310,9 +310,9 @@ struct X_KTHREAD { // all TypedGuestPointer a_prcb_ptr; // 0xC0 TypedGuestPointer another_prcb_ptr; // 0xC4 - uint8_t unk_C8; // 0xC8 - uint8_t unk_C9; // 0xC9 - uint8_t unk_CA; // 0xCA + uint8_t process_priority_class; // 0xC8 + uint8_t base_priority_copy; // 0xC9 + uint8_t max_dynamic_priority; // 0xCA uint8_t unk_CB; // 0xCB X_KSPINLOCK timer_list_lock; // 0xCC xe::be stack_alloc_base; // 0xD0 @@ -424,14 +424,19 @@ class XThread : public XObject, public cpu::Thread { // Called periodically (~20ms) by KernelState's timestamp timer to simulate // the Xenon scheduler's quantum-based priority decay for non-real-time - // threads (priority < 18). Threads that run for longer than one quantum - // (~20ms) have their effective priority decayed toward the base, which - // causes them to drop into lower host priority buckets and prevents - // starvation. + // threads (base_priority < 18). Threads that run for longer than one + // quantum (~20ms) have their effective priority decayed toward the base, + // which causes them to drop into lower host priority buckets and prevents + // starvation. On the first decay step the accumulated priority boost is + // also drained. void CheckQuantumAndDecay(); - // Resets effective priority back to base and restarts the quantum timer. - // Called when a thread wakes from a kernel wait. - void ResetQuantum(); + // Called when a thread wakes from a kernel wait. Applies a priority + // boost of |increment| above base_priority (matching the Xenon kernel's + // unwait-boost behavior) and restarts the quantum timer. The boost is + // drained on the next quantum expiry via CheckQuantumAndDecay(). + // If increment is 0 or the thread has boost disabled, the priority is + // simply restored to base_priority. + void BoostOnWake(int32_t increment); // Xbox thread IDs: // 0 - core 0, thread 0 - user @@ -504,6 +509,7 @@ class XThread : public XObject, public cpu::Thread { int32_t priority_ = 0; // current effective priority (may be decayed) int32_t base_priority_ = 0; // priority floor — decay never goes below this + int32_t boost_amount_ = 0; // accumulated priority boost above base uint64_t quantum_start_ms_ = 0; // host uptime (ms) when quantum last reset #if !XE_PLATFORM_WIN32 From 1da37db58496e3d02d5cc8dd2f96c5a8024507ef Mon Sep 17 00:00:00 2001 From: Gliniak <153369+Gliniak@users.noreply.github.com> Date: Sun, 5 Apr 2026 19:18:41 +0200 Subject: [PATCH 12/25] [Winkey] Passthrough: Added support for hid key codes. Thanks Devildwarf for initial implementation --- src/xenia/hid/winkey/winkey_input_driver.cc | 125 +++++++++++++++++++- 1 file changed, 123 insertions(+), 2 deletions(-) diff --git a/src/xenia/hid/winkey/winkey_input_driver.cc b/src/xenia/hid/winkey/winkey_input_driver.cc index 3ff5beaff..995855349 100644 --- a/src/xenia/hid/winkey/winkey_input_driver.cc +++ b/src/xenia/hid/winkey/winkey_input_driver.cc @@ -41,6 +41,125 @@ namespace xe { namespace hid { namespace winkey { +static uint8_t VirtualKeyToHIDUsage(UINT vk) { + // Letters: contiguous in both VK and HID space + if (vk >= 'A' && vk <= 'Z') { + return vk - 'A' + 0x04; + } + + // Digits 1-9 (0 is irregular: 0x27) + if (vk >= '1' && vk <= '9') { + return vk - '1' + 0x1E; + } + + // F1-F12 + if (vk >= VK_F1 && vk <= VK_F12) { + return vk - VK_F1 + 0x3A; + } + + // F13-F24 + if (vk >= VK_F13 && vk <= VK_F24) { + return vk - VK_F13 + 0x68; + } + + // Numpad 1-9 (0 is irregular: 0x62) + if (vk >= VK_NUMPAD1 && vk <= VK_NUMPAD9) { + return vk - VK_NUMPAD1 + 0x59; + } + + // Modifiers (Left side starts at 0xE0, Right at 0xE4) + if (vk >= VK_LCONTROL && vk <= VK_LWIN) { + return vk - VK_LCONTROL + 0xE0; + } + if (vk >= VK_RCONTROL && vk <= VK_RWIN) { + return vk - VK_RCONTROL + 0xE4; + } + + switch (vk) { + case '0': + return 0x27; + case VK_RETURN: + return 0x28; + case VK_ESCAPE: + return 0x29; + case VK_BACK: + return 0x2A; + case VK_TAB: + return 0x2B; + case VK_SPACE: + return 0x2C; + case VK_OEM_MINUS: + return 0x2D; + case VK_OEM_PLUS: + return 0x2E; + case VK_OEM_4: + return 0x2F; + case VK_OEM_6: + return 0x30; + case VK_OEM_5: + return 0x31; + case VK_OEM_1: + return 0x33; + case VK_OEM_7: + return 0x34; + case VK_OEM_3: + return 0x35; + case VK_OEM_COMMA: + return 0x36; + case VK_OEM_PERIOD: + return 0x37; + case VK_OEM_2: + return 0x38; + case VK_CAPITAL: + return 0x39; + case VK_SNAPSHOT: + return 0x46; + case VK_SCROLL: + return 0x47; + case VK_PAUSE: + return 0x48; + case VK_INSERT: + return 0x49; + case VK_HOME: + return 0x4A; + case VK_PRIOR: + return 0x4B; + case VK_DELETE: + return 0x4C; + case VK_END: + return 0x4D; + case VK_NEXT: + return 0x4E; + case VK_RIGHT: + return 0x4F; + case VK_LEFT: + return 0x50; + case VK_DOWN: + return 0x51; + case VK_UP: + return 0x52; + case VK_NUMLOCK: + return 0x53; + case VK_DIVIDE: + return 0x54; + case VK_MULTIPLY: + return 0x55; + case VK_SUBTRACT: + return 0x56; + case VK_ADD: + return 0x57; + case VK_NUMPAD0: + return 0x62; + case VK_DECIMAL: + return 0x63; + case VK_APPS: + return 0x65; + default: + break; + } + return 0x00; +} + bool static IsPassthroughEnabled() { return static_cast(cvars::keyboard_mode) == KeyboardMode::Passthrough; @@ -344,10 +463,12 @@ X_RESULT WinKeyInputDriver::GetKeystroke(uint32_t user_index, uint32_t flags, } if (IsPassthroughEnabled()) { + const UINT vk = static_cast(xinput_virtual_key); + hid_code = VirtualKeyToHIDUsage(vk); if (GetKeyboardState(key_map_)) { + const UINT sc = MapVirtualKey(vk, MAPVK_VK_TO_VSC); WCHAR buf; - if (ToUnicode(uint8_t(xinput_virtual_key), 0, key_map_, &buf, 1, 0) == - 1) { + if (ToUnicode(vk, sc, key_map_, &buf, 1, 0) == 1) { keystroke_flags |= 0x1000; // XINPUT_KEYSTROKE_VALIDUNICODE unicode = buf; } From a292248bee76799a53aa08d5467b70d569b400eb Mon Sep 17 00:00:00 2001 From: The-Little-Wolf <116989599+The-Little-Wolf@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:44:42 -0800 Subject: [PATCH 13/25] [XAM/CONTENT] - Stub XamContentLaunchImage - Stub XamContentLaunchImage and XamContentLaunchImageInternalEx - Creates a common function for all XamContentLaunchImage functions --- src/xenia/kernel/xam/xam_content.cc | 128 ++++++++++++++-------------- 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/src/xenia/kernel/xam/xam_content.cc b/src/xenia/kernel/xam/xam_content.cc index 10d27268b..da9db0b25 100644 --- a/src/xenia/kernel/xam/xam_content.cc +++ b/src/xenia/kernel/xam/xam_content.cc @@ -702,12 +702,42 @@ void XamLoaderGetMediaInfo_entry(lpdword_t media_type, lpdword_t unk2) { } DECLARE_XAM_EXPORT1(XamLoaderGetMediaInfo, kNone, kStub); -dword_result_t XamContentLaunchImageFromFileInternal_entry( - lpstring_t image_location, lpstring_t xex_name, dword_t unk) { - const std::string image_path = static_cast(image_location); - const std::string xex_name_ = static_cast(xex_name); +dword_result_t xeXamContentLaunchImage(dword_t user_index, + lpstring_t image_location, + lpvoid_t content_data_ptr, + dword_t content_data_size, + lpstring_t xex_path, dword_t flag) { + /* Notes: + - In code this subfunction is used by all XamContentLaunchImage + functions + - Due to the current implementation of content data we can't use + XCONTENT_DATA_INTERNAL + - flags used by XamLoaderLaunchTitleEx + - user_index used by xeXamContentOpenFile + - if image_location null use xeXamContentOpenFile else use + exXamContentCreate + - root_name is "XSYSLAUNCH" while XamLoaderLaunchTitleEx uses + "XSYSLAUNCH:\\"" + - title_id is usually written into first 8 characters of filename + */ + vfs::Entry* entry; + if (!image_location) { + XCONTENT_AGGREGATE_DATA content_data = + *content_data_ptr.as(); + const uint32_t title_id = xe::string_util::from_string( + content_data.file_name().substr(0, 8), true); - vfs::Entry* entry = kernel_state()->file_system()->ResolvePath(image_path); + // This should be done via content_manager, however as it isn't capable of + // such action we need to improvise. + const std::string package_path = + fmt::format("GAME:/Content/0000000000000000/{:08X}/{:08X}/{}", title_id, + static_cast(content_data.content_type.get()), + content_data.file_name()); + + entry = kernel_state()->file_system()->ResolvePath(package_path); + } else { + entry = kernel_state()->file_system()->ResolvePath(image_location.value()); + } if (!entry) { return X_STATUS_NO_SUCH_FILE; @@ -715,71 +745,15 @@ dword_result_t XamContentLaunchImageFromFileInternal_entry( const std::filesystem::path host_path = kernel_state()->emulator()->content_root() / entry->name(); + if (!std::filesystem::exists(host_path)) { uint64_t progress = 0; - vfs::VirtualFileSystem::ExtractContentFile( entry, kernel_state()->emulator()->content_root(), progress, true); } auto xam = kernel_state()->GetKernelModule("xam.xex"); - auto& loader_data = xam->loader_data(); - loader_data.host_path = xe::path_to_utf8(host_path); - loader_data.launch_path = xex_name_; - - xam->SaveLoaderData(); - - 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]() { - xe::ui::ImGuiDialog::ShowMessageBox( - imgui_drawer, "Launching new title!", - "Launching new title. \nPlease close Xenia and launch it again. Game " - "should load automatically."); - }); - } - - kernel_state()->TerminateTitle(); - return X_ERROR_SUCCESS; -} - -DECLARE_XAM_EXPORT1(XamContentLaunchImageFromFileInternal, kContent, kStub); - -dword_result_t XamContentLaunchImageInternal_entry(lpvoid_t content_data_ptr, - lpstring_t xex_path) { - XCONTENT_AGGREGATE_DATA content_data = *content_data_ptr.as(); - - // title_id is written into first 8 characters of filename - const uint32_t title_id = xe::string_util::from_string( - content_data.file_name().substr(0, 8), true); - - // This should be done via content_manager, however as it isn't capable of - // such action we need to improvise. - const std::string package_path = - fmt::format("GAME:/Content/0000000000000000/{:08X}/{:08X}/{}", title_id, - static_cast(content_data.content_type.get()), - content_data.file_name()); - - auto entry = kernel_state()->file_system()->ResolvePath(package_path); - - if (!entry) { - return X_STATUS_NO_SUCH_FILE; - } - - const std::filesystem::path host_path = - kernel_state()->emulator()->content_root() / entry->name(); - - if (!std::filesystem::exists(host_path)) { - uint64_t progress = 0; - kernel_state()->file_system()->ExtractContentFile( - entry, kernel_state()->emulator()->content_root(), progress, true); - } - - auto xam = kernel_state()->GetKernelModule("xam.xex"); - auto& loader_data = xam->loader_data(); loader_data.host_path = xe::path_to_utf8(host_path); loader_data.launch_path = xex_path.value(); @@ -802,8 +776,38 @@ dword_result_t XamContentLaunchImageInternal_entry(lpvoid_t content_data_ptr, return X_ERROR_SUCCESS; } +dword_result_t XamContentLaunchImageFromFileInternal_entry( + lpstring_t image_location, lpstring_t xex_name) { + return xeXamContentLaunchImage(XUserIndexNone, image_location, nullptr, NULL, + xex_name, NULL); +} +DECLARE_XAM_EXPORT1(XamContentLaunchImageFromFileInternal, kContent, kStub); + +dword_result_t XamContentLaunchImage_entry(dword_t user_index, + lpvoid_t content_data_ptr, + lpstring_t xex_path) { + return xeXamContentLaunchImage(user_index, nullptr, content_data_ptr, + sizeof(XCONTENT_DATA), xex_path, NULL); +} +DECLARE_XAM_EXPORT1(XamContentLaunchImage, kContent, kStub); + +dword_result_t XamContentLaunchImageInternal_entry(lpvoid_t content_data_ptr, + lpstring_t xex_path) { + return xeXamContentLaunchImage(XUserIndexNone, nullptr, content_data_ptr, + sizeof(XCONTENT_DATA_INTERNAL), xex_path, + NULL); +} DECLARE_XAM_EXPORT1(XamContentLaunchImageInternal, kContent, kStub); +dword_result_t XamContentLaunchImageInternalEx_entry(lpvoid_t content_data_ptr, + lpstring_t xex_path, + dword_t flags) { + return xeXamContentLaunchImage(XUserIndexNone, nullptr, content_data_ptr, + sizeof(XCONTENT_DATA_INTERNAL), xex_path, + flags); +} +DECLARE_XAM_EXPORT1(XamContentLaunchImageInternalEx, kContent, kStub); + void XamContentRegisterChangeCallback_entry(dword_t callback) { kernel_state()->xam_state()->SetContentRegisterCallback(callback); } From 80c6751b82f39a547a934897f3080d1d1c49a6ef Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:10:50 +0900 Subject: [PATCH 14/25] [UI] Eliminate mouse requirement for initial profile create dialogs Enable keyboard navigation and accept Enter to complete profile creation --- src/xenia/app/profile_dialogs.cc | 3 +++ src/xenia/kernel/xam/ui/create_profile_ui.cc | 9 +++++---- src/xenia/ui/imgui_drawer.cc | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/xenia/app/profile_dialogs.cc b/src/xenia/app/profile_dialogs.cc index 206e8e454..54b166d85 100644 --- a/src/xenia/app/profile_dialogs.cc +++ b/src/xenia/app/profile_dialogs.cc @@ -61,6 +61,9 @@ void NoProfileDialog::OnDraw(ImGuiIO& io) { const auto content_files = xe::filesystem::ListDirectories( emulator_window_->emulator()->content_root()); + if (ImGui::IsWindowAppearing()) { + ImGui::SetKeyboardFocusHere(); + } if (content_files.empty()) { if (ImGui::Button("Create Profile")) { new kernel::xam::ui::CreateProfileUI(emulator_window_->imgui_drawer(), diff --git a/src/xenia/kernel/xam/ui/create_profile_ui.cc b/src/xenia/kernel/xam/ui/create_profile_ui.cc index 0429748da..12394c339 100644 --- a/src/xenia/kernel/xam/ui/create_profile_ui.cc +++ b/src/xenia/kernel/xam/ui/create_profile_ui.cc @@ -38,12 +38,13 @@ void CreateProfileUI::OnDraw(ImGuiIO& io) { } ImGui::TextUnformatted("Gamertag:"); - if (ImGui::InputText("##Gamertag", gamertag_, sizeof(gamertag_))) { - valid_gamertag_ = profile_manager->IsGamertagValid(std::string(gamertag_)); - } + const bool enter_pressed = + ImGui::InputText("##Gamertag", gamertag_, sizeof(gamertag_), + ImGuiInputTextFlags_EnterReturnsTrue); + valid_gamertag_ = profile_manager->IsGamertagValid(std::string(gamertag_)); ImGui::BeginDisabled(!valid_gamertag_); - if (ImGui::Button("Create")) { + if (ImGui::Button("Create") || (enter_pressed && valid_gamertag_)) { bool autologin = (profile_manager->GetAccountCount() == 0); if (profile_manager->CreateProfile(std::string(gamertag_), autologin, migration_) && diff --git a/src/xenia/ui/imgui_drawer.cc b/src/xenia/ui/imgui_drawer.cc index 5ed5644ab..6aa3e1326 100644 --- a/src/xenia/ui/imgui_drawer.cc +++ b/src/xenia/ui/imgui_drawer.cc @@ -162,6 +162,7 @@ void ImGuiDrawer::Initialize() { auto& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; const float font_size = std::max((float)cvars::font_size, 8.f); const float title_font_size = font_size + 6.f; From ea02e8d31714ffea748a2aba836bf5de36aafca3 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:07:50 +0900 Subject: [PATCH 15/25] [Memory] Add free block tracker to BaseHeap for O(log n) allocation Replace linear page_table_ scans in AllocRange with a std::map-based free block index that tracks contiguous free regions. Insertions now coalesce with adjacent blocks on release and AllocFixed uses the targeted tracker for pure reserves and falls back to a full rebuild for mixed-state commits. Also fixing PhysicalHeap leaking parent memory on child allocation failure, Reset() not restoring unreserved_page_count_ and some incorrect method names in PhysicalHeap error messages --- src/xenia/base/testing/heap_test.cc | 388 ++++++++++++++++++++++++++++ src/xenia/memory.cc | 268 +++++++++++++------ src/xenia/memory.h | 14 + 3 files changed, 595 insertions(+), 75 deletions(-) create mode 100644 src/xenia/base/testing/heap_test.cc diff --git a/src/xenia/base/testing/heap_test.cc b/src/xenia/base/testing/heap_test.cc new file mode 100644 index 000000000..f83a435b6 --- /dev/null +++ b/src/xenia/base/testing/heap_test.cc @@ -0,0 +1,388 @@ +/** + ****************************************************************************** + * 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 "third_party/catch/include/catch.hpp" + +#include "xenia/memory.h" + +namespace xe { +namespace test { + +// Helper to create a VirtualHeap for testing without a full Memory instance. +// Uses reserve-only allocations to avoid needing real host memory mappings. +class TestHeap { + public: + TestHeap(uint32_t heap_base, uint32_t heap_size, uint32_t page_size) { + heap_.Initialize(nullptr, nullptr, HeapType::kGuestXex, heap_base, + heap_size, page_size); + } + + ~TestHeap() { + // Don't call Dispose — it tries to DeallocFixed on nullptr membase. + } + + VirtualHeap& heap() { return heap_; } + + // Reserve-only allocation (skips host memory commit). + bool Alloc(uint32_t size, uint32_t alignment, bool top_down, + uint32_t* out_address) { + return heap_.AllocRange(heap_.heap_base(), + heap_.heap_base() + heap_.heap_size() - 1, size, + alignment, kMemoryAllocationReserve, + kMemoryProtectRead, top_down, out_address); + } + + bool AllocRange(uint32_t low, uint32_t high, uint32_t size, + uint32_t alignment, bool top_down, uint32_t* out_address) { + return heap_.AllocRange(low, high, size, alignment, + kMemoryAllocationReserve, kMemoryProtectRead, + top_down, out_address); + } + + bool AllocFixed(uint32_t base_address, uint32_t size) { + return heap_.AllocFixed(base_address, size, heap_.page_size(), + kMemoryAllocationReserve, kMemoryProtectRead); + } + + bool Release(uint32_t address) { return heap_.Release(address); } + + uint32_t unreserved_page_count() const { + return heap_.unreserved_page_count(); + } + + uint32_t total_page_count() const { return heap_.total_page_count(); } + + private: + VirtualHeap heap_; +}; + +// ============================================================================ +// Basic allocation and release +// ============================================================================ + +TEST_CASE("heap_alloc_basic", "[heap]") { + // 1MB heap, 4KB pages = 256 pages + TestHeap h(0x80000000, 0x100000, 0x1000); + REQUIRE(h.total_page_count() == 256); + REQUIRE(h.unreserved_page_count() == 256); + + uint32_t addr = 0; + REQUIRE(h.Alloc(0x1000, 0x1000, false, &addr)); + REQUIRE(addr == 0x80000000); + REQUIRE(h.unreserved_page_count() == 255); + + REQUIRE(h.Alloc(0x2000, 0x1000, false, &addr)); + REQUIRE(addr == 0x80001000); + REQUIRE(h.unreserved_page_count() == 253); +} + +TEST_CASE("heap_alloc_top_down", "[heap]") { + TestHeap h(0x80000000, 0x100000, 0x1000); + + uint32_t addr = 0; + REQUIRE(h.Alloc(0x1000, 0x1000, true, &addr)); + // Top-down: should be at the highest aligned address. + REQUIRE(addr == 0x800FF000); + REQUIRE(h.unreserved_page_count() == 255); + + REQUIRE(h.Alloc(0x2000, 0x1000, true, &addr)); + REQUIRE(addr == 0x800FD000); + REQUIRE(h.unreserved_page_count() == 253); +} + +TEST_CASE("heap_alloc_release", "[heap]") { + TestHeap h(0x80000000, 0x100000, 0x1000); + + uint32_t addr1 = 0, addr2 = 0; + REQUIRE(h.Alloc(0x4000, 0x1000, false, &addr1)); + REQUIRE(h.Alloc(0x4000, 0x1000, false, &addr2)); + REQUIRE(addr1 == 0x80000000); + REQUIRE(addr2 == 0x80004000); + REQUIRE(h.unreserved_page_count() == 248); + + REQUIRE(h.Release(addr1)); + REQUIRE(h.unreserved_page_count() == 252); + + REQUIRE(h.Release(addr2)); + REQUIRE(h.unreserved_page_count() == 256); +} + +// ============================================================================ +// Coalescing +// ============================================================================ + +TEST_CASE("heap_coalesce_adjacent_releases", "[heap]") { + TestHeap h(0x80000000, 0x100000, 0x1000); + + // Allocate 3 adjacent 4-page blocks. + uint32_t a1 = 0, a2 = 0, a3 = 0; + REQUIRE(h.Alloc(0x4000, 0x1000, false, &a1)); + REQUIRE(h.Alloc(0x4000, 0x1000, false, &a2)); + REQUIRE(h.Alloc(0x4000, 0x1000, false, &a3)); + REQUIRE(a1 == 0x80000000); + REQUIRE(a2 == 0x80004000); + REQUIRE(a3 == 0x80008000); + + // Release middle block, then adjacent blocks — should coalesce. + REQUIRE(h.Release(a2)); + REQUIRE(h.Release(a1)); + REQUIRE(h.Release(a3)); + + // All freed. Now allocate a 12-page block — should succeed in the + // coalesced free region. + uint32_t big = 0; + REQUIRE(h.Alloc(0xC000, 0x1000, false, &big)); + REQUIRE(big == 0x80000000); +} + +TEST_CASE("heap_coalesce_merge_before", "[heap]") { + TestHeap h(0x80000000, 0x100000, 0x1000); + + uint32_t a1 = 0, a2 = 0; + REQUIRE(h.Alloc(0x4000, 0x1000, false, &a1)); + REQUIRE(h.Alloc(0x4000, 0x1000, false, &a2)); + + // Release first, then second — second should merge with first. + REQUIRE(h.Release(a1)); + REQUIRE(h.Release(a2)); + + uint32_t big = 0; + REQUIRE(h.Alloc(0x8000, 0x1000, false, &big)); + REQUIRE(big == 0x80000000); +} + +TEST_CASE("heap_coalesce_merge_after", "[heap]") { + TestHeap h(0x80000000, 0x100000, 0x1000); + + uint32_t a1 = 0, a2 = 0; + REQUIRE(h.Alloc(0x4000, 0x1000, false, &a1)); + REQUIRE(h.Alloc(0x4000, 0x1000, false, &a2)); + + // Release second, then first — first should merge with second. + REQUIRE(h.Release(a2)); + REQUIRE(h.Release(a1)); + + uint32_t big = 0; + REQUIRE(h.Alloc(0x8000, 0x1000, false, &big)); + REQUIRE(big == 0x80000000); +} + +// ============================================================================ +// Fragmentation resistance +// ============================================================================ + +TEST_CASE("heap_fragmentation_reuse", "[heap]") { + // 80KB heap, 4KB pages = 20 pages + TestHeap h(0x80000000, 0x14000, 0x1000); + + // Allocate 4 x 4-page blocks (uses 16 of 20 pages). + uint32_t a[4]; + for (int i = 0; i < 4; ++i) { + REQUIRE(h.Alloc(0x4000, 0x1000, false, &a[i])); + } + REQUIRE(h.unreserved_page_count() == 4); + + // Release alternating blocks to fragment. + REQUIRE(h.Release(a[0])); // free pages 0-3 + REQUIRE(h.Release(a[2])); // free pages 8-11 + + // Can't allocate 5 pages (no single contiguous block of 5 in gaps). + uint32_t fail_addr = 0; + REQUIRE_FALSE(h.Alloc(0x5000, 0x1000, false, &fail_addr)); + + // Can allocate 4 pages (fits in either free gap). + uint32_t ok_addr = 0; + REQUIRE(h.Alloc(0x4000, 0x1000, false, &ok_addr)); + REQUIRE(ok_addr == 0x80000000); // bottom-up, first fit. + + // Release remaining to defragment. + REQUIRE(h.Release(a[1])); + REQUIRE(h.Release(a[3])); + REQUIRE(h.Release(ok_addr)); + + // Now 20 pages should be available as one contiguous block. + REQUIRE(h.unreserved_page_count() == 20); + uint32_t big = 0; + REQUIRE(h.Alloc(0xC000, 0x1000, false, &big)); + REQUIRE(big == 0x80000000); +} + +// ============================================================================ +// Alignment +// ============================================================================ + +TEST_CASE("heap_alloc_alignment", "[heap]") { + // 1MB heap, 4KB pages + TestHeap h(0x80000000, 0x100000, 0x1000); + + // Allocate 1 page to offset the next allocation. + uint32_t first = 0; + REQUIRE(h.Alloc(0x1000, 0x1000, false, &first)); + REQUIRE(first == 0x80000000); + + // Allocate with 64KB alignment — should skip to 0x80010000. + uint32_t aligned = 0; + REQUIRE(h.Alloc(0x1000, 0x10000, false, &aligned)); + REQUIRE((aligned % 0x10000) == 0); + REQUIRE(aligned == 0x80010000); +} + +TEST_CASE("heap_alloc_alignment_top_down", "[heap]") { + // 1MB heap, 4KB pages + TestHeap h(0x80000000, 0x100000, 0x1000); + + // Allocate 1 page at the top. + uint32_t first = 0; + REQUIRE(h.Alloc(0x1000, 0x1000, true, &first)); + REQUIRE(first == 0x800FF000); + + // Allocate with 64KB alignment top-down — should align down. + uint32_t aligned = 0; + REQUIRE(h.Alloc(0x1000, 0x10000, true, &aligned)); + REQUIRE((aligned % 0x10000) == 0); + REQUIRE(aligned == 0x800F0000); +} + +// ============================================================================ +// AllocFixed +// ============================================================================ + +TEST_CASE("heap_alloc_fixed", "[heap]") { + TestHeap h(0x80000000, 0x100000, 0x1000); + + REQUIRE(h.AllocFixed(0x80010000, 0x4000)); + REQUIRE(h.unreserved_page_count() == 252); + + // Allocate bottom-up — should get 0x80000000 (before the fixed alloc). + uint32_t addr = 0; + REQUIRE(h.Alloc(0x1000, 0x1000, false, &addr)); + REQUIRE(addr == 0x80000000); + + // Allocating at the same fixed address should fail (already reserved). + REQUIRE_FALSE(h.AllocFixed(0x80010000, 0x1000)); +} + +// ============================================================================ +// Range allocation +// ============================================================================ + +TEST_CASE("heap_alloc_range", "[heap]") { + TestHeap h(0x80000000, 0x100000, 0x1000); + + // Allocate in a specific sub-range. + uint32_t addr = 0; + REQUIRE(h.AllocRange(0x80080000, 0x800FFFFF, 0x4000, 0x1000, false, &addr)); + REQUIRE(addr >= 0x80080000); + REQUIRE(addr + 0x4000 <= 0x80100000); +} + +TEST_CASE("heap_alloc_range_exhaustion", "[heap]") { + // 64KB heap, 4KB pages = 16 pages + TestHeap h(0x80000000, 0x10000, 0x1000); + + // Fill the lower half. + REQUIRE(h.AllocFixed(0x80000000, 0x8000)); + + // Try to allocate in the lower half — should fail. + // Use page-aligned high address so xe::align doesn't extend the range. + uint32_t addr = 0; + REQUIRE_FALSE( + h.AllocRange(0x80000000, 0x80007000, 0x1000, 0x1000, false, &addr)); + + // Allocate in the upper half — should succeed. + REQUIRE(h.AllocRange(0x80008000, 0x8000F000, 0x1000, 0x1000, false, &addr)); + REQUIRE(addr >= 0x80008000); +} + +// ============================================================================ +// Reset +// ============================================================================ + +TEST_CASE("heap_reset", "[heap]") { + TestHeap h(0x80000000, 0x100000, 0x1000); + + // Fill most of the heap. + uint32_t addr = 0; + while (h.Alloc(0x1000, 0x1000, false, &addr)) { + } + + // Reset should restore all pages. + h.heap().Reset(); + REQUIRE(h.unreserved_page_count() == 256); + + // Should be able to allocate a large block again. + REQUIRE(h.Alloc(0xF0000, 0x1000, false, &addr)); + REQUIRE(addr == 0x80000000); +} + +// ============================================================================ +// Stress: many alloc/release cycles +// ============================================================================ + +TEST_CASE("heap_stress_alloc_release", "[heap]") { + // 272KB heap, 4KB pages = 68 pages (extra pages avoid off-by-one in range + // check for full-heap-sized allocations). + TestHeap h(0x80000000, 0x44000, 0x1000); + + // Allocate 16 x 4-page blocks (uses 64 of 68 pages). + uint32_t addrs[16]; + for (int i = 0; i < 16; ++i) { + REQUIRE(h.Alloc(0x4000, 0x1000, false, &addrs[i])); + } + REQUIRE(h.unreserved_page_count() == 4); + + // Release all odd-indexed blocks. + for (int i = 1; i < 16; i += 2) { + REQUIRE(h.Release(addrs[i])); + } + REQUIRE(h.unreserved_page_count() == 36); + + // Re-allocate 4-page blocks into the gaps. + for (int i = 1; i < 16; i += 2) { + REQUIRE(h.Alloc(0x4000, 0x1000, false, &addrs[i])); + } + REQUIRE(h.unreserved_page_count() == 4); + + // Release everything. + for (int i = 0; i < 16; ++i) { + REQUIRE(h.Release(addrs[i])); + } + REQUIRE(h.unreserved_page_count() == 68); + + // 64-page allocation should succeed after full release (coalesced). + uint32_t full = 0; + REQUIRE(h.Alloc(0x40000, 0x1000, false, &full)); + REQUIRE(full == 0x80000000); +} + +// ============================================================================ +// 64KB page heap (like v40000000) +// ============================================================================ + +TEST_CASE("heap_64k_pages", "[heap]") { + // 4MB heap, 64KB pages = 64 pages + TestHeap h(0x40000000, 0x400000, 0x10000); + REQUIRE(h.total_page_count() == 64); + + uint32_t addr = 0; + REQUIRE(h.Alloc(0x10000, 0x10000, false, &addr)); + REQUIRE(addr == 0x40000000); + REQUIRE(h.unreserved_page_count() == 63); + + REQUIRE(h.Alloc(0x20000, 0x10000, false, &addr)); + REQUIRE(addr == 0x40010000); + REQUIRE(h.unreserved_page_count() == 61); + + REQUIRE(h.Release(0x40000000)); + REQUIRE(h.Release(0x40010000)); + REQUIRE(h.unreserved_page_count() == 64); +} + +} // namespace test +} // namespace xe diff --git a/src/xenia/memory.cc b/src/xenia/memory.cc index 1d4f15597..9d51cd40b 100644 --- a/src/xenia/memory.cc +++ b/src/xenia/memory.cc @@ -803,6 +803,10 @@ void BaseHeap::Initialize(Memory* memory, uint8_t* membase, HeapType heap_type, host_address_offset_ = host_address_offset; page_table_.resize(heap_size / page_size); unreserved_page_count_ = uint32_t(page_table_.size()); + + // Initialize free block tracker with a single block covering the entire heap. + free_blocks_.clear(); + free_blocks_[0] = uint32_t(page_table_.size()); } void BaseHeap::Dispose() { @@ -816,6 +820,7 @@ void BaseHeap::Dispose() { page_number += page_entry.region_page_count; } } + free_blocks_.clear(); } void BaseHeap::DumpMap() { @@ -938,14 +943,98 @@ bool BaseHeap::Restore(ByteStream* stream) { } } + RebuildFreeBlocks(); + return true; } +void BaseHeap::RebuildFreeBlocks() { + free_blocks_.clear(); + uint32_t run_start = UINT32_MAX; + for (uint32_t i = 0; i < uint32_t(page_table_.size()); ++i) { + if (page_table_[i].state == 0) { + if (run_start == UINT32_MAX) { + run_start = i; + } + } else { + if (run_start != UINT32_MAX) { + free_blocks_[run_start] = i - run_start; + run_start = UINT32_MAX; + } + } + } + if (run_start != UINT32_MAX) { + free_blocks_[run_start] = uint32_t(page_table_.size()) - run_start; + } +} + +void BaseHeap::RemoveFreeBlock(uint32_t start_page, uint32_t page_count) { + if (free_blocks_.empty()) { + return; + } + + // Find the free block that contains the allocated range. + auto it = free_blocks_.upper_bound(start_page); + if (it != free_blocks_.begin()) { + --it; + } + + // Verify the block actually contains our range. + uint32_t block_start = it->first; + uint32_t block_count = it->second; + uint32_t block_end = block_start + block_count; + assert_true(start_page >= block_start && + start_page + page_count <= block_end); + + free_blocks_.erase(it); + + // Insert remnant before the allocated range. + if (block_start < start_page) { + free_blocks_[block_start] = start_page - block_start; + } + + // Insert remnant after the allocated range. + uint32_t alloc_end = start_page + page_count; + if (alloc_end < block_end) { + free_blocks_[alloc_end] = block_end - alloc_end; + } +} + +void BaseHeap::InsertFreeBlock(uint32_t start_page, uint32_t page_count) { + uint32_t new_start = start_page; + uint32_t new_count = page_count; + + // Try to merge with block immediately after. + auto it_after = free_blocks_.find(start_page + page_count); + if (it_after != free_blocks_.end()) { + new_count += it_after->second; + free_blocks_.erase(it_after); + } + + // Try to merge with block immediately before. + auto it_at = free_blocks_.lower_bound(start_page); + if (it_at != free_blocks_.begin()) { + auto it_before = std::prev(it_at); + if (it_before->first + it_before->second == start_page) { + new_start = it_before->first; + new_count += it_before->second; + free_blocks_.erase(it_before); + } + } + + free_blocks_[new_start] = new_count; +} + void BaseHeap::Reset() { // TODO(DrChat): protect pages. std::memset(page_table_.data(), 0, sizeof(PageEntry) * page_table_.size()); + unreserved_page_count_ = uint32_t(page_table_.size()); // TODO(Triang3l): Remove access callbacks from pages if this is a physical // memory heap. + + // Re-initialize free block tracker. + free_blocks_.clear(); + free_blocks_[0] = uint32_t(page_table_.size()); } bool BaseHeap::Alloc(uint32_t size, uint32_t alignment, @@ -992,10 +1081,10 @@ bool BaseHeap::AllocFixed(uint32_t base_address, uint32_t size, auto global_lock = global_critical_region_.Acquire(); - // - If we are reserving the entire range requested must not be already - // reserved. + // - If we are reserving, the entire range must not be already reserved. // - If we are committing it's ok for pages within the range to already be // committed. + const bool is_pure_reserve = allocation_type == kMemoryAllocationReserve; for (uint32_t page_number = start_page_number; page_number <= end_page_number; ++page_number) { uint32_t state = page_table_[page_number].state; @@ -1042,6 +1131,7 @@ bool BaseHeap::AllocFixed(uint32_t base_address, uint32_t size, } // Set page state. + bool had_free_pages = false; for (uint32_t page_number = start_page_number; page_number <= end_page_number; ++page_number) { auto& page_entry = page_table_[page_number]; @@ -1053,11 +1143,25 @@ bool BaseHeap::AllocFixed(uint32_t base_address, uint32_t size, page_entry.allocation_protect = protect; page_entry.current_protect = protect; if (!(page_entry.state & kMemoryAllocationReserve)) { + had_free_pages = true; unreserved_page_count_--; } page_entry.state = kMemoryAllocationReserve | allocation_type; } + // Update free block tracker if any pages transitioned from free. + if (had_free_pages) { + if (is_pure_reserve) { + // Pure reserve: validation confirmed all pages were free, so the range + // is within a single coalesced free block. + RemoveFreeBlock(start_page_number, page_count); + } else { + // Mixed state (commit upgraded to reserve+commit): pages may span + // multiple free blocks, rebuild from page_table_. + RebuildFreeBlocks(); + } + } + return true; } template @@ -1094,88 +1198,85 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address, auto global_lock = global_critical_region_.Acquire(); - // Find a free page range. - // The base page must match the requested alignment, so we first scan for - // a free aligned page and only then check for continuous free pages. - // TODO(benvanik): optimized searching (free list buckets, bitmap, etc). + // Find a free page range using the free block tracker. + // The base page must match the requested alignment. uint32_t start_page_number = UINT_MAX; uint32_t end_page_number = UINT_MAX; - // chrispy:todo, page_scan_stride is probably always a power of two... uint32_t page_scan_stride = alignment >> page_size_shift_; - high_page_number = - high_page_number - QuickMod(high_page_number, page_scan_stride); + if (top_down) { - for (int64_t base_page_number = - high_page_number - xe::round_up(page_count, page_scan_stride); - base_page_number >= low_page_number; - base_page_number -= page_scan_stride) { - if (page_table_[base_page_number].state != 0) { - // Base page not free, skip to next usable page. + // Search free blocks from high addresses downward. + // Find the first block that could overlap our range. + auto it = free_blocks_.upper_bound(high_page_number); + while (it != free_blocks_.begin()) { + --it; + uint32_t block_start = it->first; + uint32_t block_count = it->second; + uint32_t block_end = block_start + block_count; + + // Block is entirely below our search range — stop. + if (block_end <= low_page_number) { + break; + } + + // Skip blocks too small to possibly fit. + if (block_count < page_count) { continue; } - // Check requested range to ensure free. - start_page_number = uint32_t(base_page_number); - end_page_number = uint32_t(base_page_number) + page_count - 1; - assert_true(end_page_number < page_table_.size()); - bool any_taken = false; - for (uint32_t page_number = uint32_t(base_page_number); - !any_taken && page_number <= end_page_number; ++page_number) { - bool is_free = page_table_[page_number].state == 0; - if (!is_free) { - // At least one page in the range is used, skip to next. - // We know we'll be starting at least before this page. - any_taken = true; - if (page_count > page_number) { - // Not enough space left to fit entire page range. Breaks outer - // loop. - base_page_number = -1; - } else { - base_page_number = page_number - page_count; - base_page_number -= QuickMod(base_page_number, page_scan_stride); - base_page_number += page_scan_stride; // cancel out loop logic - } - break; - } + + // Compute the highest aligned start within this block and range. + uint32_t usable_end = std::min(block_end, high_page_number + 1); + if (usable_end < page_count) { + continue; } - if (!any_taken) { - // Found our place. + uint32_t latest_start = usable_end - page_count; + // Align down to stride. + latest_start -= QuickMod(latest_start, page_scan_stride); + uint32_t usable_start = std::max(block_start, low_page_number); + if (latest_start >= usable_start && + latest_start + page_count <= block_end) { + start_page_number = latest_start; + end_page_number = latest_start + page_count - 1; break; } - // Retry. - start_page_number = end_page_number = UINT_MAX; } } else { - for (uint32_t base_page_number = low_page_number; - base_page_number <= high_page_number - page_count; - base_page_number += page_scan_stride) { - if (page_table_[base_page_number].state != 0) { - // Base page not free, skip to next usable page. - continue; + // Search free blocks from low addresses upward. + auto it = free_blocks_.lower_bound(low_page_number); + // Check if the previous block extends into our range. + if (it != free_blocks_.begin()) { + auto prev = std::prev(it); + if (prev->first + prev->second > low_page_number) { + it = prev; } - // Check requested range to ensure free. - start_page_number = base_page_number; - end_page_number = base_page_number + page_count - 1; - bool any_taken = false; - for (uint32_t page_number = base_page_number; - !any_taken && page_number <= end_page_number; ++page_number) { - bool is_free = page_table_[page_number].state == 0; - if (!is_free) { - // At least one page in the range is used, skip to next. - // We know we'll be starting at least after this page. - any_taken = true; - base_page_number = xe::round_up(page_number + 1, page_scan_stride); - base_page_number -= page_scan_stride; // cancel out loop logic - break; - } + } + for (; it != free_blocks_.end(); ++it) { + uint32_t block_start = it->first; + uint32_t block_count = it->second; + uint32_t block_end = block_start + block_count; + + // Block is entirely above our search range — stop. + if (block_start > high_page_number) { + break; } - if (!any_taken) { - // Found our place. + + // Skip blocks too small to possibly fit. + if (block_count < page_count) { + continue; + } + + // Compute the lowest aligned start within this block and range. + uint32_t earliest = std::max(block_start, low_page_number); + uint32_t aligned_start = xe::round_up(earliest, page_scan_stride, false); + if (aligned_start + page_count <= block_end && + aligned_start + page_count - 1 <= high_page_number) { + start_page_number = aligned_start; + end_page_number = aligned_start + page_count - 1; break; } - // Retry. - start_page_number = end_page_number = UINT_MAX; } } + if (start_page_number == UINT_MAX || end_page_number == UINT_MAX) { // Out of memory. XELOGE("BaseHeap::Alloc failed to find contiguous range"); @@ -1183,6 +1284,9 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address, return false; } + // Update free block tracker. + RemoveFreeBlock(start_page_number, page_count); + // Allocate from host. if (allocation_type == kMemoryAllocationReserve) { // Reserve is not needed, as we are mapped already. @@ -1196,6 +1300,8 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address, page_count << page_size_shift_, alloc_type, ToPageAccess(protect)); if (!result) { XELOGE("BaseHeap::Alloc failed to alloc range from host"); + // Restore the free block since we failed. + InsertFreeBlock(start_page_number, page_count); return false; } @@ -1329,6 +1435,9 @@ bool BaseHeap::Release(uint32_t base_address, uint32_t* out_region_size) { unreserved_page_count_++; } + // Insert freed block into tracker with coalescing. + InsertFreeBlock(base_page_number, base_page_entry.region_page_count); + return true; } @@ -1685,7 +1794,10 @@ bool PhysicalHeap::Alloc(uint32_t size, uint32_t alignment, alignment, allocation_type, protect, top_down, &parent_address)) { XELOGE( - "PhysicalHeap::Alloc unable to alloc physical memory in parent heap"); + "PhysicalHeap::Alloc unable to alloc physical memory in parent heap " + "(requested {} bytes, parent free {}/{} pages)", + size, parent_heap_->unreserved_page_count(), + parent_heap_->total_page_count()); return false; } @@ -1704,7 +1816,7 @@ bool PhysicalHeap::Alloc(uint32_t size, uint32_t alignment, protect)) { XELOGE( "PhysicalHeap::Alloc unable to pin physical memory in physical heap"); - // TODO(benvanik): don't leak parent memory. + parent_heap_->Release(parent_address); return false; } *out_address = address; @@ -1728,7 +1840,8 @@ bool PhysicalHeap::AllocFixed(uint32_t base_address, uint32_t size, if (!parent_heap_->AllocFixed(parent_base_address, size, alignment, allocation_type, protect)) { XELOGE( - "PhysicalHeap::Alloc unable to alloc physical memory in parent heap"); + "PhysicalHeap::AllocFixed unable to alloc physical memory in parent " + "heap"); return false; } @@ -1747,8 +1860,9 @@ bool PhysicalHeap::AllocFixed(uint32_t base_address, uint32_t size, if (!BaseHeap::AllocFixed(address, size, alignment, allocation_type, protect)) { XELOGE( - "PhysicalHeap::Alloc unable to pin physical memory in physical heap"); - // TODO(benvanik): don't leak parent memory. + "PhysicalHeap::AllocFixed unable to pin physical memory in physical " + "heap"); + parent_heap_->Release(parent_base_address); return false; } @@ -1777,7 +1891,10 @@ bool PhysicalHeap::AllocRange(uint32_t low_address, uint32_t high_address, alignment, allocation_type, protect, top_down, &parent_address)) { XELOGE( - "PhysicalHeap::Alloc unable to alloc physical memory in parent heap"); + "PhysicalHeap::AllocRange unable to alloc physical memory in parent " + "heap (requested {} bytes, parent free {}/{} pages)", + size, parent_heap_->unreserved_page_count(), + parent_heap_->total_page_count()); return false; } // Given the address we've reserved in the parent heap, pin that here. @@ -1795,8 +1912,9 @@ bool PhysicalHeap::AllocRange(uint32_t low_address, uint32_t high_address, if (!BaseHeap::AllocFixed(address, size, alignment, allocation_type, protect)) { XELOGE( - "PhysicalHeap::Alloc unable to pin physical memory in physical heap"); - // TODO(benvanik): don't leak parent memory. + "PhysicalHeap::AllocRange unable to pin physical memory in physical " + "heap"); + parent_heap_->Release(parent_address); return false; } *out_address = address; diff --git a/src/xenia/memory.h b/src/xenia/memory.h index 56de32d6b..bd9519a40 100644 --- a/src/xenia/memory.h +++ b/src/xenia/memory.h @@ -11,6 +11,7 @@ #define XENIA_MEMORY_H_ #include +#include #include #include #include @@ -210,6 +211,15 @@ class BaseHeap { uint32_t heap_base, uint32_t heap_size, uint32_t page_size, uint32_t host_address_offset = 0); + // Rebuilds free_blocks_ by scanning page_table_. Used after Restore. + void RebuildFreeBlocks(); + + // Removes (or splits) the free block covering the given page range. + void RemoveFreeBlock(uint32_t start_page, uint32_t page_count); + + // Inserts a free block and coalesces with adjacent free blocks. + void InsertFreeBlock(uint32_t start_page, uint32_t page_count); + Memory* memory_; uint8_t* membase_; HeapType heap_type_; @@ -221,6 +231,10 @@ class BaseHeap { uint32_t unreserved_page_count_; xe::global_critical_region global_critical_region_; std::vector page_table_; + + // Auxiliary free block tracker: maps start_page -> count of contiguous free + // pages. Kept in sync with page_table_ mutations. Not serialized. + std::map free_blocks_; }; // Normal heap allowing allocations from guest virtual address ranges. From c28019e333e5bd34b6f18d40491a3ddb5a6fa064 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:38:01 +0900 Subject: [PATCH 16/25] [Memory] Fix AllocRange upper bound and update PhysicalHeap tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BaseHeap::AllocRange was rounding the inclusive high_address UP via xe::align(), which could push the search range one alignment stride past the requested end. The new free-block-tracker top-down search treats high_page_number as an inclusive max (usable_end = high_page_number + 1), so the round-up caused it to return a base page one stride beyond the caller's bound — most visibly when PhysicalHeap::Alloc passed parent_heap_end = GetPhysicalAddress(...) just below a page boundary, producing a translated address one page past the child heap and tripping "passed out of range address range" in BaseHeap::AllocFixed. Drop the xe::align on the high side so high_address stays a true inclusive bound. The low_address round-up is still correct since allocations must START at or above the aligned low. --- src/xenia/base/testing/physical_heap_test.cc | 24 +++++++++----------- src/xenia/memory.cc | 3 +-- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/xenia/base/testing/physical_heap_test.cc b/src/xenia/base/testing/physical_heap_test.cc index afd9aee79..20f0a9b7f 100644 --- a/src/xenia/base/testing/physical_heap_test.cc +++ b/src/xenia/base/testing/physical_heap_test.cc @@ -161,19 +161,16 @@ TEST_CASE("PhysicalHeap vE0000000 alignment", "[memory]") { REQUIRE(translation_offset % heap.page_size() == 0); } - SECTION("alloc with alignment larger than page_size is rejected") { - // The translation offset (0xDFFFF000) is only 4KB-aligned, so a - // 64KB alignment request produces a misaligned translated address. - // - // Without the fix: BaseHeap::AllocFixed receives a misaligned address, - // hitting assert_true in debug or silently corrupting in release. - // With the fix: PhysicalHeap::Alloc detects the misalignment and - // returns false cleanly. + SECTION("alloc with alignment larger than page_size succeeds") { + // The translation offset (0xDFFFF000) is only 4KB-aligned, so the + // guest virtual address won't be 64KB-aligned. But the physical + // (host) address IS aligned, which is what matters. uint32_t alignment = 0x10000; // 64KB uint32_t addr = 0; bool ok = heap.Alloc(0x10000, alignment, kMemoryAllocationReserve, kMemoryProtectRead, false, &addr); - REQUIRE_FALSE(ok); + REQUIRE(ok); + REQUIRE(addr >= 0xE0000000); } } @@ -195,15 +192,16 @@ TEST_CASE("PhysicalHeap vE0000000 AllocRange alignment", "[memory]") { REQUIRE(addr % 0x1000 == 0); } - SECTION("AllocRange rejects misaligned translation") { - // Same scenario as Alloc: 64KB alignment on a heap whose translation - // offset (0xDFFFF000) is not 64KB-aligned. + SECTION("AllocRange with large alignment succeeds") { + // The guest virtual address won't be 64KB-aligned due to the 0x1000 + // translation offset, but the physical (host) address is aligned. uint32_t alignment = 0x10000; uint32_t addr = 0; bool ok = heap.AllocRange(0xE0000000, 0xFFFCFFFF, 0x10000, alignment, kMemoryAllocationReserve, kMemoryProtectRead, false, &addr); - REQUIRE_FALSE(ok); + REQUIRE(ok); + REQUIRE(addr >= 0xE0000000); } } diff --git a/src/xenia/memory.cc b/src/xenia/memory.cc index 9d51cd40b..f1a1e32f7 100644 --- a/src/xenia/memory.cc +++ b/src/xenia/memory.cc @@ -1182,8 +1182,7 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address, alignment = xe::round_up(alignment, page_size_); uint32_t page_count = get_page_count(size, page_size_); low_address = std::max(heap_base_, xe::align(low_address, alignment)); - high_address = std::min(heap_base_ + (heap_size_ - 1), - xe::align(high_address, alignment)); + high_address = std::min(heap_base_ + (heap_size_ - 1), high_address); uint32_t low_page_number = (low_address - heap_base_) >> page_size_shift_; uint32_t high_page_number = (high_address - heap_base_) >> page_size_shift_; From 7887efa69f5db3e45cd76d7e33629ece6ed19eb0 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Fri, 17 Oct 2025 00:18:20 +0900 Subject: [PATCH 17/25] [APU] fix potential semaphore leak on invalid client index --- src/xenia/apu/audio_system.cc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/xenia/apu/audio_system.cc b/src/xenia/apu/audio_system.cc index e5c4fd3c9..ba53454b3 100644 --- a/src/xenia/apu/audio_system.cc +++ b/src/xenia/apu/audio_system.cc @@ -248,6 +248,14 @@ void AudioSystem::SubmitFrame(size_t index, float* samples) { "(in_use={}, driver={:p})", index, index < kMaximumClientCount ? clients_[index].in_use : false, index < kMaximumClientCount ? (void*)clients_[index].driver : nullptr); + + // Submit silence instead of dropping the frame to maintain the callback + // chain. If we don't submit anything, the audio driver's OnBufferEnd + // callback will never fire, causing the semaphore to leak. + if (index < kMaximumClientCount && clients_[index].driver) { + static float silence[apu::AudioDriver::kFrameSamplesMax] = {0}; + (clients_[index].driver)->SubmitFrame(silence); + } return; } (clients_[index].driver)->SubmitFrame(samples); From c26102722524146d5dae0ff690171eee2cb3db37 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:27:51 +0900 Subject: [PATCH 18/25] [UI] Fix leaked locked_achievement_icon_ texture on drawer teardown Reset locked_achievement_icon_ alongside font_texture_ and notification_icon_textures_ when the immediate drawer is cleared, preventing a debug assertion in D3D12ImmediateDrawer's destructor. --- src/xenia/ui/imgui_drawer.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xenia/ui/imgui_drawer.cc b/src/xenia/ui/imgui_drawer.cc index 6aa3e1326..385963af4 100644 --- a/src/xenia/ui/imgui_drawer.cc +++ b/src/xenia/ui/imgui_drawer.cc @@ -564,7 +564,7 @@ void ImGuiDrawer::SetImmediateDrawer(ImmediateDrawer* new_immediate_drawer) { if (immediate_drawer_) { GetIO().Fonts->TexID = reinterpret_cast(nullptr); font_texture_.reset(); - + locked_achievement_icon_.reset(); notification_icon_textures_.clear(); } immediate_drawer_ = new_immediate_drawer; From 2590f03bc13c58a30ace8514dd775a8ae1aff832 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 27 Oct 2025 20:12:31 +0900 Subject: [PATCH 19/25] [Emulator] Delegate to UI thread instead of assert --- src/xenia/emulator.cc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 523b3fadd..ae64b8c9f 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -1445,7 +1445,15 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, const std::string_view module_path) { // Making changes to the UI (setting the icon) and executing game config // load callbacks which expect to be called from the UI thread. - assert_true(display_window_->app_context().IsInUIThread()); + // If not on UI thread, dispatch to it synchronously. + if (!display_window_->app_context().IsInUIThread()) { + X_STATUS result = X_STATUS_UNSUCCESSFUL; + display_window_->app_context().CallInUIThreadSynchronous( + [this, &path, &module_path, &result]() { + result = CompleteLaunch(path, module_path); + }); + return result; + } // Setup NullDevices for raw HDD partition accesses // Cache/STFC code baked into games tries reading/writing to these From c17a3b19fbdfe7180460d140db850aa4279276c2 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:16:09 +0900 Subject: [PATCH 20/25] [Kernel] Implement per-process TLS allocation Previous global TLS bitmap was shared across all processes. Now per-process TLS bitmaps stored X_KPROCESS structures which should better match Xbox360 architecture. --- src/xenia/kernel/kernel_state.cc | 146 +++++++++++++++--- src/xenia/kernel/kernel_state.h | 8 +- .../kernel/xboxkrnl/xboxkrnl_threading.cc | 9 +- 3 files changed, 133 insertions(+), 30 deletions(-) diff --git a/src/xenia/kernel/kernel_state.cc b/src/xenia/kernel/kernel_state.cc index 5b76f897a..19be10b0f 100644 --- a/src/xenia/kernel/kernel_state.cc +++ b/src/xenia/kernel/kernel_state.cc @@ -68,9 +68,6 @@ KernelState::KernelState(Emulator* emulator) InitializeKernelGuestGlobals(); kernel_version_ = KernelVersion(cvars::kernel_build_version); - // Hardcoded maximum of 2048 TLS slots. - tls_bitmap_.Resize(2048); - auto hc_loc_heap = memory_->LookupHeap(strange_hardcoded_page_); bool fixed_alloc_worked = hc_loc_heap->AllocFixed( strange_hardcoded_page_, 65536, 0, @@ -141,18 +138,132 @@ const std::unique_ptr KernelState::module_xdbf( return nullptr; } -uint32_t KernelState::AllocateTLS() { return uint32_t(tls_bitmap_.Acquire()); } +uint32_t KernelState::AllocateTLS(cpu::ppc::PPCContext* context) { + auto globals = + memory()->TranslateVirtual(GetKernelGuestGlobals()); + auto tls_lock = &globals->tls_lock; + auto old_irql = xboxkrnl::xeKeKfAcquireSpinLock(context, tls_lock); -void KernelState::FreeTLS(uint32_t slot) { + int result = -1; + + auto current_thread = XThread::GetCurrentThread(); + if (!current_thread) { + XELOGE("AllocateTLS: No current thread"); + xboxkrnl::xeKeKfReleaseSpinLock(context, tls_lock, old_irql); + return X_TLS_OUT_OF_INDEXES; + } + + auto process_ptr = memory()->TranslateVirtual( + current_thread->guest_object()->process); + if (!process_ptr) { + XELOGE("AllocateTLS: Failed to translate process pointer"); + xboxkrnl::xeKeKfReleaseSpinLock(context, tls_lock, old_irql); + return X_TLS_OUT_OF_INDEXES; + } + + // Search for a free TLS slot in the process bitmap + // Bitmap format: 1 = free, 0 = allocated + // 8 x 32-bit words = 256 total TLS slots + for (xe::be* i = &process_ptr->tls_slot_bitmap[0]; + i < &process_ptr->tls_slot_bitmap[8]; ++i) { + // Read bitmap value (handles big-endian conversion) + uint32_t bitmap_value = static_cast(*i); + + // Find highest free slot using lzcnt (leading zero count) + // Returns 0-31 if a bit is set, 32 if no bits are set + uint32_t leading_zeros = xe::lzcnt(bitmap_value); + + if (leading_zeros != 32) { + // Calculate absolute slot index from bitmap position and bit offset + // Each bitmap word represents 32 slots + size_t bitmap_index = i - &process_ptr->tls_slot_bitmap[0]; + uint32_t base_slot = static_cast(bitmap_index) * 32; + int calculated_slot = base_slot + leading_zeros; + + // Validate slot is within Xbox 360 TLS range + if (calculated_slot >= 0 && calculated_slot < 256) { + result = calculated_slot; + + // Clear the bit to mark as allocated + // lzcnt returns 0 for bit 31, 31 for bit 0 + uint32_t bit_index = 31 - leading_zeros; + *i = bitmap_value & ~(1U << bit_index); + break; + } else { + XELOGE("AllocateTLS: Invalid slot calculation: {}", calculated_slot); + } + } + } + + if (result == -1) { + XELOGW("AllocateTLS: All TLS slots exhausted for current process"); + } + + xboxkrnl::xeKeKfReleaseSpinLock(context, tls_lock, old_irql); + return static_cast(result); +} + +void KernelState::FreeTLS(cpu::ppc::PPCContext* context, uint32_t slot) { + if (slot >= 256) { + XELOGE("FreeTLS: Invalid slot index {}", slot); + return; + } + + auto current_thread = XThread::GetCurrentThread(); + if (!current_thread) { + XELOGE("FreeTLS: No current thread"); + return; + } + + auto current_kthread = current_thread->guest_object(); + if (!current_kthread) { + XELOGE("FreeTLS: Failed to get guest thread object"); + return; + } + + auto process_ptr = memory()->TranslateVirtual(current_kthread->process); + if (!process_ptr) { + XELOGE("FreeTLS: Failed to translate process pointer"); + return; + } + + auto globals = + memory()->TranslateVirtual(GetKernelGuestGlobals()); + auto tls_lock = &globals->tls_lock; + auto old_irql = xboxkrnl::xeKeKfAcquireSpinLock(context, tls_lock); + + uint32_t bitmap_index = slot / 32; + uint32_t bit_mask = 1U << (31 - (slot % 32)); + uint32_t bitmap_value = + static_cast(process_ptr->tls_slot_bitmap[bitmap_index]); + + if (bitmap_value & bit_mask) { + XELOGW("FreeTLS: Slot {} is already free", slot); + xboxkrnl::xeKeKfReleaseSpinLock(context, tls_lock, old_irql); + return; + } + + // Clear TLS values in all threads of this process const std::vector> threads = object_table()->GetObjectsByType(); + uint32_t current_process_ptr = current_kthread->process.m_ptr; for (const object_ref& thread : threads) { - if (thread->is_guest_thread()) { + if (!thread || !thread->is_guest_thread()) { + continue; + } + + auto thread_kthread = thread->guest_object(); + if (thread_kthread && + thread_kthread->process.m_ptr == current_process_ptr) { thread->SetTLSValue(slot, 0); } } - tls_bitmap_.Release(slot); + + // Mark slot as free in bitmap + process_ptr->tls_slot_bitmap[bitmap_index] = bitmap_value | bit_mask; + + xboxkrnl::xeKeKfReleaseSpinLock(context, tls_lock, old_irql); } void KernelState::RegisterTitleTerminateNotification(uint32_t routine, @@ -805,9 +916,6 @@ void KernelState::TerminateTitle() { // Unregister all notify listeners. notify_listeners_.clear(); - // Clear the TLS map. - tls_bitmap_.Reset(); - // Unset the executable module. executable_module_ = nullptr; @@ -1077,12 +1185,6 @@ bool KernelState::Save(ByteStream* stream) { object_table_.Save(stream); // Write the TLS allocation bitmap - auto tls_bitmap = tls_bitmap_.data(); - stream->Write(uint32_t(tls_bitmap.size())); - for (size_t i = 0; i < tls_bitmap.size(); i++) { - stream->Write(tls_bitmap[i]); - } - // We save XThreads absolutely first, as they will execute code upon save // (which could modify the kernel state) auto threads = object_table_.GetObjectsByType(); @@ -1210,12 +1312,11 @@ bool KernelState::Restore(ByteStream* stream) { // Restore the object table object_table_.Restore(stream); - // Read the TLS allocation bitmap + // TLS bitmap is now stored per-process in X_KPROCESS structures (in guest + // memory) Skip reading old global TLS bitmap if present in old save files auto num_bitmap_entries = stream->Read(); - auto& tls_bitmap = tls_bitmap_.data(); - tls_bitmap.resize(num_bitmap_entries); for (uint32_t i = 0; i < num_bitmap_entries; i++) { - tls_bitmap[i] = stream->Read(); + stream->Read(); // Discard old data } uint32_t num_threads = stream->Read(); @@ -1349,12 +1450,13 @@ void KernelState::SetProcessTLSVars(X_KPROCESS* process, int num_slots, process->tls_slot_size = 4 * slots_padded; uint32_t count_div32 = slots_padded / 32; for (unsigned word_index = 0; word_index < count_div32; ++word_index) { - process->bitmap[word_index] = -1; + process->tls_slot_bitmap[word_index] = -1; } // set remainder of bitset if (((num_slots + 3) & 0x1C) != 0) - process->bitmap[count_div32] = -1 << (32 - ((num_slots + 3) & 0x1C)); + process->tls_slot_bitmap[count_div32] = -1 + << (32 - ((num_slots + 3) & 0x1C)); } void AllocateThread(PPCContext* context) { uint32_t thread_mem_size = static_cast(context->r[3]); diff --git a/src/xenia/kernel/kernel_state.h b/src/xenia/kernel/kernel_state.h index 0b5248943..da8cb9066 100644 --- a/src/xenia/kernel/kernel_state.h +++ b/src/xenia/kernel/kernel_state.h @@ -80,7 +80,7 @@ struct X_KPROCESS { uint8_t is_terminating; // one of X_PROCTYPE_ uint8_t process_type; - xe::be bitmap[8]; + xe::be tls_slot_bitmap[8]; xe::be unk_50; X_LIST_ENTRY unk_54; xe::be unk_5C; @@ -140,6 +140,7 @@ struct KernelGuestGlobals { // this lock is only used in some Ob functions. It's odd that it is used at // all, as each table already has its own spinlock. X_KSPINLOCK ob_lock; + X_KSPINLOCK tls_lock; // protects per-process TLS bitmap allocations // if LLE emulating Xam, this is needed or you get an immediate freeze X_KEVENT UsbdBootEnumerationDoneEvent; @@ -219,8 +220,8 @@ class KernelState { return kernel_guest_globals_ + offsetof(KernelGuestGlobals, idle_process); } - uint32_t AllocateTLS(); - void FreeTLS(uint32_t slot); + uint32_t AllocateTLS(cpu::ppc::PPCContext* context); + void FreeTLS(cpu::ppc::PPCContext* context, uint32_t slot); void RegisterTitleTerminateNotification(uint32_t routine, uint32_t priority); void RemoveTitleTerminateNotification(uint32_t routine); @@ -382,7 +383,6 @@ class KernelState { std::condition_variable_any dispatch_cond_; std::list> dispatch_queue_; - BitMap tls_bitmap_; uint32_t ke_timestamp_bundle_ptr_ = 0; std::unique_ptr timestamp_timer_; uint32_t quantum_timer_counter_ = 0; diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc index f4cfac6ee..901f82e2d 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc @@ -469,8 +469,8 @@ void KeQuerySystemTime_entry(lpqword_t time_ptr, const ppc_context_t& ctx) { DECLARE_XBOXKRNL_EXPORT1(KeQuerySystemTime, kThreading, kImplemented); // https://msdn.microsoft.com/en-us/library/ms686801 -dword_result_t KeTlsAlloc_entry() { - uint32_t slot = kernel_state()->AllocateTLS(); +dword_result_t KeTlsAlloc_entry(const ppc_context_t& context) { + uint32_t slot = kernel_state()->AllocateTLS(context); XThread::GetCurrentThread()->SetTLSValue(slot, 0); return slot; @@ -478,12 +478,13 @@ dword_result_t KeTlsAlloc_entry() { DECLARE_XBOXKRNL_EXPORT1(KeTlsAlloc, kThreading, kImplemented); // https://msdn.microsoft.com/en-us/library/ms686804 -dword_result_t KeTlsFree_entry(dword_t tls_index) { +dword_result_t KeTlsFree_entry(dword_t tls_index, + const ppc_context_t& context) { if (tls_index == X_TLS_OUT_OF_INDEXES) { return 0; } - kernel_state()->FreeTLS(tls_index); + kernel_state()->FreeTLS(context, tls_index); return 1; } DECLARE_XBOXKRNL_EXPORT1(KeTlsFree, kThreading, kImplemented); From d42411ec2aaabfb96657ffe7c961eb9b66b81850 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:59:36 +0900 Subject: [PATCH 21/25] [Kernel] Fix NtReleaseSemaphore returning wrong status code --- src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc | 4 +--- src/xenia/xbox.h | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc index 901f82e2d..64dbe7b1a 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc @@ -775,13 +775,11 @@ dword_result_t NtReleaseSemaphore_entry(dword_t sem_handle, 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; + result = X_STATUS_SEMAPHORE_LIMIT_EXCEEDED; } } else { result = X_STATUS_INVALID_HANDLE; diff --git a/src/xenia/xbox.h b/src/xenia/xbox.h index 8ba7d32a7..f159a58ab 100644 --- a/src/xenia/xbox.h +++ b/src/xenia/xbox.h @@ -60,6 +60,7 @@ typedef uint32_t X_STATUS; #define X_STATUS_OBJECT_NAME_COLLISION ((X_STATUS)0xC0000035L) #define X_STATUS_INVALID_PAGE_PROTECTION ((X_STATUS)0xC0000045L) #define X_STATUS_MUTANT_NOT_OWNED ((X_STATUS)0xC0000046L) +#define X_STATUS_SEMAPHORE_LIMIT_EXCEEDED ((X_STATUS)0xC0000047L) #define X_STATUS_THREAD_IS_TERMINATING ((X_STATUS)0xC000004BL) #define X_STATUS_PROCEDURE_NOT_FOUND ((X_STATUS)0xC000007AL) #define X_STATUS_INVALID_IMAGE_FORMAT ((X_STATUS)0xC000007BL) From 658bd5db702ab127a8302ba3285e193b235451a7 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:07:53 +0900 Subject: [PATCH 22/25] [Xboxkrnl] Return error code from KeSetAffinityThread on null thread If the guest-supplied thread pointer doesn't resolve to an XThread, we previously returned STATUS_SUCCESS without setting the affinity or writing previous_affinity_ptr, leaving callers believing the call succeeded. Real NT kernel would crash on a bad pointer, but that's not easy for us to replicate so return STATUS_INVALID_HANDLE instead and log the pointer value so the condition is visible rather than silent. --- src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc index 64dbe7b1a..ced21a600 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc @@ -331,12 +331,17 @@ dword_result_t KeSetAffinityThread_entry(lpvoid_t thread_ptr, dword_t affinity, return X_STATUS_INVALID_PARAMETER; } auto thread = XObject::GetNativeObject(kernel_state(), thread_ptr); - if (thread) { - if (previous_affinity_ptr) { - *previous_affinity_ptr = uint32_t(1) << thread->active_cpu(); - } - thread->SetAffinity(affinity); + if (!thread) { + XELOGW( + "KeSetAffinityThread: guest thread pointer {:08X} did not resolve to " + "an XThread; returning STATUS_INVALID_HANDLE", + thread_ptr.guest_address()); + return X_STATUS_INVALID_HANDLE; } + if (previous_affinity_ptr) { + *previous_affinity_ptr = uint32_t(1) << thread->active_cpu(); + } + thread->SetAffinity(affinity); return X_STATUS_SUCCESS; } DECLARE_XBOXKRNL_EXPORT1(KeSetAffinityThread, kThreading, kImplemented); From 844cc6e2ed8d288fd8ebca0aa03072f774c529d6 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:07:32 +0900 Subject: [PATCH 23/25] [Memory] Remove unnecessary 256MB carve-out from v00000000 virtual heap The 4KB-page virtual heap (v00000000, 0x00000000-0x3FFFFFFF) had 256MB reserved at its top end for thread stacks, but thread stacks only use the 64KB-page heap (v40000000) at 0x70000000-0x7F000000. This wasted 256MB of allocatable address space. Keep the carve-out only on the v40000000 heap where stacks actually reside. --- src/xenia/memory.cc | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/xenia/memory.cc b/src/xenia/memory.cc index f1a1e32f7..9c9b7af43 100644 --- a/src/xenia/memory.cc +++ b/src/xenia/memory.cc @@ -1044,17 +1044,12 @@ bool BaseHeap::Alloc(uint32_t size, uint32_t alignment, size = xe::round_up(size, page_size_); alignment = xe::round_up(alignment, page_size_); - // TODO(Gliniak): Find better way to deal with this! - // Because 0x3XXXXXX and 0x7XXXXXX is used strictly as place for thread stacks - // 0x3 is probably for system threads and 0x7 for title threads + // Exclude the top 240MB of the v40000000 heap (64KB guest pages) from + // general allocation to protect the thread stack region + // (0x70000000-0x7F000000) uint32_t heap_virtual_guest_offset = 0; - if (heap_type_ == HeapType::kGuestVirtual) { - heap_virtual_guest_offset = 0x10000000; - - // Adjust for 64k pages region, to prevent having a bit too little memory - if (page_size_ == 0x10000) { - heap_virtual_guest_offset = 0x0F000000; - } + if (heap_type_ == HeapType::kGuestVirtual && page_size_ == 0x10000) { + heap_virtual_guest_offset = 0x0F000000; } uint32_t low_address = heap_base_; From 763b160c7aefb0d8fb34911986716351adfcf829 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:23:27 +0900 Subject: [PATCH 24/25] [Threading/Linux] Add fallback to nice values SCHED_FIFO requires CAP_SYS_NICE or root, try SCHED_FIFO first for real-time priority control, then fall back to setpriority() nice values if permission is denied. --- src/xenia/base/threading_posix.cc | 60 +++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/src/xenia/base/threading_posix.cc b/src/xenia/base/threading_posix.cc index 630e822a3..565a39a5f 100644 --- a/src/xenia/base/threading_posix.cc +++ b/src/xenia/base/threading_posix.cc @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -611,6 +612,7 @@ class PosixCondition final : public PosixConditionBase { /// Thread::GetCurrentThread() on the main thread explicit PosixCondition(pthread_t thread) : thread_(thread), + tid_(static_cast(syscall(SYS_gettid))), signaled_(false), exit_code_(0), state_(State::kRunning), @@ -742,31 +744,48 @@ class PosixCondition final : public PosixConditionBase { int priority() const { WaitStarted(); - int policy; - sched_param param{}; - int ret = pthread_getschedparam(thread_, &policy, ¶m); - if (ret != 0) { - return -1; + if (!fifo_failed_) { + int policy; + sched_param param{}; + int ret = pthread_getschedparam(thread_, &policy, ¶m); + if (ret != 0) { + return -1; + } + return param.sched_priority; } - - return param.sched_priority; + // When using nice values, map back to the SCHED_FIFO range (1-32) + // so callers see a consistent priority space. + int nice_val = getpriority(PRIO_PROCESS, tid_); + // nice -19..19 → fifo 32..1 + return 16 - nice_val; } void set_priority(int new_priority) const { WaitStarted(); - sched_param param{}; - param.sched_priority = new_priority; - int res = pthread_setschedparam(thread_, SCHED_FIFO, ¶m); - if (res != 0) { - switch (res) { - case EPERM: - XELOGW("Permission denied while setting priority"); - break; - case EINVAL: - assert_always(); - default: - XELOGW("Unknown error while setting priority"); + if (!fifo_failed_) { + // Try real-time SCHED_FIFO for best priority control. + sched_param param{}; + param.sched_priority = new_priority; + int res = pthread_setschedparam(thread_, SCHED_FIFO, ¶m); + if (res == 0) { + return; } + if (res == EPERM) { + fifo_failed_ = true; + } else { + XELOGW("Unexpected error {} while setting SCHED_FIFO priority", res); + fifo_failed_ = true; + } + } + // Fall back to nice values under SCHED_OTHER. + // Map SCHED_FIFO range (1-32) to nice range (19 to -19). + // Center: fifo 16 → nice 0. + int nice_val = 16 - new_priority; + // Clamp to valid nice range. + if (nice_val < -20) nice_val = -20; + if (nice_val > 19) nice_val = 19; + if (tid_ > 0) { + setpriority(PRIO_PROCESS, tid_, nice_val); } } @@ -930,6 +949,8 @@ class PosixCondition final : public PosixConditionBase { sem_destroy(&suspend_sem_); } pthread_t thread_; + pid_t tid_ = 0; // Kernel TID for setpriority() fallback + mutable bool fifo_failed_ = false; // True after SCHED_FIFO was rejected bool signaled_; int exit_code_; State state_; // Protected by state_mutex_ @@ -1243,6 +1264,7 @@ void* PosixCondition::ThreadStartRoutine(void* parameter) { delete start_data; current_thread_ = thread; + thread->handle_.tid_ = static_cast(syscall(SYS_gettid)); { std::unique_lock lock(thread->handle_.state_mutex_); thread->handle_.state_ = From 4fcb8e4498aca8bfb13a8150c2e6375a58fdf767 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:59:45 +0900 Subject: [PATCH 25/25] [Memory] Treat AllocRange high_page_number as exclusive The free-block tracker search in BaseHeap::AllocRange treats high_page_number as inclusive, shifting allocator-returned addresses by one stride relative to the old loop-based search. Some titles encode allocator-returned addresses in PPC code and break when that layout shifts (Far Cry 3, Far Cry 4, Watchdogs). In addition, reapply xe::align on the high side of AllocRange (essentially reverting c28019e33). Without the round-up, a caller passing a min/max window exactly the size of its request loses a stride at the top and fails the early page_count size check. --- src/xenia/base/testing/heap_test.cc | 17 ++++++++------- src/xenia/base/testing/physical_heap_test.cc | 22 ++++++++++++-------- src/xenia/memory.cc | 13 +++++++++--- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/xenia/base/testing/heap_test.cc b/src/xenia/base/testing/heap_test.cc index f83a435b6..b039cda46 100644 --- a/src/xenia/base/testing/heap_test.cc +++ b/src/xenia/base/testing/heap_test.cc @@ -85,14 +85,15 @@ TEST_CASE("heap_alloc_basic", "[heap]") { TEST_CASE("heap_alloc_top_down", "[heap]") { TestHeap h(0x80000000, 0x100000, 0x1000); + // Top-down treats high_page_number as exclusive, so the top page is + // never handed out. uint32_t addr = 0; REQUIRE(h.Alloc(0x1000, 0x1000, true, &addr)); - // Top-down: should be at the highest aligned address. - REQUIRE(addr == 0x800FF000); + REQUIRE(addr == 0x800FE000); REQUIRE(h.unreserved_page_count() == 255); REQUIRE(h.Alloc(0x2000, 0x1000, true, &addr)); - REQUIRE(addr == 0x800FD000); + REQUIRE(addr == 0x800FC000); REQUIRE(h.unreserved_page_count() == 253); } @@ -237,16 +238,18 @@ TEST_CASE("heap_alloc_alignment_top_down", "[heap]") { // 1MB heap, 4KB pages TestHeap h(0x80000000, 0x100000, 0x1000); - // Allocate 1 page at the top. + // Top-down skips the top page (0x800FF000), so a 1-page allocation + // lands on page 0xFE. uint32_t first = 0; REQUIRE(h.Alloc(0x1000, 0x1000, true, &first)); - REQUIRE(first == 0x800FF000); + REQUIRE(first == 0x800FE000); - // Allocate with 64KB alignment top-down — should align down. + // 64KB-aligned top-down: stride 16, exclusive high at page 0xFF, so + // the highest aligned base is page 0xE0. uint32_t aligned = 0; REQUIRE(h.Alloc(0x1000, 0x10000, true, &aligned)); REQUIRE((aligned % 0x10000) == 0); - REQUIRE(aligned == 0x800F0000); + REQUIRE(aligned == 0x800E0000); } // ============================================================================ diff --git a/src/xenia/base/testing/physical_heap_test.cc b/src/xenia/base/testing/physical_heap_test.cc index 20f0a9b7f..7e83420f9 100644 --- a/src/xenia/base/testing/physical_heap_test.cc +++ b/src/xenia/base/testing/physical_heap_test.cc @@ -161,16 +161,17 @@ TEST_CASE("PhysicalHeap vE0000000 alignment", "[memory]") { REQUIRE(translation_offset % heap.page_size() == 0); } - SECTION("alloc with alignment larger than page_size succeeds") { - // The translation offset (0xDFFFF000) is only 4KB-aligned, so the - // guest virtual address won't be 64KB-aligned. But the physical - // (host) address IS aligned, which is what matters. + SECTION("alloc with alignment larger than page_size is rejected") { + // vE0000000 has a 0x1000 physical translation offset, so a 64KB + // alignment request can't produce a 64KB-aligned guest address. + // PhysicalHeap::Alloc forces top-down, which here lands one stride + // past the end of the child heap and BaseHeap::AllocFixed rejects + // it as out of range. uint32_t alignment = 0x10000; // 64KB uint32_t addr = 0; bool ok = heap.Alloc(0x10000, alignment, kMemoryAllocationReserve, kMemoryProtectRead, false, &addr); - REQUIRE(ok); - REQUIRE(addr >= 0xE0000000); + REQUIRE_FALSE(ok); } } @@ -192,9 +193,12 @@ TEST_CASE("PhysicalHeap vE0000000 AllocRange alignment", "[memory]") { REQUIRE(addr % 0x1000 == 0); } - SECTION("AllocRange with large alignment succeeds") { - // The guest virtual address won't be 64KB-aligned due to the 0x1000 - // translation offset, but the physical (host) address is aligned. + SECTION("AllocRange with large alignment succeeds via bottom-up") { + // Bottom-up search picks a low parent address that translates to a + // guest address inside the child heap, so BaseHeap::AllocFixed accepts + // it. The PhysicalHeap alignment check is host-based + // ((addr + host_address_offset_) % alignment), so the misalignment of + // the guest address itself is not rejected here. uint32_t alignment = 0x10000; uint32_t addr = 0; bool ok = heap.AllocRange(0xE0000000, 0xFFFCFFFF, 0x10000, alignment, diff --git a/src/xenia/memory.cc b/src/xenia/memory.cc index 9c9b7af43..22ba66aee 100644 --- a/src/xenia/memory.cc +++ b/src/xenia/memory.cc @@ -1177,7 +1177,8 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address, alignment = xe::round_up(alignment, page_size_); uint32_t page_count = get_page_count(size, page_size_); low_address = std::max(heap_base_, xe::align(low_address, alignment)); - high_address = std::min(heap_base_ + (heap_size_ - 1), high_address); + high_address = std::min(heap_base_ + (heap_size_ - 1), + xe::align(high_address, alignment)); uint32_t low_page_number = (low_address - heap_base_) >> page_size_shift_; uint32_t high_page_number = (high_address - heap_base_) >> page_size_shift_; @@ -1219,7 +1220,11 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address, } // Compute the highest aligned start within this block and range. - uint32_t usable_end = std::min(block_end, high_page_number + 1); + // high_page_number is exclusive and rounded down to the stride, so + // the top stride of pages is never returned. + uint32_t high_aligned = + high_page_number - QuickMod(high_page_number, page_scan_stride); + uint32_t usable_end = std::min(block_end, high_aligned); if (usable_end < page_count) { continue; } @@ -1260,10 +1265,12 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address, } // Compute the lowest aligned start within this block and range. + // high_page_number is treated as exclusive — the page at + // high_page_number itself is never returned. uint32_t earliest = std::max(block_start, low_page_number); uint32_t aligned_start = xe::round_up(earliest, page_scan_stride, false); if (aligned_start + page_count <= block_end && - aligned_start + page_count - 1 <= high_page_number) { + aligned_start + page_count <= high_page_number) { start_page_number = aligned_start; end_page_number = aligned_start + page_count - 1; break;