diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index 924e07470..13fed2f90 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -265,6 +265,37 @@ struct PS2SoundDriverCompatLayout std::array completionCallbacks{}; std::array clearBusyCallbacks{}; + // Service ids (0 = unused). A driver that muxes everything on one SID sets both equal. + // KNOWN LIMITATION: 0 is the "unused" sentinel, so a real service whose SID is + // literally 0 (or a subcommand whose fno is literally 0) cannot be expressed. This + // is a deliberate tradeoff and adequate in practice (real SIF-RPC services are nonzero); the + // deleted placeholder constants that were 0 (IOP_SID_SNDDRV_COMMAND / _SUBMIT) were + // never live services. If a title ever needs SID 0 / fno 0, add explicit has-value + // flags rather than overloading the 0 sentinel. + uint32_t commandSid = 0; // service id carrying the submit-command-buffer subcommand + uint32_t stateSid = 0; // service id carrying the status/addr-table queries + + // Subcommand (rpcNum -> semantic) mapping. 0 = that semantic is unused. + uint32_t submitFno = 0; // submit-command-buffer + uint32_t getStatusFno = 0; // return statusAddr + uint32_t getAddrTableFno = 0; // return addrTableAddr + uint32_t streamOpenFno = 0; // write streamReadyValue to streamStateAddr + uint32_t channelConfigFno = 0; // set channelAllocFlagTableAddr[channel]=1 (channel from send word 0 if <16) + uint32_t stopFno = 0; // write 1 to stopCompletionFlagAddr + uint32_t benignStatusValue = 0xffffff9bu; // recv[0] for an unknown fno on a served SID + + // Extra addresses the new semantics need. + uint32_t streamStateAddr = 0; + uint32_t streamReadyValue = 0; + uint32_t channelAllocFlagTableAddr = 0; + uint32_t stopCompletionFlagAddr = 0; + + [[nodiscard]] bool servesSid(uint32_t sid) const + { + return (commandSid != 0u && sid == commandSid) || + (stateSid != 0u && sid == stateSid); + } + [[nodiscard]] bool hasChecksumTables() const { return primarySeCheckAddr != 0u || primaryMidiCheckAddr != 0u || diff --git a/ps2xRuntime/include/runtime/ps2_iop.h b/ps2xRuntime/include/runtime/ps2_iop.h index e3fda4322..c1163456f 100644 --- a/ps2xRuntime/include/runtime/ps2_iop.h +++ b/ps2xRuntime/include/runtime/ps2_iop.h @@ -5,17 +5,11 @@ class PS2Runtime; -constexpr uint32_t IOP_SID_SNDDRV_COMMAND = 0x00000000u; -constexpr uint32_t IOP_SID_SNDDRV_STATE = 0x00000001u; constexpr uint32_t IOP_SID_LOTR_CLFILE = 0x0000FF01u; constexpr uint32_t IOP_SID_LOTR_SOUND = 0x00012345u; constexpr uint32_t IOP_SID_LIBSD = 0x80000701u; constexpr uint32_t IOP_SID_FATAL_FRAME_SDRDRV = 0x19740512u; -constexpr uint32_t IOP_RPC_SNDDRV_SUBMIT = 0x00000000u; -constexpr uint32_t IOP_RPC_SNDDRV_GET_STATUS_ADDR = 0x00000012u; -constexpr uint32_t IOP_RPC_SNDDRV_GET_ADDR_TABLE = 0x00000013u; - class ps2_iop { public: diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp index 4f0fc7fb9..597f9bcda 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp @@ -539,14 +539,30 @@ namespace ps2_syscalls return false; } - if (sid == IOP_SID_SNDDRV_COMMAND && rpcNum == IOP_RPC_SNDDRV_SUBMIT) + PS2SoundDriverCompatLayout compat{}; + { + std::lock_guard lock(g_rpc_mutex); + compat = g_soundDriverCompatLayout; + } + + if (compat.commandSid == 0u && compat.stateSid == 0u) + { + return false; + } + + if (!compat.servesSid(sid)) + { + return false; + } + + if (compat.submitFno != 0u && rpcNum == compat.submitFno) { handleSoundDriverCommandBuffer(rdram, runtime, sendBuf, sendSize); return true; } - if (sid == IOP_SID_SNDDRV_STATE && - (rpcNum == IOP_RPC_SNDDRV_GET_STATUS_ADDR || rpcNum == IOP_RPC_SNDDRV_GET_ADDR_TABLE)) + if ((compat.getStatusFno != 0u && rpcNum == compat.getStatusFno) || + (compat.getAddrTableFno != 0u && rpcNum == compat.getAddrTableFno)) { uint32_t responseWord = 0u; { @@ -556,8 +572,8 @@ namespace ps2_syscalls return false; } responseWord = - (rpcNum == IOP_RPC_SNDDRV_GET_STATUS_ADDR) ? g_soundDriverRpcState.statusAddr - : g_soundDriverRpcState.addrTableAddr; + (rpcNum == compat.getStatusFno) ? g_soundDriverRpcState.statusAddr + : g_soundDriverRpcState.addrTableAddr; } if (recvBuf && recvSize >= sizeof(uint32_t)) @@ -574,6 +590,77 @@ namespace ps2_syscalls return true; } + if (compat.streamOpenFno != 0u && rpcNum == compat.streamOpenFno) + { + { + std::lock_guard lock(g_rpc_mutex); + if (compat.streamStateAddr != 0u) + { + (void)writeGuestU32(rdram, compat.streamStateAddr, compat.streamReadyValue); + } + } + + if (recvBuf && recvSize >= sizeof(uint32_t)) + { + (void)writeGuestU32(rdram, recvBuf, 0u); + resultPtr = recvBuf; + } + + signalNowaitCompletion = true; + return true; + } + + if (compat.channelConfigFno != 0u && rpcNum == compat.channelConfigFno) + { + uint32_t channel = 0u; + if (sendBuf != 0u) + { + (void)readGuestU32(rdram, sendBuf, channel); + } + + { + std::lock_guard lock(g_rpc_mutex); + if (compat.channelAllocFlagTableAddr != 0u && channel < 16u) + { + (void)writeGuestU32(rdram, compat.channelAllocFlagTableAddr + channel * sizeof(uint32_t), 1u); + } + } + + if (recvBuf && recvSize >= sizeof(uint32_t)) + { + (void)writeGuestU32(rdram, recvBuf, 0u); + resultPtr = recvBuf; + } + + signalNowaitCompletion = true; + return true; + } + + if (compat.stopFno != 0u && rpcNum == compat.stopFno) + { + { + std::lock_guard lock(g_rpc_mutex); + if (compat.stopCompletionFlagAddr != 0u) + { + (void)writeGuestU32(rdram, compat.stopCompletionFlagAddr, 1u); + } + } + + if (recvBuf && recvSize >= sizeof(uint32_t)) + { + (void)writeGuestU32(rdram, recvBuf, 0u); + resultPtr = recvBuf; + } + + signalNowaitCompletion = true; + return true; + } + + if (recvBuf && recvSize >= sizeof(uint32_t)) + { + (void)writeGuestU32(rdram, recvBuf, compat.benignStatusValue); + } + return false; } diff --git a/ps2xRuntime/src/lib/game_overrides.cpp b/ps2xRuntime/src/lib/game_overrides.cpp index 821491e57..f251d9089 100644 --- a/ps2xRuntime/src/lib/game_overrides.cpp +++ b/ps2xRuntime/src/lib/game_overrides.cpp @@ -267,6 +267,18 @@ namespace layout.busyFlagAddr = 0x01E212C8u; layout.completionCallbacks = {0x002EAC20u, 0x002EAC30u, 0x002FAC20u, 0x002FAC30u}; layout.clearBusyCallbacks = {0x002EAC30u, 0x002FAC30u}; + + // Service SID + subcommand (fno) numbers RE:CVX's sound driver speaks. + // On upstream main these were the hardcoded placeholder constants + // IOP_SID_SNDDRV_STATE / IOP_RPC_SNDDRV_GET_STATUS_ADDR / _GET_ADDR_TABLE; + // they must now be carried per-game so the getStatus RPC provisions the + // status/addr-table pool (which the sceSifGetOtherData checksum backfill + // depends on). The old submit path used placeholder SID 0 / fno 0 (a + // non-real service that never matched a live call) and is intentionally + // left unconfigured — the 0-sentinel design cannot express SID 0 / fno 0. + layout.stateSid = 1u; + layout.getStatusFno = 0x12u; + layout.getAddrTableFno = 0x13u; ps2_syscalls::setSoundDriverCompatLayout(layout); } diff --git a/ps2xRuntime/src/lib/ps2_debug_panel.cpp b/ps2xRuntime/src/lib/ps2_debug_panel.cpp index 2876e1289..f1d725ae6 100644 --- a/ps2xRuntime/src/lib/ps2_debug_panel.cpp +++ b/ps2xRuntime/src/lib/ps2_debug_panel.cpp @@ -75,10 +75,6 @@ namespace { switch (sid) { - case IOP_SID_SNDDRV_COMMAND: - return "SNDDRV command"; - case IOP_SID_SNDDRV_STATE: - return "SNDDRV state"; case IOP_SID_LIBSD: return "LIBSD"; case IOP_SID_FATAL_FRAME_SDRDRV: @@ -1085,8 +1081,6 @@ namespace bool dynamic; }; const ServiceRow services[] = { - {"SNDDRV command", IOP_SID_SNDDRV_COMMAND, false}, - {"SNDDRV state", IOP_SID_SNDDRV_STATE, false}, {"LIBSD", IOP_SID_LIBSD, false}, {"Fatal Frame SDRDRV", IOP_SID_FATAL_FRAME_SDRDRV, false}, {"LOTR SOUND", IOP_SID_LOTR_SOUND, false}, diff --git a/ps2xTest/src/ps2_sif_dma_tests.cpp b/ps2xTest/src/ps2_sif_dma_tests.cpp index 391205b70..9dd067a67 100644 --- a/ps2xTest/src/ps2_sif_dma_tests.cpp +++ b/ps2xTest/src/ps2_sif_dma_tests.cpp @@ -2,6 +2,7 @@ #include "ps2_runtime.h" #include "ps2_syscalls.h" #include "ps2_stubs.h" +#include "game_overrides.h" #include #include @@ -898,6 +899,8 @@ void register_ps2_sif_dma_tests() PS2SoundDriverCompatLayout compat{}; compat.primarySeCheckAddr = kPrimarySeCheckAddr; compat.primaryMidiCheckAddr = kPrimaryMidiCheckAddr; + compat.stateSid = 1u; + compat.getStatusFno = 0x12u; ps2_syscalls::setSoundDriverCompatLayout(compat); constexpr uint32_t kClientAddr = 0x00023500u; @@ -947,6 +950,88 @@ void register_ps2_sif_dma_tests() "live midi_sum for the active bank should not be clobbered by compat check arrays"); }); + tc.Run("RE:CVX sound-driver override provisions status pool for sceSifGetOtherData backfill", [](TestCase &t) + { + // Real-path regression guard for the RE:CVX override migration. + // Drives the actual SifCallRpc(getStatus) -> sceSifGetOtherData path with the + // layout supplied ONLY by the game override (applyMatching), and asserts the + // check-array -> status backfill actually ran. If applyRecvxSoundDriverCompat + // is not migrated to carry stateSid/getStatusFno, the getStatus RPC is not + // served, g_soundDriverRpcState.statusAddr is never provisioned (getStatus + // returns a zero src address), the backfill never fires, and this test fails. + TestEnv env; + + constexpr uint32_t kRdAddr = 0x00024300u; + constexpr uint32_t kDstAddr = 0x00024400u; + constexpr uint32_t kSize = 0x42u; + constexpr uint32_t kPrimarySeCheckAddr = 0x01E0EF10u; // matches the RE:CVX override + constexpr uint32_t kPrimaryMidiCheckAddr = 0x01E0EF20u; // matches the RE:CVX override + constexpr uint32_t kMidiSumOffset = 0x1Eu; + constexpr uint32_t kSeSumOffset = 0x26u; + constexpr uint32_t kLiveBank = 0u; + constexpr uint32_t kPendingBank = 1u; + + // Provision the layout via the real RE:CVX game override (elf slus_201.84). + ps2_game_overrides::applyMatching(env.runtime, "slus_201.84", 0u); + + constexpr uint32_t kClientAddr = 0x00024500u; + constexpr uint32_t kRecvAddr = 0x00024600u; + constexpr uint32_t kSid = 1u; + + ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); + setRegU32(env.ctx, 4, kClientAddr); + setRegU32(env.ctx, 5, kSid); + setRegU32(env.ctx, 6, 0u); + ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); + t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for sound-driver sid"); + + setRegU32(env.ctx, 4, kClientAddr); + setRegU32(env.ctx, 5, 0x12u); + setRegU32(env.ctx, 6, 0u); + setRegU32(env.ctx, 7, 0u); + setRegU32(env.ctx, 8, 0u); + setRegU32(env.ctx, 9, kRecvAddr); + setRegU32(env.ctx, 10, 4u); + setRegU32(env.ctx, 11, 0u); + ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); + const uint32_t kSrcAddr = readGuestU32(env.rdram.data(), kRecvAddr); + + // The override must have provisioned a nonzero status pool address; without the + // stateSid/getStatusFno migration the getStatus RPC is unhandled and this is 0. + t.IsTrue(kSrcAddr != 0u, + "getStatus RPC (served only via the migrated override) must return a nonzero status address"); + + std::memset(env.rdram.data() + kDstAddr, 0, kSize); + std::memset(env.rdram.data() + kRdAddr, 0, sizeof(SifRpcReceiveData)); + + writeGuestS16(env.rdram.data(), kSrcAddr + kSeSumOffset + (kLiveBank * 2u), static_cast(0x1111)); + writeGuestS16(env.rdram.data(), kSrcAddr + kMidiSumOffset + (kLiveBank * 2u), static_cast(0x2222)); + + writeGuestS16(env.rdram.data(), kPrimarySeCheckAddr + (kPendingBank * 2u), static_cast(0x3333)); + writeGuestS16(env.rdram.data(), kPrimaryMidiCheckAddr + (kPendingBank * 2u), static_cast(0x4444)); + + setRegU32(env.ctx, 4, kRdAddr); + setRegU32(env.ctx, 5, kSrcAddr); + setRegU32(env.ctx, 6, kDstAddr); + setRegU32(env.ctx, 7, kSize); + ps2_stubs::sceSifGetOtherData(env.rdram.data(), &env.ctx, &env.runtime); + + t.Equals(getRegS32(env.ctx, 2), 0, + "sceSifGetOtherData should succeed for sound-status transfer"); + t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kSeSumOffset + (kLiveBank * 2u)), + static_cast(0x1111), + "existing live se_sum values should remain intact"); + t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kMidiSumOffset + (kLiveBank * 2u)), + static_cast(0x2222), + "existing live midi_sum values should remain intact"); + t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kSeSumOffset + (kPendingBank * 2u)), + static_cast(0x3333), + "zero se_sum slots should backfill from the override's compat tables (proves the status pool was provisioned)"); + t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kMidiSumOffset + (kPendingBank * 2u)), + static_cast(0x4444), + "zero midi_sum slots should backfill from the override's compat tables (proves the status pool was provisioned)"); + }); + tc.Run("sceSifGetOtherData backfills zero sound-status sums for later banks", [](TestCase &t) { TestEnv env; @@ -964,6 +1049,8 @@ void register_ps2_sif_dma_tests() PS2SoundDriverCompatLayout compat{}; compat.primarySeCheckAddr = kPrimarySeCheckAddr; compat.primaryMidiCheckAddr = kPrimaryMidiCheckAddr; + compat.stateSid = 1u; + compat.getStatusFno = 0x12u; ps2_syscalls::setSoundDriverCompatLayout(compat); constexpr uint32_t kClientAddr = 0x00023900u; diff --git a/ps2xTest/src/ps2_sif_rpc_tests.cpp b/ps2xTest/src/ps2_sif_rpc_tests.cpp index bebafca34..9cabc3a18 100644 --- a/ps2xTest/src/ps2_sif_rpc_tests.cpp +++ b/ps2xTest/src/ps2_sif_rpc_tests.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include using namespace ps2_syscalls; @@ -20,6 +21,16 @@ namespace ps2_stubs void resetSifState(); } +namespace ps2_syscalls +{ + bool handleSoundDriverRpcService(uint8_t *rdram, PS2Runtime *runtime, + uint32_t sid, uint32_t rpcNum, + uint32_t sendBuf, uint32_t sendSize, + uint32_t recvBuf, uint32_t recvSize, + uint32_t &resultPtr, + bool &signalNowaitCompletion); +} + namespace { constexpr int KE_OK = 0; @@ -629,7 +640,16 @@ void register_ps2_sif_rpc_tests() constexpr uint32_t kClientAddr = 0x00028000u; constexpr uint32_t kSemaParamAddr = 0x00028100u; constexpr uint32_t kRecvAddr = 0x00028200u; - constexpr uint32_t kSid = IOP_SID_SNDDRV_STATE; + constexpr uint32_t kStateSid = 1u; + constexpr uint32_t kGetStatusFno = 0x12u; + constexpr uint32_t kGetAddrTableFno = 0x13u; + constexpr uint32_t kSid = kStateSid; + + PS2SoundDriverCompatLayout layout{}; + layout.stateSid = kStateSid; + layout.getStatusFno = kGetStatusFno; + layout.getAddrTableFno = kGetAddrTableFno; + ps2_syscalls::setSoundDriverCompatLayout(layout); SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); @@ -660,7 +680,7 @@ void register_ps2_sif_rpc_tests() std::memset(env.rdram.data() + kRecvAddr, 0, 16u); setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_STATUS_ADDR); + setRegU32(env.ctx, 5, kGetStatusFno); setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT); setRegU32(env.ctx, 7, 0u); setRegU32(env.ctx, 8, 0u); @@ -680,7 +700,7 @@ void register_ps2_sif_rpc_tests() std::memset(env.rdram.data() + kRecvAddr, 0, 16u); setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_ADDR_TABLE); + setRegU32(env.ctx, 5, kGetAddrTableFno); setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT); setRegU32(env.ctx, 7, 0u); setRegU32(env.ctx, 8, 0u); @@ -700,7 +720,7 @@ void register_ps2_sif_rpc_tests() std::memset(env.rdram.data() + kRecvAddr, 0, 16u); setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_STATUS_ADDR); + setRegU32(env.ctx, 5, kGetStatusFno); setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT); setRegU32(env.ctx, 7, 0u); setRegU32(env.ctx, 8, 0u); @@ -720,7 +740,16 @@ void register_ps2_sif_rpc_tests() constexpr uint32_t kClientAddr = 0x00028300u; constexpr uint32_t kSemaParamAddr = 0x00028400u; constexpr uint32_t kRecvAddr = 0x00028500u; - constexpr uint32_t kSid = IOP_SID_SNDDRV_STATE; + constexpr uint32_t kStateSid = 1u; + constexpr uint32_t kGetStatusFno = 0x12u; + constexpr uint32_t kGetAddrTableFno = 0x13u; + constexpr uint32_t kSid = kStateSid; + + PS2SoundDriverCompatLayout layout{}; + layout.stateSid = kStateSid; + layout.getStatusFno = kGetStatusFno; + layout.getAddrTableFno = kGetAddrTableFno; + ps2_syscalls::setSoundDriverCompatLayout(layout); SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); @@ -743,7 +772,7 @@ void register_ps2_sif_rpc_tests() writeGuestStruct(env.rdram.data(), kClientAddr, client); setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_STATUS_ADDR); + setRegU32(env.ctx, 5, kGetStatusFno); setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT); setRegU32(env.ctx, 7, 0u); setRegU32(env.ctx, 8, 0u); @@ -758,7 +787,7 @@ void register_ps2_sif_rpc_tests() "rpc 0x12 should return a low guest address like an IOP pointer"); setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_ADDR_TABLE); + setRegU32(env.ctx, 5, kGetAddrTableFno); setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT); setRegU32(env.ctx, 7, 0u); setRegU32(env.ctx, 8, 0u); @@ -781,6 +810,206 @@ void register_ps2_sif_rpc_tests() "sound-driver data base should be a later low guest address"); }); + tc.Run("snddrv HLE dispatches all configured subcommand semantics", [](TestCase &t) + { + TestEnv env; + uint8_t *rdram = env.rdram.data(); + + constexpr uint32_t kStreamState = 0x00050000u; + constexpr uint32_t kAllocTbl = 0x00050100u; + constexpr uint32_t kStopFlag = 0x00050200u; + constexpr uint32_t kSend = 0x00050300u; + constexpr uint32_t kRecv = 0x00050400u; + + std::memset(rdram + kStreamState, 0, 16u); + std::memset(rdram + kAllocTbl, 0, 16u * sizeof(uint32_t)); + std::memset(rdram + kStopFlag, 0, 16u); + std::memset(rdram + kSend, 0, 16u); + std::memset(rdram + kRecv, 0, 16u); + + constexpr uint32_t kSid = 0x80000701u; // fake service id + PS2SoundDriverCompatLayout layout{}; + layout.commandSid = kSid; + layout.stateSid = kSid; + layout.submitFno = 0x1u; + layout.getStatusFno = 0x12u; + layout.getAddrTableFno = 0x13u; + layout.streamOpenFno = 0xE620u; + layout.channelConfigFno = 0x30u; + layout.stopFno = 0x40u; + layout.streamStateAddr = kStreamState; + layout.streamReadyValue = 3u; + layout.channelAllocFlagTableAddr = kAllocTbl; + layout.stopCompletionFlagAddr = kStopFlag; + ps2_syscalls::setSoundDriverCompatLayout(layout); + + auto call = [&](uint32_t fno, uint32_t sendBuf, uint32_t sendSize, + uint32_t recvBuf, uint32_t recvSize) + { + uint32_t rp = 0xdeadbeefu; + bool now = false; + const bool r = ps2_syscalls::handleSoundDriverRpcService( + rdram, &env.runtime, kSid, fno, sendBuf, sendSize, recvBuf, recvSize, rp, now); + return std::make_tuple(r, rp, now); + }; + + // streamOpen + std::memset(rdram + kRecv, 0, 16u); + { + const auto [r, rp, now] = call(layout.streamOpenFno, 0u, 0u, kRecv, 16u); + t.IsTrue(r, "streamOpen should be handled"); + t.Equals(readGuestStruct(rdram, kStreamState), 3u, + "streamOpen should write streamReadyValue to streamStateAddr"); + t.Equals(readGuestStruct(rdram, kRecv), 0u, + "streamOpen should write 0 to recv buffer"); + t.IsTrue(now, "streamOpen should signal nowait completion"); + (void)rp; + } + + // channelConfig + { + constexpr uint32_t k = 5u; + writeGuestU32(rdram, kSend, k); + std::memset(rdram + kRecv, 0, 16u); + const auto [r, rp, now] = call(layout.channelConfigFno, kSend, 4u, kRecv, 16u); + t.IsTrue(r, "channelConfig should be handled"); + t.Equals(readGuestStruct(rdram, kAllocTbl + k * sizeof(uint32_t)), 1u, + "channelConfig should mark the requested channel allocated"); + t.Equals(readGuestStruct(rdram, kRecv), 0u, + "channelConfig should write 0 to recv buffer"); + (void)rp; + (void)now; + } + + // stop + { + std::memset(rdram + kRecv, 0, 16u); + std::memset(rdram + kStopFlag, 0, 16u); + const auto [r, rp, now] = call(layout.stopFno, 0u, 0u, kRecv, 16u); + t.IsTrue(r, "stop should be handled"); + t.Equals(readGuestStruct(rdram, kStopFlag), 1u, + "stop should write 1 to stopCompletionFlagAddr"); + t.Equals(readGuestStruct(rdram, kRecv), 0u, + "stop should write 0 to recv buffer"); + (void)rp; + (void)now; + } + + // getStatus + uint32_t statusVal = 0u; + { + std::memset(rdram + kRecv, 0, 16u); + const auto [r, rp, now] = call(layout.getStatusFno, 0u, 0u, kRecv, 16u); + statusVal = readGuestStruct(rdram, kRecv); + t.IsTrue(r, "getStatus should be handled"); + t.IsTrue(statusVal != 0u, "getStatus should return a nonzero status address"); + (void)rp; + (void)now; + } + + // getAddrTable + uint32_t addrTableVal = 0u; + { + std::memset(rdram + kRecv, 0, 16u); + const auto [r, rp, now] = call(layout.getAddrTableFno, 0u, 0u, kRecv, 16u); + addrTableVal = readGuestStruct(rdram, kRecv); + t.IsTrue(r, "getAddrTable should be handled"); + t.IsTrue(addrTableVal != 0u, "getAddrTable should return a nonzero addr-table address"); + t.IsTrue(addrTableVal != statusVal, "status and addr-table addresses should be distinct"); + (void)rp; + (void)now; + } + + // unknown fno. + // NOTE: this drives the handler directly and asserts its handler-local + // recv[0] write. In the real SifCallRpc path the benign status is NOT + // guest-visible: because the handler returns false (unhandled), the outer + // recv finalization (RPC.cpp: "if (!handled)" copy-from-send / zero branch) + // always overwrites recv[0] before the guest sees it. The assertion here + // therefore locks the handler's documented contract (spec: unknown fno on a + // served SID returns the benign discard status 0xffffff9b and falls through), + // not guest-observable state. + { + std::memset(rdram + kRecv, 0, 16u); + const auto [r, rp, now] = call(0xABCDu, 0u, 0u, kRecv, 16u); + t.IsTrue(!r, "unknown fno on a served SID should not be treated as handled"); + t.Equals(readGuestStruct(rdram, kRecv), 0xffffff9bu, + "unknown fno should write the benign status value to recv (handler-local; overwritten by the outer recv finalization in the real SifCallRpc path)"); + (void)rp; + (void)now; + } + }); + + tc.Run("snddrv HLE unconfigured layout is inert and games route independently", [](TestCase &t) + { + TestEnv env; // TestEnv already clears the layout in its constructor + uint8_t *rdram = env.rdram.data(); + + constexpr uint32_t kRecv = 0x00051000u; + writeGuestU32(rdram, kRecv, 0x11111111u); + + uint32_t rp = 0xdeadbeefu; + bool now = false; + bool r = ps2_syscalls::handleSoundDriverRpcService( + rdram, &env.runtime, 0x80000701u, 0x40u, 0u, 0u, kRecv, 16u, rp, now); + t.IsTrue(!r, "unconfigured layout should never claim an RPC"); + t.Equals(readGuestStruct(rdram, kRecv), 0x11111111u, + "unconfigured layout should not touch guest memory"); + + // Game A + constexpr uint32_t kSidA = 0x80000701u; + constexpr uint32_t kStreamOpenFnoA = 0xE620u; + constexpr uint32_t kStreamStateA = 0x00051100u; + std::memset(rdram + kStreamStateA, 0, 16u); + + PS2SoundDriverCompatLayout layoutA{}; + layoutA.commandSid = kSidA; + layoutA.stateSid = kSidA; + layoutA.streamOpenFno = kStreamOpenFnoA; + layoutA.streamStateAddr = kStreamStateA; + layoutA.streamReadyValue = 3u; + ps2_syscalls::setSoundDriverCompatLayout(layoutA); + + rp = 0xdeadbeefu; + now = false; + r = ps2_syscalls::handleSoundDriverRpcService( + rdram, &env.runtime, kSidA, kStreamOpenFnoA, 0u, 0u, 0u, 0u, rp, now); + t.IsTrue(r, "game A streamOpen should be handled"); + t.Equals(readGuestStruct(rdram, kStreamStateA), 3u, + "game A streamStateAddr should be updated with game A's ready value"); + + ps2_syscalls::clearSoundDriverCompatLayout(); + + // Game B, distinct sid/fno numbers. + constexpr uint32_t kSidB = 0x12340000u; + constexpr uint32_t kStreamOpenFnoB = 0x77u; + constexpr uint32_t kStreamStateB = 0x00051200u; + std::memset(rdram + kStreamStateB, 0, 16u); + + PS2SoundDriverCompatLayout layoutB{}; + layoutB.commandSid = kSidB; + layoutB.stateSid = kSidB; + layoutB.streamOpenFno = kStreamOpenFnoB; + layoutB.streamStateAddr = kStreamStateB; + layoutB.streamReadyValue = 9u; + ps2_syscalls::setSoundDriverCompatLayout(layoutB); + + rp = 0xdeadbeefu; + now = false; + r = ps2_syscalls::handleSoundDriverRpcService( + rdram, &env.runtime, kSidB, kStreamOpenFnoB, 0u, 0u, 0u, 0u, rp, now); + t.IsTrue(r, "game B streamOpen should be handled"); + t.Equals(readGuestStruct(rdram, kStreamStateB), 9u, + "game B streamStateAddr should be updated with game B's ready value"); + + // Game A's sid/fno should no longer be served now that the single global slot holds game B. + rp = 0xdeadbeefu; + now = false; + r = ps2_syscalls::handleSoundDriverRpcService( + rdram, &env.runtime, kSidA, kStreamOpenFnoA, 0u, 0u, 0u, 0u, rp, now); + t.IsTrue(!r, "game A's sid/fno should not be served once game B's layout is active"); + }); + tc.Run("SifCallRpc falls back to stack ABI when register pack is implausible", [](TestCase &t) { TestEnv env;