LSFG: let the generated frame wait for a display slot; unhide FSR on renderer=auto

Two bugs of mine, both found on-device with everything else working.

★ The zero-timeout acquire disabled frame generation entirely.

Forcing FIFO fixed the MAILBOX discard, and the display rate still equalled the
real rate. Under FIFO the presentation engine returns an image at a vblank, so
at steady state nothing is ever free INSTANTLY — vkAcquireNextImageKHR with a
zero timeout returns VK_NOT_READY every frame, the loop breaks, and every
interpolated frame is dropped. Silently, because a dropped generated frame is a
legitimate outcome and nothing logs it.

The reasoning behind the zero was that an interpolated frame is a bonus not
worth stalling for. That is backwards: presenting two frames per rendered frame
MEANS waiting for the second display slot. Waiting is the mechanism, not the
cost. Now a 50ms bound — six vblanks at 120Hz, so it expires only when something
is genuinely wrong, while still keeping a lost surface from wedging the GS
thread the way an unbounded wait would.

★ The in-game FSR row was gated on renderer == "vulkan", and the default is
"auto".

"auto" resolves to Vulkan on Android, so the row was hidden from anyone who had
not explicitly pinned the renderer — which is nearly everyone, and was the
reporter. Gated on the two backends that genuinely cannot run it instead.
This commit is contained in:
jpolo1224
2026-08-16 13:08:05 -04:00
parent 2f1a74c88a
commit c246b9a03c
2 changed files with 20 additions and 3 deletions
+16 -2
View File
@@ -1208,8 +1208,22 @@ namespace GSLsfg
// serialised on a vblank per generated frame and ate the very time the feature
// exists to spend. VK_NOT_READY/VK_TIMEOUT leave the semaphore unsignalled, so
// s_acquire_sems[i] stays clean for the next frame.
const VkResult acq = vkAcquireNextImageKHR(s_device, swap_chain->GetSwapChain(), 0,
s_acquire_sems[i], VK_NULL_HANDLE, &image_index);
// ★ Bounded, but NOT zero. A zero timeout looks right — an interpolated frame is
// a bonus, so why stall for one — and it silently disables the entire feature:
// under FIFO the presentation engine hands an image back at a vblank, so at
// steady state nothing is EVER free instantly, every acquire returns
// VK_NOT_READY, and every generated frame is dropped. Observed exactly that on
// an Adreno 740: LSFG active, FIFO confirmed, display rate still equal to the
// real rate. Waiting for a display slot IS the mechanism here — presenting two
// frames per rendered frame means waiting for the second slot.
//
// The bound is what keeps a lost surface from wedging the GS thread the way an
// infinite wait would (see VKSwapChain::AcquireNextImage's own timeout, and the
// background/rotate/fold case it documents). Generous next to a refresh interval
// — 6 vblanks at 120Hz — so it only expires when something is actually wrong.
static constexpr u64 kGeneratedAcquireTimeoutNs = 50ull * 1000 * 1000;
const VkResult acq = vkAcquireNextImageKHR(s_device, swap_chain->GetSwapChain(),
kGeneratedAcquireTimeoutNs, s_acquire_sems[i], VK_NULL_HANDLE, &image_index);
if (acq != VK_SUCCESS && acq != VK_SUBOPTIMAL_KHR)
break; // nothing free, out of date, or lost — still present the real frame
@@ -823,7 +823,10 @@ private fun GraphicsPane(state: EmulationMenuUiState, viewModel: EmulationMenuVi
// 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") {
// "auto" is the DEFAULT and resolves to Vulkan on Android, so gating on the literal string
// "vulkan" hid this row from almost everyone — which is exactly what happened. Only OpenGL
// and software genuinely cannot run it.
if (settings.renderer != "opengl" && settings.renderer != "software") {
val fsr1On = settings.upscaler == com.armsx2.config.Settings.UPSCALER_FSR1
MenuSwitchRow(
str("renderer.fsr1.label"),