diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index e8ce9d1c..6966b05e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -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(nativeWindow); + VulkanRendererConfig config; + config.SwapInterval = GetRequestedSwapInterval(); + pVulkanRenderer = MakeUnique(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 lock(m_eglStateMutex); + BackendObject::SetEGLSwapInterval(interval); + } + void BackendObject_DirectVulkan::ReleaseEGLSurface(EGLSurface surface) { const std::lock_guard 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; diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h index 1c7ef15c..30a14d59 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h @@ -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; diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index f135a4aa..cae6e388 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -1431,4 +1431,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer"); pVulkanRenderer->Present(); } + + namespace { + Optional g_requestedSwapInterval; + } // namespace + + void SetSwapInterval(Int interval) { + g_requestedSwapInterval = interval; + if (pVulkanRenderer) { + pVulkanRenderer->SetSwapInterval(interval); + } + } + + Optional GetRequestedSwapInterval() { + return g_requestedSwapInterval; + } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h index 74241e81..28e3b464 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h @@ -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 GetRequestedSwapInterval(); } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp index a2743a16..e79f4088 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp @@ -8,6 +8,8 @@ #include "SwapchainObject.h" +#include + #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& availablePresentModes) { - for (auto desiredPresentMode : s_desiredPresentModes) { - for (const auto& presentMode : availablePresentModes) { - if (presentMode == desiredPresentMode) { - return presentMode; - } + const Vector& availablePresentModes, Optional 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 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 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(minImageCountHint, swapchainCaps.minImageCount); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h index c22f07bd..4c36f43e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h @@ -26,13 +26,23 @@ namespace MobileGL::MG_Backend::DirectVulkan { static SwapchainCapabilities GetSwapchainCapabilities(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface); static VkSurfaceFormatKHR ChooseSwapchainSurfaceFormat(const Vector& availableFormats); - static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector& 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& availablePresentModes, + Optional 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 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& 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 m_supportedPresentModes; VkSurfaceFormatKHR m_surfaceFormat{}; VkExtent2D m_extent{}; VkExtent2D m_surfaceExtent{}; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index aa602e83..b8369408 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -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(m_physicalDevice.queueFamilies.graphicsFamily), static_cast(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; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 42909bd2..cbcde43a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -348,6 +348,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool ResolveOcclusionQueryResult(const Vector& 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 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 diff --git a/MobileGL/MG_Backend/DirectVulkan/VulkanRendererConfig.h b/MobileGL/MG_Backend/DirectVulkan/VulkanRendererConfig.h index f7ed3ce4..27e6975d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/VulkanRendererConfig.h +++ b/MobileGL/MG_Backend/DirectVulkan/VulkanRendererConfig.h @@ -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 SwapInterval; Bool DisablePipelineCache = false; #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG Bool EnableValidationLayers = true; diff --git a/MobileGL/MG_Test/CMakeLists.txt b/MobileGL/MG_Test/CMakeLists.txt index 1fac40f3..394095de 100644 --- a/MobileGL/MG_Test/CMakeLists.txt +++ b/MobileGL/MG_Test/CMakeLists.txt @@ -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) diff --git a/MobileGL/MG_Test/Swapchain/CMakeLists.txt b/MobileGL/MG_Test/Swapchain/CMakeLists.txt new file mode 100644 index 00000000..4508be61 --- /dev/null +++ b/MobileGL/MG_Test/Swapchain/CMakeLists.txt @@ -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) diff --git a/MobileGL/MG_Test/Swapchain/SwapchainPresentModeTest.cpp b/MobileGL/MG_Test/Swapchain/SwapchainPresentModeTest.cpp new file mode 100644 index 00000000..46ff98ef --- /dev/null +++ b/MobileGL/MG_Test/Swapchain/SwapchainPresentModeTest.cpp @@ -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 + +#include + +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 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 kAllClassicModes{VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR, + VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_FIFO_RELAXED_KHR}; + const Vector kFifoOnly{VK_PRESENT_MODE_FIFO_KHR}; + + VkPresentModeKHR Choose(const Vector& modes, Optional 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); +}