diff --git a/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp b/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp index e78eee1ed0..9e3c29ac44 100644 --- a/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp +++ b/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ +#include "GS/GS.h" // GSConfig, for the frame-generation FIFO override below #include "GS/Renderers/Vulkan/GSDeviceVK.h" #include "GS/Renderers/Vulkan/VKBuilders.h" #include "GS/Renderers/Vulkan/VKSwapChain.h" @@ -513,6 +514,26 @@ bool VKSwapChain::SelectPresentMode(VkSurfaceKHR surface, GSVSyncMode* vsync_mod return it != present_modes.end(); }; + // ★ Frame generation requires FIFO, and silently produces nothing without it. + // + // MAILBOX keeps only the most recent image queued for a given refresh: presenting an + // interpolated frame and then the real frame replaces the interpolated one, so it is + // generated, costs its full GPU time, and is never displayed. IMMEDIATE is no better — it + // tears the newest image in, discarding the same way. The failure is completely silent, + // because nothing errors: the interpolator succeeds, the present succeeds, and the display + // rate simply equals the real frame rate. Observed exactly that on an Adreno 740 with vsync + // off, which resolves to MAILBOX here — LSFG reported "active" and both counters read 59. + // + // Gated on the setting rather than GSLsfg::IsAvailable(), which cannot answer yet: the DLL + // path only reaches GSLsfg from EndPresent, which has not run when the swapchain is built. + // The user asking for frame generation is the intent worth honouring here. + if (GSConfig.LsfgEnabled && *vsync_mode != GSVSyncMode::FIFO) + { + WARNING_LOG("Frame generation is enabled; forcing FIFO presentation so interpolated " + "frames are actually displayed."); + *vsync_mode = GSVSyncMode::FIFO; + } + switch (*vsync_mode) { case GSVSyncMode::Disabled: diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/emulation/EmulationMenuScreen.kt b/platforms/android/app/src/main/java/com/armsx2/ui/emulation/EmulationMenuScreen.kt index db706d0d6b..e2234053b6 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/emulation/EmulationMenuScreen.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/emulation/EmulationMenuScreen.kt @@ -819,6 +819,32 @@ private fun GraphicsPane(state: EmulationMenuUiState, viewModel: EmulationMenuVi onReset = { viewModel.setUpscale(1.0f) }, onChange = { pct -> viewModel.setUpscale(pct / 100f) }, ) + // FSR sits with the resolution controls rather than the effects, because that is what it + // is: the two rows above choose how big the frame is RENDERED, and this chooses how it + // gets to the screen. In full settings it lives under Display Effects next to CAS, which + // is the wrong shelf for finding it while you are looking at the framerate. + if (settings.renderer == "vulkan") { + val fsr1On = settings.upscaler == com.armsx2.config.Settings.UPSCALER_FSR1 + MenuSwitchRow( + str("renderer.fsr1.label"), + fsr1On, + description = str("renderer.fsr1.description"), + ) { on -> + viewModel.updateSettings { + it.copy(upscaler = if (on) com.armsx2.config.Settings.UPSCALER_FSR1 else com.armsx2.config.Settings.UPSCALER_OFF) + } + } + if (fsr1On) { + com.armsx2.ui.settings.IntSliderRow( + label = str("renderer.fsr1.sharpness.label"), + value = settings.fsrSharpness.coerceIn(0, 100), + min = 0, + max = 100, + valueFormatter = { "$it%" }, + onChange = { pct -> viewModel.updateSettings { it.copy(fsrSharpness = pct) } }, + ) + } + } HorizontalOptions( title = str("renderer.displayMode.label"), options = listOf(