From c953814fe0059b43e88aa8394b3965b76d7ffb92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 26 Feb 2026 09:40:32 +0100 Subject: [PATCH 1/7] ScrollView: Make it easier to press-and-hold buttons without cancelling --- Common/Input/GestureDetector.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Common/Input/GestureDetector.cpp b/Common/Input/GestureDetector.cpp index a8b4a0f23a..0e2643a726 100644 --- a/Common/Input/GestureDetector.cpp +++ b/Common/Input/GestureDetector.cpp @@ -46,10 +46,12 @@ TouchInput GestureDetector::Update(const TouchInput &touch, const Bounds &bounds p.lastY = touch.y; } + const float dragThreshold = 5.0f / g_display.dpi_scale_y; + if (p.distanceY > p.distanceX) { if (p.down) { double timeDown = time_now_d() - p.downTime; - if (!p.active && p.distanceY * timeDown > 3) { + if (!p.active && p.distanceY > dragThreshold) { p.active |= GESTURE_DRAG_VERTICAL; // Kill the drag. TODO: Only cancel the drag in one direction. TouchInput inp2 = touch; @@ -64,7 +66,7 @@ TouchInput GestureDetector::Update(const TouchInput &touch, const Bounds &bounds if (p.distanceX > p.distanceY) { if (p.down) { double timeDown = time_now_d() - p.downTime; - if (!p.active && p.distanceX * timeDown > 3) { + if (!p.active && p.distanceX > dragThreshold) { p.active |= GESTURE_DRAG_HORIZONTAL; // Kill the drag. TODO: Only cancel the drag in one direction. TouchInput inp2 = touch; From 749e3a02cb11a44f4fea2a6494df53c50996207b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 26 Feb 2026 09:58:30 +0100 Subject: [PATCH 2/7] Jit: Add missing instruction address check from ARM jits --- Core/MIPS/ARM/ArmJit.cpp | 8 +++++--- Core/MIPS/ARM64/Arm64Jit.cpp | 7 ++++++- Core/MIPS/x86/Jit.cpp | 9 +++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Core/MIPS/ARM/ArmJit.cpp b/Core/MIPS/ARM/ArmJit.cpp index 05dee95731..2940957932 100644 --- a/Core/MIPS/ARM/ArmJit.cpp +++ b/Core/MIPS/ARM/ArmJit.cpp @@ -100,8 +100,7 @@ static u32 JitMemCheck(u32 pc) { return coreState == CORE_RUNNING_CPU || coreState == CORE_NEXTFRAME ? 0 : 1; } -namespace MIPSComp -{ +namespace MIPSComp { using namespace ArmGen; using namespace ArmJitConstants; @@ -244,7 +243,10 @@ void ArmJit::CompileDelaySlot(int flags) { void ArmJit::Compile(u32 em_address) { PROFILE_THIS_SCOPE("jitc"); - // INFO_LOG(Log::JIT, "Compiling at %08x", em_address); + if (!Memory::IsValid4AlignedAddress(em_address)) { + Core_ExecException(em_address, em_address, ExecExceptionType::JUMP); + return; + } if (GetSpaceLeft() < 0x10000 || blocks.IsFull()) { ClearCache(); diff --git a/Core/MIPS/ARM64/Arm64Jit.cpp b/Core/MIPS/ARM64/Arm64Jit.cpp index 5f79cb9b47..4b152d28c8 100644 --- a/Core/MIPS/ARM64/Arm64Jit.cpp +++ b/Core/MIPS/ARM64/Arm64Jit.cpp @@ -242,6 +242,12 @@ void Arm64Jit::CompileDelaySlot(int flags) { void Arm64Jit::Compile(u32 em_address) { PROFILE_THIS_SCOPE("jitc"); + + if (!Memory::IsValid4AlignedAddress(em_address)) { + Core_ExecException(em_address, em_address, ExecExceptionType::JUMP); + return; + } + if (GetSpaceLeft() < 0x10000 || blocks.IsFull()) { INFO_LOG(Log::JIT, "Space left: %d", (int)GetSpaceLeft()); ClearCache(); @@ -249,7 +255,6 @@ void Arm64Jit::Compile(u32 em_address) { BeginWrite(JitBlockCache::MAX_BLOCK_INSTRUCTIONS * 16); - // To debug really wacky JIT issues, it can be a good idea to bisect the block number, // and disable parts of the jit using jo.disableFlags for certain ranges of blocks. diff --git a/Core/MIPS/x86/Jit.cpp b/Core/MIPS/x86/Jit.cpp index 9e6ca8c89d..6fddefdc98 100644 --- a/Core/MIPS/x86/Jit.cpp +++ b/Core/MIPS/x86/Jit.cpp @@ -300,15 +300,16 @@ void Jit::EatInstruction(MIPSOpcode op) { void Jit::Compile(u32 em_address) { PROFILE_THIS_SCOPE("jitc"); - if (GetSpaceLeft() < 0x10000 || blocks.IsFull()) { - ClearCache(); - } - if (!Memory::IsValidAddress(em_address) || (em_address & 3) != 0) { + if (!Memory::IsValid4AlignedAddress(em_address)) { Core_ExecException(em_address, em_address, ExecExceptionType::JUMP); return; } + if (GetSpaceLeft() < 0x10000 || blocks.IsFull()) { + ClearCache(); + } + // Sometimes we compile fairly large blocks, although it's uncommon. BeginWrite(JitBlockCache::MAX_BLOCK_INSTRUCTIONS * 16); From afb005e73896c518761d7d8e760deb460ce5c00b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 26 Feb 2026 10:02:41 +0100 Subject: [PATCH 3/7] Add a sanity check in VPL --- Core/HLE/sceKernelMemory.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Core/HLE/sceKernelMemory.cpp b/Core/HLE/sceKernelMemory.cpp index 0a90988006..babb5dcca9 100644 --- a/Core/HLE/sceKernelMemory.cpp +++ b/Core/HLE/sceKernelMemory.cpp @@ -132,6 +132,10 @@ void SceKernelVplHeader::Init(u32 ptr, u32 size) { u32 SceKernelVplHeader::Allocate(u32 size) { u32 allocBlocks = ((size + 7) / 8) + 1; + if (!nextFreeBlock_.IsValid()) { + ERROR_LOG(Log::sceKernel, "VPL: nextFreeBlock invalid."); + return (u32)-1; + } auto prev = nextFreeBlock_; do { auto b = prev->next; From e5a226a309ede207b1f1c930726fc022aaa083fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 26 Feb 2026 10:16:25 +0100 Subject: [PATCH 4/7] Correct bugs in CSO reader --- Core/FileSystems/BlockDevices.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Core/FileSystems/BlockDevices.cpp b/Core/FileSystems/BlockDevices.cpp index 0fef56da50..43a6b50042 100644 --- a/Core/FileSystems/BlockDevices.cpp +++ b/Core/FileSystems/BlockDevices.cpp @@ -154,6 +154,7 @@ CISOFileBlockDevice::CISOFileBlockDevice(FileLoader *fileLoader) : BlockDevice(fileLoader) { // CISO format is fairly simple, but most tools do not write the header_size. + // NOTE: CSOv2 isn't actually a thing. It was partially implemented in maxcso but it has never been in active use. CISO_H hdr; size_t readSize = fileLoader->ReadAt(0, sizeof(CISO_H), 1, &hdr); @@ -322,7 +323,7 @@ bool CISOFileBlockDevice::ReadBlocks(u32 minBlock, int count, u8 *outPtr) { } const u32 lastBlock = std::min(minBlock + count, numBlocks) - 1; - const u32 missingBlocks = (lastBlock + 1 - minBlock) - count; + const u32 missingBlocks = count - (lastBlock + 1 - minBlock); if (lastBlock < minBlock + count) { memset(outPtr + GetBlockSize() * (count - missingBlocks), 0, GetBlockSize() * missingBlocks); } @@ -367,7 +368,12 @@ bool CISOFileBlockDevice::ReadBlocks(u32 minBlock, int count, u8 *outPtr) { } u8 *rawBuffer = &readBuffer[frameReadPos - readBufferStart]; - const int plain = idx & 0x80000000; + bool plain = (idx & 0x80000000) != 0; + if (ver_ >= 2) { + // CSO v2+ requires blocks be uncompressed if large enough to be. High bit means other things. + plain = frameReadSize >= frameSize; + } + if (plain) { memcpy(outPtr, rawBuffer + frameBlockOffset * GetBlockSize(), frameBlocks * GetBlockSize()); } else { From a03e112a8083c379dbeab3bb4c1b799afab75687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 26 Feb 2026 10:18:19 +0100 Subject: [PATCH 5/7] Android: Avoid a few string copies --- android/jni/app-android.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index d511aa5554..372afef0d4 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -311,7 +311,7 @@ static void EmuThreadFunc() { ERROR_LOG(Log::System, "No activity, clearing commands"); while (!frameCommands.empty()) frameCommands.pop(); - return; + break; } // Still under lock here. ProcessFrameCommands(env); @@ -1635,9 +1635,7 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_pushCameraImageAndroid( // Call this under frameCommandLock. static void ProcessFrameCommands(JNIEnv *env) { while (!frameCommands.empty()) { - FrameCommand frameCmd; - frameCmd = frameCommands.front(); - frameCommands.pop(); + const FrameCommand &frameCmd = frameCommands.front(); DEBUG_LOG(Log::System, "frameCommand '%s' '%s'", frameCmd.command.c_str(), frameCmd.params.c_str()); @@ -1646,6 +1644,8 @@ static void ProcessFrameCommands(JNIEnv *env) { env->CallVoidMethod(ppssppActivity, postCommand, cmd, param); env->DeleteLocalRef(cmd); env->DeleteLocalRef(param); + + frameCommands.pop(); } } From bae1a7c7f93fa3a65d74558322ccd97265535ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 26 Feb 2026 10:33:18 +0100 Subject: [PATCH 6/7] Add "FastEmulatedGPU" compat flag. For games like Outrun that have intermittent slowdowns on hardware, it's unnecessary under emulation. --- Core/Compatibility.cpp | 1 + Core/Compatibility.h | 1 + GPU/GPUCommonHW.cpp | 5 ++++- assets/compat.ini | 7 +++++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Core/Compatibility.cpp b/Core/Compatibility.cpp index 528a7bb534..372b4c64e2 100644 --- a/Core/Compatibility.cpp +++ b/Core/Compatibility.cpp @@ -163,6 +163,7 @@ void Compatibility::CheckSettings(IniFile &iniFile, const std::string &gameID) { CheckSetting(iniFile, gameID, "BoostExactFramebufferMatch", &flags_.BoostExactFramebufferMatch); CheckSetting(iniFile, gameID, "PersistentFramebuffers", &flags_.PersistentFramebuffers); CheckSetting(iniFile, gameID, "FileCreatedTimeHack", &flags_.FileCreatedTimeHack); + CheckSetting(iniFile, gameID, "FastEmulatedGPU", &flags_.FastEmulatedGPU); } void Compatibility::CheckVRSettings(IniFile &iniFile, const std::string &gameID) { diff --git a/Core/Compatibility.h b/Core/Compatibility.h index 3c73cbbb6f..2485f07143 100644 --- a/Core/Compatibility.h +++ b/Core/Compatibility.h @@ -122,6 +122,7 @@ struct CompatFlags { bool BoostExactFramebufferMatch; bool PersistentFramebuffers; bool FileCreatedTimeHack; + bool FastEmulatedGPU; }; struct VRCompat { diff --git a/GPU/GPUCommonHW.cpp b/GPU/GPUCommonHW.cpp index 042fd3a16a..a0d7b57659 100644 --- a/GPU/GPUCommonHW.cpp +++ b/GPU/GPUCommonHW.cpp @@ -1290,7 +1290,10 @@ bail: int cycles = vertexCost_ * totalVertCount; gpuStats.vertexGPUCycles += cycles; - cyclesExecuted += cycles; + + if (!PSP_CoreParameter().compat.flags().FastEmulatedGPU) { + cyclesExecuted += cycles; + } } void GPUCommonHW::Execute_Bezier(u32 op, u32 diff) { diff --git a/assets/compat.ini b/assets/compat.ini index 9bb6c03868..e9141b65cd 100644 --- a/assets/compat.ini +++ b/assets/compat.ini @@ -2140,3 +2140,10 @@ NPUZ00006 = true # MJA 2 # Silent Hill: Shattered Memories does some strange checks of the file-created time, which won't pass on some platforms. See issue #13781 ULES01352 = true ULUS10450 = true + +[FastEmulatedGPU] +# Crude hack that makes the emulated GPU appear a lot faster, removing internal slowdowns in games. This makes Outrun stick to 60fps. +# Outrun 2006: Coast to Coast - issue #11358 (car reflections) +ULES00262 = true +ULUS10064 = true +ULKS46087 = true From 68a59e42cdc1105b2e52993de10865ea142f3ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 26 Feb 2026 10:44:21 +0100 Subject: [PATCH 7/7] Fix a few bugs in app-android.cpp --- android/jni/app-android.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 372afef0d4..8d030a838e 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -847,6 +847,7 @@ retry: INFO_LOG(Log::System, "Failed to initialize Vulkan, switching to OpenGL"); g_Config.iGPUBackend = (int)GPUBackend::OPENGL; SetGPUBackend(GPUBackend::OPENGL); + delete ctx; goto retry; } else { graphicsContext = ctx; @@ -1693,6 +1694,7 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_PpssppActivity_requestExitVulkanR } // TODO: Merge with the Win32 EmuThread and so on, and the Java EmuThread? +// This function must release the window reference. static void VulkanEmuThread(ANativeWindow *wnd) { SetCurrentThreadName("EmuThread"); @@ -1703,6 +1705,7 @@ static void VulkanEmuThread(ANativeWindow *wnd) { ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context."); renderLoopRunning = false; exitRenderLoop = false; + ANativeWindow_release(wnd); return; } @@ -1710,6 +1713,7 @@ static void VulkanEmuThread(ANativeWindow *wnd) { WARN_LOG(Log::G3D, "runVulkanRenderLoop: ExitRenderLoop requested at start, skipping the whole thing."); renderLoopRunning = false; exitRenderLoop = false; + ANativeWindow_release(wnd); return; } @@ -1729,6 +1733,7 @@ static void VulkanEmuThread(ANativeWindow *wnd) { delete graphicsContext; graphicsContext = nullptr; renderLoopRunning = false; + ANativeWindow_release(wnd); return; } @@ -1764,6 +1769,7 @@ static void VulkanEmuThread(ANativeWindow *wnd) { graphicsContext->ShutdownFromRenderThread(); renderLoopRunning = false; exitRenderLoop = false; + ANativeWindow_release(wnd); WARN_LOG(Log::G3D, "Render loop function exited."); } @@ -1798,15 +1804,15 @@ Java_org_ppsspp_ppsspp_ShortcutActivity_queryGameInfo(JNIEnv * env, jclass, jobj if (info) { INFO_LOG(Log::System, "GetInfo successful, waiting"); - // Wait for both name and icon - int attempts = 1000; + // Wait for both name and icon for a second. + int attempts = 100; while ((!info->Ready(GameInfoFlags::PARAM_SFO) || !info->Ready(GameInfoFlags::ICON)) && attempts > 0) { - sleep_ms(1, "info-icon-poll"); + sleep_ms(10, "info-icon-poll"); attempts--; } INFO_LOG(Log::System, "Done waiting"); - if (info->fileType != IdentifiedFileType::UNKNOWN) { + if (attempts > 0 && info->fileType != IdentifiedFileType::UNKNOWN) { // Get the game title gameName = info->GetTitle(); if (gameName.length() > strlen("The ") && startsWithNoCase(gameName, "The ")) {