[A64] Fix RESERVED_STORE LSE path leaving labels unbound

The LSE paths in RESERVED_STORE_I32/I64 returned early from Emit,
leaving forward-referenced no_reserve/done labels unbound. Xbyak
encodes these as offset-zero branches (branch-to-self), causing
infinite loops when the reservation check fails on LSE hardware.

Replace early return with branch-to-done inside an if/else so
labels are always bound regardless of the LSE path taken.

Adds tests for RESERVED_STORE I64 success path and I32/I64
no-reservation failure path with timeout-based hang detection.
This commit is contained in:
Herman S.
2026-03-29 17:39:29 +09:00
parent d3d0478403
commit 904c6c8b1c
2 changed files with 152 additions and 30 deletions

View File

@@ -1199,9 +1199,8 @@ struct RESERVED_STORE_I32
e.casal(e.w5, e.w6, ptr(e.x4)); e.casal(e.w5, e.w6, ptr(e.x4));
e.cmp(e.w5, e.w0); e.cmp(e.w5, e.w0);
e.cset(i.dest, Xbyak_aarch64::EQ); e.cset(i.dest, Xbyak_aarch64::EQ);
return; e.b(done);
} } else {
// LDXR/STXR loop. // LDXR/STXR loop.
auto& cas_loop = e.NewCachedLabel(); auto& cas_loop = e.NewCachedLabel();
auto& cas_fail = e.NewCachedLabel(); auto& cas_fail = e.NewCachedLabel();
@@ -1216,6 +1215,7 @@ struct RESERVED_STORE_I32
e.b(done); e.b(done);
e.L(cas_fail); e.L(cas_fail);
e.clrex(15); e.clrex(15);
}
e.L(no_reserve); e.L(no_reserve);
e.mov(i.dest, 0); e.mov(i.dest, 0);
e.L(done); e.L(done);
@@ -1256,9 +1256,8 @@ struct RESERVED_STORE_I64
e.casal(e.x5, e.x6, ptr(e.x4)); e.casal(e.x5, e.x6, ptr(e.x4));
e.cmp(e.x5, e.x0); e.cmp(e.x5, e.x0);
e.cset(i.dest, Xbyak_aarch64::EQ); e.cset(i.dest, Xbyak_aarch64::EQ);
return; e.b(done);
} } else {
auto& cas_loop = e.NewCachedLabel(); auto& cas_loop = e.NewCachedLabel();
auto& cas_fail = e.NewCachedLabel(); auto& cas_fail = e.NewCachedLabel();
e.L(cas_loop); e.L(cas_loop);
@@ -1271,6 +1270,7 @@ struct RESERVED_STORE_I64
e.b(done); e.b(done);
e.L(cas_fail); e.L(cas_fail);
e.clrex(15); e.clrex(15);
}
e.L(no_reserve); e.L(no_reserve);
e.mov(i.dest, 0); e.mov(i.dest, 0);
e.L(done); e.L(done);

View File

@@ -13,9 +13,12 @@
#include "xenia/cpu/testing/util.h" #include "xenia/cpu/testing/util.h"
#include <atomic>
#include <cmath> #include <cmath>
#include <cstdlib>
#include <cstring> #include <cstring>
#include <limits> #include <limits>
#include <thread>
using namespace xe; using namespace xe;
using namespace xe::cpu; using namespace xe::cpu;
@@ -356,6 +359,125 @@ TEST_CASE("RESERVED_LOAD_STORE_I32", "[atomic]") {
test.memory->SystemHeapFree(guest_addr); test.memory->SystemHeapFree(guest_addr);
} }
TEST_CASE("RESERVED_LOAD_STORE_I64", "[atomic]") {
TestFunction test([](HIRBuilder& b) {
auto addr = LoadGPR(b, 4);
auto loaded = b.LoadWithReserve(addr, INT64_TYPE);
StoreGPR(b, 3, loaded);
auto new_val = b.Add(loaded, b.LoadConstantInt64(1));
auto success = b.StoreWithReserve(addr, new_val, INT64_TYPE);
StoreGPR(b, 5, b.ZeroExtend(success, INT64_TYPE));
b.Return();
});
uint32_t guest_addr = test.memory->SystemHeapAlloc(8);
REQUIRE(guest_addr != 0);
auto* host_ptr =
reinterpret_cast<uint64_t*>(test.memory->TranslateVirtual(guest_addr));
test.Run(
[&](PPCContext* ctx) {
*host_ptr = 100;
ctx->r[4] = guest_addr;
},
[&](PPCContext* ctx) {
REQUIRE(ctx->r[3] == 100); // loaded value
REQUIRE(ctx->r[5] == 1); // store succeeded
REQUIRE(*host_ptr == 101); // incremented
});
test.memory->SystemHeapFree(guest_addr);
}
TEST_CASE("RESERVED_STORE_I32_NO_RESERVATION", "[atomic]") {
// StoreWithReserve without a preceding LoadWithReserve must fail (return 0).
// Run with a timeout: the bug this catches produces an infinite loop.
TestFunction test([](HIRBuilder& b) {
auto addr = LoadGPR(b, 4);
auto val = b.Truncate(LoadGPR(b, 5), INT32_TYPE);
auto success = b.StoreWithReserve(addr, val, INT32_TYPE);
StoreGPR(b, 3, b.ZeroExtend(success, INT64_TYPE));
b.Return();
});
uint32_t guest_addr = test.memory->SystemHeapAlloc(4);
REQUIRE(guest_addr != 0);
auto* host_ptr =
reinterpret_cast<uint32_t*>(test.memory->TranslateVirtual(guest_addr));
std::atomic<bool> completed{false};
std::thread worker([&]() {
test.Run(
[&](PPCContext* ctx) {
*host_ptr = 42;
ctx->r[4] = guest_addr;
ctx->r[5] = 99;
},
[&](PPCContext* ctx) {
CHECK(ctx->r[3] == 0); // store must fail
CHECK(*host_ptr == 42); // memory unchanged
});
completed.store(true);
});
worker.detach();
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (!completed.load() && std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
if (!completed.load()) {
// Detached thread is stuck in JIT'd code — can't unwind safely.
FAIL("Timed out: no-reservation path likely has an infinite loop");
std::_Exit(1);
}
test.memory->SystemHeapFree(guest_addr);
}
TEST_CASE("RESERVED_STORE_I64_NO_RESERVATION", "[atomic]") {
// Run with a timeout: the bug this catches produces an infinite loop.
TestFunction test([](HIRBuilder& b) {
auto addr = LoadGPR(b, 4);
auto val = LoadGPR(b, 5);
auto success = b.StoreWithReserve(addr, val, INT64_TYPE);
StoreGPR(b, 3, b.ZeroExtend(success, INT64_TYPE));
b.Return();
});
uint32_t guest_addr = test.memory->SystemHeapAlloc(8);
REQUIRE(guest_addr != 0);
auto* host_ptr =
reinterpret_cast<uint64_t*>(test.memory->TranslateVirtual(guest_addr));
std::atomic<bool> completed{false};
std::thread worker([&]() {
test.Run(
[&](PPCContext* ctx) {
*host_ptr = 42;
ctx->r[4] = guest_addr;
ctx->r[5] = 99;
},
[&](PPCContext* ctx) {
CHECK(ctx->r[3] == 0); // store must fail
CHECK(*host_ptr == 42); // memory unchanged
});
completed.store(true);
});
worker.detach();
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (!completed.load() && std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
if (!completed.load()) {
// Detached thread is stuck in JIT'd code — can't unwind safely.
FAIL("Timed out: no-reservation path likely has an infinite loop");
std::_Exit(1);
}
test.memory->SystemHeapFree(guest_addr);
}
// ============================================================================ // ============================================================================
// LVL / LVR — partial vector load left/right // LVL / LVR — partial vector load left/right
// Detailed alignment semantics are tested by PPC instruction tests (lvlx etc.) // Detailed alignment semantics are tested by PPC instruction tests (lvlx etc.)