From 539490b0c833bb18dd990f5a9ad9947ef7a32bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Jos=C3=A9=20Garc=C3=ADa=20Garc=C3=ADa?= Date: Tue, 20 Oct 2026 17:40:32 +0200 Subject: [PATCH 1/7] HLE: sceAmctrl_driver, on libkirk's amctrl amctrl.prx's PGD (BB) MAC and cipher functions, for kernel modules that import them: the firmware's POPS manager checks and decrypts a store PS1 game's disc image with them. libkirk's amctrl.c reimplements the module, and its MAC_KEY and CIPHER_KEY have the layout of the contexts the callers keep in their own memory, so those are used in place. 6.60 NIDs. --- Core/CMakeLists.txt | 2 + Core/Core.vcxproj | 2 + Core/Core.vcxproj.filters | 6 ++ Core/HLE/FunctionWrappers.h | 5 ++ Core/HLE/HLETables.cpp | 2 + Core/HLE/sceAmctrl.cpp | 117 ++++++++++++++++++++++++++++ Core/HLE/sceAmctrl.h | 17 ++++ UWP/CoreUWP/CoreUWP.vcxproj | 2 + UWP/CoreUWP/CoreUWP.vcxproj.filters | 2 + android/jni/Android.mk | 1 + libretro/Makefile.common | 1 + 11 files changed, 157 insertions(+) create mode 100644 Core/HLE/sceAmctrl.cpp create mode 100644 Core/HLE/sceAmctrl.h diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index 556d938586..fcb2f3c3e1 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -396,6 +396,8 @@ add_library(Core STATIC HLE/__sceAudio.h HLE/sceAdler.cpp HLE/sceAdler.h + HLE/sceAmctrl.cpp + HLE/sceAmctrl.h HLE/AtracBase.h HLE/AtracCtx.cpp HLE/AtracCtx.h diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index fff3741a09..1c07a66c9a 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -608,6 +608,7 @@ + @@ -946,6 +947,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index f3fd104aae..cd7094d8b3 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -198,6 +198,9 @@ HLE\Libraries + + HLE\Libraries + HLE\Libraries @@ -1503,6 +1506,9 @@ HLE\Libraries + + HLE\Libraries + HLE\Libraries diff --git a/Core/HLE/FunctionWrappers.h b/Core/HLE/FunctionWrappers.h index 3cb7eac601..3647227e7a 100644 --- a/Core/HLE/FunctionWrappers.h +++ b/Core/HLE/FunctionWrappers.h @@ -327,6 +327,11 @@ template void WrapI_UCI(MIPSState *mips) { RETURN(retval); } +template void WrapI_UIIUUU(MIPSState *mips) { + int retval = HLECall(mips, PARAM(0), PARAM(1), PARAM(2), PARAM(3), PARAM(4), PARAM(5)); + RETURN(retval); +} + template void WrapU_UIIIII(MIPSState *mips) { u32 retval = HLECall(mips, PARAM(0), PARAM(1), PARAM(2), PARAM(3), PARAM(4), PARAM(5)); RETURN(retval); diff --git a/Core/HLE/HLETables.cpp b/Core/HLE/HLETables.cpp index 13731b43c9..b3a58074b7 100644 --- a/Core/HLE/HLETables.cpp +++ b/Core/HLE/HLETables.cpp @@ -87,6 +87,7 @@ #include "sceSfmt19937.h" #include "sceG729.h" #include "KUBridge.h" +#include "sceAmctrl.h" #include "sceNetInet.h" #include "sceNetResolver.h" // #include "sceNp2.h" @@ -359,6 +360,7 @@ void RegisterAllModules() { Register_ExceptionManagerForKernel(); Register_sceSysreg_driver(); Register_sceSysEventForKernel(); + Register_sceAmctrl_driver(); // Not ready to enable this due to apparent softlocks in Patapon 3. // Register_sceNpMatching2(); diff --git a/Core/HLE/sceAmctrl.cpp b/Core/HLE/sceAmctrl.cpp new file mode 100644 index 0000000000..33f4b34c25 --- /dev/null +++ b/Core/HLE/sceAmctrl.cpp @@ -0,0 +1,117 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// amctrl.prx's PGD MAC and cipher functions, which the POPS manager uses on a store PS1 game's disc. +// libkirk's amctrl.c reimplements the module, and its MAC_KEY and CIPHER_KEY have the layout of the +// contexts callers keep in their memory, so they're used in place. + +#include "ext/libkirk/amctrl.h" + +#include "Core/Debugger/MemBlockInfo.h" +#include "Core/HLE/HLE.h" +#include "Core/HLE/FunctionWrappers.h" +#include "Core/HLE/sceAmctrl.h" +#include "Core/HLE/sceChnnlsv.h" +#include "Core/MemMap.h" + +static MAC_KEY *MacKey(u32 addr) { + return Memory::IsValidRange(addr, sizeof(MAC_KEY)) ? (MAC_KEY *)Memory::GetPointerWriteUnchecked(addr) : nullptr; +} + +static CIPHER_KEY *CipherKey(u32 addr) { + return Memory::IsValidRange(addr, sizeof(CIPHER_KEY)) ? (CIPHER_KEY *)Memory::GetPointerWriteUnchecked(addr) : nullptr; +} + +// A 16-byte key argument, which may be absent. +static u8 *Key16(u32 addr) { + return Memory::IsValidRange(addr, 16) ? Memory::GetPointerWriteUnchecked(addr) : nullptr; +} + +static int Hle_sceDrmBBMacInit(u32 mkeyAddr, int type) { + MAC_KEY *mkey = MacKey(mkeyAddr); + if (!mkey) { + return hleLogError(Log::sceKernel, -1, "bad context"); + } + return hleLogDebug(Log::sceKernel, sceDrmBBMacInit(mkey, type)); +} + +static int Hle_sceDrmBBMacUpdate(u32 mkeyAddr, u32 buf, int size) { + MAC_KEY *mkey = MacKey(mkeyAddr); + if (!mkey || size < 0 || !Memory::IsValidRange(buf, size)) { + return hleLogError(Log::sceKernel, -1, "bad arguments"); + } + return hleLogDebug(Log::sceKernel, sceDrmBBMacUpdate(__ChnnlsvKirkState(), mkey, Memory::GetPointerWriteUnchecked(buf), size)); +} + +static int Hle_sceDrmBBMacFinal(u32 mkeyAddr, u32 out, u32 vkey) { + MAC_KEY *mkey = MacKey(mkeyAddr); + if (!mkey || !Key16(out)) { + return hleLogError(Log::sceKernel, -1, "bad arguments"); + } + return hleLogDebug(Log::sceKernel, sceDrmBBMacFinal(__ChnnlsvKirkState(), mkey, Key16(out), Key16(vkey))); +} + +// Compares with the expected MAC: 0x80510300 when they differ. +static int Hle_sceDrmBBMacFinal2(u32 mkeyAddr, u32 mac, u32 vkey) { + MAC_KEY *mkey = MacKey(mkeyAddr); + if (!mkey || !Key16(mac)) { + return hleLogError(Log::sceKernel, -1, "bad arguments"); + } + const int result = sceDrmBBMacFinal2(__ChnnlsvKirkState(), mkey, Key16(mac), Key16(vkey)); + if (result != 0) { + return hleLogWarning(Log::sceKernel, result, "MAC mismatch"); + } + return hleLogDebug(Log::sceKernel, result); +} + +static int Hle_sceDrmBBCipherInit(u32 ckeyAddr, int type, int mode, u32 headerKey, u32 versionKey, u32 seed) { + CIPHER_KEY *ckey = CipherKey(ckeyAddr); + if (!ckey || !Key16(headerKey)) { + return hleLogError(Log::sceKernel, -1, "bad arguments"); + } + return hleLogDebug(Log::sceKernel, sceDrmBBCipherInit(__ChnnlsvKirkState(), ckey, type, mode, Key16(headerKey), Key16(versionKey), seed)); +} + +static int Hle_sceDrmBBCipherUpdate(u32 ckeyAddr, u32 data, int size) { + CIPHER_KEY *ckey = CipherKey(ckeyAddr); + if (!ckey || size < 0 || !Memory::IsValidRange(data, size)) { + return hleLogError(Log::sceKernel, -1, "bad arguments"); + } + const int result = sceDrmBBCipherUpdate(__ChnnlsvKirkState(), ckey, Memory::GetPointerWriteUnchecked(data), size); + NotifyMemInfo(MemBlockFlags::WRITE, data, size, "BBCipher"); + return hleLogDebug(Log::sceKernel, result); +} + +static int Hle_sceDrmBBCipherFinal(u32 ckeyAddr) { + CIPHER_KEY *ckey = CipherKey(ckeyAddr); + if (!ckey) { + return hleLogError(Log::sceKernel, -1, "bad context"); + } + return hleLogDebug(Log::sceKernel, sceDrmBBCipherFinal(ckey)); +} + +// 6.60 NIDs. +const HLEFunction sceAmctrl_driver[] = { + { 0x525B8218, &WrapI_UI, "sceDrmBBMacInit", 'i', "xi", HLE_KERNEL_SYSCALL }, + { 0x58163FBE, &WrapI_UUI, "sceDrmBBMacUpdate", 'i', "xxi", HLE_KERNEL_SYSCALL }, + { 0xEF95A213, &WrapI_UUU, "sceDrmBBMacFinal", 'i', "xxx", HLE_KERNEL_SYSCALL }, + { 0xF5186D8E, &WrapI_UUU, "sceDrmBBMacFinal2", 'i', "xxx", HLE_KERNEL_SYSCALL }, + { 0x1CCB66D2, &WrapI_UIIUUU, "sceDrmBBCipherInit", 'i', "xiixxx", HLE_KERNEL_SYSCALL }, + { 0x0785C974, &WrapI_UUI, "sceDrmBBCipherUpdate", 'i', "xxi", HLE_KERNEL_SYSCALL }, + { 0x9951C50F, &WrapI_U, "sceDrmBBCipherFinal", 'i', "x", HLE_KERNEL_SYSCALL }, +}; + +void Register_sceAmctrl_driver() { + RegisterHLEModule("sceAmctrl_driver", ARRAY_SIZE(sceAmctrl_driver), sceAmctrl_driver); +} diff --git a/Core/HLE/sceAmctrl.h b/Core/HLE/sceAmctrl.h new file mode 100644 index 0000000000..c3f6746e1b --- /dev/null +++ b/Core/HLE/sceAmctrl.h @@ -0,0 +1,17 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +#pragma once + +void Register_sceAmctrl_driver(); diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index ea06a733ed..ec65fce10b 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -184,6 +184,7 @@ + @@ -484,6 +485,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index 8f36ee64f4..1213e96991 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -92,6 +92,7 @@ + @@ -510,6 +511,7 @@ + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 5a7db3140c..9b670d77c3 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -753,6 +753,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/HLE/SocketManager.cpp \ $(SRC)/Core/HLE/Plugins.cpp \ $(SRC)/Core/HLE/sceAdler.cpp \ + $(SRC)/Core/HLE/sceAmctrl.cpp \ $(SRC)/Core/HLE/sceAtrac.cpp \ $(SRC)/Core/HLE/AtracCtx.cpp \ $(SRC)/Core/HLE/AtracCtx2.cpp \ diff --git a/libretro/Makefile.common b/libretro/Makefile.common index a65a8f875c..793294ec1c 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -769,6 +769,7 @@ SOURCES_CXX += \ $(COREDIR)/HLE/AtracCtx.cpp \ $(COREDIR)/HLE/AtracCtx2.cpp \ $(COREDIR)/HLE/sceAdler.cpp \ + $(COREDIR)/HLE/sceAmctrl.cpp \ $(COREDIR)/HLE/sceAtrac.cpp \ $(COREDIR)/HLE/sceAudio.cpp \ $(COREDIR)/HLE/sceAudiocodec.cpp \ From 6936d22b38a0e1eac18105e9313a0ac7e5af51c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 20 Oct 2026 22:53:16 +0200 Subject: [PATCH 2/7] HLE: sceSysreg_driver's 6.60 NIDs that the POPS manager uses popsman.prx starts the Media Engine and sets up audio through them. The ME reset, bus clock and AVC reset are the calls the older NIDs already have. The others set bits of registers lowio.prx 6.61 writes for them (0x50 bit 15+n, 0x58 bit 16+n, 0x60 bit 18, 0x78 bits 7+n and 11), and C36775AD (called with 0, 1 before audio is set up) isn't read yet. The system register device now keeps every register it doesn't model. Under --vsh these stand in for lowio.prx's own, which write the same registers. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HLE/sceSysreg.cpp | 59 ++++++++++++++++++++++++++++++++++++++---- Core/HW/MeMMIO.cpp | 2 +- Core/MIPS/MeCore.cpp | 1 + Core/MIPS/MeCore.h | 1 + 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/Core/HLE/sceSysreg.cpp b/Core/HLE/sceSysreg.cpp index 7226e60282..d94208f8c5 100644 --- a/Core/HLE/sceSysreg.cpp +++ b/Core/HLE/sceSysreg.cpp @@ -15,11 +15,10 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -// The parts of the firmware's system register driver homebrew uses to start the Media Engine. Each -// sets or clears a bit of a register the way the driver does (lowio.prx 6.61 at 080001b8 and -// 080004b8, sysreg.prx 1.50 the same), returning whether it was set. Only the NIDs of the firmware -// versions homebrew was built for (1.50, and 3.71 and later's for the ME calls), so the VSH's own -// later lowio.prx keeps its exports. +// The parts of the firmware's system register driver homebrew and the POPS manager use to start the +// Media Engine. Each sets or clears a bit of a register the way the driver does (lowio.prx 6.61 at +// 080001b8, 080004b8, 08000880 and 08000cd8, sysreg.prx 1.50 the same), returning whether it was +// set. Under --vsh these stand in for lowio.prx's own, which write the same registers. #include "Common/CommonTypes.h" #include "Core/HLE/HLE.h" @@ -32,6 +31,9 @@ enum : u32 { SYSREG_RESET = 0xBC10004C, SYSREG_BUS_CLOCK = 0xBC100050, + SYSREG_CLOCK_58 = 0xBC100058, + SYSREG_60 = 0xBC100060, + SYSREG_IO_78 = 0xBC100078, RESET_ME = 0x04, RESET_VME = 0x10, @@ -77,6 +79,42 @@ static int sceSysregAvcResetDisable(MIPSState *mips) { return hleLogDebug(Log::ME, SetSysregBit(mips, SYSREG_RESET, RESET_AVC, false)); } +// The 6.60/6.61 calls popsman.prx makes whose names aren't known, after what lowio.prx does. +// Those taking an index only take 0 or 1. +static int SetSysregIndexedBit(MIPSState *mips, u32 reg, u32 bit0, int index) { + if (index < 0 || index >= 2) + return hleLogError(Log::ME, 0x80000102, "bad index"); + return hleLogDebug(Log::ME, SetSysregBit(mips, reg, bit0 << index, true)); +} + +static int sceSysreg_driver_8B1DD83A(MIPSState *mips, int index) { + return SetSysregIndexedBit(mips, SYSREG_BUS_CLOCK, 0x8000, index); +} + +static int sceSysreg_driver_B1751B06(MIPSState *mips, int index) { + return SetSysregIndexedBit(mips, SYSREG_CLOCK_58, 0x10000, index); +} + +static int sceSysreg_driver_4634C9DC(MIPSState *mips, int index) { + return SetSysregIndexedBit(mips, SYSREG_IO_78, 0x80, index); +} + +static int sceSysreg_driver_4B4CCE80(MIPSState *mips) { + return hleLogDebug(Log::ME, SetSysregBit(mips, SYSREG_IO_78, 0x800, true)); +} + +// Sets 0x60 bit 18 to value, returning what it was. +static int sceSysreg_driver_41B0337B(MIPSState *mips, int value) { + if (value < 0 || value >= 2) + return hleLogError(Log::ME, 0x800001FE, "bad value"); + return hleLogDebug(Log::ME, SetSysregBit(mips, SYSREG_60, 1 << 18, value != 0)); +} + +// popsman calls it with (0, 1) before setting up audio. Not read yet. +static int sceSysreg_driver_C36775AD(int a, int b) { + return hleLogWarning(Log::ME, 0, "unimplemented"); +} + const HLEFunction sceSysreg_driver[] = { {0xDE59DACB, &WrapI_V, "sceSysregMeResetEnable", 'i', "", HLE_KERNEL_SYSCALL }, {0x2DB0EB28, &WrapI_V, "sceSysregMeResetDisable", 'i', "", HLE_KERNEL_SYSCALL }, @@ -90,6 +128,17 @@ const HLEFunction sceSysreg_driver[] = { {0xA9997109, &WrapI_V, "sceSysregMeResetEnable371", 'i', "", HLE_KERNEL_SYSCALL }, {0x76220E94, &WrapI_V, "sceSysregMeResetDisable371", 'i', "", HLE_KERNEL_SYSCALL }, {0x3199CF1C, &WrapI_V, "sceSysregMeBusClockEnable371", 'i', "", HLE_KERNEL_SYSCALL }, + // 6.60 and 6.61, which popsman.prx uses. + {0x457FEBA9, &WrapI_V, "sceSysregMeResetEnable660", 'i', "", HLE_KERNEL_SYSCALL }, + {0x48F1C4AD, &WrapI_V, "sceSysregMeResetDisable660", 'i', "", HLE_KERNEL_SYSCALL }, + {0x38527743, &WrapI_V, "sceSysregMeBusClockEnable660", 'i', "", HLE_KERNEL_SYSCALL }, + {0x0AE8E549, &WrapI_V, "sceSysregAvcResetEnable660", 'i', "", HLE_KERNEL_SYSCALL }, + {0x8B1DD83A, &WrapI_I, "sceSysreg_driver_8B1DD83A", 'i', "i", HLE_KERNEL_SYSCALL }, + {0xB1751B06, &WrapI_I, "sceSysreg_driver_B1751B06", 'i', "i", HLE_KERNEL_SYSCALL }, + {0x4634C9DC, &WrapI_I, "sceSysreg_driver_4634C9DC", 'i', "i", HLE_KERNEL_SYSCALL }, + {0x4B4CCE80, &WrapI_V, "sceSysreg_driver_4B4CCE80", 'i', "", HLE_KERNEL_SYSCALL }, + {0x41B0337B, &WrapI_I, "sceSysreg_driver_41B0337B", 'i', "i", HLE_KERNEL_SYSCALL }, + {0xC36775AD, &WrapI_II, "sceSysreg_driver_C36775AD", 'i', "ii", HLE_KERNEL_SYSCALL }, }; // Custom-core homebrew (mcidclan's libraries) walks this to find me_wrapper's "SceMeRpc" handler, diff --git a/Core/HW/MeMMIO.cpp b/Core/HW/MeMMIO.cpp index 7aca8d16e4..4f01f5baf4 100644 --- a/Core/HW/MeMMIO.cpp +++ b/Core/HW/MeMMIO.cpp @@ -117,7 +117,7 @@ private: case 0x54: return &g_meState->sysreg54; case 0x5C: return &g_meState->sysreg5C; case 0x70: return &g_meState->sysreg70; - default: return nullptr; + default: return (offset & 3) ? nullptr : &g_meState->sysregOther[offset / 4]; } } }; diff --git a/Core/MIPS/MeCore.cpp b/Core/MIPS/MeCore.cpp index 94edfe7fcd..47a238aef2 100644 --- a/Core/MIPS/MeCore.cpp +++ b/Core/MIPS/MeCore.cpp @@ -318,6 +318,7 @@ void DoState(PointerWrap &p) { Do(p, me->sysreg5C); Do(p, me->sysreg70); Do(p, me->booting); + DoArray(p, me->sysregOther, ARRAY_SIZE(me->sysregOther)); DoArray(p, me->memProtect, ARRAY_SIZE(me->memProtect)); DoArray(p, me->vmeControl, ARRAY_SIZE(me->vmeControl)); DoArray(p, me->clocks, ARRAY_SIZE(me->clocks)); diff --git a/Core/MIPS/MeCore.h b/Core/MIPS/MeCore.h index 790f477c09..1e4e715d1b 100644 --- a/Core/MIPS/MeCore.h +++ b/Core/MIPS/MeCore.h @@ -60,6 +60,7 @@ struct MEState { u32 sysreg54 = 0; // 0x54. Sony's ME firmware clears bit 0. Unknown. u32 sysreg5C = 0; // 0x5C. Sony's ME firmware sets bit 1. Unknown. u32 sysreg70 = 0; // 0x70. MIST's meEasyShot sets bits 0 and 2. Unknown. + u32 sysregOther[0x40]{}; // The rest, kept: clock and IO enables (0x58, 0x60, 0x6C, 0x78...). // Sony's ME firmware still has to boot to its idle halt (RunUntilIdle). bool booting = false; u32 memProtect[20]{}; // 0xBC000000-0xBC00004C From bafdb4512835bc2d612b26e76bb49acb4b160a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 20 Oct 2026 22:54:27 +0200 Subject: [PATCH 3/7] ME: Syscalls to the firmware's cache functions do nothing Kernel code the ME runs calls a few firmware functions directly: POPS's ME loop calls sceKernelDcacheWritebackInvalidateAll for every sample. On a PSP the loader links kernel imports as direct jumps, so the ME runs the function's code; here they're HLE syscall stubs. The cache functions only work on the calling CPU's caches, and the ME's aren't emulated, so they return 0 and do nothing, as does an unresolved import (as on the main CPU). Others are still an error, now naming the function. The IR ends the block after one, as for a syscall on the main CPU: a jr with it in its delay slot leaves the exit to it, and the ME ran on into the next stub. A syscall instruction of the ME's own should take its exception vector (ExcCode 8); none has come up yet. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/MIPS/IR/IRCompBranch.cpp | 18 ++++++++++++++++-- Core/MIPS/Interpreter.cpp | 15 ++++++++++++--- Core/MIPS/MeCore.cpp | 20 ++++++++++++++++++++ Core/MIPS/MeCore.h | 5 +++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/Core/MIPS/IR/IRCompBranch.cpp b/Core/MIPS/IR/IRCompBranch.cpp index dd196c0af6..3c7344303c 100644 --- a/Core/MIPS/IR/IRCompBranch.cpp +++ b/Core/MIPS/IR/IRCompBranch.cpp @@ -24,6 +24,7 @@ #include "Core/HLE/HLETables.h" #include "Core/MIPS/MIPS.h" +#include "Core/MIPS/MeCore.h" #include "Core/MIPS/MIPSCodeUtils.h" #include "Core/MIPS/MIPSAnalyst.h" #include "Core/MIPS/MIPSTables.h" @@ -464,8 +465,21 @@ void IRFrontend::Comp_JumpReg(MIPSOpcode op) { void IRFrontend::Comp_Syscall(MIPSOpcode op) { if (IsMediaEngine()) { - // There's no HLE on the ME, the interpreter traps it. - Comp_Generic(op); + // There's no HLE on the ME. A call to a stub that does nothing there returns 0, ending the + // block as a syscall does on the main CPU (a jr with it in its delay slot leaves the exit to + // it); the interpreter traps the rest. + if (!MeCore::SyscallDoesNothing(op, GetCompilerPC())) { + Comp_Generic(op); + return; + } + ir.Write(IROp::Downcount, 0, 0, 0, js.downcountAmount); + js.downcountAmount = 0; + if (!js.inDelaySlot) + ir.Write(IROp::SetPCConst, 0, 0, 0, GetCompilerPC() + 4); + ir.WriteSetConstant(MIPS_REG_V0, 0); + FlushAll(); + ir.Write(IROp::ExitToPC); + js.compiling = false; return; } // Like the interpreter, the syscall sees every earlier instruction charged (including a branch it's diff --git a/Core/MIPS/Interpreter.cpp b/Core/MIPS/Interpreter.cpp index 92cd84ecaa..9b8fe9b630 100644 --- a/Core/MIPS/Interpreter.cpp +++ b/Core/MIPS/Interpreter.cpp @@ -183,9 +183,18 @@ namespace MIPSInt { void Int_Syscall(MIPSState *mips, MIPSOpcode op) { if (mips->IsMediaEngine()) { - // There's no HLE on the ME. TODO: Its exception vector. - ERROR_LOG(Log::ME, "syscall on the Media Engine at %08x", mips->pc); - Core_ExecException(mips->pc, mips->pc, ExecExceptionType::ILLEGAL); + // TODO: Its exception vector. + if (!MeCore::SyscallDoesNothing(op, mips->pc)) { + Core_ExecException(mips->pc, mips->pc, ExecExceptionType::ILLEGAL); + return; + } + mips->r[MIPS_REG_V0] = 0; + if (mips->inDelaySlot) { + mips->pc = mips->nextPC; + } else { + mips->pc += 4; + } + mips->inDelaySlot = false; return; } const u32 syscallPC = mips->pc - 4; diff --git a/Core/MIPS/MeCore.cpp b/Core/MIPS/MeCore.cpp index 47a238aef2..b4dd5d00e8 100644 --- a/Core/MIPS/MeCore.cpp +++ b/Core/MIPS/MeCore.cpp @@ -27,6 +27,7 @@ #include "Core/CoreTiming.h" #include "Core/ELF/PrxDecrypter.h" #include "Core/FileSystems/MetaFileSystem.h" +#include "Core/HLE/HLE.h" #include "Core/HLE/sceKernelInterrupt.h" #include "Core/HLE/sceKernelMemory.h" #include "Core/MemMap.h" @@ -552,6 +553,25 @@ static void RunUntilIdle(MEState *me) { } } +bool SyscallDoesNothing(MIPSOpcode op, u32 pc) { + static const char *const cacheFunctions[] = { + "sceKernelDcacheWritebackAll", + "sceKernelDcacheWritebackInvalidateAll", + "sceKernelDcacheWritebackRange", + "sceKernelDcacheWritebackInvalidateRange", + "sceKernelDcacheInvalidateRange", + }; + const HLEFunction *func = GetSyscallFunctionData(op, pc); + if (!func) + return true; + for (const char *name : cacheFunctions) { + if (!strcmp(func->name, name)) + return true; + } + ERROR_LOG(Log::ME, "syscall to %s on the Media Engine at %08x", func->name, pc); + return false; +} + void SetReset(bool asserted) { MEState *me = g_meState; if (asserted) { diff --git a/Core/MIPS/MeCore.h b/Core/MIPS/MeCore.h index 1e4e715d1b..9048f1a5f1 100644 --- a/Core/MIPS/MeCore.h +++ b/Core/MIPS/MeCore.h @@ -112,6 +112,11 @@ u64 GetTicks(); u32 ReadCop0(int reg); void WriteCop0(int reg, u32 value); void Halt(); +// Whether a syscall the ME makes is to a firmware function it may call, all of which do nothing on +// it: kernel code it runs (POPS's ME loop) calls the cache functions, which only work on the calling +// CPU's caches, and those aren't emulated. An unresolved import does nothing, as on the main CPU. +// Logs the others. +bool SyscallDoesNothing(MIPSOpcode op, u32 pc); // mtic. void SetInterruptEnable(u32 value); From 9d55b8046c5a3d00baac716548d98e6493a5e11f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 20 Oct 2026 22:56:06 +0200 Subject: [PATCH 4/7] ME: The audio block, which the ME plays through The audio hardware at 0xBE000000, as POPS's ME loop (popsman.prx +2F88) drives it: after setting it up, it writes one stereo sample (left in the low half) to 0x70 whenever status 0x28 bit 5 says the FIFO has room, and sleeps in halt in between on the ME's interrupt source 12. Measured from the ME on a PSP-1000 (pspautotest me/audiofifo): the FIFO holds 24 samples and plays them at 44.1kHz while 0x04 bit 1 is set; status 0x28 has bit 4 while playing, bit 5 while there's room, and bit 1 once it has played out its last sample, a sample after the FIFO empties; and source 12 is a level, up while there's room, which halt wakes on with interrupts disabled. What it plays is mixed into the output. A halted ME used to sleep until the main CPU's next slice. It now wakes within the slice when the full FIFO next has room, as POPS's loop sleeps for every sample. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HLE/__sceAudio.cpp | 6 ++ Core/HW/MMIO.cpp | 1 + Core/HW/MeMMIO.cpp | 142 ++++++++++++++++++++++++++++++++++++++++ Core/HW/MeMMIO.h | 8 +++ Core/MIPS/MeCore.cpp | 15 ++++- Core/MIPS/MeCore.h | 15 +++++ 6 files changed, 186 insertions(+), 1 deletion(-) diff --git a/Core/HLE/__sceAudio.cpp b/Core/HLE/__sceAudio.cpp index 266b46674f..1f35e600b9 100644 --- a/Core/HLE/__sceAudio.cpp +++ b/Core/HLE/__sceAudio.cpp @@ -43,6 +43,8 @@ #include "Core/HLE/sceKernel.h" #include "Core/HLE/sceKernelThread.h" #include "Core/HLE/sceKernelInterrupt.h" +#include "Core/HW/MeMMIO.h" +#include "Core/MIPS/MeCore.h" #include "Core/Util/AudioFormat.h" // Should be used to lock anything related to the outAudioQueue. @@ -641,6 +643,10 @@ void __AudioUpdate(bool resetRecording) { woke |= __AudioMixChannel(chan); } woke |= __AudioMixSRC(g_audioSRC); + // What the ME played through the audio block (the PS1 emulator's sound). + if (MeCore::g_meState) { + MeMMIO::AudioMix(mixBuffer, hwBlockSize); + } audioMixing = false; if (g_Config.bEnableSound) { diff --git a/Core/HW/MMIO.cpp b/Core/HW/MMIO.cpp index 9f0651755a..0aeb93e564 100644 --- a/Core/HW/MMIO.cpp +++ b/Core/HW/MMIO.cpp @@ -101,6 +101,7 @@ void Init() { MeMMIO::VmeControlDevice(), MeMMIO::ClockDevice(), MeMMIO::DdrDevice(), + MeMMIO::AudioDevice(), }; for (MMIODevice *device : g_devices) { device->Reset(); diff --git a/Core/HW/MeMMIO.cpp b/Core/HW/MeMMIO.cpp index 4f01f5baf4..bf3cf36e39 100644 --- a/Core/HW/MeMMIO.cpp +++ b/Core/HW/MeMMIO.cpp @@ -20,6 +20,7 @@ #include #include "Common/Log.h" +#include "Core/CoreTiming.h" #include "Core/Debugger/MemBlockInfo.h" #include "Core/HLE/sceDisplay.h" #include "Core/HLE/sceMpegbase.h" @@ -514,6 +515,94 @@ public: void DoState(PointerWrap &p) override {} }; +// The audio block (0xBE000000), as POPS's ME loop (popsman.prx +2F88) drives it: it sets the block +// up, then writes one stereo sample (left in the low half) to 0x70 whenever status 0x28 bit 5 says +// the FIFO has room, sleeping in halt in between on interrupt source 12. As measured from the ME on +// a PSP-1000 (pspautotest me/audiofifo): the FIFO holds 24 samples and plays them at 44.1kHz while +// 0x04 bit 1 is set; status 0x28 has bit 4 while playing, bit 5 while there's room, and bit 1 once +// it has played out its last sample, which comes a sample after the FIFO is empty; and source 12 is a level, up while there's room, which halt +// wakes on with interrupts disabled. +class MeAudioDevice : public MMIODevice { +public: + MeAudioDevice() : MMIODevice("audio", 0xBE000000, 0x80) {} + + bool Read(MIPSState *mips, u32 offset, int size, u32 *value) override { + if (!g_meState || (offset & 3)) + return false; + MeMMIO::AudioUpdate(MeCore::GetTicks()); + const MeCore::MEState::Audio &a = g_meState->audio; + switch (offset) { + case REG_STATUS: *value = 0; break; + case REG_FIFO_STATUS: *value = FifoStatus(a); break; + default: *value = a.regs[offset / 4]; break; + } + return true; + } + + bool Write(MIPSState *mips, u32 offset, int size, u32 value) override { + if (!g_meState || (offset & 3)) + return false; + const s64 now = MeCore::GetTicks(); + MeMMIO::AudioUpdate(now); + MeCore::MEState::Audio &a = g_meState->audio; + if (offset != REG_DATA) { + a.regs[offset / 4] = value; + return true; + } + if (a.fifoCount == FIFO_DEPTH) { + WARN_LOG(Log::ME, "Audio FIFO overflow"); + return true; + } + if (a.fifoCount == 0) + a.nextPlayed = now + SamplePeriod(); + a.fifo[a.fifoCount++] = value; + a.underrun = false; + a.lastPlayed = INT64_MAX; + MeMMIO::AudioUpdate(now); + return true; + } + + void Reset() override {} + void DoState(PointerWrap &p) override {} + + static constexpr u32 FIFO_DEPTH = ARRAY_SIZE(MeCore::MEState::Audio::fifo); + static constexpr u32 REG_CONTROL = 0x04; + static constexpr u32 REG_STATUS = 0x0C; + static constexpr u32 REG_FIFO_STATUS = 0x28; + static constexpr u32 REG_DATA = 0x70; + static constexpr u32 CONTROL_PLAY = 2; + static constexpr u32 FIFO_ROOM = 0x20; + static constexpr u32 FIFO_PLAYING = 0x10; + static constexpr u32 FIFO_UNDERRUN = 0x02; + static constexpr u32 INTR_AUDIO = 1 << 12; + + static s64 SamplePeriod() { + return CoreTiming::GetClockFrequencyHz() / 44100; + } + + static bool Playing(const MeCore::MEState::Audio &a) { + return (a.regs[REG_CONTROL / 4] & CONTROL_PLAY) != 0; + } + + static u32 FifoStatus(const MeCore::MEState::Audio &a) { + u32 status = a.fifoCount < FIFO_DEPTH ? FIFO_ROOM : 0; + if (Playing(a)) + status |= FIFO_PLAYING; + if (a.underrun) + status |= FIFO_UNDERRUN; + return status; + } +}; + +// Played samples on their way to the host's mix. Not saved: a load starts with silence. +static std::vector g_audioPlayed; +static size_t g_audioPlayedRead = 0; +// The ME plays in bursts, a PS1 frame at a time: keep a little more than a frame queued. +static const size_t AUDIO_MIX_PRIME = 1536; +static const size_t AUDIO_MIX_MAX = 8192; +static bool g_audioMixPrimed = false; + +static MeAudioDevice g_audio; static MeDdrDevice g_ddr; static MeClockDevice g_clocks; static MeVmeControlDevice g_vmeControl; @@ -550,4 +639,57 @@ MMIODevice *DdrDevice() { return &g_ddr; } +MMIODevice *AudioDevice() { + return &g_audio; +} + +void AudioUpdate(s64 now) { + MeCore::MEState::Audio &a = g_meState->audio; + const bool playing = MeAudioDevice::Playing(a); + while (playing && a.fifoCount > 0 && a.nextPlayed <= now) { + if (g_audioPlayed.size() - g_audioPlayedRead < AUDIO_MIX_MAX * 2) + g_audioPlayed.push_back(a.fifo[0]); + memmove(a.fifo, a.fifo + 1, (a.fifoCount - 1) * sizeof(u32)); + a.fifoCount--; + a.nextPlayed += MeAudioDevice::SamplePeriod(); + // The sample leaving the FIFO still has to play: it ran dry a sample later. + a.lastPlayed = a.nextPlayed; + } + if (playing && a.fifoCount == 0 && a.lastPlayed <= now) + a.underrun = true; + if (playing && a.fifoCount < MeAudioDevice::FIFO_DEPTH) + g_meState->intcRaised |= MeAudioDevice::INTR_AUDIO; + else + g_meState->intcRaised &= ~MeAudioDevice::INTR_AUDIO; +} + +s64 AudioNextEvent() { + const MeCore::MEState::Audio &a = g_meState->audio; + return MeAudioDevice::Playing(a) && a.fifoCount == MeAudioDevice::FIFO_DEPTH ? a.nextPlayed : INT64_MAX; +} + +void AudioMix(s32 *mix, int count) { + size_t queued = g_audioPlayed.size() - g_audioPlayedRead; + if (!g_audioMixPrimed) { + if (queued < AUDIO_MIX_PRIME) + return; + g_audioMixPrimed = true; + } + if (queued > AUDIO_MIX_MAX) { + g_audioPlayedRead = g_audioPlayed.size() - AUDIO_MIX_PRIME; + queued = AUDIO_MIX_PRIME; + } + if (queued < (size_t)count) + g_audioMixPrimed = false; + for (int i = 0; i < count && g_audioPlayedRead < g_audioPlayed.size(); i++) { + const u32 sample = g_audioPlayed[g_audioPlayedRead++]; + mix[i * 2] += (s16)(sample & 0xFFFF); + mix[i * 2 + 1] += (s16)(sample >> 16); + } + if (g_audioPlayedRead > AUDIO_MIX_MAX * 4) { + g_audioPlayed.erase(g_audioPlayed.begin(), g_audioPlayed.begin() + g_audioPlayedRead); + g_audioPlayedRead = 0; + } +} + } // namespace MeMMIO diff --git a/Core/HW/MeMMIO.h b/Core/HW/MeMMIO.h index 56a47ebff8..e4d3555de8 100644 --- a/Core/HW/MeMMIO.h +++ b/Core/HW/MeMMIO.h @@ -38,5 +38,13 @@ MMIODevice *VmeControlDevice(); MMIODevice *MemProtectDevice(); // The DDR controller's flush register (0xBD000004), always done. MMIODevice *DdrDevice(); +// The audio block (0xBE000000), which the ME feeds in a POPS boot. +MMIODevice *AudioDevice(); +// Plays what's due by the ME's clock, and sets the ME's audio interrupt level. +void AudioUpdate(s64 now); +// When a full FIFO next has room, in ME ticks, or INT64_MAX. +s64 AudioNextEvent(); +// Adds up to count of the samples it played to a stereo mix. +void AudioMix(s32 *mix, int count); } // namespace MeMMIO diff --git a/Core/MIPS/MeCore.cpp b/Core/MIPS/MeCore.cpp index b4dd5d00e8..367dd299a5 100644 --- a/Core/MIPS/MeCore.cpp +++ b/Core/MIPS/MeCore.cpp @@ -30,6 +30,7 @@ #include "Core/HLE/HLE.h" #include "Core/HLE/sceKernelInterrupt.h" #include "Core/HLE/sceKernelMemory.h" +#include "Core/HW/MeMMIO.h" #include "Core/MemMap.h" #include "Core/System.h" #include "Core/MIPS/MeCore.h" @@ -320,6 +321,12 @@ void DoState(PointerWrap &p) { Do(p, me->sysreg70); Do(p, me->booting); DoArray(p, me->sysregOther, ARRAY_SIZE(me->sysregOther)); + DoArray(p, me->audio.regs, ARRAY_SIZE(me->audio.regs)); + DoArray(p, me->audio.fifo, ARRAY_SIZE(me->audio.fifo)); + Do(p, me->audio.fifoCount); + Do(p, me->audio.underrun); + Do(p, me->audio.lastPlayed); + Do(p, me->audio.nextPlayed); DoArray(p, me->memProtect, ARRAY_SIZE(me->memProtect)); DoArray(p, me->vmeControl, ARRAY_SIZE(me->vmeControl)); DoArray(p, me->clocks, ARRAY_SIZE(me->clocks)); @@ -515,10 +522,16 @@ void RunFor(int cycles) { // The main CPU may have given a waiting loop what it waits for. me->waiting = false; while (cpu.downcount >= 0 && coreState == CORE_RUNNING_CPU) { + MeMMIO::AudioUpdate(GetTicks()); CheckInterrupts(me); if (!me->halted && !me->waiting) me->waiting = WaitsForMainCPU(me); - if (me->halted || me->waiting) { + if (me->halted) { + // Asleep until the audio FIFO next has room, if that's within this slice: POPS's ME + // loop sleeps for each sample it plays. + const s64 wake = MeMMIO::AudioNextEvent(); + cpu.downcount = wake <= me->tickBase ? (int)(me->tickBase - wake) : -1; + } else if (me->waiting) { cpu.downcount = -1; } else { // Run a part, with the rest set aside so the clock stays right. diff --git a/Core/MIPS/MeCore.h b/Core/MIPS/MeCore.h index 9048f1a5f1..7fa72971ae 100644 --- a/Core/MIPS/MeCore.h +++ b/Core/MIPS/MeCore.h @@ -76,6 +76,21 @@ struct MEState { u32 channels[4][5]{}; // 0x180-0x1FF: source, destination, next LLI, control, configuration } dmacplus; + // The audio block (0xBE000000), which the ME feeds directly in a POPS boot: a FIFO of stereo + // samples (0x70) that plays at 44.1kHz. Only what POPS's ME loop uses. + struct Audio { + u32 regs[0x20]{}; + u32 fifo[24]{}; + u32 fifoCount = 0; + // It played out its last sample. Status 0x28 bit 1, until the next one is written. + bool underrun = false; + // When the last sample to leave the FIFO has played, if it's empty. INT64_MAX until one has + // left it since the last write. + s64 lastPlayed = INT64_MAX; + // When the oldest sample in the FIFO has played, in ME ticks. + s64 nextPlayed = 0; + } audio; + // The sysevent handler list sceKernelReferSysEventHandler returns, in kernel memory. u32 sysEventHandlers = 0; From c1e04f05161c3e3aff3ffc2bb59e166dc16feebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 20 Oct 2026 23:59:14 +0200 Subject: [PATCH 5/7] pspautotests: me/audiofifo The audio block's FIFO, measured from the ME on a PSP-1000. In the next set: it needs the ME on, and Sony's ME firmware to hand the ME back to. Co-Authored-By: Claude Opus 5.5 (1M context) --- test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test.py b/test.py index e816220c14..730444c6ab 100755 --- a/test.py +++ b/test.py @@ -632,6 +632,7 @@ tests_next = [ "gpu/signals/simple", "gpu/simple/simple", "intr/waits", + "me/audiofifo", # needs the Media Engine on, and Sony's ME firmware installed "me/vram", # needs the Media Engine on, and Sony's ME firmware installed "sysmem/kernel/heapgrow", "io/file/file", From e906b9f7002c7ce7205a1f68733e71f9ccfa33f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 20 Oct 2026 23:59:14 +0200 Subject: [PATCH 6/7] CoreTiming: Catch the ME up when the main CPU reads the boot page The ME runs a slice behind the main CPU, so a main CPU polling a mailbox in the boot page saw the ME's progress up to a slice late. Reading the boot page now runs the ME up to where the main CPU is in its slice first, and the end of the slice runs only the rest. me/audiofifo, which times the ME's playing from the main CPU, read 43900Hz for 44100. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/CoreTiming.cpp | 16 +++++++++++++++- Core/CoreTiming.h | 3 +++ Core/HW/MMIO.cpp | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Core/CoreTiming.cpp b/Core/CoreTiming.cpp index 8b06f48617..b64b877c84 100644 --- a/Core/CoreTiming.cpp +++ b/Core/CoreTiming.cpp @@ -58,6 +58,8 @@ static Event *eventPool = 0; // Downcount has been moved to currentMIPS, to save a couple of clocks in every ARM JIT block // as we can already reach that structure through a register. int slicelength; +// How much of the current slice the other CPUs have already run (CatchUpOtherCPUs). +static int otherCPUsRan = 0; alignas(16) static s64 globalTimer; static s64 idledCycles; @@ -218,6 +220,7 @@ void UnregisterAllEvents() { void Init(MIPSState *mips) { mips->downcount = INITIAL_SLICE_LENGTH; slicelength = INITIAL_SLICE_LENGTH; + otherCPUsRan = 0; globalTimer = 0; idledCycles = 0; lastGlobalTimeTicks = 0; @@ -417,7 +420,16 @@ void ProcessEvents() { // The Media Engine catches up with the main CPU. static void RunOtherCPUs(int cycles) { if (MeCore::g_meState) { - MeCore::RunFor(cycles); + MeCore::RunFor(cycles - otherCPUsRan); + } + otherCPUsRan = 0; +} + +void CatchUpOtherCPUs(MIPSState *mips) { + const int ran = slicelength - mips->downcount; + if (MeCore::g_meState && ran > otherCPUsRan) { + MeCore::RunFor(ran - otherCPUsRan); + otherCPUsRan = ran; } } @@ -553,6 +565,8 @@ void DoState(PointerWrap &p) { auto s = p.Section("CoreTiming", 1, 3); if (!s) return; + // States are saved between slices. + otherCPUsRan = 0; int n = (int)event_types.size(); int current = n; diff --git a/Core/CoreTiming.h b/Core/CoreTiming.h index bc52e6cc38..0a4cf2870c 100644 --- a/Core/CoreTiming.h +++ b/Core/CoreTiming.h @@ -127,6 +127,9 @@ namespace CoreTiming { bool IsScheduled(int event_type); void Advance(MIPSState *mips); void ForceCheck(MIPSState *mips); + // Runs the other CPUs (the Media Engine) up to where this one is in the slice, as they otherwise + // run at its end: for when this one looks at what they share, such as the boot page. + void CatchUpOtherCPUs(MIPSState *mips); // Pretend that the main CPU has executed enough cycles to reach the next event. void Idle(MIPSState *mips, int maxIdle = 0); diff --git a/Core/HW/MMIO.cpp b/Core/HW/MMIO.cpp index 0aeb93e564..5e3dfa451d 100644 --- a/Core/HW/MMIO.cpp +++ b/Core/HW/MMIO.cpp @@ -21,6 +21,7 @@ #include "Common/Log.h" #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" +#include "Core/CoreTiming.h" #include "Core/HW/GpioMMIO.h" #include "Core/HW/MMIO.h" #include "Core/HW/MeMMIO.h" @@ -37,6 +38,9 @@ public: bool Read(MIPSState *mips, u32 offset, int size, u32 *value) override { if (offset + size > Memory::BOOT_PAGE_SIZE) return false; + // What the ME has written by now: a main CPU polling a mailbox here sees it as on a PSP. + if (mips == currentMIPS) + CoreTiming::CatchUpOtherCPUs(mips); const u8 *ptr = Memory::GetBootPage() + offset; switch (size) { case 1: *value = *ptr; break; From 657e99fcbf74b024c22087fdd4e750b9eee9ced7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 21 Oct 2026 06:56:35 +0200 Subject: [PATCH 7/7] ME: Let kernel code on the main CPU reach RAM and VRAM through kseg1 Yabause-PSP's me.prx ORs 0xA0000000 onto its own addresses to share a buffer with the ME uncached, which is 0xA8xxxxxx for a module in kernel RAM. The main CPU's space has no views there, so it crashed. All of kernel segment 1 (0xA0000000-0xBFFFFFFF) now counts as MMIO, and a device below the hardware registers maps RAM and VRAM onto their uncached mirrors at 0x40000000. The main CPU's native kernel loads and stores send the whole segment to that path; the ME keeps 0xBC000000 as the boundary, since its space maps the rest. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HW/MMIO.cpp | 38 ++++++++++++++++++++++++ Core/MIPS/ARM64/Arm64IRCompLoadStore.cpp | 12 ++++++-- Core/MIPS/x86/X64IRCompLoadStore.cpp | 5 ++-- Core/MemMap.h | 6 ++-- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/Core/HW/MMIO.cpp b/Core/HW/MMIO.cpp index 5e3dfa451d..d2e06770a1 100644 --- a/Core/HW/MMIO.cpp +++ b/Core/HW/MMIO.cpp @@ -91,6 +91,43 @@ public: static BootPageDevice g_bootPage; +// Kernel code reaches RAM and VRAM uncached at 0xA8000000 and 0xA4000000 (Yabause's me.prx does, by +// ORing 0xA0000000 onto its own addresses). The main CPU's space has no views there, so it goes +// through the uncached mirrors at 0x40000000 instead. The Media Engine's space maps them. +class KernelUncachedDevice : public MMIODevice { +public: + KernelUncachedDevice() : MMIODevice("kseg1 memory", 0xA0000000, 0x1C000000) {} + + bool Read(MIPSState *mips, u32 offset, int size, u32 *value) override { + const u32 address = 0x40000000 + offset; + if ((mips && mips->IsMediaEngine()) || !Memory::IsValidRange(address, size)) + return false; + switch (size) { + case 1: *value = Memory::ReadUnchecked_U8(address); break; + case 2: *value = Memory::ReadUnchecked_U16(address); break; + default: *value = Memory::ReadUnchecked_U32(address); break; + } + return true; + } + + bool Write(MIPSState *mips, u32 offset, int size, u32 value) override { + const u32 address = 0x40000000 + offset; + if ((mips && mips->IsMediaEngine()) || !Memory::IsValidRange(address, size)) + return false; + switch (size) { + case 1: Memory::WriteUnchecked_U8((u8)value, address); break; + case 2: Memory::WriteUnchecked_U16((u16)value, address); break; + default: Memory::WriteUnchecked_U32(value, address); break; + } + return true; + } + + void Reset() override {} + void DoState(PointerWrap &p) override {} +}; + +static KernelUncachedDevice g_kernelUncached; + static std::vector g_devices; void Init() { @@ -106,6 +143,7 @@ void Init() { MeMMIO::ClockDevice(), MeMMIO::DdrDevice(), MeMMIO::AudioDevice(), + &g_kernelUncached, }; for (MMIODevice *device : g_devices) { device->Reset(); diff --git a/Core/MIPS/ARM64/Arm64IRCompLoadStore.cpp b/Core/MIPS/ARM64/Arm64IRCompLoadStore.cpp index 0301f21d20..3ba1d2f2ce 100644 --- a/Core/MIPS/ARM64/Arm64IRCompLoadStore.cpp +++ b/Core/MIPS/ARM64/Arm64IRCompLoadStore.cpp @@ -411,7 +411,8 @@ void Arm64JitBackend::CompIR_LoadShift(IRInst inst) { } // Memory directly, or through the fixed code for a hardware register (0xBC000000-0xBFFFFFFF), where -// the address may also be one the CPU has no memory at. +// the address may also be one the CPU has no memory at. The main CPU has no views anywhere in kernel +// segment 1 (0xA0000000-0xBFFFFFFF), so all of it goes there (see Memory::IsMMIOAccess). void Arm64JitBackend::CompIR_KernelLoadStore(IRInst inst) { CONDITIONAL_DISABLE; @@ -420,8 +421,13 @@ void Arm64JitBackend::CompIR_KernelLoadStore(IRInst inst) { ADDI2R(SCRATCH1, regs_.R(inst.src1), inst.constant, SCRATCH2); const ARM64Reg reg = isStore ? regs_.R(inst.src3) : regs_.R(inst.dest); - LSR(SCRATCH2, SCRATCH1, 26); - CMPI2R(SCRATCH2, 0xBC000000 >> 26); + if (mips_->IsMediaEngine()) { + LSR(SCRATCH2, SCRATCH1, 26); + CMPI2R(SCRATCH2, 0xBC000000 >> 26); + } else { + LSR(SCRATCH2, SCRATCH1, 29); + CMPI2R(SCRATCH2, 0xA0000000 >> 29); + } FixupBranch memory = B(CC_NEQ); if (isStore) MOV(SCRATCH2, reg); diff --git a/Core/MIPS/x86/X64IRCompLoadStore.cpp b/Core/MIPS/x86/X64IRCompLoadStore.cpp index 61d2842834..e1d1a7bb2a 100644 --- a/Core/MIPS/x86/X64IRCompLoadStore.cpp +++ b/Core/MIPS/x86/X64IRCompLoadStore.cpp @@ -89,7 +89,8 @@ Gen::OpArg X64JitBackend::PrepareSrc1Address(IRInst inst) { #if PPSSPP_ARCH(AMD64) // Memory directly, or through the fixed code for a hardware register (0xBC000000-0xBFFFFFFF), where -// the address may also be one the CPU has no memory at. +// the address may also be one the CPU has no memory at. The main CPU has no views anywhere in kernel +// segment 1 (0xA0000000-0xBFFFFFFF), so all of it goes there (see Memory::IsMMIOAccess). void X64JitBackend::CompIR_KernelLoadStore(IRInst inst) { CONDITIONAL_DISABLE; @@ -98,7 +99,7 @@ void X64JitBackend::CompIR_KernelLoadStore(IRInst inst) { LEA(32, SCRATCH1, MDisp(regs_.RX(inst.src1), inst.constant)); const X64Reg reg = isStore ? regs_.RX(inst.src3) : regs_.RX(inst.dest); - CMP(32, R(SCRATCH1), Imm32(0xBC000000)); + CMP(32, R(SCRATCH1), Imm32(mips_->IsMediaEngine() ? 0xBC000000 : 0xA0000000)); FixupBranch below = J_CC(CC_B); CMP(32, R(SCRATCH1), Imm32(0xC0000000)); FixupBranch above = J_CC(CC_AE); diff --git a/Core/MemMap.h b/Core/MemMap.h index be2bea54a2..63307b937c 100644 --- a/Core/MemMap.h +++ b/Core/MemMap.h @@ -409,9 +409,11 @@ inline bool IsDepthTexVRAMAddress(const u32 address) { return ((address & 0xBFE00000) == 0x04200000) || ((address & 0xBFE00000) == 0x04600000); } -// TODO: To re-evaluate. +// Kernel segment 1 (0xA0000000-0xBFFFFFFF): the hardware registers from 0xBC000000, and below them +// uncached RAM and VRAM, which only the Media Engine's space maps. Kernel code reaches the rest +// through MMIO::Read/Write. inline bool IsMMIOAccess(const u32 address) { - return ((address & 0xFC000000) == 0xBC000000); + return (address & 0xE0000000) == 0xA0000000; } // 0x08000000 -> 0x08800000