diff --git a/gfx/common/vulkan_common.c b/gfx/common/vulkan_common.c index d595c598b8c7..e150cd271be8 100644 --- a/gfx/common/vulkan_common.c +++ b/gfx/common/vulkan_common.c @@ -1403,6 +1403,23 @@ static void vulkan_destroy_swapchain(gfx_ctx_vulkan_data_t *vk) vk->context.num_recycled_acquire_semaphores = 0; } +bool vulkan_surface_destroy(gfx_ctx_vulkan_data_t *vk) +{ + if (!vk || !vk->context.instance) + return false; + + vulkan_destroy_swapchain(vk); + + if (vk->vk_surface != VK_NULL_HANDLE) + { + vkDestroySurfaceKHR(vk->context.instance, + vk->vk_surface, NULL); + vk->vk_surface = VK_NULL_HANDLE; + } + + return true; +} + static void vulkan_acquire_clear_fences(gfx_ctx_vulkan_data_t *vk) { unsigned i; @@ -1696,16 +1713,48 @@ bool vulkan_surface_create(gfx_ctx_vulkan_data_t *vk, return false; } - /* Must create device after surface since we need to be able to query queues to use for presentation. */ - if (!vulkan_context_init_device(vk)) - return false; + /* Must create device after surface since we need to be able to query queues + * to use for presentation. When replacing a lost surface, retain the + * existing device and verify that its queue can present to the new one. */ + if (vk->context.device == VK_NULL_HANDLE) + { + if (!vulkan_context_init_device(vk)) + goto error_surface; + } + else + { + VkResult res; + VkBool32 supported = VK_FALSE; + + res = vkGetPhysicalDeviceSurfaceSupportKHR( + vk->context.gpu, + vk->context.graphics_queue_index, + vk->vk_surface, &supported); + if (res != VK_SUCCESS || !supported) + { + RARCH_ERR("[Vulkan] Existing queue cannot present to replacement surface (err = %d).\n", + (int)res); + goto error_surface; + } + } if (!vulkan_create_swapchain( vk, width, height, swap_interval)) - return false; + goto error_swapchain; vulkan_acquire_next_image(vk); return true; + +error_swapchain: + vulkan_destroy_swapchain(vk); +error_surface: + if (vk->vk_surface != VK_NULL_HANDLE) + { + vkDestroySurfaceKHR(vk->context.instance, + vk->vk_surface, NULL); + vk->vk_surface = VK_NULL_HANDLE; + } + return false; } uint32_t vulkan_find_memory_type( @@ -2587,6 +2636,10 @@ bool vulkan_create_swapchain(gfx_ctx_vulkan_data_t *vk, /* Force driver to reset swapchain image handles. */ vk->context.flags |= VK_CTX_FLAG_INVALID_SWAPCHAIN; vk->context.flags &= ~VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN; + /* A replacement swapchain can have fewer images than its predecessor. + * Do not retain an image index from the old swapchain while waiting for + * the first acquire from the new one. */ + vk->context.current_swapchain_index = 0; vulkan_create_wait_fences(vk); if (vk->flags & VK_DATA_FLAG_EMULATING_MAILBOX) diff --git a/gfx/common/vulkan_common.h b/gfx/common/vulkan_common.h index a4f04275d220..3db32cb3d387 100644 --- a/gfx/common/vulkan_common.h +++ b/gfx/common/vulkan_common.h @@ -338,6 +338,8 @@ bool vulkan_surface_create(gfx_ctx_vulkan_data_t *vk, unsigned width, unsigned height, int8_t swap_interval); +bool vulkan_surface_destroy(gfx_ctx_vulkan_data_t *vk); + void vulkan_present(gfx_ctx_vulkan_data_t *vk, unsigned index); void vulkan_acquire_next_image(gfx_ctx_vulkan_data_t *vk); diff --git a/gfx/drivers/vulkan.c b/gfx/drivers/vulkan.c index f6bb8f40f6d6..8edea87311bf 100644 --- a/gfx/drivers/vulkan.c +++ b/gfx/drivers/vulkan.c @@ -5216,6 +5216,9 @@ static void *vulkan_init(const video_info_t *video, #endif vulkan_init_readback(vk, settings->bools.video_gpu_record); + + /* Driver resources now match the context's current swapchain. */ + vk->context->flags &= ~VK_CTX_FLAG_INVALID_SWAPCHAIN; return vk; error: @@ -5349,6 +5352,25 @@ static void vulkan_check_swapchain(vk_t *vk) RARCH_ERR("[Vulkan] Failed to update filter chain info.\n"); } +static bool vulkan_recreate_context_swapchain(vk_t *vk) +{ + unsigned width = vk->context->swapchain_width; + unsigned height = vk->context->swapchain_height; + + if (!vk->ctx_driver->set_resize) + return false; + + /* set_resize is the context-owned recreation path. It replaces the + * swapchain before returning, releasing any acquired image that cannot be + * submitted safely by this frame. */ + vk->context->flags |= VK_CTX_FLAG_INVALID_SWAPCHAIN; + if (!vk->ctx_driver->set_resize(vk->ctx_data, width, height)) + return false; + + vulkan_check_swapchain(vk); + return true; +} + static void vulkan_set_nonblock_state(void *data, bool state, bool adaptive_vsync_enabled, unsigned swap_interval) @@ -6360,12 +6382,27 @@ static bool vulkan_frame(void *data, const void *frame, #ifdef HAVE_GFX_WIDGETS bool widgets_active = video_info->widgets_active; #endif - unsigned frame_index = - vk->context->current_frame_index; - unsigned swapchain_index = - vk->context->current_swapchain_index; + unsigned frame_index; + unsigned swapchain_index; bool overlay_behind_menu = video_info->overlay_behind_menu; + /* The context may recreate its swapchain while acquiring the next + * image. Rebuild driver-owned framebuffers before recording commands + * against images from the new swapchain. */ + if (vk->context->flags & VK_CTX_FLAG_INVALID_SWAPCHAIN) + vulkan_check_swapchain(vk); + + frame_index = vk->context->current_frame_index; + swapchain_index = vk->context->current_swapchain_index; + + if ( frame_index >= vk->num_swapchain_images + || swapchain_index >= vk->num_swapchain_images) + { + RARCH_ERR("[Vulkan] Invalid swapchain indices (frame %u, image %u, count %u).\n", + frame_index, swapchain_index, vk->num_swapchain_images); + return vulkan_recreate_context_swapchain(vk); + } + /* Fast toggle shader filter chain logic */ filter_chain = vk->filter_chain; @@ -6748,8 +6785,17 @@ static bool vulkan_frame(void *data, const void *frame, backbuffer = &vk->offscreen_buffer; #endif /* VULKAN_HDR_SWAPCHAIN */ + if ( (backbuffer->image != VK_NULL_HANDLE) + && (backbuffer->framebuffer == VK_NULL_HANDLE)) + { + RARCH_ERR("[Vulkan] Swapchain image %u has no framebuffer.\n", + swapchain_index); + return vulkan_recreate_context_swapchain(vk); + } + /* Render to backbuffer. */ if ( (backbuffer->image != VK_NULL_HANDLE) + && (backbuffer->framebuffer != VK_NULL_HANDLE) && (vk->context->flags & VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN)) { rp_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; diff --git a/gfx/drivers_context/android_vk_ctx.c b/gfx/drivers_context/android_vk_ctx.c index c606b442f5a8..cc0340f2b72e 100644 --- a/gfx/drivers_context/android_vk_ctx.c +++ b/gfx/drivers_context/android_vk_ctx.c @@ -39,6 +39,7 @@ typedef struct unsigned width; unsigned height; int swap_interval; + bool surface_lost; } android_ctx_data_vk_t; /* FORWARD DECLARATION */ @@ -168,11 +169,60 @@ static bool android_gfx_ctx_vk_set_video_mode(void *data, RARCH_ERR("[Vulkan] Failed to create surface.\n"); return false; } + and->surface_lost = false; RARCH_LOG("[Vulkan] Native window size: %ux%u.\n", and->width, and->height); return true; } +static bool android_gfx_ctx_vk_create_surface(void *data) +{ + struct android_app *android_app = (struct android_app*)g_android; + android_ctx_data_vk_t *and = (android_ctx_data_vk_t*)data; + + /* APP_CMD_INIT_WINDOW can remain pending after startup even though the + * Vulkan surface is already active. */ + if (and && !and->surface_lost + && and->vk.vk_surface != VK_NULL_HANDLE) + { + RARCH_LOG("[Vulkan] Ignoring duplicate Android window initialization.\n"); + return true; + } + + if (!android_app || !android_app->window || !and) + return false; + + and->width = ANativeWindow_getWidth(android_app->window); + and->height = ANativeWindow_getHeight(android_app->window); + + if (!vulkan_surface_create(&and->vk, VULKAN_WSI_ANDROID, + NULL, android_app->window, + and->width, and->height, and->swap_interval)) + { + RARCH_ERR("[Vulkan] Failed to recreate Android surface.\n"); + return false; + } + + and->surface_lost = false; + RARCH_LOG("[Vulkan] Recreated Android surface: %ux%u.\n", + and->width, and->height); + return true; +} + +static bool android_gfx_ctx_vk_destroy_surface(void *data) +{ + android_ctx_data_vk_t *and = (android_ctx_data_vk_t*)data; + + if (!and) + return false; + + and->surface_lost = true; + if (!vulkan_surface_destroy(&and->vk)) + return false; + + return true; +} + static void android_gfx_ctx_vk_input_driver(void *data, const char *joypad_name, input_driver_t **input, void **input_data) @@ -201,6 +251,10 @@ static void android_gfx_ctx_vk_swap_buffers(void *data) { android_ctx_data_vk_t *and = (android_ctx_data_vk_t*)data; + if (!and || and->surface_lost + || and->vk.vk_surface == VK_NULL_HANDLE) + return; + if (and->vk.context.flags & VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN) { and->vk.context.flags &= ~VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN; @@ -281,6 +335,6 @@ const gfx_ctx_driver_t gfx_ctx_vk_android = { android_gfx_ctx_vk_bind_hw_render, android_gfx_ctx_vk_get_context_data, NULL, /* make_current */ - NULL, /* create_surface */ - NULL /* destroy_surface */ + android_gfx_ctx_vk_create_surface, + android_gfx_ctx_vk_destroy_surface }; diff --git a/gfx/video_driver.h b/gfx/video_driver.h index 109895406b58..7911bd5d4255 100644 --- a/gfx/video_driver.h +++ b/gfx/video_driver.h @@ -584,14 +584,14 @@ typedef struct gfx_ctx_driver * active for this thread. */ void (*make_current)(bool release); - /* Optional. Creates and binds a new window surface, destroying the original - * window surface if applicable. Returns true on success and false on error. - * Currently only for OpenGL. */ + /* Optional. Creates and binds a replacement window surface without + * reinitializing the underlying graphics context/device. Returns true on + * success and false on error. */ bool (*create_surface)(void *data); - /* Optional. Destroys the current window surface. Returns true on success or - * or if there is no currently bound window surface and false on error. - * Currently only for OpenGL. */ + /* Optional. Destroys the current window surface without reinitializing the + * underlying graphics context/device. Returns true on success, or if no + * window surface is bound, and false on error. */ bool (*destroy_surface)(void *data); } gfx_ctx_driver_t; diff --git a/input/drivers/android_input.c b/input/drivers/android_input.c index 6ecadc868fa7..12d589738bfc 100644 --- a/input/drivers/android_input.c +++ b/input/drivers/android_input.c @@ -48,6 +48,10 @@ #include "../../runloop.h" #include "../input_driver.h" +#ifdef HAVE_THREADS +#include "../../gfx/video_thread_wrapper.h" +#endif + #define MAX_TOUCH 16 #define MAX_NUM_KEYBOARDS 3 #define DEFAULT_ASENSOR_EVENT_RATE 60 @@ -365,6 +369,20 @@ bool android_input_can_be_keyboard(void *data, int port) return android_input_can_be_keyboard_jni(device->id); } +static void android_input_destroy_surface(video_driver_state_t *state) +{ + if (!state || !state->current_video_context.destroy_surface) + return; + +#ifdef HAVE_THREADS + /* The video worker may still be recording a frame that references the + * surface. Drain it before the context driver frees the surface. */ + video_thread_wait_idle(); +#endif + + state->current_video_context.destroy_surface(state->context_data); +} + static void android_input_poll_main_cmd(void) { int8_t cmd; @@ -421,32 +439,64 @@ static void android_input_poll_main_cmd(void) case APP_CMD_RESUME: case APP_CMD_START: case APP_CMD_PAUSE: + { + video_driver_state_t *state = video_state_get_ptr(); + + slock_lock(android_app->mutex); + android_app->activityState = cmd; + /* RESUME/START can arrive before INIT_WINDOW. In that case, + * wait for INIT_WINDOW rather than falling back to a full + * video-driver reinitialization without a native window. */ + if ( (cmd == APP_CMD_RESUME || cmd == APP_CMD_START) + && state->current_video_context.ident + && string_is_equal(state->current_video_context.ident, + "vk_android") + && android_app->window + && state->current_video_context.create_surface) + android_app->reinitRequested = 1; + scond_broadcast(android_app->cond); + slock_unlock(android_app->mutex); + break; + } + case APP_CMD_STOP: + { + video_driver_state_t *state = video_state_get_ptr(); + slock_lock(android_app->mutex); android_app->activityState = cmd; scond_broadcast(android_app->cond); slock_unlock(android_app->mutex); + + /* Android may retain the same ANativeWindow while the app is + * backgrounded. Release Vulkan's acquired buffers anyway so BLAST + * cannot wedge before APP_CMD_TERM_WINDOW is delivered. */ + if ( state->current_video_context.ident + && string_is_equal(state->current_video_context.ident, + "vk_android")) + android_input_destroy_surface(state); break; + } case APP_CMD_CONFIG_CHANGED: AConfiguration_fromAssetManager(android_app->config, android_app->activity->assetManager); break; case APP_CMD_TERM_WINDOW: + { + video_driver_state_t *state = video_state_get_ptr(); + + android_input_destroy_surface(state); + slock_lock(android_app->mutex); /* The window is being hidden or closed, clean it up. */ /* terminate display/EGL context here */ - { - video_driver_state_t *state = video_state_get_ptr(); - if (state->current_video_context.destroy_surface != NULL) - state->current_video_context.destroy_surface(state->context_data); - } - android_app->window = NULL; scond_broadcast(android_app->cond); slock_unlock(android_app->mutex); break; + } case APP_CMD_GAINED_FOCUS: { @@ -1884,11 +1934,10 @@ static void android_input_reinit(void) if (runloop_flags & RUNLOOP_FLAG_PAUSED) { - /* When using OpenGL, pausing the app (e.g. by opening the app switcher) - * will result in the EGL window surface being destroyed, but the actual - * OpenGL context will be preserved on most devices, so we may be able to - * get away with reinitializing only the window surface without having to - * do a full video driver reinitialization. */ + /* When the app is paused (e.g. by opening the app switcher), Android may + * destroy the presentation surface while retaining the OpenGL context or + * Vulkan device. Recreate only the surface when supported before falling + * back to a full video driver reinitialization. */ video_driver_state_t *state = video_state_get_ptr(); if (state->current_video_context.create_surface == NULL || !state->current_video_context.create_surface(state->context_data)) command_event(CMD_EVENT_REINIT, NULL);