From ea5e39c440b11b507232140956fc7df0161b8303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 22 Oct 2026 20:29:21 +0200 Subject: [PATCH 1/2] Android Vulkan: Bring back the stale surface extent workaround e32e558aab dropped it, assuming the stale extent came from our own resize race. A Redmi 9A (PowerVR, Android 11) shows it doesn't: after a rotation, vkGetPhysicalDeviceSurfaceCapabilitiesKHR returns the new transform with the old extent, even when queried after surfaceChanged and ApplyResize have given us the new size, and the wrong swapchain then stays until the next rotation. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/GPU/Vulkan/VulkanContext.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Common/GPU/Vulkan/VulkanContext.cpp b/Common/GPU/Vulkan/VulkanContext.cpp index 827737b195..d1e3cd2494 100644 --- a/Common/GPU/Vulkan/VulkanContext.cpp +++ b/Common/GPU/Vulkan/VulkanContext.cpp @@ -1551,6 +1551,19 @@ bool VulkanContext::InitSwapchain(VkPresentModeKHR desiredPresentMode) { } } +#if PPSSPP_PLATFORM(ANDROID) + // After an in-place rotation, the surface can report the new transform together with the previous + // orientation's extent. A Redmi 9A (PowerVR, Android 11) keeps doing it after surfaceChanged has given us the + // new size, so the swapchain stays wrong until the next rotation. The window size we were told about is + // current, so if the extent is exactly that transposed, it's stale. + if (g_display.pixel_xres > 0 && g_display.pixel_yres > 0 && g_display.pixel_xres != g_display.pixel_yres && + currentExtent.width == (uint32_t)g_display.pixel_yres && currentExtent.height == (uint32_t)g_display.pixel_xres) { + WARN_LOG(Log::G3D, "Surface extent %dx%d is the window transposed, probably stale after a rotation. Using %dx%d.", + currentExtent.width, currentExtent.height, g_display.pixel_xres, g_display.pixel_yres); + std::swap(currentExtent.width, currentExtent.height); + } +#endif + swapChainExtent_.width = clamp(currentExtent.width, surfCapabilities_.minImageExtent.width, surfCapabilities_.maxImageExtent.width); swapChainExtent_.height = clamp(currentExtent.height, surfCapabilities_.minImageExtent.height, surfCapabilities_.maxImageExtent.height); From f8e7efc8b08d0f16dd8ad3b54ad63188b3abe4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 22 Oct 2026 20:48:49 +0200 Subject: [PATCH 2/2] Android Vulkan: Check the surface extent against the latest window size g_display lags behind surfaceChanged until the render thread applies the resize, and a recreate after a suboptimal present can run in between. The check then swapped a correct extent for a frame. Android now sets the draw size callback to the latest size from backbufferResize, and the check uses that. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/GPU/Vulkan/VulkanContext.cpp | 12 +++++++----- Common/GPU/Vulkan/VulkanContext.h | 1 + android/jni/app-android.cpp | 11 +++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Common/GPU/Vulkan/VulkanContext.cpp b/Common/GPU/Vulkan/VulkanContext.cpp index d1e3cd2494..31bb8a46a4 100644 --- a/Common/GPU/Vulkan/VulkanContext.cpp +++ b/Common/GPU/Vulkan/VulkanContext.cpp @@ -1554,12 +1554,14 @@ bool VulkanContext::InitSwapchain(VkPresentModeKHR desiredPresentMode) { #if PPSSPP_PLATFORM(ANDROID) // After an in-place rotation, the surface can report the new transform together with the previous // orientation's extent. A Redmi 9A (PowerVR, Android 11) keeps doing it after surfaceChanged has given us the - // new size, so the swapchain stays wrong until the next rotation. The window size we were told about is - // current, so if the extent is exactly that transposed, it's stale. - if (g_display.pixel_xres > 0 && g_display.pixel_yres > 0 && g_display.pixel_xres != g_display.pixel_yres && - currentExtent.width == (uint32_t)g_display.pixel_yres && currentExtent.height == (uint32_t)g_display.pixel_xres) { + // new size, so the swapchain stays wrong until the next rotation. The latest window size from surfaceChanged + // is current (g_display can lag behind it, until the render thread applies the resize), so if the extent is + // exactly that transposed, it's stale. + const VkExtent2D windowSize = cbGetDrawSize_ ? cbGetDrawSize_() : VkExtent2D{}; + if (windowSize.width > 0 && windowSize.height > 0 && windowSize.width != windowSize.height && + currentExtent.width == windowSize.height && currentExtent.height == windowSize.width) { WARN_LOG(Log::G3D, "Surface extent %dx%d is the window transposed, probably stale after a rotation. Using %dx%d.", - currentExtent.width, currentExtent.height, g_display.pixel_xres, g_display.pixel_yres); + currentExtent.width, currentExtent.height, windowSize.width, windowSize.height); std::swap(currentExtent.width, currentExtent.height); } #endif diff --git a/Common/GPU/Vulkan/VulkanContext.h b/Common/GPU/Vulkan/VulkanContext.h index e00b878fdf..40b1c24090 100644 --- a/Common/GPU/Vulkan/VulkanContext.h +++ b/Common/GPU/Vulkan/VulkanContext.h @@ -522,6 +522,7 @@ private: // that we really don't want in everything that uses VulkanContext. void *winsysData1_ = nullptr; void *winsysData2_ = nullptr; + // The window's current size in pixels, from the platform. Optional. std::function cbGetDrawSize_; VkInstance instance_ = VK_NULL_HANDLE; diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 587140580b..1324aebeed 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -89,6 +89,7 @@ struct JNIEnv {}; #include "Common/VR/PPSSPPVR.h" #include "Common/GPU/Vulkan/VulkanLoader.h" #include "Common/GPU/GraphicsContext.h" +#include "Common/GPU/Vulkan/VulkanContext.h" #include "Common/GPU/Vulkan/VulkanGraphicsContext.h" #include "android/jni/AndroidEGLGraphicsContext.h" #include "Common/StringUtils.h" @@ -202,6 +203,10 @@ static void SetPermissionStatusFromJava(std::string_view name, PermissionStatus static GraphicsContext *graphicsContext; +// The latest surface size from backbufferResize, width in the high half, for Vulkan to check the surface's extent +// against. Atomic rather than under g_pendingResizeLock (below), since a resize applied directly can recreate the swapchain. +static std::atomic g_surfaceSize; + #if PPSSPP_ARCH(ARMV7) // Old Android workaround extern "C" { @@ -730,6 +735,10 @@ retry: delete ctx; goto retry; } + ((VulkanContext *)ctx->GetAPIContext())->SetCbGetDrawSize([]() { + const uint64_t size = g_surfaceSize; + return VkExtent2D{ (uint32_t)(size >> 32), (uint32_t)size }; + }); graphicsContext = ctx; // Now, we wait until we get a surface to continue initializing Vulkan. break; @@ -908,6 +917,8 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_backbufferResize(JNIEnv const bool rotated = lastRotation != -1 && rotation != lastRotation; lastRotation = rotation; + g_surfaceSize = ((uint64_t)(uint32_t)pixel_xres << 32) | (uint32_t)pixel_yres; + // pixel_*res is the backbuffer resolution. if (IsVREnabled()) { GetVRResolutionPerEye(&pixel_xres, &pixel_yres);