diff --git a/docs/tested-configurations.md b/docs/tested-configurations.md index 23e0e0458..2bd1e9c91 100644 --- a/docs/tested-configurations.md +++ b/docs/tested-configurations.md @@ -15,6 +15,7 @@ otherwise. | Date | Build | OS / desktop | GPU / driver | CPU / RAM | Settings | Where | Result | |---|---|---|---|---|---|---|---| | 2026-11-10 | v2.0.14 (source 3a9998c3), Hookshot Builder; later v2.0.14 + the scale-image SAMPLED fix (as in #1940) | Windows 11 Pro 26200, fullscreen | RTX 5090, 621.14 | Ryzen 7 5800X3D, 64 GB | 3840x2160 output, DLSS Quality (67%), Steam Controller (2026) | Castle Town market, Kokiri Forest | Plays well. On the release: HDR Expanded a black picture, Native flat (#1613); with the fix Expanded is right, frame generation 3x too. One crash in `nvoglv64.dll` (0xc0000005) after about 17,000 frames with frame generation 5x. Steam Controller started from Steam: no gyro until Steam's `SDL_GAMECONTROLLER_IGNORE_DEVICES` and `SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD` were cleared (#1897) | +| 2026-11-10 | `cc80fde` on `9447e1d`, Vulkan API 14 | Windows 11 Pro 26300, windowed | RTX 5070 Ti, 621.01 | Ryzen 7 9800X3D, 32 GB | 3440x1440 game output, fixed FG 2x, parallel replay (2 workers, batch 1), pacing off | Link's house in Kokiri, copied checkpoint | 6,688 presents logged. Swapchain 1444x720 → 1440x829 → 1444x720, then 1444x720 after minimize/restore. The scene returned and the window closed normally. Surface loss and deferred admission were exercised with injected results in the source fixture. | | 2026-11-10 | v2.0.11-449-g1987d8c (main caaee0b + the VRAM compaction branch, unmerged then) | Windows 11, no window (unattended, scripted input) | RTX 4070 SUPER 12 GB, 617.42 | Ryzen 9 9950X3D (SMT off), 32 GB | 2560x1440, DLAA, `RX_VK_COMPACT=1`; other programs open (10.2-10.5 GB of VRAM allowed) | Hyrule Field, a scripted five-minute walk (not standing still) | Two runs: 58.2, 58.6 fps mean; VRAM 8.3, 7.9 GB; GPU time over 12.5 ms a frame in 6 of 122 and 10 of 123 120-present windows; 88, 62 frames over 33 ms; 1% low 21.1, 26.5 fps. No device lost | | 2026-11-10 | as above | as above | as above | as above | as above, without the switch | as above | Two runs: 53.2, 54.4 fps mean; VRAM 9.5, 9.6 GB; the write-back shadows given back in 73 of 111 and 80 of 114 windows; GPU time over 12.5 ms in 69 of 111 and 55 of 114; 107, 112 frames over 33 ms; 1% low 19.4, 20.6 fps. No device lost | | 2026-11-10 | host `v2.0.11-438-gcd544a7` (source `8bdd7a54`); renderer main `8bac8aa` + #1502 + the host-clock capture change | Windows 11 Home N, 26300 | RX 9070 XT 16 GB, 32.0.32015.12012 | Ryzen 7 5700X3D, 64 GB | 2560x720 window, 32:9 camera, serial, FG off, dense capture, core validation; fresh prepared shader bank | Boot, title, save load; gameplay not reached | 96 s: 1,476 consecutive original capture records; all 1,461 records covered by the flushed native trace align one-to-one with native returns. Movement window rejected: no filled gameplay interval. No new validation IDs; existing `02251`, and two `08737` errors during full preparation. No gameplay speed or performance claim | diff --git a/gpu/vk/vk_framegen.cpp b/gpu/vk/vk_framegen.cpp index 57d98b91d..bc923c30f 100644 --- a/gpu/vk/vk_framegen.cpp +++ b/gpu/vk/vk_framegen.cpp @@ -449,7 +449,7 @@ void display_main() { // 5080, 60 fps), and a VRR display's refresh changed every frame (flicker on OLED). Now a set's copies go to the GPU // together, the first one's end is waited for, and only the presents keep their times on the line. static const bool ahead_env = !getenv("RX_VK_PRESENT_AHEAD") || atoi(getenv("RX_VK_PRESENT_AHEAD")) != 0; - const bool ahead = present_ahead_wanted(ahead_env, pace, shown, dev.swap_spare); + const bool ahead = present_ahead_wanted(ahead_env, pace, shown, dev.swap_spare.load(std::memory_order_relaxed)); PendingPresent pending[kMaxMult]; if (ahead) { static bool said; diff --git a/gpu/vk/vk_framegen.h b/gpu/vk/vk_framegen.h index 80e39a359..7264b1c32 100644 --- a/gpu/vk/vk_framegen.h +++ b/gpu/vk/vk_framegen.h @@ -23,6 +23,7 @@ struct PendingPresent { uint32_t index = 0; VkFence done = VK_NULL_HANDLE; // (the copy's fence) VkSwapchainKHR swapchain = VK_NULL_HANDLE; + uint64_t swapchain_generation = 0; }; extern thread_local PendingPresent* present_defer_into; bool present_commit(const PendingPresent& p); // (false: not shown, nothing deferred or a swapchain made again since) diff --git a/gpu/vk/vk_internal.h b/gpu/vk/vk_internal.h index a3b3d34de..7c924c57c 100644 --- a/gpu/vk/vk_internal.h +++ b/gpu/vk/vk_internal.h @@ -75,12 +75,13 @@ struct Device { // the window VkSurfaceKHR surface = VK_NULL_HANDLE; VkSwapchainKHR swapchain = VK_NULL_HANDLE; + uint64_t swapchain_generation = 0; // (under the presentation lock) VkFormat swap_format = VK_FORMAT_UNDEFINED; VkExtent2D swap_extent{}; std::vector swap_images; // (hookshot) how many of its images may be held acquired at once with no acquire timeout: the count less the surface's // minImageCount (the Vulkan spec's limit). vk_framegen.cpp's present ahead holds two - uint32_t swap_spare = 0; + std::atomic swap_spare{0}; std::vector swap_views; // (hookshot) a view of each, for Dear ImGui's overlays (VK_NULL_HANDLE: none) VkFormat swap_view_format = VK_FORMAT_UNDEFINED; // (their format: the UNORM one of an sRGB swapchain when it can be) bool swap_mutable = false; // (VK_KHR_swapchain_mutable_format: an sRGB swapchain's images viewed as UNORM) @@ -90,6 +91,7 @@ struct Device { // (hookshot) a queue of a compute family for vkQueuePresentKHR alone (vk_main.cpp create_device; RX_VK_PRESENT_QUEUE=0 / 1). // VK_NULL_HANDLE: presents go on `queue` under present_mu. Its own lock: taken after present_mu, never before it VkQueue present_queue = VK_NULL_HANDLE; + bool present_queue_supported = false; std::mutex present_queue_mu; uint32_t present_family = 0; // (present_queue's family, the swapchain's images shared with it) }; @@ -293,6 +295,8 @@ void vma_log_image_blocks(); // (the image pools' blocks: what they hold, and PFN_vkGetInstanceProcAddr instance_proc(); // vk_main.cpp bool create_swapchain(uint32_t width, uint32_t height); +void retire_swapchain(); // (dev.present_mu held) +bool invalidate_lost_surface(VkResult result); // (dev.present_mu held) bool replay_start(); // vk_replay.cpp bool precompile_wanted(); // vk_draw.cpp void precompile_run(void (*progress)(size_t done, size_t total)); diff --git a/gpu/vk/vk_main.cpp b/gpu/vk/vk_main.cpp index 8ba9a27df..e6595daef 100644 --- a/gpu/vk/vk_main.cpp +++ b/gpu/vk/vk_main.cpp @@ -536,9 +536,50 @@ void swap_views_make(VkImageUsageFlags usage, VkFormat format) { } } +void retire_swapchain() { + if (!dev.swapchain) return; + device_wait_idle_locked(); + reflex_swapchain_destroyed(); + swap_views_drop(); + for (auto semaphore : dev.swap_rendered) vkDestroySemaphore(dev.device, semaphore, nullptr); + vkDestroySwapchainKHR(dev.device, dev.swapchain, nullptr); + dev.swapchain = VK_NULL_HANDLE; + ++dev.swapchain_generation; + dev.swap_rendered.clear(); + dev.swap_images.clear(); + dev.swap_spare = 0; +} + +bool invalidate_lost_surface(VkResult result) { + if (result != VK_ERROR_SURFACE_LOST_KHR) return false; + retire_swapchain(); + if (dev.surface) vkDestroySurfaceKHR(dev.instance, dev.surface, nullptr); + dev.surface = VK_NULL_HANDLE; + dev.present_queue_supported = false; + static unsigned logged = 0; + if (logged++ < 8) logf("vk: presentation surface lost; will retry with a replacement"); + return true; +} + +namespace { +bool open_window_surface(bool log_failure); +void window_size(uint32_t* width, uint32_t* height); +} + bool create_swapchain(uint32_t width, uint32_t height) { + if (!dev.surface) { + window_size(&width, &height); + if (!width || !height) return false; + using Clock = std::chrono::steady_clock; + static Clock::time_point next_attempt; + const auto now = Clock::now(); + if (now < next_attempt) return false; + next_attempt = now + std::chrono::milliseconds(250); + if (!open_window_surface(false)) return false; + } VkSurfaceCapabilitiesKHR caps; VkResult result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(dev.physical, dev.surface, &caps); + if (invalidate_lost_surface(result)) return false; if (result != VK_SUCCESS) { logf("vk: querying surface capabilities failed (VkResult %d)", (int)result); return false; @@ -555,12 +596,15 @@ bool create_swapchain(uint32_t width, uint32_t height) { if (!width || !height) return false; uint32_t n = 0; result = vkGetPhysicalDeviceSurfaceFormatsKHR(dev.physical, dev.surface, &n, nullptr); + if (invalidate_lost_surface(result)) return false; if (result != VK_SUCCESS || !n) { logf("vk: querying surface formats failed (VkResult %d, count %u)", (int)result, n); return false; } std::vector formats(n); - vkGetPhysicalDeviceSurfaceFormatsKHR(dev.physical, dev.surface, &n, formats.data()); + result = vkGetPhysicalDeviceSurfaceFormatsKHR(dev.physical, dev.surface, &n, formats.data()); + if (invalidate_lost_surface(result) || result != VK_SUCCESS || !n) return false; + formats.resize(n); VkSurfaceFormatKHR fmt = formats[0]; for (const auto& f : formats) // (an UNORM target: the game's window textures are presented as they are) // (in sRGB: RADV on Wayland lists every format again with PASS_THROUGH_EXT and the compositor's colour spaces, @@ -596,6 +640,7 @@ bool create_swapchain(uint32_t width, uint32_t height) { modes.resize(n); result = vkGetPhysicalDeviceSurfacePresentModesKHR(dev.physical, dev.surface, &n, modes.data()); } + if (invalidate_lost_surface(result)) return false; if (result != VK_SUCCESS || modes.empty()) logf("vk: querying present modes failed (VkResult %d, count %u); trying mandatory FIFO mode", (int)result, n); // (hookshot) the Insert menu's V-Sync (menu_vsync: RX_VK_FIFO=1 On, RX_VK_IMMEDIATE=1 Off for a run): On is FIFO, as on a @@ -642,7 +687,7 @@ bool create_swapchain(uint32_t width, uint32_t height) { } ci.presentMode = mode; const uint32_t sharing[2] = {dev.queue_family, dev.present_family}; - if (dev.present_queue && dev.present_family != dev.queue_family) { // (presented from another family: no ownership moves) + if (dev.present_queue_supported && dev.present_family != dev.queue_family) { // (presented from another family: no ownership moves) ci.imageSharingMode = VK_SHARING_MODE_CONCURRENT; ci.queueFamilyIndexCount = 2; ci.pQueueFamilyIndices = sharing; @@ -665,17 +710,8 @@ bool create_swapchain(uint32_t width, uint32_t height) { VkSwapchainKHR sc; result = vkCreateSwapchainKHR(dev.device, &ci, nullptr, &sc); // Passing oldSwapchain retires it even when creation fails. Nothing may acquire from it again. - if (dev.swapchain) { - device_wait_idle_locked(); // (the present queue's presents of the old swapchain done too) - reflex_swapchain_destroyed(); // (no latency sleep or markers may use the destroyed chain) - swap_views_drop(); // (hookshot: the overlays' views of its images, before it) - for (auto semaphore : dev.swap_rendered) vkDestroySemaphore(dev.device, semaphore, nullptr); - vkDestroySwapchainKHR(dev.device, dev.swapchain, nullptr); - dev.swapchain = VK_NULL_HANDLE; - dev.swap_rendered.clear(); - dev.swap_images.clear(); - dev.swap_spare = 0; - } + retire_swapchain(); + if (invalidate_lost_surface(result)) return false; if (result != VK_SUCCESS) { logf("vk: vkCreateSwapchainKHR failed (VkResult %d, extent %ux%u, format %d, usage %#x, mode %d, images %u)", (int)result, width, height, (int)fmt.format, ci.imageUsage, (int)mode, ci.minImageCount); @@ -983,16 +1019,44 @@ bool started_up() { } // (the window's surface made: the presents' own queue kept where its family can present to it, create_device) static void choose_present_queue() { + dev.present_queue_supported = false; if (!dev.present_queue) return; VkBool32 ok = VK_FALSE; if (vkGetPhysicalDeviceSurfaceSupportKHR(dev.physical, dev.present_family, dev.surface, &ok) != VK_SUCCESS || !ok) { logf("vk: queue family %u cannot present to the window: presents on the replay's queue", dev.present_family); - dev.present_queue = VK_NULL_HANDLE, dev.present_family = dev.queue_family; return; } + dev.present_queue_supported = true; logf("vk: presents on a queue of their own, of compute family %u (RX_VK_PRESENT_QUEUE=0: off)", dev.present_family); } +bool open_window_surface(bool log_failure) { + VkSurfaceKHR surface = VK_NULL_HANDLE; +#ifdef _WIN32 + VkWin32SurfaceCreateInfoKHR si{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR}; + si.hinstance = GetModuleHandleA(nullptr); + si.hwnd = main_window; + if (!IsWindow(main_window) || vkCreateWin32SurfaceKHR(dev.instance, &si, nullptr, &surface) != VK_SUCCESS) return false; +#else + const MkwVkWindow* w = prep_window; + if (!w || !w->create_surface(w->window, (void*)dev.instance, (void*)&surface)) { + if (log_failure) + logf("vk: SDL_Vulkan_CreateSurface failed; check SDL_VIDEODRIVER and the Vulkan window-system extensions"); + return false; + } +#endif + VkBool32 ok = VK_FALSE; + const VkResult result = vkGetPhysicalDeviceSurfaceSupportKHR(dev.physical, dev.queue_family, surface, &ok); + if (result != VK_SUCCESS || !ok) { + if (log_failure) logf("vk: queue-family present support failed (VkResult %d, supported %u)", (int)result, ok); + vkDestroySurfaceKHR(dev.instance, surface, nullptr); + return false; + } + dev.surface = surface; + choose_present_queue(); + return true; +} + int start(void* hwnd) { main_window = (HWND)hwnd; if (!hwnd) { // (HOS_HEADLESS: no window; the frames are rendered all the same, RX_VK_SHOTS writes them out) @@ -1005,14 +1069,7 @@ int start(void* hwnd) { return 1; } #ifdef _WIN32 - VkWin32SurfaceCreateInfoKHR si{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR}; - si.hinstance = GetModuleHandleA(nullptr); - si.hwnd = (HWND)hwnd; - if (vkCreateWin32SurfaceKHR(dev.instance, &si, nullptr, &dev.surface) != VK_SUCCESS) return 0; - VkBool32 ok = VK_FALSE; - vkGetPhysicalDeviceSurfaceSupportKHR(dev.physical, dev.queue_family, dev.surface, &ok); - if (!ok) return 0; - choose_present_queue(); + if (!open_window_surface(true)) return 0; RECT r{}; GetClientRect((HWND)hwnd, &r); if (!create_swapchain((uint32_t)(r.right - r.left), (uint32_t)(r.bottom - r.top))) return 0; @@ -1023,17 +1080,7 @@ int start(void* hwnd) { logf("vk: unsupported SDL window bridge version %u (expected %u)", w->version, MKWVK_WINDOW_VERSION); return 0; } - if (!w->create_surface(w->window, (void*)dev.instance, (void*)&dev.surface)) { - logf("vk: SDL_Vulkan_CreateSurface failed; check SDL_VIDEODRIVER and the Vulkan window-system extensions"); - return 0; - } - VkBool32 ok = VK_FALSE; - VkResult present = vkGetPhysicalDeviceSurfaceSupportKHR(dev.physical, dev.queue_family, dev.surface, &ok); - if (present != VK_SUCCESS || !ok) { - logf("vk: queue-family present support failed (VkResult %d, supported %u)", (int)present, ok); - return 0; - } - choose_present_queue(); + if (!open_window_surface(true)) return 0; int width = 0, height = 0; w->drawable_size(w->window, &width, &height); if (width <= 0 || height <= 0) { diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index 7b72aa34d..234eafd2b 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -3513,7 +3513,7 @@ std::atomic present_kind{0}; // (vk_framegen.cpp: 1 while a made fra // for the submit and the present. Held over vkAcquireNextImageKHR and the fence, it stopped the replay's submits for as long as // the swapchain had no image free, twice a frame with frame interpolation. std::mutex present_image_mu; -static void present_out(uint32_t index, std::unique_lock& qk); // (present_image's end, below) +static bool present_out(uint32_t index, std::unique_lock& qk); // (present_image's end, below) thread_local PendingPresent* present_defer_into = nullptr; void present_image(VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkImageView view) { std::lock_guard lk(present_image_mu); @@ -3537,6 +3537,9 @@ void present_image(VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkI } hdr_was = opts.hdr, vsync_was = menu_vsync(); } + // Resize or surface recovery can replace the chain after the display thread chose present-ahead. + // A deferred pair needs two spare images before either one can be presented. + if (present_defer_into && dev.swap_spare.load(std::memory_order_relaxed) < 2) return; if (src) { // (the Window scaling filter's row: whether the window is the picture's size. Only the game's frames and the // made ones: the start-up screen (no src) is drawn at the window's own size, which made the row read "Not // needed" while the game's output was another size; until the first frame the size is not known: needed) @@ -3567,6 +3570,11 @@ void present_image(VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkI VkResult res = vkAcquireNextImageKHR(dev.device, dev.swapchain, acquire_timeout, f.acquired, VK_NULL_HANDLE, &index); if (res == VK_ERROR_DEVICE_LOST) device_lost(); if (acquire_t0) fps_wait(fps_stats::kPace, fps_clock() - acquire_t0); + if (res == VK_ERROR_SURFACE_LOST_KHR) { + std::lock_guard qk(dev.present_mu); + invalidate_lost_surface(res); + return; + } #ifndef _WIN32 if (res == VK_TIMEOUT || res == VK_NOT_READY) { if (!unseen) logf("vk: the window is not shown (no image back in 100 ms): frames are not shown until it is"); @@ -3712,6 +3720,7 @@ void present_image(VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkI present_defer_into->index = index; present_defer_into->done = f.done; present_defer_into->swapchain = dev.swapchain; + present_defer_into->swapchain_generation = dev.swapchain_generation; return; } present_out(index, qk); @@ -3721,13 +3730,12 @@ bool present_commit(const PendingPresent& p) { if (!p.used) return false; std::lock_guard lk(present_image_mu); std::unique_lock qk(dev.present_mu); - if (p.swapchain != dev.swapchain) return false; // (made again since the copy: that image is gone) - present_out(p.index, qk); - return true; + if (p.swapchain != dev.swapchain || p.swapchain_generation != dev.swapchain_generation) return false; + return present_out(p.index, qk); } // The present of a swapchain image whose copy is submitted, as present_image ended before; qk holds dev.present_mu -static void present_out(uint32_t index, std::unique_lock& qk) { +static bool present_out(uint32_t index, std::unique_lock& qk) { VkResult res; VkPresentInfoKHR pi{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR}; pi.pNext = reflex_present_id(nullptr); // (NVIDIA Reflex: the frame's present id, and its present's markers) @@ -3742,7 +3750,7 @@ static void present_out(uint32_t index, std::unique_lock& qk) { // ~15% to submit. With a queue of their own (dev.present_queue, a compute family's: vk_main.cpp create_device) the presents go // out with the submits' lock let go: the blit above signals swap_rendered on the replay's queue (submitted already, as a binary // semaphore's wait needs). (Waiting for the blit's fence before the present, with the lock let go, was worse.) - if (dev.present_queue) { + if (dev.present_queue_supported) { qk.unlock(); std::lock_guard pq(dev.present_queue_mu); FpsWait fw(fps_stats::kPace); // (the FPS counter's Breakdown: vsync) @@ -3753,9 +3761,14 @@ static void present_out(uint32_t index, std::unique_lock& qk) { } if (res == VK_ERROR_DEVICE_LOST) device_lost(); reflex_presented(); + if (res == VK_ERROR_SURFACE_LOST_KHR) { + if (!qk.owns_lock()) qk.lock(); + invalidate_lost_surface(res); + return false; + } if (res < 0 && res != VK_ERROR_OUT_OF_DATE_KHR) { // (a present that failed: said, a few times) static int logged; - if (logged++ < 8) logf("vk: present failed (VkResult %d%s)", (int)res, dev.present_queue ? ", on the presents' own queue" : ""); + if (logged++ < 8) logf("vk: present failed (VkResult %d%s)", (int)res, dev.present_queue_supported ? ", on the presents' own queue" : ""); } { // (RX_VK_FRAME_LOG=: one CSV line a present: frame, seconds since the first, frame time in ms. Frame // pacing on the Steam Deck, where MangoHud's log stays empty under gamescope) @@ -3775,6 +3788,7 @@ static void present_out(uint32_t index, std::unique_lock& qk) { if (!qk.owns_lock()) qk.lock(); create_swapchain(dev.swap_extent.width, dev.swap_extent.height); } + return res == VK_SUCCESS || res == VK_SUBOPTIMAL_KHR; } diff --git a/tests/test_antilag.py b/tests/test_antilag.py index 26b80a466..348d060c6 100644 --- a/tests/test_antilag.py +++ b/tests/test_antilag.py @@ -39,7 +39,7 @@ def block(text, marker): raise SystemExit("vk_main.cpp: Anti-Lag 2's feature joins the chain after the features were queried") replay = (ROOT / "gpu" / "vk" / "vk_replay.cpp").read_text(encoding="utf-8") # (present ahead, #1720: present_image's present is its end, present_out, which present_commit also runs) -present = block(replay, "static void present_out(uint32_t index, std::unique_lock& qk) {") +present = block(replay, "static bool present_out(uint32_t index, std::unique_lock& qk) {") if "antilag_present();" not in present or present.index("antilag_present();") > present.index("vkQueuePresentKHR("): raise SystemExit("present_out: Anti-Lag 2's present stage is not given before vkQueuePresentKHR") if "reflex_sleep(); antilag_input();" not in block(replay, "void do_present("): diff --git a/tests/test_present_semaphores.py b/tests/test_present_semaphores.py index 7db9a3137..079dabdef 100644 --- a/tests/test_present_semaphores.py +++ b/tests/test_present_semaphores.py @@ -20,7 +20,7 @@ present = replay[start:replay.index('\n}', start) + 2] # (present ahead, #1702: the present itself is present_image's end, present_out, which present_commit also runs; its wait # is read from there, the complete function as above) -out_start = replay.index('static void present_out(uint32_t index, std::unique_lock& qk) {') +out_start = replay.index('static bool present_out(uint32_t index, std::unique_lock& qk) {') present_out = replay[out_start:replay.index('\n}', out_start) + 2] signal = re.search(r' signal\.semaphore = [^;]+;', present)[0] wait = re.search(r' pi\.pWaitSemaphores = [^;]+;', present_out)[0] @@ -30,6 +30,16 @@ start = main.index(' ci.oldSwapchain = dev.swapchain;') end = main.index(' logf("vk: swapchain', start) tail = main[start:end] +retire_start = main.index('void retire_swapchain()') +retire = main[retire_start:main.index('\n}', retire_start) + 2] +loss_start = main.index('bool invalidate_lost_surface(') +loss = main[loss_start:main.index('\n}', loss_start) + 2] +acquire_loss_start = present.index(' if (res == VK_ERROR_SURFACE_LOST_KHR)') +acquire_loss = present[acquire_loss_start:present.index('\n#ifndef _WIN32', acquire_loss_start)] +present_loss_start = present_out.index(' if (res == VK_ERROR_SURFACE_LOST_KHR)') +present_loss = present_out[present_loss_start:present_out.index('\n if (res < 0', present_loss_start)] +commit_start = replay.index('bool present_commit(const PendingPresent& p)') +commit = replay[commit_start:replay.index('\n}', commit_start) + 2] begin = present[present.index('{') + 1:present.index(' if (src) {')] reflex = (source/'gpu/vk/vk_reflex.cpp').read_text() reset_start = reflex.index('void reflex_swapchain_destroyed(') if 'void reflex_swapchain_destroyed(' in reflex else -1 @@ -45,17 +55,20 @@ using VkSemaphore=uint64_t; using VkImage=uint64_t; using VkSwapchainKHR=uint64_t; using VkResult=int; constexpr uint64_t VK_NULL_HANDLE=0; constexpr int VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO=1, VK_SUCCESS=0; +constexpr VkResult VK_ERROR_SURFACE_LOST_KHR = -1000000000; constexpr int VK_PRESENT_MODE_MAILBOX_KHR=1, VK_PRESENT_MODE_IMMEDIATE_KHR=2; struct VkSemaphoreCreateInfo { int type; }; struct Extent { uint32_t width,height; }; -struct Device { int device=1;uint64_t swapchain=0;int swap_format=0;Extent swap_extent{};bool swap_mutable=false;uint32_t swap_spare=0; +struct Device { int device=1;uint64_t swapchain=0;int swap_format=0;Extent swap_extent{};bool swap_mutable=false;std::atomic swap_spare{0}; std::vector swap_images;std::vector swap_rendered; - std::atomic swap_resize{0};std::mutex present_mu; } dev; + std::atomic swap_resize{0};std::mutex present_mu;uint64_t swapchain_generation=0; + int instance=1;uint64_t surface=1;bool present_queue_supported=true; } dev; struct { int hdr=0; } opts; // (the Insert menu's HDR row: present_image remakes the swapchain when it changes) unsigned image_count=4, made=0, fail_at=0;uint64_t serial=1000; int query_result=0,data_result=0;uint64_t destroyed_swapchain=0; bool idle=false, bad_destroy=false; std::set live,used; std::set chains, retired; +unsigned surfaces_destroyed=0; uint64_t next_chain=0;int create_result=0;bool bad_chain=false; struct CreateInfo {uint64_t oldSwapchain=0;unsigned imageUsage=1,minImageCount=4,flags=0;const void* pNext=nullptr;}; // (hookshot: the overlays' mutable sRGB swapchain, vk_main.cpp: its format list, never used by these formats) @@ -97,6 +110,11 @@ // (vk_replay.cpp's helper: held submits out, then the device idle; the real one also takes the present queue's own lock) void device_wait_idle_locked() {queue_flush_locked();vkDeviceWaitIdle(dev.device);} void swap_views_drop() {} // (hookshot: the overlays' views, vk_main.cpp) +void vkDestroySurfaceKHR(int, uint64_t surface, void*) { + if (!surface || !chains.empty() || !live.empty() || !idle) bad_destroy=true; + ++surfaces_destroyed; +} +''' + retire + '\n' + loss + r''' bool recreate() { uint32_t n=0,width=640,height=480; struct {int format=1;} fmt; struct {uint32_t minImageCount=2;} caps; // (the surface's: present ahead holds images past it, vk_framegen.cpp) @@ -117,6 +135,25 @@ bool reflex_swapchain_stale() {return stale;} int menu_vsync() {return 0;} // (vk_menu.cpp: the V-Sync row, Auto) std::mutex present_image_mu; +struct PendingPresent { + bool used; + uint32_t index; + uint64_t swapchain, swapchain_generation; +}; +PendingPresent* present_defer_into=nullptr; +unsigned committed=0, after_acquire=0; +bool present_out(uint32_t, std::unique_lock&) {++committed; return true;} +''' + commit + r''' +void acquire_with_result(VkResult res) { +''' + acquire_loss + r''' + ++after_acquire; +} +bool present_with_result(VkResult res, bool dedicated) { + std::unique_lock qk(dev.present_mu); + if (dedicated) qk.unlock(); +''' + present_loss + r''' + return true; +} void present_begin() { ''' + begin + r''' if(!dev.swapchain || !chains.count(dev.swapchain) || retired.count(dev.swapchain))bad_chain=true; @@ -205,10 +242,44 @@ if(acquires!=count || dev.swapchain!=active || !dev.swap_resize.load())return 27; preflight_fail=false;present_begin(); if(acquires!=count+1 || !dev.swapchain || dev.swap_resize.load())return 28; + // The display thread chose present-ahead on a chain with two spare images. + PendingPresent deferred{}; + present_defer_into=&deferred; + image_count=3; + dev.swap_resize.store(pack(1280,720)); + count=acquires; + present_begin(); + if (dev.swap_spare!=1 || acquires!=count) return 40; + // The next ordinary frame can use the smaller replacement. + present_defer_into=nullptr; + present_begin(); + if (acquires!=count+1) return 41; + const PendingPresent pending_old{true, 0, dev.swapchain, dev.swapchain_generation}; + idle=false; + acquire_with_result(VK_SUCCESS); + acquire_with_result(VK_ERROR_SURFACE_LOST_KHR); + if (after_acquire!=1 || surfaces_destroyed!=1 || dev.surface || dev.swapchain || + dev.present_queue_supported || bad_destroy || present_commit(pending_old)) return 33; + dev.surface=2; + next_chain=pending_old.swapchain-1; // A driver is free to reuse the old handle. + if (!recreate() || dev.swapchain!=pending_old.swapchain || present_commit(pending_old) || committed) return 34; + const PendingPresent pending_new{true, 0, dev.swapchain, dev.swapchain_generation}; + if (!present_commit(pending_new) || committed!=1) return 35; + idle=false; + if (present_with_result(VK_ERROR_SURFACE_LOST_KHR, true) || surfaces_destroyed!=2 || + dev.surface || dev.swapchain || bad_destroy || present_commit(pending_new)) return 36; + if (!present_with_result(VK_SUCCESS, false) || surfaces_destroyed!=2) return 37; + dev.surface=3; + if (!recreate()) return 38; + create_result=VK_ERROR_SURFACE_LOST_KHR; + idle=false; + if (recreate() || surfaces_destroyed!=3 || dev.surface || dev.swapchain || bad_destroy) return 39; if(bad_destroy || bad_chain)return 8; reflex_swapchain_destroyed(); if(cached.swapchain || cached.latency || cached.frame_marked || cached.mode!=-1)return 26; std::puts("PASS: per-image semaphores, retirement, query/allocation/create failures, latest-resize retry and acquire guard"); + std::puts("PASS: acquire/present/create surface loss, ordered destruction, and deferred present invalidation with handle reuse"); + std::puts("PASS: a smaller replacement blocks deferred acquire and allows the ordinary path"); } ''' with tempfile.TemporaryDirectory(prefix='present-semaphores-') as directory: diff --git a/tests/test_recorder_sync.py b/tests/test_recorder_sync.py index 82b02f794..84496205f 100644 --- a/tests/test_recorder_sync.py +++ b/tests/test_recorder_sync.py @@ -61,7 +61,8 @@ # can be held then: RX_VK_RECORDER=defer drains only through vk_work.h's macros and the replay lock) BOUNDARY_OK = { "vk_coopmat.cpp": "the start-up probe: before any replay, its own vkCmd* direct (nothing held)", - "vk_main.cpp": "create_swapchain: after device_wait_idle_locked() (vkDeviceWaitIdle through vk_work.h in vk_replay.cpp: drained)", + "vk_main.cpp": "retire_swapchain drains through device_wait_idle_locked before destroying views, semaphores and the chain. " + "Surface destruction follows retirement, or discards an unpublished surface without a chain.", "vk_shader.cpp": "program_cache_borrow's and a dropped try's own modules and layouts, which no recorded command uses", "vk_imgui.cpp": "its own font/logo texture views, which only its direct recordings use", } diff --git a/tests/test_vk_swapchain_compat.py b/tests/test_vk_swapchain_compat.py index a7aeef35f..725629020 100644 --- a/tests/test_vk_swapchain_compat.py +++ b/tests/test_vk_swapchain_compat.py @@ -46,9 +46,19 @@ def check_swapchain(source): def check_linux_start(source): body = function(source, "int start(void* hwnd)") linux = body[body.index("#else"):body.index("#endif")] + factory = function(source, "bool open_window_surface(bool log_failure) {") + for item in ("w->create_surface", "queue-family present support failed"): + if item not in factory: + raise SystemExit("Vulkan surface creation no longer checks %s" % item) + for diagnostic in ("SDL_Vulkan_CreateSurface failed", "queue-family present support failed"): + before = factory[:factory.index('logf("vk: ' + diagnostic)].rstrip() + if not before.endswith("if (log_failure)"): + raise SystemExit("Surface recovery repeats the startup diagnostic: %s" % diagnostic) + swapchain = function(source, "bool create_swapchain(") + if "open_window_surface(false)" not in swapchain: + raise SystemExit("Swapchain recovery must suppress repeated surface startup diagnostics") for item in ( - "w->create_surface", - "queue-family present support failed", + "open_window_surface(true)", "w->drawable_size", "SDL returned zero drawable size", "swapchain creation failed for drawable",