[Fix] (Magma): eglSwapInterval reaches the swapchain - 0 picks IMMEDIATE, else MAILBOX, else FIFO; >= 1 FIFO; < 0 FIFO_RELAXED, else FIFO; rebuilt after the next present when the mode changes (MC 26.3 vsync on: 190-290 fps -> 120)

This commit is contained in:
2026-09-29 05:51:18 -04:00
parent b1d60523e4
commit c60639db1d
12 changed files with 203 additions and 14 deletions
@@ -344,7 +344,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Any renderer instance this assignment replaces is destroyed here;
// fence/timer-query handles stamped with the old generation go stale.
BumpRendererGeneration();
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow);
VulkanRendererConfig config;
config.SwapInterval = GetRequestedSwapInterval();
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow, config);
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
pVulkanRenderer->Initialize();
return true;
@@ -455,6 +457,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return BackendObject::SwapEGLBuffers(dpy, draw);
}
void BackendObject_DirectVulkan::SetEGLSwapInterval(Int interval) {
// Same lock as SwapEGLBuffers: the renderer reads the request in Present.
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
BackendObject::SetEGLSwapInterval(interval);
}
void BackendObject_DirectVulkan::ReleaseEGLSurface(EGLSurface surface) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
BackendObject::ReleaseEGLSurface(surface);
@@ -696,6 +704,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static Bool funcsTableInitialized = false;
if (!funcsTableInitialized) {
funcsTable.Present = Present;
funcsTable.SetSwapInterval = SetSwapInterval;
funcsTable.GL.DrawArrays = DrawArrays;
funcsTable.GL.DrawElements = DrawElements;
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
@@ -34,6 +34,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
void SetEGLSwapInterval(Int interval) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
@@ -1431,4 +1431,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer");
pVulkanRenderer->Present();
}
namespace {
Optional<Int> g_requestedSwapInterval;
} // namespace
void SetSwapInterval(Int interval) {
g_requestedSwapInterval = interval;
if (pVulkanRenderer) {
pVulkanRenderer->SetSwapInterval(interval);
}
}
Optional<Int> GetRequestedSwapInterval() {
return g_requestedSwapInterval;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -137,4 +137,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// only exist as vkCmdWriteTimestamp results); the frontend falls back.
Int64 GetGpuTimestampNs();
void Present();
// eglSwapInterval. Remembered across renderers: one created for a later window surface
// starts from the app's last request (GetRequestedSwapInterval) instead of the default.
void SetSwapInterval(Int interval);
Optional<Int> GetRequestedSwapInterval();
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -8,6 +8,8 @@
#include "SwapchainObject.h"
#include <span>
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_State/GLState/TextureState/TextureObject2D.h"
@@ -135,12 +137,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
VkPresentModeKHR SwapchainObject::ChooseSwapchainPresentMode(
const Vector<VkPresentModeKHR>& availablePresentModes) {
for (auto desiredPresentMode : s_desiredPresentModes) {
for (const auto& presentMode : availablePresentModes) {
if (presentMode == desiredPresentMode) {
return presentMode;
}
const Vector<VkPresentModeKHR>& availablePresentModes, Optional<Int> swapInterval) {
static constexpr VkPresentModeKHR s_unthrottledPresentModes[]{
VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR};
static constexpr VkPresentModeKHR s_vsyncPresentModes[]{VK_PRESENT_MODE_FIFO_KHR};
static constexpr VkPresentModeKHR s_adaptiveVsyncPresentModes[]{VK_PRESENT_MODE_FIFO_RELAXED_KHR,
VK_PRESENT_MODE_FIFO_KHR};
std::span<const VkPresentModeKHR> ranked = s_desiredPresentModes;
if (swapInterval) {
if (*swapInterval == 0) {
ranked = s_unthrottledPresentModes;
} else if (*swapInterval > 0) {
ranked = s_vsyncPresentModes;
} else {
ranked = s_adaptiveVsyncPresentModes;
}
}
for (auto desiredPresentMode : ranked) {
if (std::find(availablePresentModes.begin(), availablePresentModes.end(), desiredPresentMode) !=
availablePresentModes.end()) {
return desiredPresentMode;
}
}
@@ -148,9 +164,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return availablePresentModes[0];
}
const char* SwapchainObject::GetPresentModeName(VkPresentModeKHR presentMode) {
return string_VkPresentModeKHR(presentMode);
}
void SwapchainObject::Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface,
Uint32 graphicsQueueFamily, Uint32 presentQueueFamily, Uint32 minImageCountHint,
VkExtent2D desiredExtent) {
VkExtent2D desiredExtent, Optional<Int> swapInterval) {
const auto swapchainCapabilities = GetSwapchainCapabilities(physicalDevice, surface);
MOBILEGL_ASSERT(swapchainCapabilities.IsComplete(),
"SwapchainObject::Create failed: incomplete swapchain capabilities");
@@ -169,8 +189,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
}
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes, swapInterval);
MGLOG_I("Picked present mode: %s", string_VkPresentModeKHR(presentMode));
m_presentMode = presentMode;
m_supportedPresentModes = swapchainCapabilities.presentModes;
const auto& swapchainCaps = swapchainCapabilities.capabilities;
Uint32 targetImageCount = std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount);
@@ -26,13 +26,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static SwapchainCapabilities GetSwapchainCapabilities(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface);
static VkSurfaceFormatKHR ChooseSwapchainSurfaceFormat(const Vector<VkSurfaceFormatKHR>& availableFormats);
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes);
// swapInterval is the app's eglSwapInterval, empty until it asks (s_desiredPresentModes
// ranks the modes then). 0 wants IMMEDIATE, else MAILBOX, else FIFO; >= 1 wants FIFO,
// which cannot skip vblanks, so 2 and up pace like 1; a negative interval (adaptive
// vsync) wants FIFO_RELAXED, else FIFO.
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes,
Optional<Int> swapInterval = Nullopt);
static const char* GetPresentModeName(VkPresentModeKHR presentMode);
void Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, Uint32 graphicsQueueFamily,
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent);
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent,
Optional<Int> swapInterval);
void Shutdown(VkDevice device);
VkSwapchainKHR GetHandle() const { return m_swapchain; }
VkPresentModeKHR GetPresentMode() const { return m_presentMode; }
// The surface's present modes as queried when this swapchain was created.
const Vector<VkPresentModeKHR>& GetSupportedPresentModes() const { return m_supportedPresentModes; }
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
VkExtent2D GetExtent() const { return m_extent; }
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
@@ -79,6 +89,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
VkPresentModeKHR m_presentMode = VK_PRESENT_MODE_FIFO_KHR;
Vector<VkPresentModeKHR> m_supportedPresentModes;
VkSurfaceFormatKHR m_surfaceFormat{};
VkExtent2D m_extent{};
VkExtent2D m_surfaceExtent{};
@@ -2980,7 +2980,10 @@ void main() {
}
VulkanRenderer::VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg)
: m_window(window), m_config(cfg) {
: m_window(window), m_config(cfg), m_presentsToAppWindow(window != NativeWindowType{}) {
if (!m_presentsToAppWindow) {
m_config.SwapInterval.reset();
}
// Initialize();
}
@@ -12957,8 +12960,22 @@ void main() {
if (!m_swapchainResizeRequested && SwapchainIsOutOfDate()) {
m_swapchainResizeRequested = true;
}
// A new eglSwapInterval lands here too, between frames: this one is on screen and the
// next has recorded nothing. Only a different present mode is worth the rebuild (it
// drops every pipeline); otherwise the live swapchain already paces as asked.
if (m_config.SwapInterval != m_swapchainSwapInterval) {
if (SwapchainObject::ChooseSwapchainPresentMode(m_swapchainObject.GetSupportedPresentModes(),
m_config.SwapInterval) !=
m_swapchainObject.GetPresentMode()) {
m_swapchainResizeRequested = true;
} else {
MGLOG_D("DirectVulkan: swap interval %d -> %s", *m_config.SwapInterval,
SwapchainObject::GetPresentModeName(m_swapchainObject.GetPresentMode()));
m_swapchainSwapInterval = m_config.SwapInterval;
}
}
if (m_swapchainResizeRequested) {
MGLOG_D("Present, processing requested swapchain resize");
MGLOG_D("Present, processing requested swapchain rebuild (resize or swap interval)");
if (!RecreateSwapchain()) {
m_presentSuspended = true;
m_swapchainResizeRequested = false;
@@ -14442,7 +14459,12 @@ void main() {
m_swapchainObject.Create(m_device, m_physicalDevice.handle, m_surface,
static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily),
static_cast<Uint32>(m_physicalDevice.queueFamilies.presentFamily),
m_config.MaxFramesInFlight, desiredExtent);
m_config.MaxFramesInFlight, desiredExtent, m_config.SwapInterval);
if (m_config.SwapInterval && m_config.SwapInterval != m_swapchainSwapInterval) {
MGLOG_D("DirectVulkan: swap interval %d -> %s", *m_config.SwapInterval,
SwapchainObject::GetPresentModeName(m_swapchainObject.GetPresentMode()));
}
m_swapchainSwapInterval = m_config.SwapInterval;
// The FragCoordYFlip variants bake this height in; it is the only input to a shader
// module that lives outside the GL program, so the factory has to learn it here (and on
// every recreation, which is the only way it can change).
@@ -14886,6 +14908,12 @@ void main() {
m_swapchainResizeRequested = true;
}
void VulkanRenderer::SetSwapInterval(Int interval) {
if (m_presentsToAppWindow) {
m_config.SwapInterval = interval;
}
}
VkInstance VulkanRenderer::GetInstance() const {
return m_instance;
}
@@ -348,6 +348,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples);
void RequestSwapchainResize(Uint32 width, Uint32 height);
// eglSwapInterval. Records the request; Present rebuilds the swapchain for it once the
// current frame is on screen, and only when it maps to a different present mode. A
// renderer created without an app window ignores it: its surface (headless, or an
// AImageReader nobody consumes) is never displayed, and FIFO there would block once
// the reader's queue fills.
void SetSwapInterval(Int interval);
// Re-query the surface and report whether the live swapchain no longer matches it
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
// rebuild, so a surface the driver merely considers suboptimal cannot thrash.
@@ -514,6 +520,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Shutdown() deletes it.
void* m_fallbackImageReader = nullptr;
VulkanRendererConfig m_config;
// Constructed for the app's window rather than a pbuffer (m_window is later filled in
// for the pbuffer fallbacks too, so it cannot tell them apart).
Bool m_presentsToAppWindow = false;
// The m_config.SwapInterval the live swapchain was built for; Present compares the two.
Optional<Int> m_swapchainSwapInterval;
Bool m_swapchainResizeRequested = false;
// Presentation is suspended while the window is zero-area (minimized): the
// swapchain is unusable/out of date, so Present drops frames instead of
@@ -22,6 +22,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 CacheVersion = MG_Config::CacheVersion;
Uint32 SurfaceWidth = 1;
Uint32 SurfaceHeight = 1;
// The app's eglSwapInterval; empty until it asks. Window renderers only - see
// VulkanRenderer::SetSwapInterval.
Optional<Int> SwapInterval;
Bool DisablePipelineCache = false;
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
Bool EnableValidationLayers = true;
+1
View File
@@ -85,6 +85,7 @@ add_subdirectory(VertexArray)
add_subdirectory(Program)
add_subdirectory(Query)
add_subdirectory(Pipeline)
add_subdirectory(Swapchain)
add_subdirectory(ShaderTranspiler)
add_subdirectory(Util)
add_subdirectory(SelfTest)
+26
View File
@@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.14)
add_executable(
SwapchainPresentModeTest
SwapchainPresentModeTest.cpp
)
target_include_directories(SwapchainPresentModeTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/xxHash
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
)
target_link_libraries(
SwapchainPresentModeTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
if (MSVC)
target_compile_options(SwapchainPresentModeTest PRIVATE /Zc:preprocessor)
endif()
include(GoogleTest)
gtest_discover_tests(SwapchainPresentModeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -0,0 +1,57 @@
// MobileGL - MobileGL/MG_Test/Swapchain/SwapchainPresentModeTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#include <MG_Backend/DirectVulkan/Renderer/SwapchainObject.h>
using namespace MobileGL;
using MobileGL::MG_Backend::DirectVulkan::SwapchainObject;
namespace {
// What each surface reports. Android's loader exposes MAILBOX and FIFO (plus the shared
// modes with VK_KHR_shared_presentable_image) but never IMMEDIATE; Mesa's X11 WSI has
// all four classic modes.
const Vector<VkPresentModeKHR> kAndroidModes{
VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_SHARED_DEMAND_REFRESH_KHR,
VK_PRESENT_MODE_SHARED_CONTINUOUS_REFRESH_KHR};
const Vector<VkPresentModeKHR> kAllClassicModes{VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR,
VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_FIFO_RELAXED_KHR};
const Vector<VkPresentModeKHR> kFifoOnly{VK_PRESENT_MODE_FIFO_KHR};
VkPresentModeKHR Choose(const Vector<VkPresentModeKHR>& modes, Optional<Int> swapInterval) {
return SwapchainObject::ChooseSwapchainPresentMode(modes, swapInterval);
}
} // namespace
// Before the app calls eglSwapInterval the backend keeps its own ranking (MAILBOX first).
TEST(SwapchainPresentMode, NoRequestKeepsTheDefaultRanking) {
EXPECT_EQ(Choose(kAllClassicModes, Nullopt), VK_PRESENT_MODE_MAILBOX_KHR);
EXPECT_EQ(Choose(kAndroidModes, Nullopt), VK_PRESENT_MODE_MAILBOX_KHR);
EXPECT_EQ(Choose(kFifoOnly, Nullopt), VK_PRESENT_MODE_FIFO_KHR);
}
TEST(SwapchainPresentMode, IntervalZeroPrefersImmediateThenMailboxThenFifo) {
EXPECT_EQ(Choose(kAllClassicModes, 0), VK_PRESENT_MODE_IMMEDIATE_KHR);
EXPECT_EQ(Choose(kAndroidModes, 0), VK_PRESENT_MODE_MAILBOX_KHR);
EXPECT_EQ(Choose({VK_PRESENT_MODE_FIFO_RELAXED_KHR, VK_PRESENT_MODE_FIFO_KHR}, 0), VK_PRESENT_MODE_FIFO_KHR);
EXPECT_EQ(Choose(kFifoOnly, 0), VK_PRESENT_MODE_FIFO_KHR);
}
TEST(SwapchainPresentMode, PositiveIntervalIsFifo) {
for (Int interval : {1, 2, 4}) {
EXPECT_EQ(Choose(kAllClassicModes, interval), VK_PRESENT_MODE_FIFO_KHR) << "interval " << interval;
EXPECT_EQ(Choose(kAndroidModes, interval), VK_PRESENT_MODE_FIFO_KHR) << "interval " << interval;
}
}
TEST(SwapchainPresentMode, NegativeIntervalIsFifoRelaxedWhenSupported) {
EXPECT_EQ(Choose(kAllClassicModes, -1), VK_PRESENT_MODE_FIFO_RELAXED_KHR);
EXPECT_EQ(Choose(kAndroidModes, -1), VK_PRESENT_MODE_FIFO_KHR);
EXPECT_EQ(Choose(kFifoOnly, -1), VK_PRESENT_MODE_FIFO_KHR);
}