LSFG: force FIFO presentation, and surface FSR in the in-game menu

★ Frame generation produced nothing on a MAILBOX swapchain, silently.

Reported on an Adreno 740: LSFG logged 'active: 1920x1080 x2 frames, 3.1p',
cached its 52 shaders, never logged a single failure — and both the FPS and the
LSFG display counters read 59. The interpolator was working perfectly and its
output was being thrown away.

MAILBOX keeps only the most recent image queued for a given refresh. Presenting
an interpolated frame and then the real frame immediately after replaces the
interpolated one, so it is generated, costs its full GPU time, and is never
displayed. IMMEDIATE discards the same way. Nothing errors anywhere along that
path, which is why the only symptom is a display rate identical to the real one.

The device landed on MAILBOX because vsync was off — 'Immediate not supported
for vsync-disabled, using mailbox'. SelectPresentMode now forces FIFO while
frame generation is enabled. Eden reached the same conclusion; their setting
text reads 'Forces FIFO presentation while enabled'.

Gated on the setting rather than GSLsfg::IsAvailable(), which cannot answer at
swapchain-creation time: the DLL path only reaches GSLsfg from EndPresent.

Also adds the FSR rows to the in-game GraphicsPane. In full settings FSR sits
under Display Effects beside CAS, which is the right shelf for a post-effect and
the wrong one for finding it — it is an upscaler, so in the quick menu it goes
with the internal-resolution controls, which is where you reach while watching
the framerate. Vulkan-only, so it is never a dead toggle on OpenGL.
This commit is contained in:
jpolo1224
2026-08-16 12:49:03 -04:00
parent bc268a7e83
commit 2f1a74c88a
2 changed files with 47 additions and 0 deletions
+21
View File
@@ -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:
@@ -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(