mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user