From 9f7f0fb8a29f19c3ceebaa0992d54f119514617d Mon Sep 17 00:00:00 2001 From: Solessfir <34831419+Solessfir@users.noreply.github.com> Date: Sun, 20 Sep 2026 19:03:11 +0200 Subject: [PATCH 1/8] Added Android Vulkan swapchain support --- Engine/CMakeLists.txt | 10 +++-- Engine/gapi/vulkan/vswapchain.cpp | 59 ++++++++++++++++++++++++++-- Engine/gapi/vulkan/vswapchain.h | 7 ++++ Engine/gapi/vulkanapi.cpp | 3 ++ Engine/system/api/androidapi.cpp | 11 ++---- Examples/Android/AndroidManifest.xml | 1 + Examples/Android/CMakeLists.txt | 3 +- Examples/Android/README.md | 8 ++-- Examples/Android/main.cpp | 39 +++++++++++++++++- 9 files changed, 119 insertions(+), 22 deletions(-) diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 35c2be93..3e99ba81 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -159,10 +159,12 @@ if(TEMPEST_BUILD_VULKAN) add_definitions(-DVULKAN_HPP_NO_EXCEPTIONS) add_definitions(-DVULKAN_HPP_NO_SMART_HANDLE) - if("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") - target_link_directories(${PROJECT_NAME} PRIVATE "$ENV{VULKAN_SDK}/lib") - else() - target_link_directories(${PROJECT_NAME} PRIVATE "$ENV{VULKAN_SDK}/Lib32") + if(NOT ANDROID) + if("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") + target_link_directories(${PROJECT_NAME} PRIVATE "$ENV{VULKAN_SDK}/lib") + else() + target_link_directories(${PROJECT_NAME} PRIVATE "$ENV{VULKAN_SDK}/Lib32") + endif() endif() target_include_directories(${PROJECT_NAME} PRIVATE "$ENV{VULKAN_SDK}/include") diff --git a/Engine/gapi/vulkan/vswapchain.cpp b/Engine/gapi/vulkan/vswapchain.cpp index 658ef4f3..b06d27f0 100644 --- a/Engine/gapi/vulkan/vswapchain.cpp +++ b/Engine/gapi/vulkan/vswapchain.cpp @@ -16,6 +16,9 @@ # define VK_USE_PLATFORM_WIN32_KHR # include # include +#elif defined(__ANDROID__) +# include +# include #elif defined(__UNIX__) # define VK_USE_PLATFORM_XLIB_KHR # include @@ -103,7 +106,8 @@ VSwapchain::FenceList::~FenceList() { } void VSwapchain::FenceList::waitAll() { - vkWaitForFences(dev, size, data.get(), VK_TRUE,std::numeric_limits::max()); + if(size>0) + vkWaitForFences(dev, size, data.get(), VK_TRUE,std::numeric_limits::max()); } @@ -153,12 +157,12 @@ VSwapchain::VSwapchain(VDevice &device, SystemApi::Window* hwnd) :device(device), hwnd(hwnd) { try { surface = createSurface(device.instance, hwnd); + createSwapchain(device); } catch(...) { cleanup(); throw; } - createSwapchain(device); } VSwapchain::~VSwapchain() { @@ -168,6 +172,11 @@ VSwapchain::~VSwapchain() { bool VSwapchain::checkPresentSupport(VkPhysicalDevice device, uint32_t queueFamilyIndex) { #if defined(__WINDOWS__) const bool presentSupport = vkGetPhysicalDeviceWin32PresentationSupportKHR(device, queueFamilyIndex)!=VK_FALSE; +#elif defined(__ANDROID__) + // All Android graphics queues support presentation. + (void)device; + (void)queueFamilyIndex; + const bool presentSupport = true; #elif defined(__UNIX__) bool presentSupport = false; if(auto dpy = reinterpret_cast(X11Api::display())){ @@ -209,16 +218,25 @@ void VSwapchain::cleanupSwapchain() noexcept { swapChain = VK_NULL_HANDLE; swapChainImageFormat = VK_FORMAT_UNDEFINED; swapChainExtent = {}; + imgIndex = 0; + frameId = 0; } void VSwapchain::cleanupSurface() noexcept { if(surface!=VK_NULL_HANDLE) vkDestroySurfaceKHR(device.instance,surface,nullptr); surface = VK_NULL_HANDLE; +#ifdef __ANDROID__ + nativeWindow = nullptr; +#endif } void VSwapchain::reset() { cleanupSwapchain(); +#ifdef __ANDROID__ + cleanupSurface(); + surface = createSurface(device.instance, hwnd); +#endif createSwapchain(device); } @@ -238,6 +256,16 @@ VkSurfaceKHR VSwapchain::createSurface(VkInstance instance, void* hwnd) { createInfo.hwnd = HWND(hwnd); if(vkCreateWin32SurfaceKHR(instance,&createInfo,nullptr,&ret)!=VK_SUCCESS) throw std::system_error(Tempest::GraphicsErrc::NoDevice); +#elif defined(__ANDROID__) + auto window = *reinterpret_cast(hwnd); + if(window==nullptr) + return VK_NULL_HANDLE; + VkAndroidSurfaceCreateInfoKHR createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; + createInfo.window = window; + vkAssert(vkCreateAndroidSurfaceKHR(instance,&createInfo,nullptr,&ret)); + // VkSurfaceKHR retains the native window until cleanupSurface(). + nativeWindow = window; #elif defined(__UNIX__) VkXlibSurfaceCreateInfoKHR createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR; @@ -267,6 +295,10 @@ void VSwapchain::createSwapchain(VDevice& device) { auto code = createSwapchain(device,support,rect,imgCount); if(isSwapchainLost(code)) { cleanupSwapchain(); +#ifdef __ANDROID__ + cleanupSurface(); + surface = createSurface(device.instance, hwnd); +#endif continue; } break; @@ -310,7 +342,13 @@ VkResult VSwapchain::createSwapchain(VDevice& device, const SwapChainSupport& sw } createInfo.preTransform = swapChainSupport.capabilities.currentTransform; - createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + for(auto alpha : {VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR, + VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR, VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR}) { + if((swapChainSupport.capabilities.supportedCompositeAlpha & alpha)!=0) { + createInfo.compositeAlpha = alpha; + break; + } + } createInfo.presentMode = presentMode; createInfo.clipped = VK_FALSE; @@ -423,11 +461,20 @@ uint32_t VSwapchain::findImageCount(const SwapChainSupport& support) const { } bool VSwapchain::isSwapchainLost(VkResult code) const { +#ifdef __ANDROID__ + if(code==VK_ERROR_SURFACE_LOST_KHR) + return true; +#endif if(code==VK_SUBOPTIMAL_KHR) { // WA for issues on some linux distros (https://github.com/Try/OpenGothic/issues/977) // probably would have to exclude currentTransform soon due to android VkSurfaceCapabilitiesKHR capabilities = {}; +#ifdef __ANDROID__ + if(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device.physicalDevice, surface, &capabilities)==VK_ERROR_SURFACE_LOST_KHR) + return true; +#else vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device.physicalDevice, surface, &capabilities); +#endif capabilities.currentExtent = findSwapExtent(capabilities, swapChainExtent.width, swapChainExtent.height); @@ -453,6 +500,10 @@ void VSwapchain::acquireNextImage() { } uint32_t VSwapchain::currentBackBufferIndex() { +#ifdef __ANDROID__ + if(nativeWindow!=*reinterpret_cast(hwnd)) + throw SwapchainSuboptimal(); +#endif return imgIndex; } @@ -475,7 +526,7 @@ VkResult VSwapchain::implAcquireNextImage() { aquireFence[frameId], &id); - if(code==VK_ERROR_OUT_OF_DATE_KHR) { + if(code<0 && isSwapchainLost(code)) { auto rc = vkxRevertFence(device.device.impl, &aquireFence[frameId]); if(rc!=VK_SUCCESS) std::terminate(); // unrecoverable diff --git a/Engine/gapi/vulkan/vswapchain.h b/Engine/gapi/vulkan/vswapchain.h index faa6bc70..b8961a79 100644 --- a/Engine/gapi/vulkan/vswapchain.h +++ b/Engine/gapi/vulkan/vswapchain.h @@ -3,6 +3,10 @@ #include #include "vulkan_sdk.h" +#ifdef __ANDROID__ +struct ANativeWindow; +#endif + namespace Tempest { namespace Detail { @@ -97,6 +101,9 @@ class VSwapchain : public AbstractGraphicsApi::Swapchain { VDevice& device; SystemApi::Window* hwnd = nullptr; VkSurfaceKHR surface = VK_NULL_HANDLE; +#ifdef __ANDROID__ + ANativeWindow* nativeWindow = nullptr; +#endif uint32_t imgIndex = 0; uint32_t frameId = 0; diff --git a/Engine/gapi/vulkanapi.cpp b/Engine/gapi/vulkanapi.cpp index 711a04b5..5edcd9d6 100644 --- a/Engine/gapi/vulkanapi.cpp +++ b/Engine/gapi/vulkanapi.cpp @@ -24,9 +24,12 @@ using namespace Tempest::Detail; #define VK_KHR_WIN32_SURFACE_EXTENSION_NAME "VK_KHR_win32_surface" #define VK_KHR_XLIB_SURFACE_EXTENSION_NAME "VK_KHR_xlib_surface" +#define VK_KHR_ANDROID_SURFACE_EXTENSION_NAME "VK_KHR_android_surface" #if defined(__WINDOWS__) #define SURFACE_EXTENSION_NAME VK_KHR_WIN32_SURFACE_EXTENSION_NAME +#elif defined(__ANDROID__) +#define SURFACE_EXTENSION_NAME VK_KHR_ANDROID_SURFACE_EXTENSION_NAME #elif defined(__UNIX__) #define SURFACE_EXTENSION_NAME VK_KHR_XLIB_SURFACE_EXTENSION_NAME #endif diff --git a/Engine/system/api/androidapi.cpp b/Engine/system/api/androidapi.cpp index f9b3996d..c88a5e37 100644 --- a/Engine/system/api/androidapi.cpp +++ b/Engine/system/api/androidapi.cpp @@ -55,11 +55,6 @@ void AndroidApi::updateWindow() { void AndroidApi::onAppCmd(void*, int32_t cmd) { switch(cmd) { case APP_CMD_INIT_WINDOW: - if(mainWindow!=nullptr) { - // TODO: handle native surface recreation in the Vulkan swapchain. - Log::e("Android native window recreation is not implemented"); - std::terminate(); - } updateWindow(); break; case APP_CMD_TERM_WINDOW: @@ -116,7 +111,7 @@ SystemApi::Window* AndroidApi::createAndroidWindow(Tempest::Window* owner) { if(isExit.load() || app->destroyRequested!=0) return nullptr; mainWindow = owner; - return reinterpret_cast(app->window); + return reinterpret_cast(&app->window); } SystemApi::Window* AndroidApi::implCreateWindow(Tempest::Window* owner, uint32_t, uint32_t) { @@ -137,7 +132,9 @@ void AndroidApi::implExit() { } Rect AndroidApi::implWindowClientRect(SystemApi::Window* w) { - const auto window = reinterpret_cast(w); + const auto window = *reinterpret_cast(w); + if(window==nullptr) + return Rect(); return Rect(0,0,ANativeWindow_getWidth(window),ANativeWindow_getHeight(window)); } diff --git a/Examples/Android/AndroidManifest.xml b/Examples/Android/AndroidManifest.xml index 1915b1bd..4256a610 100644 --- a/Examples/Android/AndroidManifest.xml +++ b/Examples/Android/AndroidManifest.xml @@ -1,5 +1,6 @@ + +#include #include +#include #include +#include #include class Example final : public Tempest::Window { + public: + Example(Tempest::Device& device) : device(device), swapchain(device,hwnd()) { + } + + ~Example() override { + device.waitIdle(); + } + private: + void render() override { + try { + fence.wait(); + { + auto enc = commands.startEncoding(device); + const float blue = float(Tempest::Application::tickCount()%2000)/2000.f; + enc.setFramebuffer({{swapchain[swapchain.currentImage()],Tempest::Vec4(0,0,blue,1),Tempest::Preserve}}); + } + fence = device.submit(commands); + device.present(swapchain); + } + catch(const Tempest::SwapchainSuboptimal&) { + device.waitIdle(); + swapchain.reset(); + } + } + void resizeEvent(Tempest::SizeEvent& event) override { Tempest::Log::i("Window resized: ",event.w,"x",event.h); + device.waitIdle(); + swapchain.reset(); Tempest::Window::resizeEvent(event); } @@ -14,10 +44,17 @@ class Example final : public Tempest::Window { Tempest::Log::i(event.in ? "Window resumed" : "Window paused"); Tempest::Window::focusEvent(event); } + + Tempest::Device& device; + Tempest::Swapchain swapchain; + Tempest::CommandBuffer commands; + Tempest::Fence fence; }; int main(int, char**) { Tempest::Application app; - Example window; + Tempest::VulkanApi api; + Tempest::Device device(api); + Example window(device); return app.exec(); } From 46df38a49d2a7ab9f98db96bedf0dd6aa3f71988 Mon Sep 17 00:00:00 2001 From: Solessfir <34831419+Solessfir@users.noreply.github.com> Date: Sun, 20 Sep 2026 20:37:05 +0200 Subject: [PATCH 2/8] Retained Android windows across surface replacement --- Engine/gapi/vulkan/vswapchain.cpp | 26 ++++++++++++-------------- Engine/gapi/vulkan/vswapchain.h | 1 + Engine/system/api/androidapi.cpp | 15 ++++++++++++--- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/Engine/gapi/vulkan/vswapchain.cpp b/Engine/gapi/vulkan/vswapchain.cpp index b06d27f0..dc7f825a 100644 --- a/Engine/gapi/vulkan/vswapchain.cpp +++ b/Engine/gapi/vulkan/vswapchain.cpp @@ -258,8 +258,6 @@ VkSurfaceKHR VSwapchain::createSurface(VkInstance instance, void* hwnd) { throw std::system_error(Tempest::GraphicsErrc::NoDevice); #elif defined(__ANDROID__) auto window = *reinterpret_cast(hwnd); - if(window==nullptr) - return VK_NULL_HANDLE; VkAndroidSurfaceCreateInfoKHR createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; createInfo.window = window; @@ -293,7 +291,7 @@ void VSwapchain::createSwapchain(VDevice& device) { auto support = device.querySwapChainSupport(surface); uint32_t imgCount = findImageCount(support); auto code = createSwapchain(device,support,rect,imgCount); - if(isSwapchainLost(code)) { + if(isSwapchainLost(code) || isSurfaceLost(code)) { cleanupSwapchain(); #ifdef __ANDROID__ cleanupSurface(); @@ -460,21 +458,21 @@ uint32_t VSwapchain::findImageCount(const SwapChainSupport& support) const { return imageCount; } -bool VSwapchain::isSwapchainLost(VkResult code) const { +bool VSwapchain::isSurfaceLost(VkResult code) const { #ifdef __ANDROID__ - if(code==VK_ERROR_SURFACE_LOST_KHR) - return true; + return code==VK_ERROR_SURFACE_LOST_KHR; +#else + (void)code; + return false; #endif + } + +bool VSwapchain::isSwapchainLost(VkResult code) const { if(code==VK_SUBOPTIMAL_KHR) { // WA for issues on some linux distros (https://github.com/Try/OpenGothic/issues/977) // probably would have to exclude currentTransform soon due to android VkSurfaceCapabilitiesKHR capabilities = {}; -#ifdef __ANDROID__ - if(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device.physicalDevice, surface, &capabilities)==VK_ERROR_SURFACE_LOST_KHR) - return true; -#else vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device.physicalDevice, surface, &capabilities); -#endif capabilities.currentExtent = findSwapExtent(capabilities, swapChainExtent.width, swapChainExtent.height); @@ -493,7 +491,7 @@ bool VSwapchain::isSwapchainLost(VkResult code) const { void VSwapchain::acquireNextImage() { VkResult code = implAcquireNextImage(); - if(isSwapchainLost(code)) + if(isSwapchainLost(code) || isSurfaceLost(code)) throw SwapchainSuboptimal(); vkAssert(code); @@ -526,7 +524,7 @@ VkResult VSwapchain::implAcquireNextImage() { aquireFence[frameId], &id); - if(code<0 && isSwapchainLost(code)) { + if(code==VK_ERROR_OUT_OF_DATE_KHR || isSurfaceLost(code)) { auto rc = vkxRevertFence(device.device.impl, &aquireFence[frameId]); if(rc!=VK_SUCCESS) std::terminate(); // unrecoverable @@ -588,7 +586,7 @@ void VSwapchain::present() { auto tx = Application::tickCount(); VkResult code = device.presentQueue->present(presentInfo); - if(isSwapchainLost(code)) + if(isSwapchainLost(code) || isSurfaceLost(code)) throw SwapchainSuboptimal(); tx = Application::tickCount()-tx; if(tx > 2) { diff --git a/Engine/gapi/vulkan/vswapchain.h b/Engine/gapi/vulkan/vswapchain.h index b8961a79..b59854a9 100644 --- a/Engine/gapi/vulkan/vswapchain.h +++ b/Engine/gapi/vulkan/vswapchain.h @@ -127,6 +127,7 @@ class VSwapchain : public AbstractGraphicsApi::Swapchain { uint32_t findImageCount(const SwapChainSupport& support) const; bool isSwapchainLost(VkResult code) const; + bool isSurfaceLost(VkResult code) const; VkResult implAcquireNextImage(); void acquireNextImage(); }; diff --git a/Engine/system/api/androidapi.cpp b/Engine/system/api/androidapi.cpp index c88a5e37..beef305e 100644 --- a/Engine/system/api/androidapi.cpp +++ b/Engine/system/api/androidapi.cpp @@ -23,6 +23,7 @@ extern "C" void android_main(android_app* state); static android_app* app = nullptr; static Tempest::Window* mainWindow = nullptr; +static ANativeWindow* nativeWindow = nullptr; static std::atomic_bool isExit = false; static bool resumed = false; static bool focused = false; @@ -48,6 +49,12 @@ void AndroidApi::updateWindow() { if(mainWindow==nullptr) return; + if(nativeWindow!=app->window) { + ANativeWindow_acquire(app->window); + ANativeWindow_release(nativeWindow); + nativeWindow = app->window; + } + SizeEvent event(ANativeWindow_getWidth(app->window),ANativeWindow_getHeight(app->window)); AndroidApi::dispatchResize(*mainWindow,event); } @@ -111,7 +118,9 @@ SystemApi::Window* AndroidApi::createAndroidWindow(Tempest::Window* owner) { if(isExit.load() || app->destroyRequested!=0) return nullptr; mainWindow = owner; - return reinterpret_cast(&app->window); + nativeWindow = app->window; + ANativeWindow_acquire(nativeWindow); + return reinterpret_cast(&nativeWindow); } SystemApi::Window* AndroidApi::implCreateWindow(Tempest::Window* owner, uint32_t, uint32_t) { @@ -123,6 +132,8 @@ SystemApi::Window* AndroidApi::implCreateWindow(Tempest::Window* owner, ShowMode } void AndroidApi::implDestroyWindow(SystemApi::Window*) { + ANativeWindow_release(nativeWindow); + nativeWindow = nullptr; mainWindow = nullptr; } @@ -133,8 +144,6 @@ void AndroidApi::implExit() { Rect AndroidApi::implWindowClientRect(SystemApi::Window* w) { const auto window = *reinterpret_cast(w); - if(window==nullptr) - return Rect(); return Rect(0,0,ANativeWindow_getWidth(window),ANativeWindow_getHeight(window)); } From c62ff1a80c132d9ea26b6aa8069a98c7294e9493 Mon Sep 17 00:00:00 2001 From: Solessfir <34831419+Solessfir@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:20:43 +0200 Subject: [PATCH 3/8] Centralized Android surface recreation --- Engine/gapi/vulkan/vswapchain.cpp | 54 +++++++++++++++++++------------ Engine/gapi/vulkan/vswapchain.h | 2 ++ 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/Engine/gapi/vulkan/vswapchain.cpp b/Engine/gapi/vulkan/vswapchain.cpp index dc7f825a..5d1f9656 100644 --- a/Engine/gapi/vulkan/vswapchain.cpp +++ b/Engine/gapi/vulkan/vswapchain.cpp @@ -156,7 +156,6 @@ VSwapchain::SemaphoreList::~SemaphoreList() { VSwapchain::VSwapchain(VDevice &device, SystemApi::Window* hwnd) :device(device), hwnd(hwnd) { try { - surface = createSurface(device.instance, hwnd); createSwapchain(device); } catch(...) { @@ -233,10 +232,6 @@ void VSwapchain::cleanupSurface() noexcept { void VSwapchain::reset() { cleanupSwapchain(); -#ifdef __ANDROID__ - cleanupSurface(); - surface = createSurface(device.instance, hwnd); -#endif createSwapchain(device); } @@ -248,6 +243,14 @@ void VSwapchain::cleanup() noexcept { VkSurfaceKHR VSwapchain::createSurface(VkInstance instance, void* hwnd) { if(hwnd==nullptr) return VK_NULL_HANDLE; +#ifdef __ANDROID__ + if(nativeWindow==*reinterpret_cast(hwnd) && !surfaceLost) + return surface; + cleanupSurface(); +#else + if(surface!=VK_NULL_HANDLE) + return surface; +#endif VkSurfaceKHR ret = VK_NULL_HANDLE; #ifdef __WINDOWS__ VkWin32SurfaceCreateInfoKHR createInfo={}; @@ -264,6 +267,7 @@ VkSurfaceKHR VSwapchain::createSurface(VkInstance instance, void* hwnd) { vkAssert(vkCreateAndroidSurfaceKHR(instance,&createInfo,nullptr,&ret)); // VkSurfaceKHR retains the native window until cleanupSurface(). nativeWindow = window; + surfaceLost = false; #elif defined(__UNIX__) VkXlibSurfaceCreateInfoKHR createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR; @@ -288,15 +292,12 @@ void VSwapchain::createSwapchain(VDevice& device) { return; try { + surface = createSurface(device.instance, hwnd); auto support = device.querySwapChainSupport(surface); uint32_t imgCount = findImageCount(support); auto code = createSwapchain(device,support,rect,imgCount); if(isSwapchainLost(code) || isSurfaceLost(code)) { cleanupSwapchain(); -#ifdef __ANDROID__ - cleanupSurface(); - surface = createSurface(device.instance, hwnd); -#endif continue; } break; @@ -340,13 +341,7 @@ VkResult VSwapchain::createSwapchain(VDevice& device, const SwapChainSupport& sw } createInfo.preTransform = swapChainSupport.capabilities.currentTransform; - for(auto alpha : {VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR, - VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR, VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR}) { - if((swapChainSupport.capabilities.supportedCompositeAlpha & alpha)!=0) { - createInfo.compositeAlpha = alpha; - break; - } - } + createInfo.compositeAlpha = findAlphaMode(swapChainSupport.capabilities.supportedCompositeAlpha); createInfo.presentMode = presentMode; createInfo.clipped = VK_FALSE; @@ -432,6 +427,15 @@ VkPresentModeKHR VSwapchain::findSwapPresentMode(const std::vector::max()) { return capabilities.currentExtent; @@ -498,14 +502,14 @@ void VSwapchain::acquireNextImage() { } uint32_t VSwapchain::currentBackBufferIndex() { -#ifdef __ANDROID__ - if(nativeWindow!=*reinterpret_cast(hwnd)) - throw SwapchainSuboptimal(); -#endif return imgIndex; } VkResult VSwapchain::implAcquireNextImage() { +#ifdef __ANDROID__ + if(nativeWindow!=*reinterpret_cast(hwnd)) + return VK_ERROR_OUT_OF_DATE_KHR; +#endif auto dev = device.device.impl; auto& slot = sync[frameId]; @@ -523,6 +527,9 @@ VkResult VSwapchain::implAcquireNextImage() { aquireSem[frameId], aquireFence[frameId], &id); +#ifdef __ANDROID__ + surfaceLost = isSurfaceLost(code); +#endif if(code==VK_ERROR_OUT_OF_DATE_KHR || isSurfaceLost(code)) { auto rc = vkxRevertFence(device.device.impl, &aquireFence[frameId]); @@ -543,6 +550,10 @@ VkResult VSwapchain::implAcquireNextImage() { } void VSwapchain::present() { +#ifdef __ANDROID__ + if(nativeWindow!=*reinterpret_cast(hwnd)) + throw SwapchainSuboptimal(); +#endif auto& slot = sync[frameId]; auto& pf = presentFence[frameId]; auto ps = presentSem [imgIndex]; @@ -586,6 +597,9 @@ void VSwapchain::present() { auto tx = Application::tickCount(); VkResult code = device.presentQueue->present(presentInfo); +#ifdef __ANDROID__ + surfaceLost = isSurfaceLost(code); +#endif if(isSwapchainLost(code) || isSurfaceLost(code)) throw SwapchainSuboptimal(); tx = Application::tickCount()-tx; diff --git a/Engine/gapi/vulkan/vswapchain.h b/Engine/gapi/vulkan/vswapchain.h index b59854a9..a1d4ad72 100644 --- a/Engine/gapi/vulkan/vswapchain.h +++ b/Engine/gapi/vulkan/vswapchain.h @@ -103,6 +103,7 @@ class VSwapchain : public AbstractGraphicsApi::Swapchain { VkSurfaceKHR surface = VK_NULL_HANDLE; #ifdef __ANDROID__ ANativeWindow* nativeWindow = nullptr; + bool surfaceLost = false; #endif uint32_t imgIndex = 0; @@ -123,6 +124,7 @@ class VSwapchain : public AbstractGraphicsApi::Swapchain { VkSurfaceFormatKHR findSwapSurfaceFormat(const std::vector& availableFormats) const; VkPresentModeKHR findSwapPresentMode(const std::vector& availablePresentModes) const; + VkCompositeAlphaFlagBitsKHR findAlphaMode(VkCompositeAlphaFlagsKHR supported) const; VkExtent2D findSwapExtent(const VkSurfaceCapabilitiesKHR &capabilities, uint32_t w, uint32_t h) const; uint32_t findImageCount(const SwapChainSupport& support) const; From 5263484d019e4acb59fbdd1dea055301b982a32e Mon Sep 17 00:00:00 2001 From: Solessfir <34831419+Solessfir@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:55:50 +0200 Subject: [PATCH 4/8] Returned Android swapchain recovery to the event loop --- Engine/gapi/vulkan/vdevice.cpp | 33 ++++++++++++++------- Engine/gapi/vulkan/vdevice.h | 4 +-- Engine/gapi/vulkan/vswapchain.cpp | 49 +++++++++++++++++++++++-------- Engine/gapi/vulkan/vswapchain.h | 2 +- Examples/Android/README.md | 2 ++ Examples/Android/main.cpp | 26 +++++++++++----- 6 files changed, 83 insertions(+), 33 deletions(-) diff --git a/Engine/gapi/vulkan/vdevice.cpp b/Engine/gapi/vulkan/vdevice.cpp index e1b88bf4..7133dde8 100644 --- a/Engine/gapi/vulkan/vdevice.cpp +++ b/Engine/gapi/vulkan/vdevice.cpp @@ -843,21 +843,34 @@ void VDevice::deviceQueueProps(VkPhysicalDevice device, VkProps& props) { props.presentFamily = present; } -VDevice::SwapChainSupport VDevice::querySwapChainSupport(VkPhysicalDevice device, VkSurfaceKHR surface) { - SwapChainSupport details; - vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities); +VkResult VDevice::querySwapChainSupport(VkPhysicalDevice device, VkSurfaceKHR surface, SwapChainSupport& details) { + auto code = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities); + if(code!=VK_SUCCESS) + return code; uint32_t formatCount = 0; - vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr); + do { + code = vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr); + if(code!=VK_SUCCESS) + return code; + details.formats.resize(formatCount); + code = vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data()); + } while(code==VK_INCOMPLETE); + if(code!=VK_SUCCESS) + return code; details.formats.resize(formatCount); - vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data()); uint32_t presentModeCount = 0; - vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr); - details.presentModes.resize(presentModeCount); - vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data()); - - return details; + do { + code = vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr); + if(code!=VK_SUCCESS) + return code; + details.presentModes.resize(presentModeCount); + code = vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data()); + } while(code==VK_INCOMPLETE); + if(code==VK_SUCCESS) + details.presentModes.resize(presentModeCount); + return code; } VDevice::MemIndex VDevice::memoryTypeIndex(uint32_t typeBits, VkMemoryPropertyFlags props, VkImageTiling tiling) const { diff --git a/Engine/gapi/vulkan/vdevice.h b/Engine/gapi/vulkan/vdevice.h index 43629a60..f57c4305 100644 --- a/Engine/gapi/vulkan/vdevice.h +++ b/Engine/gapi/vulkan/vdevice.h @@ -395,7 +395,7 @@ class VDevice : public AbstractGraphicsApi::Device { static void deviceFormatProps(VkPhysicalDevice device, VkProps& props); static void deviceQueueProps(VkPhysicalDevice device, VkProps& props); - SwapChainSupport querySwapChainSupport(VkSurfaceKHR surface) { return querySwapChainSupport(physicalDevice,surface); } + VkResult querySwapChainSupport(VkSurfaceKHR surface, SwapChainSupport& support) { return querySwapChainSupport(physicalDevice,surface,support); } MemIndex memoryTypeIndex(uint32_t typeBits, VkMemoryPropertyFlags props, VkImageTiling tiling) const; using DataMgr = UploadEngine; @@ -480,7 +480,7 @@ class VDevice : public AbstractGraphicsApi::Device { void pickPhysicalDevice(); - SwapChainSupport querySwapChainSupport(VkPhysicalDevice device, VkSurfaceKHR surface); + VkResult querySwapChainSupport(VkPhysicalDevice device, VkSurfaceKHR surface, SwapChainSupport& support); }; }} diff --git a/Engine/gapi/vulkan/vswapchain.cpp b/Engine/gapi/vulkan/vswapchain.cpp index 5d1f9656..cf4a0ac4 100644 --- a/Engine/gapi/vulkan/vswapchain.cpp +++ b/Engine/gapi/vulkan/vswapchain.cpp @@ -293,12 +293,25 @@ void VSwapchain::createSwapchain(VDevice& device) { try { surface = createSurface(device.instance, hwnd); - auto support = device.querySwapChainSupport(surface); + SwapChainSupport support; + auto code = device.querySwapChainSupport(surface,support); +#ifdef __ANDROID__ + surfaceLost |= isSurfaceLost(code); + if(surfaceLost) + throw SwapchainSuboptimal(); +#endif + vkAssert(code); uint32_t imgCount = findImageCount(support); - auto code = createSwapchain(device,support,rect,imgCount); + code = createSwapchain(device,support,rect,imgCount); if(isSwapchainLost(code) || isSurfaceLost(code)) { +#ifdef __ANDROID__ + surfaceLost |= isSurfaceLost(code); + // Return to the event loop before retrying a replaced Android window. + throw SwapchainSuboptimal(); +#else cleanupSwapchain(); continue; +#endif } break; } @@ -312,7 +325,10 @@ void VSwapchain::createSwapchain(VDevice& device) { VkResult VSwapchain::createSwapchain(VDevice& device, const SwapChainSupport& swapChainSupport, const Rect& rect, uint32_t imgCount) { VkBool32 support=false; - vkGetPhysicalDeviceSurfaceSupportKHR(device.physicalDevice,device.presentQueue->family,surface,&support); + auto code = vkGetPhysicalDeviceSurfaceSupportKHR(device.physicalDevice,device.presentQueue->family,surface,&support); + if(isSurfaceLost(code)) + return code; + vkAssert(code); if(!support) throw std::system_error(Tempest::GraphicsErrc::NoDevice); @@ -345,7 +361,12 @@ VkResult VSwapchain::createSwapchain(VDevice& device, const SwapChainSupport& sw createInfo.presentMode = presentMode; createInfo.clipped = VK_FALSE; - if(vkCreateSwapchainKHR(device.device.impl, &createInfo, nullptr, &swapChain) != VK_SUCCESS) + code = vkCreateSwapchainKHR(device.device.impl, &createInfo, nullptr, &swapChain); +#ifdef __ANDROID__ + if(code==VK_ERROR_OUT_OF_DATE_KHR || isSurfaceLost(code)) + return code; +#endif + if(code!=VK_SUCCESS) throw std::system_error(Tempest::GraphicsErrc::NoDevice); swapChainImageFormat = surfaceFormat.format; @@ -471,12 +492,19 @@ bool VSwapchain::isSurfaceLost(VkResult code) const { #endif } -bool VSwapchain::isSwapchainLost(VkResult code) const { +bool VSwapchain::isSwapchainLost(VkResult code) { if(code==VK_SUBOPTIMAL_KHR) { // WA for issues on some linux distros (https://github.com/Try/OpenGothic/issues/977) // probably would have to exclude currentTransform soon due to android VkSurfaceCapabilitiesKHR capabilities = {}; - vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device.physicalDevice, surface, &capabilities); + auto result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device.physicalDevice, surface, &capabilities); +#ifdef __ANDROID__ + if(isSurfaceLost(result)) { + surfaceLost = true; + return true; + } +#endif + vkAssert(result); capabilities.currentExtent = findSwapExtent(capabilities, swapChainExtent.width, swapChainExtent.height); @@ -506,10 +534,6 @@ uint32_t VSwapchain::currentBackBufferIndex() { } VkResult VSwapchain::implAcquireNextImage() { -#ifdef __ANDROID__ - if(nativeWindow!=*reinterpret_cast(hwnd)) - return VK_ERROR_OUT_OF_DATE_KHR; -#endif auto dev = device.device.impl; auto& slot = sync[frameId]; @@ -528,7 +552,7 @@ VkResult VSwapchain::implAcquireNextImage() { aquireFence[frameId], &id); #ifdef __ANDROID__ - surfaceLost = isSurfaceLost(code); + surfaceLost |= isSurfaceLost(code); #endif if(code==VK_ERROR_OUT_OF_DATE_KHR || isSurfaceLost(code)) { @@ -551,6 +575,7 @@ VkResult VSwapchain::implAcquireNextImage() { void VSwapchain::present() { #ifdef __ANDROID__ + // Window events may have replaced the surface since the previous frame. if(nativeWindow!=*reinterpret_cast(hwnd)) throw SwapchainSuboptimal(); #endif @@ -598,7 +623,7 @@ void VSwapchain::present() { auto tx = Application::tickCount(); VkResult code = device.presentQueue->present(presentInfo); #ifdef __ANDROID__ - surfaceLost = isSurfaceLost(code); + surfaceLost |= isSurfaceLost(code); #endif if(isSwapchainLost(code) || isSurfaceLost(code)) throw SwapchainSuboptimal(); diff --git a/Engine/gapi/vulkan/vswapchain.h b/Engine/gapi/vulkan/vswapchain.h index a1d4ad72..5ec9b49a 100644 --- a/Engine/gapi/vulkan/vswapchain.h +++ b/Engine/gapi/vulkan/vswapchain.h @@ -128,7 +128,7 @@ class VSwapchain : public AbstractGraphicsApi::Swapchain { VkExtent2D findSwapExtent(const VkSurfaceCapabilitiesKHR &capabilities, uint32_t w, uint32_t h) const; uint32_t findImageCount(const SwapChainSupport& support) const; - bool isSwapchainLost(VkResult code) const; + bool isSwapchainLost(VkResult code); bool isSurfaceLost(VkResult code) const; VkResult implAcquireNextImage(); void acquireNextImage(); diff --git a/Examples/Android/README.md b/Examples/Android/README.md index 680ab4ea..ae8db7d7 100644 --- a/Examples/Android/README.md +++ b/Examples/Android/README.md @@ -4,6 +4,8 @@ A minimal NativeActivity application using Tempest's Android window and Vulkan s Immersive-mode switching is not implemented yet. +The example creates the swapchain in the render callback and defers reset there after resize or `SwapchainSuboptimal`. Creation and reset can both throw `SwapchainSuboptimal`; returning to the event loop lets pending Android window events progress before another attempt. + With JDK 17, Gradle 8.9, Ninja and the Android SDK configured (`ANDROID_HOME`), install SDK 35, build-tools 35.0.0, NDK 27.0.12077973 and CMake 3.22.1. Set `VULKAN_SDK` to a host Vulkan SDK installation for current headers and put its `glslangValidator` on `PATH`. Android links the NDK's Vulkan loader. Replace `/path/to/ndk` below with the NDK installation directory. ```sh diff --git a/Examples/Android/main.cpp b/Examples/Android/main.cpp index f4f792ca..b5621509 100644 --- a/Examples/Android/main.cpp +++ b/Examples/Android/main.cpp @@ -6,9 +6,11 @@ #include #include +#include + class Example final : public Tempest::Window { public: - Example(Tempest::Device& device) : device(device), swapchain(device,hwnd()) { + Example(Tempest::Device& device) : device(device) { } ~Example() override { @@ -18,25 +20,32 @@ class Example final : public Tempest::Window { private: void render() override { try { + if(!swapchain) { + swapchain.emplace(device,hwnd()); + resetPending = false; + } + else if(resetPending) { + device.waitIdle(); + swapchain->reset(); + resetPending = false; + } fence.wait(); { auto enc = commands.startEncoding(device); const float blue = float(Tempest::Application::tickCount()%2000)/2000.f; - enc.setFramebuffer({{swapchain[swapchain.currentImage()],Tempest::Vec4(0,0,blue,1),Tempest::Preserve}}); + enc.setFramebuffer({{(*swapchain)[swapchain->currentImage()],Tempest::Vec4(0,0,blue,1),Tempest::Preserve}}); } fence = device.submit(commands); - device.present(swapchain); + device.present(*swapchain); } catch(const Tempest::SwapchainSuboptimal&) { - device.waitIdle(); - swapchain.reset(); + resetPending = true; } } void resizeEvent(Tempest::SizeEvent& event) override { Tempest::Log::i("Window resized: ",event.w,"x",event.h); - device.waitIdle(); - swapchain.reset(); + resetPending = true; Tempest::Window::resizeEvent(event); } @@ -46,9 +55,10 @@ class Example final : public Tempest::Window { } Tempest::Device& device; - Tempest::Swapchain swapchain; + std::optional swapchain; Tempest::CommandBuffer commands; Tempest::Fence fence; + bool resetPending = false; }; int main(int, char**) { From 22954b149faf972320af9c83b6d8633cad972f0c Mon Sep 17 00:00:00 2001 From: Solessfir <34831419+Solessfir@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:41:04 +0200 Subject: [PATCH 5/8] Made surface loss fatal during Android swapchain recovery --- Engine/gapi/vulkan/vswapchain.cpp | 59 +++++----------------------- Engine/gapi/vulkan/vswapchain.h | 4 +- Examples/Android/AndroidManifest.xml | 2 +- Examples/Android/README.md | 2 +- Examples/Android/main.cpp | 26 ++++-------- 5 files changed, 20 insertions(+), 73 deletions(-) diff --git a/Engine/gapi/vulkan/vswapchain.cpp b/Engine/gapi/vulkan/vswapchain.cpp index cf4a0ac4..55e1a6db 100644 --- a/Engine/gapi/vulkan/vswapchain.cpp +++ b/Engine/gapi/vulkan/vswapchain.cpp @@ -244,7 +244,7 @@ VkSurfaceKHR VSwapchain::createSurface(VkInstance instance, void* hwnd) { if(hwnd==nullptr) return VK_NULL_HANDLE; #ifdef __ANDROID__ - if(nativeWindow==*reinterpret_cast(hwnd) && !surfaceLost) + if(nativeWindow==*reinterpret_cast(hwnd)) return surface; cleanupSurface(); #else @@ -267,7 +267,6 @@ VkSurfaceKHR VSwapchain::createSurface(VkInstance instance, void* hwnd) { vkAssert(vkCreateAndroidSurfaceKHR(instance,&createInfo,nullptr,&ret)); // VkSurfaceKHR retains the native window until cleanupSurface(). nativeWindow = window; - surfaceLost = false; #elif defined(__UNIX__) VkXlibSurfaceCreateInfoKHR createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR; @@ -295,24 +294,14 @@ void VSwapchain::createSwapchain(VDevice& device) { surface = createSurface(device.instance, hwnd); SwapChainSupport support; auto code = device.querySwapChainSupport(surface,support); -#ifdef __ANDROID__ - surfaceLost |= isSurfaceLost(code); - if(surfaceLost) - throw SwapchainSuboptimal(); -#endif vkAssert(code); uint32_t imgCount = findImageCount(support); code = createSwapchain(device,support,rect,imgCount); - if(isSwapchainLost(code) || isSurfaceLost(code)) { -#ifdef __ANDROID__ - surfaceLost |= isSurfaceLost(code); - // Return to the event loop before retrying a replaced Android window. - throw SwapchainSuboptimal(); -#else + if(isSwapchainLost(code)) { cleanupSwapchain(); continue; -#endif } + vkAssert(code); break; } catch(...) { @@ -326,8 +315,6 @@ VkResult VSwapchain::createSwapchain(VDevice& device, const SwapChainSupport& sw const Rect& rect, uint32_t imgCount) { VkBool32 support=false; auto code = vkGetPhysicalDeviceSurfaceSupportKHR(device.physicalDevice,device.presentQueue->family,surface,&support); - if(isSurfaceLost(code)) - return code; vkAssert(code); if(!support) throw std::system_error(Tempest::GraphicsErrc::NoDevice); @@ -362,12 +349,8 @@ VkResult VSwapchain::createSwapchain(VDevice& device, const SwapChainSupport& sw createInfo.clipped = VK_FALSE; code = vkCreateSwapchainKHR(device.device.impl, &createInfo, nullptr, &swapChain); -#ifdef __ANDROID__ - if(code==VK_ERROR_OUT_OF_DATE_KHR || isSurfaceLost(code)) - return code; -#endif if(code!=VK_SUCCESS) - throw std::system_error(Tempest::GraphicsErrc::NoDevice); + return code; swapChainImageFormat = surfaceFormat.format; swapChainExtent = extent; @@ -483,27 +466,12 @@ uint32_t VSwapchain::findImageCount(const SwapChainSupport& support) const { return imageCount; } -bool VSwapchain::isSurfaceLost(VkResult code) const { -#ifdef __ANDROID__ - return code==VK_ERROR_SURFACE_LOST_KHR; -#else - (void)code; - return false; -#endif - } - -bool VSwapchain::isSwapchainLost(VkResult code) { +bool VSwapchain::isSwapchainLost(VkResult code) const { if(code==VK_SUBOPTIMAL_KHR) { // WA for issues on some linux distros (https://github.com/Try/OpenGothic/issues/977) // probably would have to exclude currentTransform soon due to android VkSurfaceCapabilitiesKHR capabilities = {}; auto result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device.physicalDevice, surface, &capabilities); -#ifdef __ANDROID__ - if(isSurfaceLost(result)) { - surfaceLost = true; - return true; - } -#endif vkAssert(result); capabilities.currentExtent = findSwapExtent(capabilities, swapChainExtent.width, swapChainExtent.height); @@ -523,7 +491,7 @@ bool VSwapchain::isSwapchainLost(VkResult code) { void VSwapchain::acquireNextImage() { VkResult code = implAcquireNextImage(); - if(isSwapchainLost(code) || isSurfaceLost(code)) + if(isSwapchainLost(code)) throw SwapchainSuboptimal(); vkAssert(code); @@ -551,20 +519,14 @@ VkResult VSwapchain::implAcquireNextImage() { aquireSem[frameId], aquireFence[frameId], &id); -#ifdef __ANDROID__ - surfaceLost |= isSurfaceLost(code); -#endif - - if(code==VK_ERROR_OUT_OF_DATE_KHR || isSurfaceLost(code)) { + if(code!=VK_SUCCESS && code!=VK_SUBOPTIMAL_KHR) { + // Failed acquisition leaves the fence unsignaled, including on fatal errors. auto rc = vkxRevertFence(device.device.impl, &aquireFence[frameId]); if(rc!=VK_SUCCESS) std::terminate(); // unrecoverable return code; } - if(code!=VK_SUCCESS && code!=VK_SUBOPTIMAL_KHR) - vkAssert(code); - imgIndex = id; slot.state = S_Pending; slot.imgId = id; @@ -622,10 +584,7 @@ void VSwapchain::present() { auto tx = Application::tickCount(); VkResult code = device.presentQueue->present(presentInfo); -#ifdef __ANDROID__ - surfaceLost |= isSurfaceLost(code); -#endif - if(isSwapchainLost(code) || isSurfaceLost(code)) + if(isSwapchainLost(code)) throw SwapchainSuboptimal(); tx = Application::tickCount()-tx; if(tx > 2) { diff --git a/Engine/gapi/vulkan/vswapchain.h b/Engine/gapi/vulkan/vswapchain.h index 5ec9b49a..e13ef8af 100644 --- a/Engine/gapi/vulkan/vswapchain.h +++ b/Engine/gapi/vulkan/vswapchain.h @@ -103,7 +103,6 @@ class VSwapchain : public AbstractGraphicsApi::Swapchain { VkSurfaceKHR surface = VK_NULL_HANDLE; #ifdef __ANDROID__ ANativeWindow* nativeWindow = nullptr; - bool surfaceLost = false; #endif uint32_t imgIndex = 0; @@ -128,8 +127,7 @@ class VSwapchain : public AbstractGraphicsApi::Swapchain { VkExtent2D findSwapExtent(const VkSurfaceCapabilitiesKHR &capabilities, uint32_t w, uint32_t h) const; uint32_t findImageCount(const SwapChainSupport& support) const; - bool isSwapchainLost(VkResult code); - bool isSurfaceLost(VkResult code) const; + bool isSwapchainLost(VkResult code) const; VkResult implAcquireNextImage(); void acquireNextImage(); }; diff --git a/Examples/Android/AndroidManifest.xml b/Examples/Android/AndroidManifest.xml index 4256a610..bb43ae66 100644 --- a/Examples/Android/AndroidManifest.xml +++ b/Examples/Android/AndroidManifest.xml @@ -4,7 +4,7 @@ + android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|screenLayout|smallestScreenSize|density|uiMode"> diff --git a/Examples/Android/README.md b/Examples/Android/README.md index ae8db7d7..719cefbb 100644 --- a/Examples/Android/README.md +++ b/Examples/Android/README.md @@ -4,7 +4,7 @@ A minimal NativeActivity application using Tempest's Android window and Vulkan s Immersive-mode switching is not implemented yet. -The example creates the swapchain in the render callback and defers reset there after resize or `SwapchainSuboptimal`. Creation and reset can both throw `SwapchainSuboptimal`; returning to the event loop lets pending Android window events progress before another attempt. +The manifest handles screen, keyboard, navigation and UI-mode configuration changes without recreating the Activity. Vulkan surface loss (`VK_ERROR_SURFACE_LOST_KHR`) is fatal; these flags do not guarantee that Android will preserve the drawing surface. With JDK 17, Gradle 8.9, Ninja and the Android SDK configured (`ANDROID_HOME`), install SDK 35, build-tools 35.0.0, NDK 27.0.12077973 and CMake 3.22.1. Set `VULKAN_SDK` to a host Vulkan SDK installation for current headers and put its `glslangValidator` on `PATH`. Android links the NDK's Vulkan loader. Replace `/path/to/ndk` below with the NDK installation directory. diff --git a/Examples/Android/main.cpp b/Examples/Android/main.cpp index b5621509..f4f792ca 100644 --- a/Examples/Android/main.cpp +++ b/Examples/Android/main.cpp @@ -6,11 +6,9 @@ #include #include -#include - class Example final : public Tempest::Window { public: - Example(Tempest::Device& device) : device(device) { + Example(Tempest::Device& device) : device(device), swapchain(device,hwnd()) { } ~Example() override { @@ -20,32 +18,25 @@ class Example final : public Tempest::Window { private: void render() override { try { - if(!swapchain) { - swapchain.emplace(device,hwnd()); - resetPending = false; - } - else if(resetPending) { - device.waitIdle(); - swapchain->reset(); - resetPending = false; - } fence.wait(); { auto enc = commands.startEncoding(device); const float blue = float(Tempest::Application::tickCount()%2000)/2000.f; - enc.setFramebuffer({{(*swapchain)[swapchain->currentImage()],Tempest::Vec4(0,0,blue,1),Tempest::Preserve}}); + enc.setFramebuffer({{swapchain[swapchain.currentImage()],Tempest::Vec4(0,0,blue,1),Tempest::Preserve}}); } fence = device.submit(commands); - device.present(*swapchain); + device.present(swapchain); } catch(const Tempest::SwapchainSuboptimal&) { - resetPending = true; + device.waitIdle(); + swapchain.reset(); } } void resizeEvent(Tempest::SizeEvent& event) override { Tempest::Log::i("Window resized: ",event.w,"x",event.h); - resetPending = true; + device.waitIdle(); + swapchain.reset(); Tempest::Window::resizeEvent(event); } @@ -55,10 +46,9 @@ class Example final : public Tempest::Window { } Tempest::Device& device; - std::optional swapchain; + Tempest::Swapchain swapchain; Tempest::CommandBuffer commands; Tempest::Fence fence; - bool resetPending = false; }; int main(int, char**) { From f8e5487a48dfddd8884d64e97f9adac7ab3a1c8f Mon Sep 17 00:00:00 2001 From: Try Date: Thu, 1 Oct 2026 23:33:48 +0200 Subject: [PATCH 6/8] cleanups --- Engine/exceptions/exception.h | 5 ++ Engine/gapi/vulkan/vdevice.cpp | 58 +++++++++--------- Engine/gapi/vulkan/vdevice.h | 6 +- Engine/gapi/vulkan/vswapchain.cpp | 99 +++++++++++++++++-------------- Engine/gapi/vulkan/vswapchain.h | 5 +- 5 files changed, 95 insertions(+), 78 deletions(-) diff --git a/Engine/exceptions/exception.h b/Engine/exceptions/exception.h index cfa9b17f..32ed4d45 100644 --- a/Engine/exceptions/exception.h +++ b/Engine/exceptions/exception.h @@ -28,6 +28,11 @@ class SwapchainSuboptimal : public std::exception { const char* what() const noexcept override { return "swapchain no longer matches the surface properties"; } }; +class SurfaceLost: public std::exception { + public: + const char* what() const noexcept override { return "surface is no longer valid"; } + }; + class IncompleteFboException : public std::exception { public: const char* what() const noexcept override { return "inconsistent framebuffer dimensions"; } diff --git a/Engine/gapi/vulkan/vdevice.cpp b/Engine/gapi/vulkan/vdevice.cpp index 7133dde8..3a32b81a 100644 --- a/Engine/gapi/vulkan/vdevice.cpp +++ b/Engine/gapi/vulkan/vdevice.cpp @@ -824,7 +824,7 @@ void VDevice::deviceQueueProps(VkPhysicalDevice device, VkProps& props) { static const VkQueueFlags rqFlag = (VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT); const bool graphicsSupport = ((queueFamily.queueFlags & rqFlag)==rqFlag); - const bool presentSupport = VSwapchain::checkPresentSupport(device, i); + const bool presentSupport = VSwapchain::checkPresentationSupport(device, i); if(graphicsSupport) graphics = i; @@ -843,34 +843,34 @@ void VDevice::deviceQueueProps(VkPhysicalDevice device, VkProps& props) { props.presentFamily = present; } -VkResult VDevice::querySwapChainSupport(VkPhysicalDevice device, VkSurfaceKHR surface, SwapChainSupport& details) { - auto code = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities); - if(code!=VK_SUCCESS) - return code; - - uint32_t formatCount = 0; - do { - code = vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr); - if(code!=VK_SUCCESS) - return code; - details.formats.resize(formatCount); - code = vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data()); - } while(code==VK_INCOMPLETE); - if(code!=VK_SUCCESS) - return code; - details.formats.resize(formatCount); - - uint32_t presentModeCount = 0; - do { - code = vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr); - if(code!=VK_SUCCESS) - return code; - details.presentModes.resize(presentModeCount); - code = vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data()); - } while(code==VK_INCOMPLETE); - if(code==VK_SUCCESS) - details.presentModes.resize(presentModeCount); - return code; +VDevice::SwapChainSupport VDevice::querySwapChainSupport(VkPhysicalDevice device, VkSurfaceKHR surface) const { + SwapChainSupport support; + vkAssert(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &support.capabilities)); + + // NOTE: SurfaceFormats query either works, or returns error. + // Vulkan spec doesn't allow for semi-errors, such as surface-lost + while(true) { + uint32_t formatCount = 0; + vkAssert(vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr)); + support.formats.resize(formatCount); + auto code = vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, support.formats.data()); + if(code==VK_INCOMPLETE) + continue; + vkAssert(code); + break; + } + + while(true) { + uint32_t presentModeCount = 0; + vkAssert(vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr)); + support.presentModes.resize(presentModeCount); + auto code = vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, support.presentModes.data()); + if(code==VK_INCOMPLETE) + continue; + vkAssert(code); + break; + } + return support; } VDevice::MemIndex VDevice::memoryTypeIndex(uint32_t typeBits, VkMemoryPropertyFlags props, VkImageTiling tiling) const { diff --git a/Engine/gapi/vulkan/vdevice.h b/Engine/gapi/vulkan/vdevice.h index f57c4305..73272e50 100644 --- a/Engine/gapi/vulkan/vdevice.h +++ b/Engine/gapi/vulkan/vdevice.h @@ -41,6 +41,8 @@ inline void vkAssert(VkResult code){ case VK_ERROR_OUT_OF_HOST_MEMORY: //throw std::system_error(Tempest::GraphicsErrc::OutOfHostMemory); throw std::bad_alloc(); + case VK_ERROR_SURFACE_LOST_KHR: + throw SurfaceLost(); case VK_ERROR_DEVICE_LOST: throw DeviceLostException(); @@ -395,7 +397,7 @@ class VDevice : public AbstractGraphicsApi::Device { static void deviceFormatProps(VkPhysicalDevice device, VkProps& props); static void deviceQueueProps(VkPhysicalDevice device, VkProps& props); - VkResult querySwapChainSupport(VkSurfaceKHR surface, SwapChainSupport& support) { return querySwapChainSupport(physicalDevice,surface,support); } + SwapChainSupport querySwapChainSupport(VkSurfaceKHR surface) const { return querySwapChainSupport(physicalDevice,surface); } MemIndex memoryTypeIndex(uint32_t typeBits, VkMemoryPropertyFlags props, VkImageTiling tiling) const; using DataMgr = UploadEngine; @@ -480,7 +482,7 @@ class VDevice : public AbstractGraphicsApi::Device { void pickPhysicalDevice(); - VkResult querySwapChainSupport(VkPhysicalDevice device, VkSurfaceKHR surface, SwapChainSupport& support); + SwapChainSupport querySwapChainSupport(VkPhysicalDevice device, VkSurfaceKHR surface) const; }; }} diff --git a/Engine/gapi/vulkan/vswapchain.cpp b/Engine/gapi/vulkan/vswapchain.cpp index 55e1a6db..238d188a 100644 --- a/Engine/gapi/vulkan/vswapchain.cpp +++ b/Engine/gapi/vulkan/vswapchain.cpp @@ -168,14 +168,15 @@ VSwapchain::~VSwapchain() { cleanup(); } -bool VSwapchain::checkPresentSupport(VkPhysicalDevice device, uint32_t queueFamilyIndex) { +bool VSwapchain::checkPresentationSupport(VkPhysicalDevice device, uint32_t queueFamilyIndex) { #if defined(__WINDOWS__) const bool presentSupport = vkGetPhysicalDeviceWin32PresentationSupportKHR(device, queueFamilyIndex)!=VK_FALSE; + return presentSupport; #elif defined(__ANDROID__) // All Android graphics queues support presentation. (void)device; (void)queueFamilyIndex; - const bool presentSupport = true; + return true; #elif defined(__UNIX__) bool presentSupport = false; if(auto dpy = reinterpret_cast(X11Api::display())){ @@ -183,10 +184,36 @@ bool VSwapchain::checkPresentSupport(VkPhysicalDevice device, uint32_t queueFami auto visualId = XVisualIDFromVisual(DefaultVisual(dpy,screen)); presentSupport = vkGetPhysicalDeviceXlibPresentationSupportKHR(device,queueFamilyIndex,dpy,visualId)!=VK_FALSE; } + return presentSupport; #else -#warning "wsi for vulkan not implemented on this platform" +# warning "wsi for vulkan not implemented on this platform" + return false; +#endif + } + +VkResult VSwapchain::createSurface(VkInstance instance, void* hwnd, VkSurfaceKHR* pSurface) { +#ifdef __WINDOWS__ + VkWin32SurfaceCreateInfoKHR createInfo={}; + createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; + createInfo.hinstance = GetModuleHandleA(nullptr); + createInfo.hwnd = HWND(hwnd); + return vkCreateWin32SurfaceKHR(instance,&createInfo,nullptr,pSurface); +#elif defined(__ANDROID__) + auto window = *reinterpret_cast(hwnd); + VkAndroidSurfaceCreateInfoKHR createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; + createInfo.window = window; + return vkCreateAndroidSurfaceKHR(instance,&createInfo,nullptr,pSurface); +#elif defined(__UNIX__) + VkXlibSurfaceCreateInfoKHR createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR; + createInfo.dpy = reinterpret_cast(X11Api::display()); + createInfo.window = ::Window(hwnd); + return vkCreateXlibSurfaceKHR(instance, &createInfo, nullptr, pSurface); +#else +# warning "wsi for vulkan not implemented on this platform" + return VK_ERROR_UNKNOWN; #endif - return presentSupport; } void VSwapchain::cleanupSwapchain() noexcept { @@ -244,38 +271,16 @@ VkSurfaceKHR VSwapchain::createSurface(VkInstance instance, void* hwnd) { if(hwnd==nullptr) return VK_NULL_HANDLE; #ifdef __ANDROID__ - if(nativeWindow==*reinterpret_cast(hwnd)) - return surface; - cleanupSurface(); -#else + if(nativeWindow!=*reinterpret_cast(hwnd)) + cleanupSurface(); +#endif if(surface!=VK_NULL_HANDLE) return surface; -#endif VkSurfaceKHR ret = VK_NULL_HANDLE; -#ifdef __WINDOWS__ - VkWin32SurfaceCreateInfoKHR createInfo={}; - createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; - createInfo.hinstance = GetModuleHandleA(nullptr); - createInfo.hwnd = HWND(hwnd); - if(vkCreateWin32SurfaceKHR(instance,&createInfo,nullptr,&ret)!=VK_SUCCESS) + if(createSurface(instance, hwnd, &ret)!=VK_SUCCESS) throw std::system_error(Tempest::GraphicsErrc::NoDevice); -#elif defined(__ANDROID__) - auto window = *reinterpret_cast(hwnd); - VkAndroidSurfaceCreateInfoKHR createInfo = {}; - createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; - createInfo.window = window; - vkAssert(vkCreateAndroidSurfaceKHR(instance,&createInfo,nullptr,&ret)); - // VkSurfaceKHR retains the native window until cleanupSurface(). +#ifdef __ANDROID__ nativeWindow = window; -#elif defined(__UNIX__) - VkXlibSurfaceCreateInfoKHR createInfo = {}; - createInfo.sType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR; - createInfo.dpy = reinterpret_cast(X11Api::display()); - createInfo.window = ::Window(hwnd); - if(vkCreateXlibSurfaceKHR(instance, &createInfo, nullptr, &ret)!=VK_SUCCESS) - throw std::system_error(Tempest::GraphicsErrc::NoDevice); -#else -#warning "wsi for vulkan not implemented on this platform" #endif return ret; } @@ -292,11 +297,8 @@ void VSwapchain::createSwapchain(VDevice& device) { try { surface = createSurface(device.instance, hwnd); - SwapChainSupport support; - auto code = device.querySwapChainSupport(surface,support); - vkAssert(code); - uint32_t imgCount = findImageCount(support); - code = createSwapchain(device,support,rect,imgCount); + auto support = device.querySwapChainSupport(surface); + VkResult code = createSwapchain(device,support,rect); if(isSwapchainLost(code)) { cleanupSwapchain(); continue; @@ -311,8 +313,7 @@ void VSwapchain::createSwapchain(VDevice& device) { } } -VkResult VSwapchain::createSwapchain(VDevice& device, const SwapChainSupport& swapChainSupport, - const Rect& rect, uint32_t imgCount) { +VkResult VSwapchain::createSwapchain(VDevice& device, const SwapChainSupport& swapChainSupport, const Rect& rect) { VkBool32 support=false; auto code = vkGetPhysicalDeviceSurfaceSupportKHR(device.physicalDevice,device.presentQueue->family,surface,&support); vkAssert(code); @@ -322,6 +323,8 @@ VkResult VSwapchain::createSwapchain(VDevice& device, const SwapChainSupport& sw VkSurfaceFormatKHR surfaceFormat = findSwapSurfaceFormat(swapChainSupport.formats); VkPresentModeKHR presentMode = findSwapPresentMode (swapChainSupport.presentModes); VkExtent2D extent = findSwapExtent (swapChainSupport.capabilities,uint32_t(rect.w),uint32_t(rect.h)); + auto alphaMode = findAlphaMode(swapChainSupport.capabilities.supportedCompositeAlpha); + uint32_t imgCount = findImageCount (swapChainSupport); VkSwapchainCreateInfoKHR createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; @@ -344,7 +347,7 @@ VkResult VSwapchain::createSwapchain(VDevice& device, const SwapChainSupport& sw } createInfo.preTransform = swapChainSupport.capabilities.currentTransform; - createInfo.compositeAlpha = findAlphaMode(swapChainSupport.capabilities.supportedCompositeAlpha); + createInfo.compositeAlpha = alphaMode; createInfo.presentMode = presentMode; createInfo.clipped = VK_FALSE; @@ -370,9 +373,9 @@ VkResult VSwapchain::createSwapchain(VDevice& device, const SwapChainSupport& sw void VSwapchain::createImageViews(VDevice &device) { uint32_t imgCount=0; - vkGetSwapchainImagesKHR(device.device.impl, swapChain, &imgCount, nullptr); + vkAssert(vkGetSwapchainImagesKHR(device.device.impl, swapChain, &imgCount, nullptr)); images.resize(imgCount); - vkGetSwapchainImagesKHR(device.device.impl, swapChain, &imgCount, images.data()); + vkAssert(vkGetSwapchainImagesKHR(device.device.impl, swapChain, &imgCount, images.data())); views.resize(images.size()); @@ -432,8 +435,14 @@ VkPresentModeKHR VSwapchain::findSwapPresentMode(const std::vector modes={ + VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, + VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR, + VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR, + VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR + }; + + for(auto alpha : modes) { if((supported & alpha)!=0) return alpha; } @@ -471,8 +480,8 @@ bool VSwapchain::isSwapchainLost(VkResult code) const { // WA for issues on some linux distros (https://github.com/Try/OpenGothic/issues/977) // probably would have to exclude currentTransform soon due to android VkSurfaceCapabilitiesKHR capabilities = {}; - auto result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device.physicalDevice, surface, &capabilities); - vkAssert(result); + if(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device.physicalDevice, surface, &capabilities)!=VK_SUCCESS) + return true; capabilities.currentExtent = findSwapExtent(capabilities, swapChainExtent.width, swapChainExtent.height); diff --git a/Engine/gapi/vulkan/vswapchain.h b/Engine/gapi/vulkan/vswapchain.h index e13ef8af..c152c3aa 100644 --- a/Engine/gapi/vulkan/vswapchain.h +++ b/Engine/gapi/vulkan/vswapchain.h @@ -21,7 +21,7 @@ class VSwapchain : public AbstractGraphicsApi::Swapchain { ~VSwapchain() override; VSwapchain& operator=(VSwapchain&& other) = delete; - static bool checkPresentSupport(VkPhysicalDevice device, uint32_t queueFamilyIndex); + static bool checkPresentationSupport(VkPhysicalDevice device, uint32_t queueFamilyIndex); struct SwapChainSupport final { VkSurfaceCapabilitiesKHR capabilities={}; @@ -116,9 +116,10 @@ class VSwapchain : public AbstractGraphicsApi::Swapchain { void cleanupSurface() noexcept; void cleanup() noexcept; + static VkResult createSurface(VkInstance instance, void* hwnd, VkSurfaceKHR* pSurface); VkSurfaceKHR createSurface(VkInstance instance, void* hwnd); void createSwapchain(VDevice& device); - VkResult createSwapchain(VDevice& device, const SwapChainSupport& support, const Rect& rect, uint32_t imgCount); + VkResult createSwapchain(VDevice& device, const SwapChainSupport& support, const Rect& rect); void createImageViews(VDevice &device); VkSurfaceFormatKHR findSwapSurfaceFormat(const std::vector& availableFormats) const; From 0d301baa862de3dd7b7c2e1129f8d6fdc7b82c62 Mon Sep 17 00:00:00 2001 From: Try Date: Fri, 2 Oct 2026 00:20:36 +0200 Subject: [PATCH 7/8] cleanup: use RAII for imageviews --- Engine/gapi/vulkan/vcommandbuffer.cpp | 4 +- Engine/gapi/vulkan/vswapchain.cpp | 126 +++++++++++++++----------- Engine/gapi/vulkan/vswapchain.h | 34 +++++-- 3 files changed, 102 insertions(+), 62 deletions(-) diff --git a/Engine/gapi/vulkan/vcommandbuffer.cpp b/Engine/gapi/vulkan/vcommandbuffer.cpp index c13c9434..140a653e 100644 --- a/Engine/gapi/vulkan/vcommandbuffer.cpp +++ b/Engine/gapi/vulkan/vcommandbuffer.cpp @@ -155,7 +155,7 @@ static VkImage toVkResource(const AbstractGraphicsApi::BarrierDesc& b) { } auto& s = *reinterpret_cast(b.swapchain); - return s.images[b.swId]; + return s.image(b.swId); } @@ -300,7 +300,7 @@ void VCommandBuffer::beginRendering(const FrameBufferDesc& fbo, size_t fboSize, for(size_t i=0; i(fbo.sw[i]); - imageView = t.views[fbo.imgId[i]]; + imageView = t.view(fbo.imgId[i]); imageFormat = t.format(); } else { diff --git a/Engine/gapi/vulkan/vswapchain.cpp b/Engine/gapi/vulkan/vswapchain.cpp index 238d188a..7f0cb039 100644 --- a/Engine/gapi/vulkan/vswapchain.cpp +++ b/Engine/gapi/vulkan/vswapchain.cpp @@ -153,6 +153,69 @@ VSwapchain::SemaphoreList::~SemaphoreList() { vkDestroySemaphore(dev,data[i],nullptr); } +VSwapchain::ImageList::ImageList(VDevice& device, VkSwapchainKHR swapChain, VkFormat format) + : device(&device) { + try { + uint32_t imgCount=0; + vkAssert(vkGetSwapchainImagesKHR(device.device.impl, swapChain, &imgCount, nullptr)); + images.resize(imgCount); + vkAssert(vkGetSwapchainImagesKHR(device.device.impl, swapChain, &imgCount, images.data())); + + views.resize(images.size()); + + for(size_t i=0; ifboMap; + for(auto imageView : views) + if(imageView!=VK_NULL_HANDLE) + map.notifyDestroy(imageView); + cleanup(); + } + +void VSwapchain::ImageList::cleanup() { + for(auto imageView : views) + if(imageView!=VK_NULL_HANDLE) + vkDestroyImageView(device->device.impl,imageView,nullptr); + } + + VSwapchain::VSwapchain(VDevice &device, SystemApi::Window* hwnd) :device(device), hwnd(hwnd) { try { @@ -226,18 +289,9 @@ void VSwapchain::cleanupSwapchain() noexcept { presentFence = FenceList(); aquireSem = SemaphoreList(); presentSem = SemaphoreList(); - - for(auto imageView : views) - if(map!=nullptr && imageView!=VK_NULL_HANDLE) - map->notifyDestroy(imageView); - map = nullptr; - for(auto imageView : views) - if(imageView!=VK_NULL_HANDLE) - vkDestroyImageView(device.device.impl,imageView,nullptr); - views.clear(); + imageList = ImageList(); sync.clear(); - images.clear(); if(swapChain!=VK_NULL_HANDLE) vkDestroySwapchainKHR(device.device.impl,swapChain,nullptr); @@ -259,6 +313,10 @@ void VSwapchain::cleanupSurface() noexcept { void VSwapchain::reset() { cleanupSwapchain(); +#ifdef __ANDROID__ + if(nativeWindow!=*reinterpret_cast(hwnd)) + cleanupSurface(); +#endif createSwapchain(device); } @@ -270,10 +328,6 @@ void VSwapchain::cleanup() noexcept { VkSurfaceKHR VSwapchain::createSurface(VkInstance instance, void* hwnd) { if(hwnd==nullptr) return VK_NULL_HANDLE; -#ifdef __ANDROID__ - if(nativeWindow!=*reinterpret_cast(hwnd)) - cleanupSurface(); -#endif if(surface!=VK_NULL_HANDLE) return surface; VkSurfaceKHR ret = VK_NULL_HANDLE; @@ -359,47 +413,18 @@ VkResult VSwapchain::createSwapchain(VDevice& device, const SwapChainSupport& sw swapChainExtent = extent; swapChaincurrentCaps = swapChainSupport.capabilities; - createImageViews(device); + imageList = ImageList(device, swapChain, swapChainImageFormat); - sync.resize(views.size()); + sync.resize(imageList.size()); - aquireFence = FenceList(device.device.impl, uint32_t(views.size())); - presentFence = FenceList(device.device.impl, uint32_t(views.size())); - presentSem = SemaphoreList(device.device.impl, uint32_t(views.size())); - aquireSem = SemaphoreList(device.device.impl, uint32_t(views.size())); + aquireFence = FenceList(device.device.impl, imageList.size()); + presentFence = FenceList(device.device.impl, imageList.size()); + presentSem = SemaphoreList(device.device.impl, imageList.size()); + aquireSem = SemaphoreList(device.device.impl, imageList.size()); return implAcquireNextImage(); } -void VSwapchain::createImageViews(VDevice &device) { - uint32_t imgCount=0; - vkAssert(vkGetSwapchainImagesKHR(device.device.impl, swapChain, &imgCount, nullptr)); - images.resize(imgCount); - vkAssert(vkGetSwapchainImagesKHR(device.device.impl, swapChain, &imgCount, images.data())); - - views.resize(images.size()); - - for(size_t i=0; i& availableFormats) const { if(availableFormats.size()==1 && availableFormats[0].format==VK_FORMAT_UNDEFINED) return {VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}; @@ -586,7 +611,7 @@ void VSwapchain::present() { presentInfo.pSwapchains = &swapChain; presentInfo.pImageIndices = &imgIndex; - frameId = (frameId + 1)%images.size(); + frameId = (frameId + 1)%imageList.size(); slot.state = S_Idle; slot.imgId = uint32_t(-1); slot.acquire = VK_NULL_HANDLE; @@ -610,4 +635,3 @@ void VSwapchain::present() { } #endif - diff --git a/Engine/gapi/vulkan/vswapchain.h b/Engine/gapi/vulkan/vswapchain.h index c152c3aa..fa898262 100644 --- a/Engine/gapi/vulkan/vswapchain.h +++ b/Engine/gapi/vulkan/vswapchain.h @@ -34,16 +34,13 @@ class VSwapchain : public AbstractGraphicsApi::Swapchain { uint32_t h() const override { return swapChainExtent.height; } void reset() override; - uint32_t imageCount() const override { return uint32_t(views.size()); } + uint32_t imageCount() const override { return uint32_t(imageList.views.size()); } uint32_t currentBackBufferIndex() override; void present(); - VFramebufferMap* map = nullptr; - - VkSwapchainKHR swapChain=VK_NULL_HANDLE; - std::vector views; - std::vector images; + VkImage image(size_t i) const { return imageList.images[i]; } + VkImageView view (size_t i) const { return imageList.views[i]; } enum SyncState : uint8_t { S_Idle, @@ -93,14 +90,34 @@ class VSwapchain : public AbstractGraphicsApi::Swapchain { uint32_t size = 0; }; + class ImageList { + public: + ImageList() = default; + ImageList(VDevice& dev, VkSwapchainKHR swapChain, VkFormat format); + ImageList(ImageList&& oth); + ImageList& operator = (ImageList&& oth); + ~ImageList(); + + uint32_t size() const { return uint32_t(images.size()); } + + std::vector views; + std::vector images; + + private: + void cleanup(); + VDevice* device = {}; + }; + FenceList aquireFence; SemaphoreList aquireSem; FenceList presentFence; SemaphoreList presentSem; + ImageList imageList; VDevice& device; - SystemApi::Window* hwnd = nullptr; - VkSurfaceKHR surface = VK_NULL_HANDLE; + SystemApi::Window* hwnd = nullptr; + VkSurfaceKHR surface = VK_NULL_HANDLE; + VkSwapchainKHR swapChain = VK_NULL_HANDLE; #ifdef __ANDROID__ ANativeWindow* nativeWindow = nullptr; #endif @@ -120,7 +137,6 @@ class VSwapchain : public AbstractGraphicsApi::Swapchain { VkSurfaceKHR createSurface(VkInstance instance, void* hwnd); void createSwapchain(VDevice& device); VkResult createSwapchain(VDevice& device, const SwapChainSupport& support, const Rect& rect); - void createImageViews(VDevice &device); VkSurfaceFormatKHR findSwapSurfaceFormat(const std::vector& availableFormats) const; VkPresentModeKHR findSwapPresentMode(const std::vector& availablePresentModes) const; From a52be83f051efbc9c663dbe478b6bcb34b231780 Mon Sep 17 00:00:00 2001 From: Solessfir <34831419+Solessfir@users.noreply.github.com> Date: Fri, 2 Oct 2026 00:50:26 +0200 Subject: [PATCH 8/8] Closed Android windows on native surface destruction --- Engine/gapi/vulkan/vswapchain.cpp | 18 +----------------- Engine/gapi/vulkan/vswapchain.h | 7 ------- Engine/system/api/androidapi.cpp | 29 +++++++++++------------------ Examples/Android/README.md | 4 ++-- 4 files changed, 14 insertions(+), 44 deletions(-) diff --git a/Engine/gapi/vulkan/vswapchain.cpp b/Engine/gapi/vulkan/vswapchain.cpp index 7f0cb039..6422d942 100644 --- a/Engine/gapi/vulkan/vswapchain.cpp +++ b/Engine/gapi/vulkan/vswapchain.cpp @@ -262,10 +262,9 @@ VkResult VSwapchain::createSurface(VkInstance instance, void* hwnd, VkSurfaceKHR createInfo.hwnd = HWND(hwnd); return vkCreateWin32SurfaceKHR(instance,&createInfo,nullptr,pSurface); #elif defined(__ANDROID__) - auto window = *reinterpret_cast(hwnd); VkAndroidSurfaceCreateInfoKHR createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; - createInfo.window = window; + createInfo.window = reinterpret_cast(hwnd); return vkCreateAndroidSurfaceKHR(instance,&createInfo,nullptr,pSurface); #elif defined(__UNIX__) VkXlibSurfaceCreateInfoKHR createInfo = {}; @@ -306,17 +305,10 @@ void VSwapchain::cleanupSurface() noexcept { if(surface!=VK_NULL_HANDLE) vkDestroySurfaceKHR(device.instance,surface,nullptr); surface = VK_NULL_HANDLE; -#ifdef __ANDROID__ - nativeWindow = nullptr; -#endif } void VSwapchain::reset() { cleanupSwapchain(); -#ifdef __ANDROID__ - if(nativeWindow!=*reinterpret_cast(hwnd)) - cleanupSurface(); -#endif createSwapchain(device); } @@ -333,9 +325,6 @@ VkSurfaceKHR VSwapchain::createSurface(VkInstance instance, void* hwnd) { VkSurfaceKHR ret = VK_NULL_HANDLE; if(createSurface(instance, hwnd, &ret)!=VK_SUCCESS) throw std::system_error(Tempest::GraphicsErrc::NoDevice); -#ifdef __ANDROID__ - nativeWindow = window; -#endif return ret; } @@ -570,11 +559,6 @@ VkResult VSwapchain::implAcquireNextImage() { } void VSwapchain::present() { -#ifdef __ANDROID__ - // Window events may have replaced the surface since the previous frame. - if(nativeWindow!=*reinterpret_cast(hwnd)) - throw SwapchainSuboptimal(); -#endif auto& slot = sync[frameId]; auto& pf = presentFence[frameId]; auto ps = presentSem [imgIndex]; diff --git a/Engine/gapi/vulkan/vswapchain.h b/Engine/gapi/vulkan/vswapchain.h index fa898262..4c36c4bb 100644 --- a/Engine/gapi/vulkan/vswapchain.h +++ b/Engine/gapi/vulkan/vswapchain.h @@ -3,10 +3,6 @@ #include #include "vulkan_sdk.h" -#ifdef __ANDROID__ -struct ANativeWindow; -#endif - namespace Tempest { namespace Detail { @@ -118,9 +114,6 @@ class VSwapchain : public AbstractGraphicsApi::Swapchain { SystemApi::Window* hwnd = nullptr; VkSurfaceKHR surface = VK_NULL_HANDLE; VkSwapchainKHR swapChain = VK_NULL_HANDLE; -#ifdef __ANDROID__ - ANativeWindow* nativeWindow = nullptr; -#endif uint32_t imgIndex = 0; uint32_t frameId = 0; diff --git a/Engine/system/api/androidapi.cpp b/Engine/system/api/androidapi.cpp index beef305e..fde1e76a 100644 --- a/Engine/system/api/androidapi.cpp +++ b/Engine/system/api/androidapi.cpp @@ -23,7 +23,6 @@ extern "C" void android_main(android_app* state); static android_app* app = nullptr; static Tempest::Window* mainWindow = nullptr; -static ANativeWindow* nativeWindow = nullptr; static std::atomic_bool isExit = false; static bool resumed = false; static bool focused = false; @@ -49,12 +48,6 @@ void AndroidApi::updateWindow() { if(mainWindow==nullptr) return; - if(nativeWindow!=app->window) { - ANativeWindow_acquire(app->window); - ANativeWindow_release(nativeWindow); - nativeWindow = app->window; - } - SizeEvent event(ANativeWindow_getWidth(app->window),ANativeWindow_getHeight(app->window)); AndroidApi::dispatchResize(*mainWindow,event); } @@ -64,9 +57,6 @@ void AndroidApi::onAppCmd(void*, int32_t cmd) { case APP_CMD_INIT_WINDOW: updateWindow(); break; - case APP_CMD_TERM_WINDOW: - hasWindow = false; - break; case APP_CMD_WINDOW_RESIZED: case APP_CMD_CONFIG_CHANGED: updateWindow(); @@ -87,7 +77,9 @@ void AndroidApi::onAppCmd(void*, int32_t cmd) { resumed = false; updateFocus(); break; + case APP_CMD_TERM_WINDOW: case APP_CMD_DESTROY: + hasWindow = false; if(mainWindow!=nullptr) { CloseEvent event; AndroidApi::dispatchClose(*mainWindow,event); @@ -105,6 +97,8 @@ static void pollAndroid(android_app* state, int timeout) { while(ALooper_pollOnce(timeout,nullptr,&pending,reinterpret_cast(&source))>=0) { if(source!=nullptr) source->process(state,source); + if(isExit.load()) + break; timeout = 0; } } @@ -118,9 +112,8 @@ SystemApi::Window* AndroidApi::createAndroidWindow(Tempest::Window* owner) { if(isExit.load() || app->destroyRequested!=0) return nullptr; mainWindow = owner; - nativeWindow = app->window; - ANativeWindow_acquire(nativeWindow); - return reinterpret_cast(&nativeWindow); + ANativeWindow_acquire(app->window); + return reinterpret_cast(app->window); } SystemApi::Window* AndroidApi::implCreateWindow(Tempest::Window* owner, uint32_t, uint32_t) { @@ -131,9 +124,8 @@ SystemApi::Window* AndroidApi::implCreateWindow(Tempest::Window* owner, ShowMode return createAndroidWindow(owner); } -void AndroidApi::implDestroyWindow(SystemApi::Window*) { - ANativeWindow_release(nativeWindow); - nativeWindow = nullptr; +void AndroidApi::implDestroyWindow(SystemApi::Window* w) { + ANativeWindow_release(reinterpret_cast(w)); mainWindow = nullptr; } @@ -143,7 +135,7 @@ void AndroidApi::implExit() { } Rect AndroidApi::implWindowClientRect(SystemApi::Window* w) { - const auto window = *reinterpret_cast(w); + const auto window = reinterpret_cast(w); return Rect(0,0,ANativeWindow_getWidth(window),ANativeWindow_getHeight(window)); } @@ -240,7 +232,8 @@ extern "C" void android_main(android_app* state) { if(app->destroyRequested==0) ANativeActivity_finish(app->activity); // Re-entering main would reuse application statics from the previous run. - std::exit(result); + // The application has unwound; process-wide destructors can race Android runtime threads. + std::_Exit(result); } #endif diff --git a/Examples/Android/README.md b/Examples/Android/README.md index 719cefbb..b53aca18 100644 --- a/Examples/Android/README.md +++ b/Examples/Android/README.md @@ -1,10 +1,10 @@ # Android Vulkan window -A minimal NativeActivity application using Tempest's Android window and Vulkan swapchain. The blue background animates while rendering and resumes after switching apps or unlocking the screen. +A minimal NativeActivity application using Tempest's Android window and Vulkan swapchain. The blue background animates while rendering. Immersive-mode switching is not implemented yet. -The manifest handles screen, keyboard, navigation and UI-mode configuration changes without recreating the Activity. Vulkan surface loss (`VK_ERROR_SURFACE_LOST_KHR`) is fatal; these flags do not guarantee that Android will preserve the drawing surface. +The manifest handles screen, keyboard, navigation and UI-mode configuration changes without recreating the Activity. Native-window destruction closes the application, and Vulkan surface loss (`VK_ERROR_SURFACE_LOST_KHR`) is fatal. Android may destroy the window when the application is backgrounded or the screen is locked. With JDK 17, Gradle 8.9, Ninja and the Android SDK configured (`ANDROID_HOME`), install SDK 35, build-tools 35.0.0, NDK 27.0.12077973 and CMake 3.22.1. Set `VULKAN_SDK` to a host Vulkan SDK installation for current headers and put its `glslangValidator` on `PATH`. Android links the NDK's Vulkan loader. Replace `/path/to/ndk` below with the NDK installation directory.