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] [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;