Feature/agressive recompiler (#146)

* feat: added guestBranchKind enum to categorize branch types
feat: added missingFunctionPolicy enum to define behaviors for missing function scenarios
refactor: added handle guest branches and report missing functions
feat lookupFunction to utilize new dispatch logic and improve error handling for unregistered functions

* fix: fix test conflict

* feat: added debug sound driver logs

* feat: emmiter for return

* feat: added recompiler reporter
feat: added strict diagnostics flag for heavy debug calls

* feat: staticc table insted of hashmap for runtime

* feat: back file to ignore

* feat: explode code across helpers and classes

* feat: update codegen test
feat: better guest nop check

* feat: fix link problem on linux

* feat: fix Segmentation fault

* feat: added recompile replace for DMA and MMIO
feat: added a clean memory helpers
feat: use memory helpers across the project
feat: fix ucrt on msvc

* feat: undo messup merge
This commit is contained in:
Ranieri
2026-07-07 10:14:25 -03:00
committed by GitHub
parent 61621b8313
commit 52edf07657
24 changed files with 1756 additions and 70 deletions
+168
View File
@@ -35,6 +35,50 @@ static Instruction makeNop(uint32_t address)
return inst;
}
static uint32_t signExtend16(uint16_t value)
{
return static_cast<uint32_t>(static_cast<int32_t>(static_cast<int16_t>(value)));
}
static Instruction makeIType(uint32_t address, uint32_t opcode, uint8_t rs, uint8_t rt, uint16_t immediate)
{
Instruction inst{};
inst.address = address;
inst.opcode = opcode;
inst.rs = rs;
inst.rt = rt;
inst.immediate = immediate;
inst.simmediate = signExtend16(immediate);
inst.raw = (opcode << 26) | (static_cast<uint32_t>(rs) << 21) |
(static_cast<uint32_t>(rt) << 16) | immediate;
return inst;
}
static Instruction makeLui(uint32_t address, uint8_t rt, uint16_t immediate)
{
return makeIType(address, OPCODE_LUI, 0, rt, immediate);
}
static Instruction makeOri(uint32_t address, uint8_t rt, uint8_t rs, uint16_t immediate)
{
return makeIType(address, OPCODE_ORI, rs, rt, immediate);
}
static Instruction makeAddiu(uint32_t address, uint8_t rt, uint8_t rs, uint16_t immediate)
{
return makeIType(address, OPCODE_ADDIU, rs, rt, immediate);
}
static Instruction makeLw(uint32_t address, uint8_t rt, uint8_t rs, uint16_t immediate)
{
return makeIType(address, OPCODE_LW, rs, rt, immediate);
}
static Instruction makeSw(uint32_t address, uint8_t rt, uint8_t rs, uint16_t immediate)
{
return makeIType(address, OPCODE_SW, rs, rt, immediate);
}
static std::string readFileFromCandidates(const std::vector<std::string> &candidates)
{
for (const auto &path : candidates)
@@ -152,6 +196,130 @@ void register_code_generator_tests()
"MULT1 should write low product to rd on R5900");
});
tc.Run("constant MMIO store emits direct runtime store", [](TestCase &t) {
Function func;
func.name = "mmio_store";
func.start = 0x1000;
func.end = 0x1010;
func.isRecompiled = true;
std::vector<Instruction> instructions;
instructions.push_back(makeLui(0x1000, 1, 0x1000));
instructions.push_back(makeOri(0x1004, 1, 1, 0xE020));
instructions.push_back(makeSw(0x1008, 2, 1, 0));
CodeGenerator gen({}, {});
std::string generated = gen.generateFunction(func, instructions, false);
printGeneratedCode("constant MMIO store emits direct runtime store", generated);
t.IsTrue(generated.find("runtime->Store32(rdram, ctx, 0x1000E020u, GPR_U32(ctx, 2));") != std::string::npos,
"constant MMIO SW should emit a direct runtime Store32");
t.IsTrue(generated.find("WRITE32(ADD32(GPR_U32(ctx, 1)") == std::string::npos,
"constant MMIO SW should not go through WRITE32 address classification");
});
tc.Run("constant RDRAM load and store emit fast memory access", [](TestCase &t) {
Function func;
func.name = "rdram_access";
func.start = 0x2000;
func.end = 0x2014;
func.isRecompiled = true;
std::vector<Instruction> instructions;
instructions.push_back(makeLui(0x2000, 1, 0x0012));
instructions.push_back(makeOri(0x2004, 1, 1, 0x3450));
instructions.push_back(makeLw(0x2008, 3, 1, 0x0010));
instructions.push_back(makeSw(0x200C, 4, 1, 0x0014));
CodeGenerator gen({}, {});
std::string generated = gen.generateFunction(func, instructions, false);
printGeneratedCode("constant RDRAM load and store emit fast memory access", generated);
t.IsTrue(generated.find("SET_GPR_S32(ctx, 3, (int32_t)FAST_READ32(0x123460u));") != std::string::npos,
"constant RDRAM LW should emit FAST_READ32 with the resolved address");
t.IsTrue(generated.find("FAST_WRITE32(0x123464u, _value);") != std::string::npos,
"constant RDRAM SW should emit FAST_WRITE32 with the resolved address");
t.IsTrue(generated.find("READ32(ADD32(GPR_U32(ctx, 1)") == std::string::npos,
"constant RDRAM LW should not go through READ32 address classification");
t.IsTrue(generated.find("WRITE32(ADD32(GPR_U32(ctx, 1)") == std::string::npos,
"constant RDRAM SW should not go through WRITE32 address classification");
});
tc.Run("known GIF DMA MMIO sequence emits native kick helper", [](TestCase &t) {
Function func;
func.name = "gif_dma_kick";
func.start = 0x3000;
func.end = 0x3030;
func.isRecompiled = true;
std::vector<Instruction> instructions;
instructions.push_back(makeAddiu(0x3000, 2, 0, 4));
instructions.push_back(makeLui(0x3004, 1, 0x1000));
instructions.push_back(makeOri(0x3008, 1, 1, 0xE020));
instructions.push_back(makeSw(0x300C, 2, 1, 0));
instructions.push_back(makeLui(0x3010, 1, 0x1000));
instructions.push_back(makeOri(0x3014, 1, 1, 0xE010));
instructions.push_back(makeSw(0x3018, 2, 1, 0));
instructions.push_back(makeLui(0x301C, 1, 0x1000));
instructions.push_back(makeOri(0x3020, 1, 1, 0xA030));
instructions.push_back(makeSw(0x3024, 4, 1, 0));
instructions.push_back(makeAddiu(0x3028, 5, 0, 0x0105));
instructions.push_back(makeAddiu(0x302C, 1, 1, 0xFFD0));
instructions.push_back(makeSw(0x3030, 5, 1, 0));
CodeGenerator gen({}, {});
std::string generated = gen.generateFunction(func, instructions, false);
printGeneratedCode("known GIF DMA MMIO sequence emits native kick helper", generated);
t.IsTrue(generated.find("uint32_t gifDmaKickValue_3024_2 = GPR_U32(ctx, 4);") != std::string::npos,
"dynamic GIF TADR source should be captured when the store is coalesced");
t.IsTrue(generated.find("runtime->kickGifDmaChainFromMMIO(rdram, ctx, 0x4u, 0x4u, gifDmaKickValue_3024_2, 0x105u);") != std::string::npos,
"known GIF DMA MMIO stores should coalesce into the native kick helper");
t.IsTrue(generated.find("runtime->Store32(rdram, ctx, 0x1000E020u") == std::string::npos,
"coalesced D_PCR store should not remain as an individual Store32");
t.IsTrue(generated.find("runtime->Store32(rdram, ctx, 0x1000A000u") == std::string::npos,
"coalesced GIF CHCR store should not remain as an individual Store32");
});
tc.Run("GIF DMA kick coalesces when CHCR store is a return delay slot", [](TestCase &t) {
Function func;
func.name = "loadImage_like";
func.start = 0x2E7C90;
func.end = 0x2E7CC8;
func.isRecompiled = true;
std::vector<Instruction> instructions;
instructions.push_back(makeBranch(0x2E7C90, 2));
instructions.push_back(makeLui(0x2E7C94, 5, 0x1000));
instructions.push_back(makeLui(0x2E7C98, 5, 0x1000));
instructions.push_back(makeAddiu(0x2E7C9C, 6, 0, 4));
instructions.push_back(makeOri(0x2E7CA0, 3, 5, 0xE020));
instructions.push_back(makeSw(0x2E7CA4, 6, 3, 0));
instructions.push_back(makeOri(0x2E7CA8, 3, 5, 0xE010));
instructions.push_back(makeSw(0x2E7CAC, 6, 3, 0));
instructions.push_back(makeOri(0x2E7CB0, 3, 5, 0xA030));
instructions.push_back(makeSw(0x2E7CB4, 4, 3, 0));
instructions.push_back(makeAddiu(0x2E7CB8, 4, 0, 0x0105));
instructions.push_back(makeOri(0x2E7CBC, 3, 5, 0xA000));
instructions.push_back(makeJr(0x2E7CC0, 31));
instructions.push_back(makeSw(0x2E7CC4, 4, 3, 0));
CodeGenerator gen({}, {});
std::string generated = gen.generateFunction(func, instructions, false);
printGeneratedCode("GIF DMA kick coalesces when CHCR store is a return delay slot", generated);
t.IsTrue(generated.find("label_2e7c9c:") != std::string::npos,
"test should cover a branch target inside the GIF DMA setup");
t.IsTrue(generated.find("uint32_t gifDmaKickValue_2e7cb4_2 = GPR_U32(ctx, 4);") != std::string::npos,
"TADR value should be captured before a0 is reused for CHCR");
t.IsTrue(generated.find("ctx->in_delay_slot = true;") != std::string::npos,
"coalesced helper should still run as the return delay slot");
t.IsTrue(generated.find("runtime->kickGifDmaChainFromMMIO(rdram, ctx, 0x4u, 0x4u, gifDmaKickValue_2e7cb4_2, 0x105u);") != std::string::npos,
"loadImage-like GIF DMA stores should coalesce into the native kick helper");
t.IsTrue(generated.find("WRITE32(ADD32(GPR_U32(ctx, 3), 0), GPR_U32(ctx, 4));") == std::string::npos,
"coalesced delay-slot CHCR store should not remain as an individual WRITE32");
});
tc.Run("emits labels and gotos for internal branches", [](TestCase &t) {
Function func;
func.name = "test_func";
+67
View File
@@ -65,6 +65,12 @@ namespace
std::memcpy(dst.data() + pos, &value, sizeof(uint64_t));
}
void appendGifAd(std::vector<uint8_t> &dst, uint64_t value, uint64_t reg)
{
appendU64(dst, value);
appendU64(dst, reg);
}
template <typename Predicate>
bool waitUntil(Predicate pred, std::chrono::milliseconds timeout)
{
@@ -1917,6 +1923,67 @@ void register_ps2_gs_tests()
t.IsTrue(same, "GIF IMAGE transfer should write payload bytes into GS VRAM");
});
tc.Run("GIF load-image packet uses native upload fast path", [](TestCase &t)
{
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GS gs;
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
const uint64_t bitblt =
(static_cast<uint64_t>(0u) << 0) |
(static_cast<uint64_t>(1u) << 16) |
(static_cast<uint64_t>(0u) << 24) |
(static_cast<uint64_t>(0u) << 32) |
(static_cast<uint64_t>(1u) << 48) |
(static_cast<uint64_t>(0u) << 56);
const uint64_t trxpos = 0ull;
const uint64_t trxreg = (2ull << 0) | (2ull << 32);
const uint64_t trxdir = 0ull;
const uint8_t payload[16] = {
0x10u, 0x11u, 0x12u, 0x13u,
0x20u, 0x21u, 0x22u, 0x23u,
0x30u, 0x31u, 0x32u, 0x33u,
0x40u, 0x41u, 0x42u, 0x43u,
};
std::vector<uint8_t> packet;
appendU64(packet, makeGifTag(4u, GIF_FMT_PACKED, 1u, false));
appendU64(packet, 0x0Eull);
appendGifAd(packet, bitblt, GS_REG_BITBLTBUF);
appendGifAd(packet, trxpos, GS_REG_TRXPOS);
appendGifAd(packet, trxreg, GS_REG_TRXREG);
appendGifAd(packet, trxdir, GS_REG_TRXDIR);
appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true));
appendU64(packet, 0ull);
packet.insert(packet.end(), payload, payload + sizeof(payload));
gs.processGIFPacket(packet.data(), static_cast<uint32_t>(packet.size()));
t.Equals(gs.nativeImageUploadCount(), 1ull, "load-image packet should use the native image upload fast path");
bool same = true;
for (uint32_t y = 0; y < 2u && same; ++y)
{
for (uint32_t x = 0; x < 2u; ++x)
{
const uint32_t pixelIndex = y * 2u + x;
const uint32_t off = referenceAddrPSMCT32(0u, 1u, x, y);
for (uint32_t c = 0; c < 4u; ++c)
{
if (vram[off + c] != payload[pixelIndex * 4u + c])
{
same = false;
break;
}
}
if (!same)
break;
}
}
t.IsTrue(same, "native load-image upload should preserve pixel payload");
});
tc.Run("GS local-to-host transfer supports partial incremental reads", [](TestCase &t)
{
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
+163
View File
@@ -113,6 +113,49 @@ namespace
return tag;
}
uint64_t makeGifTagPrim(uint16_t nloop, uint16_t prim, uint8_t flg, uint8_t nreg, bool eop = true, bool pre = true)
{
uint64_t tag = makeGifTag(nloop, flg, nreg, eop);
if (pre)
tag |= (1ull << 46);
tag |= (static_cast<uint64_t>(prim & 0x7FFu) << 47);
return tag;
}
uint64_t makeGsFrame(uint32_t fbp, uint32_t fbw, uint32_t psm, uint32_t mask = 0u)
{
return static_cast<uint64_t>(fbp & 0x1FFu) |
(static_cast<uint64_t>(fbw & 0x3Fu) << 16u) |
(static_cast<uint64_t>(psm & 0x3Fu) << 24u) |
(static_cast<uint64_t>(mask) << 32u);
}
uint64_t makeGsScissor(uint32_t x0, uint32_t x1, uint32_t y0, uint32_t y1)
{
return static_cast<uint64_t>(x0 & 0x7FFu) |
(static_cast<uint64_t>(x1 & 0x7FFu) << 16u) |
(static_cast<uint64_t>(y0 & 0x7FFu) << 32u) |
(static_cast<uint64_t>(y1 & 0x7FFu) << 48u);
}
void appendPackedRgbaq(std::vector<uint8_t> &packet, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
{
appendU64(packet, static_cast<uint64_t>(r) | (static_cast<uint64_t>(g) << 32u));
appendU64(packet, static_cast<uint64_t>(b) | (static_cast<uint64_t>(a) << 32u));
}
void appendPackedXyzf2(std::vector<uint8_t> &packet, uint32_t x, uint32_t y, uint32_t z)
{
appendU64(packet, static_cast<uint64_t>(x & 0xFFFFu) | (static_cast<uint64_t>(y & 0xFFFFu) << 32u));
appendU64(packet, static_cast<uint64_t>(z & 0xFFFFFFu) << 4u);
}
void appendPackedUv(std::vector<uint8_t> &packet, uint32_t u, uint32_t v)
{
appendU64(packet, static_cast<uint64_t>(u & 0x3FFFu) | (static_cast<uint64_t>(v & 0x3FFFu) << 32u));
appendU64(packet, 0u);
}
uint32_t makeVuLowerSpecial(uint8_t specialOp, uint8_t is, uint8_t it = 0u, uint8_t id = 0u, uint8_t dest = 0u)
{
return (0x40u << 25) |
@@ -970,6 +1013,126 @@ void register_ps2_memory_tests()
t.IsTrue(contentOk, "scratchpad alias chain payload should match scratchpad bytes");
});
tc.Run("native GIF image upload recognizes canonical load-image chain", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
GS gs;
gs.init(mem.getGSVRAM(), static_cast<uint32_t>(PS2_GS_VRAM_SIZE), &mem.gs());
constexpr uint32_t kGifCh = 0x1000A000u;
constexpr uint32_t kDStat = 0x1000E010u;
constexpr uint32_t kChain = 0x00028000u;
constexpr uint32_t kPixels = 0x00029000u;
constexpr uint32_t kQwc = 1u;
uint8_t *rdram = mem.getRDRAM();
for (uint32_t i = 0; i < kQwc * 16u; ++i)
{
rdram[kPixels + i] = static_cast<uint8_t>(0x40u + i);
}
uint32_t chain = kChain;
chain = writeTextureUploadSetup(rdram, chain, 0u, GS_PSM_CT32);
chain = writeTextureImageRef(rdram, chain, kQwc, kPixels);
writeDmaTag(rdram, chain, makeDmaTag(0u, 7u, 0u, false)); // END.
t.IsTrue(mem.writeIORegister(kGifCh + 0x30u, kChain), "write GIF TADR should succeed");
t.IsTrue(mem.tryProcessNativeGifImageUploadChain(gs, kChain, 0x105u),
"canonical load-image chain should use the native upload path");
t.Equals(gs.nativeImageUploadCount(), 1ull, "native GIF DMA chain should upload through GS fast path");
t.Equals(mem.gifCopyCount(), 1ull, "native GIF DMA chain should still count as a GIF DMA copy");
t.IsTrue((mem.readIORegister(kDStat) & (1u << 2u)) != 0u,
"native GIF DMA chain should raise D_STAT GIF completion");
t.Equals(mem.readIORegister(kGifCh + 0x20u), 0u, "native GIF DMA chain should clear GIF QWC");
t.Equals(mem.readIORegister(kGifCh + 0x00u) & 0x100u, 0u,
"native GIF DMA chain should clear GIF STR");
t.Equals(mem.readIORegister(kGifCh + 0x00u) & 0x70000000u, 0x70000000u,
"native GIF DMA chain should latch the terminal END tag id");
bool pixelsOk = true;
for (uint32_t x = 0; x < 4u && pixelsOk; ++x)
{
const uint32_t dstOff = GSPSMCT32::addrPSMCT32(0u, 1u, x, 0u);
const uint32_t srcOff = kPixels + x * 4u;
for (uint32_t c = 0; c < 4u; ++c)
{
if (mem.getGSVRAM()[dstOff + c] != rdram[srcOff + c])
{
pixelsOk = false;
break;
}
}
}
t.IsTrue(pixelsOk, "native GIF DMA chain should upload image payload into GS VRAM");
});
tc.Run("native GIF packed chain matches generic packed primitive packet", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
GS nativeGs;
nativeGs.init(mem.getGSVRAM(), static_cast<uint32_t>(PS2_GS_VRAM_SIZE), &mem.gs());
GSRegisters genericRegs{};
std::vector<uint8_t> genericVram(PS2_GS_VRAM_SIZE, 0u);
GS genericGs;
genericGs.init(genericVram.data(), static_cast<uint32_t>(genericVram.size()), &genericRegs);
std::vector<uint8_t> packet;
appendU64(packet, makeGifTag(4u, GIF_FMT_PACKED, 1u, false));
appendU64(packet, 0x0Eull);
appendU64(packet, makeGsFrame(0u, 1u, GS_PSM_CT32));
appendU64(packet, GS_REG_FRAME_1);
appendU64(packet, makeGsScissor(0u, 7u, 0u, 7u));
appendU64(packet, GS_REG_SCISSOR_1);
appendU64(packet, 1ull << 17u); // ZTST always.
appendU64(packet, GS_REG_TEST_1);
appendU64(packet, 1ull << 32u); // Mask Z writes so the test framebuffer remains visible.
appendU64(packet, GS_REG_ZBUF_1);
constexpr uint16_t kSpritePrim = static_cast<uint16_t>(GS_PRIM_SPRITE);
appendU64(packet, makeGifTagPrim(2u, kSpritePrim, GIF_FMT_PACKED, 3u, true, true));
appendU64(packet, static_cast<uint64_t>(GS_REG_UV) |
(static_cast<uint64_t>(GS_REG_RGBAQ) << 4u) |
(static_cast<uint64_t>(GS_REG_XYZF2) << 8u));
appendPackedUv(packet, 0u, 0u);
appendPackedRgbaq(packet, 0x20u, 0x40u, 0x80u, 0x80u);
appendPackedXyzf2(packet, 0u, 0u, 0u);
appendPackedUv(packet, 0u, 0u);
appendPackedRgbaq(packet, 0xE0u, 0x30u, 0x10u, 0x80u);
appendPackedXyzf2(packet, 64u, 64u, 0u);
genericGs.processGIFPacket(packet.data(), static_cast<uint32_t>(packet.size()));
constexpr uint32_t kGifCh = 0x1000A000u;
constexpr uint32_t kDStat = 0x1000E010u;
constexpr uint32_t kScratchTag = 0xF0000000u;
uint8_t *scratch = mem.getScratchpad();
writeDmaTag(scratch, 0u, makeDmaTag(static_cast<uint16_t>(packet.size() / 16u), 7u, 0u, false));
std::memcpy(scratch + 16u, packet.data(), packet.size());
t.IsTrue(mem.writeIORegister(kGifCh + 0x30u, kScratchTag), "write GIF TADR scratchpad alias should succeed");
t.IsTrue(mem.tryProcessNativeGifPackedChain(nativeGs, kScratchTag, 0x105u),
"packed primitive chain should use the native packed GIF path");
t.Equals(nativeGs.nativePackedGIFPacketCount(), 1ull, "native packed GIF packet counter should increment");
t.Equals(mem.gifCopyCount(), 1ull, "native packed GIF chain should still count as a GIF DMA copy");
t.IsTrue((mem.readIORegister(kDStat) & (1u << 2u)) != 0u,
"native packed GIF chain should raise D_STAT GIF completion");
t.Equals(mem.readIORegister(kGifCh + 0x20u), 0u, "native packed GIF chain should clear GIF QWC");
t.Equals(mem.readIORegister(kGifCh + 0x00u) & 0x100u, 0u,
"native packed GIF chain should clear GIF STR");
const uint32_t nativePixel = nativeGs.ReadVram(GS_PSM_CT32, 0u, 1u, 1u, 1u);
const uint32_t genericPixel = genericGs.ReadVram(GS_PSM_CT32, 0u, 1u, 1u, 1u);
t.IsTrue(genericPixel != 0u, "generic packed primitive packet should draw a test pixel");
t.Equals(nativePixel, genericPixel, "native packed GIF chain should match generic GS packet output");
});
tc.Run("GIF DMA chain REF keeps CT32 image data after paletted upload", [](TestCase &t)
{
PS2Memory mem;
@@ -554,6 +554,58 @@ void register_ps2_runtime_interrupt_tests()
cleanupRuntime(env);
});
tc.Run("native GIF DMA MMIO kick dispatches completed DMAC handler", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
t.IsTrue(env.runtime.memory().initialize(), "runtime memory initialize should succeed");
constexpr uint32_t kHandlerAddr = 0x00ABD1C0u;
constexpr uint32_t kDStat = 0x1000E010u;
constexpr uint32_t kDPcr = 0x1000E020u;
constexpr uint32_t kTag0 = 0x00028400u;
uint8_t *rdram = env.runtime.memory().getRDRAM();
writeDmaTag(rdram, kTag0, makeDmaTag(1u, 7u, 0u, false)); // END
writeGuestU64(rdram, kTag0 + 0x10u, 0x1122334455667788ull);
writeGuestU64(rdram, kTag0 + 0x18u, 0x99AABBCCDDEEFF00ull);
g_dmacSendHits.store(0u, std::memory_order_relaxed);
g_dmacSendLastCause.store(0u, std::memory_order_relaxed);
g_dmacSendLastChcr.store(0u, std::memory_order_relaxed);
env.runtime.registerFunction(kHandlerAddr, &testDmacSendHandler);
R5900Context addCtx{};
setRegU32(addCtx, 4, 2u);
setRegU32(addCtx, 5, kHandlerAddr);
setRegU32(addCtx, 6, 0u);
setRegU32(addCtx, 7, 0u);
ps2_syscalls::AddDmacHandler(rdram, &addCtx, &env.runtime);
t.IsTrue(getRegS32(addCtx, 2) > 0, "AddDmacHandler should register GIF handler");
R5900Context enableCtx{};
setRegU32(enableCtx, 4, 2u);
ps2_syscalls::EnableDmac(rdram, &enableCtx, &env.runtime);
t.Equals(getRegS32(enableCtx, 2), KE_OK, "EnableDmac should enable GIF cause");
R5900Context kickCtx{};
env.runtime.kickGifDmaChainFromMMIO(rdram, &kickCtx, 4u, 4u, kTag0, 0x105u);
t.Equals(env.runtime.memory().readIORegister(kDPcr), 4u, "native GIF kick should preserve D_PCR write");
t.IsTrue((env.runtime.memory().readIORegister(kDStat) & (1u << 2)) != 0u,
"native GIF kick should raise D_STAT GIF completion status");
t.Equals(g_dmacSendHits.load(std::memory_order_relaxed), 1u,
"native GIF kick should dispatch the GIF DMAC handler");
t.Equals(g_dmacSendLastCause.load(std::memory_order_relaxed), 2u,
"DMAC handler should observe GIF cause");
t.Equals(g_dmacSendLastChcr.load(std::memory_order_relaxed) & 0x100u, 0u,
"handler should see GIF STR cleared");
t.Equals(g_dmacSendLastChcr.load(std::memory_order_relaxed) & 0x70000000u, 0x70000000u,
"handler should see the latched END tag id");
cleanupRuntime(env);
});
tc.Run("negative interrupt-safe EE syscall ids dispatch", [](TestCase &t)
{
notifyRuntimeStop();