From fbea51c6843a2bbe0906c4fb50a7d0bed7711c8f Mon Sep 17 00:00:00 2001 From: popcoder565 Date: Sat, 10 Oct 2026 21:00:09 -0400 Subject: [PATCH] agc: implement sceAgcSetNop (#1343) * agc: implement sceAgcSetNop Star Wars Outlaws calls sceAgcSetNop (K2mciNVxUCE) and crashes right after the unresolved stub returns, without the packet being disabled. The call turns a packet into a NOP of the same length. Clear the emulator's custom NOP code as well, so packets that the HLE encodes as NOPs, such as end-of-pipe actions, stop executing too. Co-Authored-By: Claude Opus 5.5 * agc: keep packets disabled by sceAgcSetNop from reading as markers A disabled packet whose first payload word carries the 0x6875 marker signature still reached the marker handler, which can flip or stop on an unknown marker ID. Clear the signature so the packet is skipped like any other NOP. Co-Authored-By: Claude Opus 5.5 * agc: tidy SetNop logging and comments --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: nmzik --- src/libs/agc.cpp | 14 ++++++++++ src/libs/agc.h | 1 + src/libs/libAgcDriver.cpp | 1 + tests/ShaderRecompilerComputeTests.cpp | 37 ++++++++++++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/src/libs/agc.cpp b/src/libs/agc.cpp index dbe00bd74..c116706a8 100644 --- a/src/libs/agc.cpp +++ b/src/libs/agc.cpp @@ -3714,6 +3714,20 @@ int KYTY_SYSV_ABI AgcSetPacketPredication(uint32_t* packet, uint32_t predication return OK; } +int KYTY_SYSV_ABI AgcSetNop(uint32_t* packet) { + PRINT_NAME(); + + const auto size = AgcGetPacketSize(packet); + + packet[0] = (packet[0] & 0xffff0003u) | (Pm4::IT_NOP << 8u); + // Prevent the disabled payload from being treated as an executable marker. + if (size > 1 && (packet[1] & 0xffff0000u) == 0x68750000u) { + packet[1] &= 0xffffu; + } + + return OK; +} + int KYTY_SYSV_ABI AgcSetRangePredication(uint32_t* start, const volatile uint32_t* end, uint32_t predication) { PRINT_NAME(); diff --git a/src/libs/agc.h b/src/libs/agc.h index bac31c03a..2244a5b22 100644 --- a/src/libs/agc.h +++ b/src/libs/agc.h @@ -270,6 +270,7 @@ uint32_t KYTY_SYSV_ABI AgcGetPacketSize(uint32_t* packet); int KYTY_SYSV_ABI AgcSetPacketPredication(uint32_t* packet, uint32_t predication); int KYTY_SYSV_ABI AgcSetRangePredication(uint32_t* start, const volatile uint32_t* end, uint32_t predication); +int KYTY_SYSV_ABI AgcSetNop(uint32_t* packet); int KYTY_SYSV_ABI AgcRewindPatchSetRewindState(uint32_t* cmd, uint8_t state); int KYTY_SYSV_ABI AgcCondExecPatchSetEnd(uint32_t* cmd, const volatile uint32_t* buffer); int KYTY_SYSV_ABI AgcCondExecPatchSetCommandAddress(uint32_t* cmd, diff --git a/src/libs/libAgcDriver.cpp b/src/libs/libAgcDriver.cpp index 0e0ceddb3..5cb83dc81 100644 --- a/src/libs/libAgcDriver.cpp +++ b/src/libs/libAgcDriver.cpp @@ -143,6 +143,7 @@ LIB_DEFINE(InitAgcDriver_1) { LIB_FUNC("Lkf86B98qPc", Gen5::AgcGetPacketSize); LIB_FUNC("w6Dj1VJt5qY", Gen5::AgcSetPacketPredication); LIB_FUNC("n8vgpaQg6dA", Gen5::AgcSetRangePredication); + LIB_FUNC("K2mciNVxUCE", Gen5::AgcSetNop); LIB_FUNC("i1jyy49AjXU", Gen5::AgcDcbWriteData); LIB_FUNC("p9tI+yTvx68", Gen5::AgcDcbWriteDataGetSize); LIB_FUNC("vuSXe69VILM", Gen5::AgcDcbGetLodStats); diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 22a735b01..a2f39402f 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -42630,6 +42630,43 @@ void CheckPm4IndirectControlFlow(RenderContext &renderer) { Gen5::AgcCbNop(reinterpret_cast(&payload_dcb), 3) == payload.data() && payload == std::array{0xc0011000u, 0x12345678u, 0x9abcdef0u}, "NOP emitter overwrote the caller's reserved payload"); + alignas(8) uint64_t released = 0; + std::array disabled_commands{}; + const auto disabled_write = write(&returned, 99); + std::copy(disabled_write.begin(), disabled_write.end(), disabled_commands.begin()); + disabled_commands[5] = KYTY_PM4(2, Pm4::IT_NUM_INSTANCES, 0); + disabled_commands[6] = 0x68750123u; + CommandBufferLayout release_dcb{disabled_commands.data() + 7, disabled_commands.data() + 15, + disabled_commands.data() + 7, disabled_commands.data() + 15, nullptr, nullptr, 0}; + auto* release = Gen5::AgcCbReleaseMem( + reinterpret_cast(&release_dcb), 0x28, 0, 0, 0, + reinterpret_cast(&released), 2, 77, 0, 0, 0, 0); + const auto enabled_write = write(&selected, 88); + std::copy(enabled_write.begin(), enabled_write.end(), disabled_commands.begin() + 15); + const auto unpatched_commands = disabled_commands; + Require(name, "SetNop packets", + release == disabled_commands.data() + 7 && + Gen5::AgcSetNop(disabled_commands.data()) == 0 && + Gen5::AgcSetNop(disabled_commands.data() + 5) == 0 && Gen5::AgcSetNop(release) == 0 && + disabled_commands[0] == ((disabled_write[0] & 0xffff00ffu) | (Pm4::IT_NOP << 8u)) && + disabled_commands[5] == KYTY_PM4(2, Pm4::IT_NOP, Pm4::R_ZERO) && + disabled_commands[6] == 0x123u && + release[0] == KYTY_PM4(8, Pm4::IT_NOP, Pm4::R_ZERO) && + unpatched_commands[7] == KYTY_PM4(8, Pm4::IT_NOP, Pm4::R_RELEASE_MEM) && + Gen5::AgcGetPacketSize(disabled_commands.data()) == disabled_write.size() && + Gen5::AgcGetPacketSize(disabled_commands.data() + 5) == 2 && + Gen5::AgcGetPacketSize(release) == 8 && + std::equal(disabled_commands.begin() + 1, disabled_commands.begin() + 5, + unpatched_commands.begin() + 1) && + std::equal(disabled_commands.begin() + 8, disabled_commands.end(), + unpatched_commands.begin() + 8), + "SetNop changed a packet length, kept its operation or marker, or touched its payload"); + selected = returned = 0; + Pm4Execution disabled_execution; + Require(name, "SetNop execution", + processor.Process(disabled_execution, disabled_commands) == Pm4ProcessResult::Complete && + returned == 0 && released == 0 && selected == 88, + "a disabled packet still executed or hid the packet after it"); const auto then_commands = write(&selected, 11); const auto else_commands = write(&selected, 33); const auto branch_suffix = write(&selected, 44);