// Vulkan surface and swapchain, present blit, 60 Hz vsync thread, focus: the window-system-free half of the window // (the other half: gfx_window_win.c with Win32, gfx_window_sdl.c with SDL, through gfx_wsys.h). // Swapchain set-up ported from LAS Recomp, runtime/nvn/vulkan/device.c:254-330 (https://github.com/peiikai/LASRecomp @ // da35b83a9611d046a9a681aae668d7085954d37f, MIT, Copyright (c) 2026 Peiikai). Focus and vsync behaviour from // MacNativeGames, games/totk/runtime/frontend.m:88-97, 175-176, 350-352 (https://github.com/ly05010419/MacNativeGames @ // b75be8cfa704df3701e86130c8ced268e79d6b11, MIT, Copyright (c) 2026 MacNativeGames contributors). // // Threads: the window thread (the back end's) owns the window and never runs guest code. The vsync thread signals the // bound system events. Presents come from any host thread (the back end's present worker) and take g_pl; every queue // submission takes the recursive queue lock (vk_queue_lock). The swapchain is rebuilt lazily: a resize only marks it // dirty (gfx_window_mark_dirty), the next present (or gfx_window_resize) rebuilds it. // // The in-game menu (ui/menu.h) draws over the game image in the present's command buffer, after the blit. The window's // size and fullscreen state are the settings TOTK_WINDOW_SIZE and TOTK_FULLSCREEN, which the back end applies at start // and whenever a cfg_set changes them (window_settings_live -> gfx_window_apply_settings). #include #include #include #include "cfg.h" #include "flight.h" #include "fs.h" #include "gfx.h" #include "gfx_wsys.h" #include "nvn_api.h" #include "platform/platform.h" #include "ui/menu.h" #include "vk.h" #define FRAMES 2 #define MAX_IMAGES 16 #define VSYNC_NS 1000000000ull #define VSYNC_HZ 60 // ---- queue lock // The recorder queues the parts it records and submits them later (vk_rec.c rec_submit): the first lock of a thread // waits until the parts queued before it are submitted, so whatever it puts on the queue comes after them, as when // the submitting thread submitted them itself. Not inside a held lock: the recorder takes this lock to submit. static PlatRMutex g_ql; static _Thread_local int ql_depth; void vk_queue_lock(void) { if (!ql_depth) rec_queue_wait(); plat_rmutex_lock(&g_ql); ql_depth++; } void vk_queue_unlock(void) { ql_depth--; plat_rmutex_unlock(&g_ql); } // ---- window state shared with the back end static VkSurfaceKHR g_surface; static volatile int32_t g_dirty; // the window was resized; swapchain stale static volatile int32_t g_restart; // gfx_window_request_restart: closing the window starts the game again static volatile int32_t g_focused = 1; static volatile int32_t g_focus_msgs; static PlatRMutex g_start_lock; // gfx_window_start / vsync start static int g_started; void gfx_window_mark_dirty(void) { plat_xchg32(&g_dirty, 1); } // The window gained or lost the focus: the game hears it as nn::oe's focus state and a notification message. The line // carries the local wall time, the only one in the log: when a GPU hang takes the whole system down, the log ends // without an error, and this time set against the driver's entry in the Windows event log (nvlddmkm 153, Display 4101) // tells whether an alt-tab or an overlay came before the hang or followed it. void gfx_window_focus_changed(int in) { plat_xchg32(&g_focused, in); oe_set_focus(in); // pinned to 1 by TOTK_ASSUME_FOCUS inside oe_push_message(OE_MSG_FOCUS_STATE_CHANGED); plat_inc32(&g_focus_msgs); struct tm t; plat_local_time(&t); rt_log("gfx: window %s the focus at %04d-%02d-%02d %02d:%02d:%02d local time", in ? "gained" : "lost", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec); } // The window was closed: the process ends with it, as in MNG, once a save Commit still running in the background is // done. After gfx_window_request_restart the executable then starts again with the same command line and environment // (plat_restart), and the new process waits until this one is gone (main.c) before it moves the log and opens the // RomFS and the saves. void gfx_window_closed(void) { int restart = plat_cas32(&g_restart, 0, 0) != 0; rt_log("gfx: window closed; %s", restart ? "restarting" : "exiting"); fs_save_flush(); vk_pipeline_save(); playtime_save(); prof_write(); // plat_exit runs no atexit handlers if (restart) { plat_setenv("TOTK_REEXEC", NULL); // the new process counts its own address-space retries (memory.c) if (!plat_restart()) rt_log("gfx: the restart could not start the game again"); } plat_exit(0); } void gfx_window_request_restart(void) { plat_xchg32(&g_restart, 1); gfx_window_request_close(); } // TOTK_FULLSCREEN=1: borderless window over the monitor. TOTK_WINDOW_SIZE=x: client area of the window (the // swapchain follows the window; the game image is scaled to it). int gfx_cfg_fullscreen(void) { return cfg_bool("TOTK_FULLSCREEN"); } void gfx_cfg_window_size(int *w, int *h) { int x = 0, y = 0; if (sscanf(cfg_value("TOTK_WINDOW_SIZE"), "%dx%d", &x, &y) != 2 || x < 640 || y < 360 || x > 7680 || y > 4320) sscanf(cfg_meta("WINDOW_SIZE")->def, "%dx%d", &x, &y); // unparsable or out of range: the default *w = x; *h = y; } // The refresh rate of the window's monitor (TOTK_FPS_LIMIT=display), measured by the back end's window thread when the // window opens, lands on another monitor or the display mode changes; readers take the stored value. static volatile int32_t g_rate_mhz; // millihertz, 0 unknown void gfx_window_set_refresh(double hz) { int32_t mhz = (int32_t)(hz * 1000 + 0.5); if (plat_xchg32(&g_rate_mhz, mhz) != mhz) rt_log("gfx: monitor refresh rate %.3f Hz", hz); } double gfx_window_refresh_hz(void) { return plat_cas32(&g_rate_mhz, 0, 0) / 1000.0; } // ---- swapchain and present state (g_pl) static PlatRMutex g_pl; static VkSwapchainKHR g_sc; static VkFormat g_sc_fmt; static int g_sc_hdr; // HDR10 (TOTK_HDR, vk_hdr.c): PQ code values in Rec. 2020 static VkExtent2D g_sc_ext; static int g_scaler; // the last present: 0 the game image 1:1, 1 scaled by the blit, 2 by FSR static uint32_t g_sc_n; static VkImage g_sc_img[MAX_IMAGES]; static VkImageView g_sc_view[MAX_IMAGES]; // colour attachment views for the menu overlay static VkSemaphore g_render_done[MAX_IMAGES]; static VkCommandPool g_pool; static VkCommandBuffer g_cb[FRAMES]; static VkFence g_fence[FRAMES]; static VkSemaphore g_acquire[FRAMES]; static unsigned g_frame; static int g_capture; static VkBuffer g_cap_buf; static VmaAllocation g_cap_alloc; static uint8_t *g_cap_ptr; static VkDeviceSize g_cap_size; static int g_cap_w, g_cap_h, g_cap_valid; static int g_flip_on; // vk_present_blit submitted the blit of swapchain image g_flip_idx; vk_present_flip presents it static uint32_t g_flip_idx; // ---- TOTK_FLIGHT thumbnails (flight.c; g_pl): each present also blits the swapchain image down into slot frame % g_th_n // of a host-visible ring. A slot's tag names the frame it holds complete; recording a new frame clears the tag first, // and the tag is set once the fence of that present has signalled. #define TH_BYTES ((VkDeviceSize)FLIGHT_THUMB_W * FLIGHT_THUMB_H * 4) static int g_th_n, g_th_failed; static uint64_t g_th_frame; // frame of the next vk_present_image call (vk_thumbs_frame) static uint64_t *g_th_tag; // per ring slot static uint64_t g_th_pending[FRAMES]; // per fence slot: the frame whose thumbnail its submission writes static VkImage g_th_img; static VmaAllocation g_th_img_alloc; static VkFormat g_th_fmt; static VkBuffer g_th_buf; static VmaAllocation g_th_alloc; static uint8_t *g_th_ptr; // ---- vsync static PlatMutex g_vs_lock = PLAT_MUTEX_INIT; static uint64_t g_vs_ev[16]; static int g_vs_nev; static volatile int32_t *g_vs_ctr[4]; static int g_vs_nctr; static volatile int64_t g_ticks; static volatile int64_t g_vs_start_ns; static int g_vs_started; static void vsync_main(void *arg) { (void)arg; plat_thread_priority(PLAT_PRIO_HIGHEST); uint64_t n = 0; for (;;) { uint64_t start = (uint64_t)g_vs_start_ns; uint64_t due = start + (n + 1) * VSYNC_NS / VSYNC_HZ; uint64_t now = kern_now_ns(); if (now > due + 100000000ull) { // the process stalled: restart the schedule, no catch-up burst g_vs_start_ns = (int64_t)(now - (n + 1) * VSYNC_NS / VSYNC_HZ); } else if (due > now) { plat_wait_ns(due - now); } n++; plat_xchg64(&g_ticks, (int64_t)n); uint64_t ev[16]; volatile int32_t *ctr[4]; int ne, nc; plat_mutex_lock_shared(&g_vs_lock); ne = g_vs_nev; nc = g_vs_nctr; memcpy(ev, g_vs_ev, sizeof ev); memcpy(ctr, g_vs_ctr, sizeof ctr); plat_mutex_unlock_shared(&g_vs_lock); for (int i = 0; i < nc; i++) plat_inc32(ctr[i]); for (int i = 0; i < ne; i++) kern_signal_system_event(ev[i]); } return; } static void vsync_ensure(void) { plat_rmutex_lock(&g_start_lock); if (!g_vs_started) { g_vs_started = 1; g_vs_start_ns = (int64_t)kern_now_ns(); if (!plat_thread_spawn(vsync_main, NULL, "vsync", 0)) rt_fatal("gfx: cannot start the vsync thread"); } plat_rmutex_unlock(&g_start_lock); } void gfx_vsync_bind(uint64_t system_event) { plat_mutex_lock(&g_vs_lock); int have = 0; for (int i = 0; i < g_vs_nev; i++) have |= g_vs_ev[i] == system_event; if (!have) { if (g_vs_nev == 16) rt_fatal("gfx: more than 16 vsync events"); g_vs_ev[g_vs_nev++] = system_event; } plat_mutex_unlock(&g_vs_lock); vsync_ensure(); } void gfx_vsync_bind_counter(volatile int32_t *counter) { plat_mutex_lock(&g_vs_lock); if (g_vs_nctr == 4) rt_fatal("gfx: more than 4 vsync counters"); g_vs_ctr[g_vs_nctr++] = counter; plat_mutex_unlock(&g_vs_lock); vsync_ensure(); } uint64_t gfx_vsync_ticks(void) { return (uint64_t)plat_cas64(&g_ticks, 0, 0); } uint64_t gfx_vsync_time_ns(uint64_t tick) { return (uint64_t)g_vs_start_ns + tick * VSYNC_NS / VSYNC_HZ; } // ---- focus int gfx_window_focused(void) { return g_focused; } unsigned gfx_window_focus_messages(void) { return (unsigned)g_focus_msgs; } // Present path (g_pl held): a settings change (cfg_version) reaches the window thread, which applies a changed size or // fullscreen; a changed TOTK_VK_PRESENT rebuilds the swapchain at this present. static void reflex_apply_locked(void); static void window_settings_live(void) { static unsigned seen; static int started; static char present[16]; // the TOTK_VK_PRESENT the swapchain was made with unsigned v = cfg_version(); if (started && v == seen) return; seen = v; res_fov_live(); res_dynres_live(); const char *e = cfg_value("TOTK_VK_PRESENT"); if (!started || strncmp(e, present, sizeof present - 1)) { snprintf(present, sizeof present, "%s", e); if (started) plat_xchg32(&g_dirty, 1); } if (started) gfx_window_apply_settings(); reflex_apply_locked(); started = 1; } // ---- NVIDIA Reflex Low Latency (VK_NV_low_latency2) static PFN_vkSetLatencySleepModeNV g_reflex_set_mode; static PFN_vkLatencySleepNV g_reflex_sleep; static PFN_vkSetLatencyMarkerNV g_reflex_marker; static VkSemaphore g_reflex_sem; static uint64_t g_reflex_val; // Present IDs (VK_KHR_present_id, VK_NV_low_latency2): each frame gets its ID once, when the game acquires its window // texture (SIMULATION_START, vk_reflex_frame_begin), and the frame carries it to its input sample, its submissions // (vk_reflex_submit_id: the game thread's current frame, or the frame a replay or present thread set for the work it // does) and its present (PresentJob.pid, vk_reflex_present_frame, flip_locked), so the driver can match every marker // of a frame to the present of that frame. static uint64_t g_reflex_frame_next; // the last ID handed out static uint64_t g_reflex_frame; // the frame the game simulates now static _Thread_local uint64_t t_reflex_submit; // the frame whose work this thread submits; 0: g_reflex_frame static uint64_t g_blit_pid, g_flip_pid; // the frame of the blit being made and of the present waiting (g_pl) static uint64_t g_last_pid; // the last present ID presented (g_pl): present IDs only go up static int g_reflex_mode = -1; // 0=off, 1=on, 2=boost static uint32_t g_reflex_min_us = 1; static int reflex_mode_from_cfg(void) { const char *v = cfg_value("TOTK_REFLEX"); if (!v || !strcmp(v, "off") || !strcmp(v, "0")) return 0; if (!strcmp(v, "boost") || !strcmp(v, "2")) return 2; return 1; } static void reflex_apply_locked(void) { VkCtx *c = vk_ctx(); if (!c || !c->has_nv_reflex || !g_sc) return; if (!g_reflex_set_mode) { g_reflex_set_mode = (PFN_vkSetLatencySleepModeNV)vkGetDeviceProcAddr(c->device, "vkSetLatencySleepModeNV"); g_reflex_sleep = (PFN_vkLatencySleepNV)vkGetDeviceProcAddr(c->device, "vkLatencySleepNV"); g_reflex_marker = (PFN_vkSetLatencyMarkerNV)vkGetDeviceProcAddr(c->device, "vkSetLatencyMarkerNV"); } if (!g_reflex_set_mode) return; if (!g_reflex_sem) { VkSemaphoreTypeCreateInfo ti = { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO, .semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE, .initialValue = 0 }; VkSemaphoreCreateInfo si = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, .pNext = &ti}; if (vkCreateSemaphore(c->device, &si, NULL, &g_reflex_sem) != VK_SUCCESS) return; } int mode = reflex_mode_from_cfg(); uint32_t min_us = 0; int limit = cfg_int("TOTK_FPS_LIMIT"); if (limit > 0) min_us = (uint32_t)(1000000 / limit); if (mode == g_reflex_mode && min_us == g_reflex_min_us) return; g_reflex_mode = mode; g_reflex_min_us = min_us; VkLatencySleepModeInfoNV mi = { .sType = VK_STRUCTURE_TYPE_LATENCY_SLEEP_MODE_INFO_NV, .lowLatencyMode = mode > 0 ? VK_TRUE : VK_FALSE, .lowLatencyBoost = mode == 2 ? VK_TRUE : VK_FALSE, .minimumIntervalUs = min_us }; VkResult r = g_reflex_set_mode(c->device, g_sc, &mi); rt_log("REFLEX: NVIDIA Reflex Low Latency -> %s (minInterval=%u us, VK_NV_low_latency2=%s)", mode == 2 ? "ON + BOOST" : mode == 1 ? "ON" : "OFF", min_us, r == VK_SUCCESS ? "OK" : "ERR"); } int vk_has_reflex(void) { VkCtx *c = vk_ctx(); return c && c->has_nv_reflex; } // TOTK_TRACE=perf, every LAT_EVERY presents: the time from a frame's start (its window texture acquired) to its // present call on the host, with Reflex off or on, and with Reflex on the driver's own reports of the same frames // (vkGetLatencyTimingsNV): how many came back with every marker matched to their present, and their simulation start // to GPU render end. The frame's start is kept by its ID, so it also shows that a present carries its frame's ID. #define LAT_RING 64 #define LAT_EVERY 600 static struct { uint64_t id, start_ns; } g_lat_start[LAT_RING]; static struct { uint64_t n, ns, presents; } g_lat; static PFN_vkGetLatencyTimingsNV g_reflex_timings; uint64_t vk_reflex_frame_begin(void) { uint64_t id = __atomic_add_fetch(&g_reflex_frame_next, 1, __ATOMIC_RELAXED); __atomic_store_n(&g_reflex_frame, id, __ATOMIC_RELAXED); if (rt_trace & RT_TRACE_PERF) g_lat_start[id % LAT_RING].start_ns = kern_now_ns(), g_lat_start[id % LAT_RING].id = id; return id; } // g_pl held, after the present of frame pid static void latency_note(VkCtx *c, uint64_t pid) { if (!(rt_trace & RT_TRACE_PERF) || !pid) return; if (g_lat_start[pid % LAT_RING].id == pid) g_lat.n++, g_lat.ns += kern_now_ns() - g_lat_start[pid % LAT_RING].start_ns; if (++g_lat.presents % LAT_EVERY) return; char drv[200] = ""; if (g_reflex_mode > 0 && g_sc) { if (!g_reflex_timings) g_reflex_timings = (PFN_vkGetLatencyTimingsNV)vkGetDeviceProcAddr(c->device, "vkGetLatencyTimingsNV"); static VkLatencyTimingsFrameReportNV rep[64]; VkGetLatencyMarkerInfoNV gi = {.sType = VK_STRUCTURE_TYPE_GET_LATENCY_MARKER_INFO_NV}; if (g_reflex_timings) { g_reflex_timings(c->device, g_sc, &gi); // the count first if (gi.timingCount > 64) gi.timingCount = 64; for (uint32_t i = 0; i < gi.timingCount; i++) rep[i] = (VkLatencyTimingsFrameReportNV){.sType = VK_STRUCTURE_TYPE_LATENCY_TIMINGS_FRAME_REPORT_NV}; gi.pTimings = rep; if (gi.timingCount) g_reflex_timings(c->device, g_sc, &gi); uint32_t full = 0; double sum = 0; for (uint32_t i = 0; i < gi.timingCount; i++) { const VkLatencyTimingsFrameReportNV *r = &rep[i]; if (r->simStartTimeUs && r->simEndTimeUs && r->renderSubmitStartTimeUs && r->presentStartTimeUs && r->gpuRenderEndTimeUs > r->simStartTimeUs) full++, sum += (double)(r->gpuRenderEndTimeUs - r->simStartTimeUs); } snprintf(drv, sizeof drv, "; driver reports: %u of %u frames with every marker, simulation start to GPU render " "end %.2f ms", full, gi.timingCount, full ? sum / full / 1000.0 : 0.0); } } rt_log("reflex: frame start to present %.2f ms over %llu frames (reflex %s)%s", g_lat.n ? g_lat.ns / 1e6 / g_lat.n : 0.0, (unsigned long long)g_lat.n, g_reflex_mode == 2 ? "boost" : g_reflex_mode == 1 ? "on" : "off", drv); g_lat.n = g_lat.ns = 0; } uint64_t vk_reflex_frame(void) { return __atomic_load_n(&g_reflex_frame, __ATOMIC_RELAXED); } void vk_reflex_submit_frame(uint64_t id) { t_reflex_submit = id; } uint64_t vk_reflex_submit_id(void) { return t_reflex_submit ? t_reflex_submit : vk_reflex_frame(); } // The present thread, before the blit of frame id: the present of that blit carries id. void vk_reflex_present_frame(uint64_t id) { // g_blit_pid feeds flip_locked's VkPresentIdKHR (Reflex) and latency_note (TOTK_TRACE=perf); with neither, the // present skips the lock if (!vk_has_reflex() && !(rt_trace & RT_TRACE_PERF)) return; plat_rmutex_lock(&g_pl); g_blit_pid = id; plat_rmutex_unlock(&g_pl); } void vk_reflex_marker(int marker, uint64_t pid) { VkCtx *c = vk_ctx(); if (!c || !c->has_nv_reflex || !g_reflex_marker || g_reflex_mode <= 0 || !g_sc || !pid) return; plat_rmutex_lock(&g_pl); if (g_sc && g_reflex_marker) { VkSetLatencyMarkerInfoNV mi = { .sType = VK_STRUCTURE_TYPE_SET_LATENCY_MARKER_INFO_NV, .presentID = pid, .marker = (VkLatencyMarkerNV)marker }; g_reflex_marker(c->device, g_sc, &mi); } plat_rmutex_unlock(&g_pl); } void vk_reflex_sleep(void) { VkCtx *c = vk_ctx(); if (!c || !c->has_nv_reflex || !g_reflex_sleep || !g_reflex_sem || g_reflex_mode <= 0 || !g_sc) return; uint64_t val = __atomic_add_fetch(&g_reflex_val, 1, __ATOMIC_RELAXED); VkLatencySleepInfoNV si = { .sType = VK_STRUCTURE_TYPE_LATENCY_SLEEP_INFO_NV, .signalSemaphore = g_reflex_sem, .value = val }; // vkLatencySleepNV uses the swapchain, which Vulkan wants externally synchronised with the acquire and the present // (they run under g_pl on the present thread). The wait for the semaphore stays outside the lock. VkResult r = VK_ERROR_INITIALIZATION_FAILED; plat_rmutex_lock(&g_pl); if (g_sc) r = g_reflex_sleep(c->device, g_sc, &si); plat_rmutex_unlock(&g_pl); if (r == VK_SUCCESS) { VkSemaphoreWaitInfo wi = { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO, .semaphoreCount = 1, .pSemaphores = &g_reflex_sem, .pValues = &val }; vkWaitSemaphores(c->device, &wi, 100000000ULL); } } // ---- swapchain static const char *g_present_got = ""; // the TOTK_VK_PRESENT value of the mode the swapchain got; "" before one static VkPresentModeKHR pick_present_mode(VkPhysicalDevice pd) { const char *e = cfg_value("TOTK_VK_PRESENT"); VkPresentModeKHR want = !strcmp(e, "mailbox") ? VK_PRESENT_MODE_MAILBOX_KHR : !strcmp(e, "immediate") ? VK_PRESENT_MODE_IMMEDIATE_KHR : VK_PRESENT_MODE_FIFO_KHR; uint32_t n = 0; vkGetPhysicalDeviceSurfacePresentModesKHR(pd, g_surface, &n, NULL); VkPresentModeKHR m[16]; if (n > 16) n = 16; vkGetPhysicalDeviceSurfacePresentModesKHR(pd, g_surface, &n, m); VkPresentModeKHR got = VK_PRESENT_MODE_FIFO_KHR; for (uint32_t i = 0; i < n; i++) if (m[i] == want) got = want; const char *name = got == VK_PRESENT_MODE_MAILBOX_KHR ? "mailbox" : got == VK_PRESENT_MODE_IMMEDIATE_KHR ? "immediate" : "fifo"; __atomic_store_n(&g_present_got, name, __ATOMIC_RELEASE); static int logged = -1; // the mode last logged: a change from the menu shows in the log if ((int)got != logged) { logged = (int)got; rt_log("gfx: present mode %s (TOTK_VK_PRESENT=%s)", name, e); } return got; } const char *gfx_window_present_mode(void) { return __atomic_load_n(&g_present_got, __ATOMIC_ACQUIRE); } // Queues the present of a blit vk_present_blit left waiting (g_pl held). A swapchain rebuild and the next blit flush it // first; the device release drops it. static void flip_locked(void) { if (!g_flip_on) return; g_flip_on = 0; if (!g_sc) return; VkCtx *c = vk_ctx(); uint64_t pid = 0; VkPresentIdKHR pid_info = {0}; if (c && c->has_nv_reflex) { // The frame's own ID; a present without one (a probe, a skipped frame's neighbour) takes the next free value, // as present IDs must grow on a swapchain pid = g_flip_pid > g_last_pid ? g_flip_pid : g_last_pid + 1; g_last_pid = pid; pid_info.sType = VK_STRUCTURE_TYPE_PRESENT_ID_KHR; pid_info.swapchainCount = 1; pid_info.pPresentIds = &pid; vk_reflex_marker(4, pid); // VK_LATENCY_MARKER_PRESENT_START_NV } VkPresentInfoKHR pi = {.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, .pNext = (c && c->has_nv_reflex) ? &pid_info : NULL, .waitSemaphoreCount = 1, .pWaitSemaphores = &g_render_done[g_flip_idx], .swapchainCount = 1, .pSwapchains = &g_sc, .pImageIndices = &g_flip_idx}; vk_queue_lock(); VkResult r = vkQueuePresentKHR(c->gfx_queue, &pi); vk_queue_unlock(); if (c && c->has_nv_reflex) vk_reflex_marker(5, pid); // VK_LATENCY_MARKER_PRESENT_END_NV if (c) latency_note(c, pid ? pid : g_flip_pid); vk_device_fault_report(r); if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) plat_xchg32(&g_dirty, 1); } static void free_capture(void) { VkCtx *c = vk_ctx(); if (g_cap_buf && c) vmaDestroyBuffer(c->vma, g_cap_buf, g_cap_alloc); g_cap_buf = VK_NULL_HANDLE; g_cap_ptr = NULL; g_cap_size = 0; g_cap_valid = 0; } static int build_swapchain(void) { flip_locked(); // the acquired image goes out before its swapchain is replaced VkCtx *c = vk_ctx(); VkSurfaceCapabilitiesKHR caps; if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(c->phys, g_surface, &caps) != VK_SUCCESS) return 0; int cw = 0, ch = 0; wsys_client_size(&cw, &ch); VkExtent2D ext = caps.currentExtent.width != UINT32_MAX ? caps.currentExtent : (VkExtent2D){(uint32_t)cw, (uint32_t)ch}; if (!ext.width || !ext.height) return 0; uint32_t nf = 0; vkGetPhysicalDeviceSurfaceFormatsKHR(c->phys, g_surface, &nf, NULL); VkSurfaceFormatKHR fmts[64]; if (nf > 64) nf = 64; vkGetPhysicalDeviceSurfaceFormatsKHR(c->phys, g_surface, &nf, fmts); if (!nf) return 0; // UNORM: the blit writes the bytes the game wrote (no sRGB encode). TOTK_HDR: HDR10 when the surface offers it. VkSurfaceFormatKHR sf = fmts[0]; int hdr = vk_hdr_surface_format(fmts, nf, &sf), found = hdr; for (uint32_t i = 0; i < nf && !found; i++) if (fmts[i].format == VK_FORMAT_B8G8R8A8_UNORM && fmts[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { sf = fmts[i]; found = 1; } for (uint32_t i = 0; i < nf && !found; i++) if (fmts[i].format == VK_FORMAT_R8G8B8A8_UNORM && fmts[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { sf = fmts[i]; found = 1; } uint32_t count = caps.minImageCount + 1; if (caps.maxImageCount && count > caps.maxImageCount) count = caps.maxImageCount; if (count > 8) count = 8; vk_queue_lock(); vkDeviceWaitIdle(c->device); VkSwapchainKHR old = g_sc; VkSwapchainLatencyCreateInfoNV sci = { .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_LATENCY_CREATE_INFO_NV, .latencyModeEnable = VK_TRUE }; VkSwapchainCreateInfoKHR ci = { .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, .pNext = (c->has_nv_reflex) ? &sci : NULL, .surface = g_surface, .minImageCount = count, .imageFormat = sf.format, .imageColorSpace = sf.colorSpace, .imageExtent = ext, .imageArrayLayers = 1, .imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, // every surface supports it (the menu overlay draws into it) .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE, .preTransform = caps.currentTransform, .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, .presentMode = pick_present_mode(c->phys), .clipped = VK_TRUE, .oldSwapchain = old}; VkResult r = vkCreateSwapchainKHR(c->device, &ci, NULL, &g_sc); if (r != VK_SUCCESS) { rt_log("gfx: vkCreateSwapchainKHR failed (%d)", (int)r); g_sc = old; vk_queue_unlock(); return 0; } for (uint32_t i = 0; i < g_sc_n; i++) { vkDestroySemaphore(c->device, g_render_done[i], NULL); if (g_sc_view[i]) vkDestroyImageView(c->device, g_sc_view[i], NULL); g_sc_view[i] = VK_NULL_HANDLE; } if (old) vkDestroySwapchainKHR(c->device, old, NULL); g_sc_fmt = sf.format; g_sc_hdr = hdr; vk_hdr_set_active(hdr); if (hdr) vk_hdr_metadata(g_sc, 1); g_sc_ext = ext; g_sc_n = MAX_IMAGES; vkGetSwapchainImagesKHR(c->device, g_sc, &g_sc_n, g_sc_img); VkSemaphoreCreateInfo semi = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; for (uint32_t i = 0; i < g_sc_n; i++) { vkCreateSemaphore(c->device, &semi, NULL, &g_render_done[i]); VkImageViewCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = g_sc_img[i], .viewType = VK_IMAGE_VIEW_TYPE_2D, .format = sf.format, .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}}; if (vkCreateImageView(c->device, &vi, NULL, &g_sc_view[i]) != VK_SUCCESS) g_sc_view[i] = VK_NULL_HANDLE; } free_capture(); vk_queue_unlock(); // The Reflex sleep mode belongs to the swapchain: a new one (resize, fullscreen, out of date) starts with low // latency off, so the mode is set on it again even though the setting did not change. g_reflex_mode = -1; reflex_apply_locked(); return 1; } // Returns 1 with a usable swapchain, 0 when the window has no area (minimised). Called with g_pl held. The stale mark // goes before the surface is read: a resize during the build keeps it. The switch to fullscreen at start lands there // on the window thread, and the driver does not report the swapchain as out of date. static int create_swapchain(void) { plat_xchg32(&g_dirty, 0); if (build_swapchain()) return 1; plat_xchg32(&g_dirty, 1); return 0; } static void sync_barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags2 s0, VkAccessFlags2 a0, VkPipelineStageFlags2 s1, VkAccessFlags2 a1) { VkImageMemoryBarrier2 b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = s0, .srcAccessMask = a0, .dstStageMask = s1, .dstAccessMask = a1, .oldLayout = from, .newLayout = to, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = img, .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}}; VkDependencyInfo d = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &b}; vkCmdPipelineBarrier2(cb, &d); } static int ensure_capture_buffer(void) { VkCtx *c = vk_ctx(); VkDeviceSize need = (VkDeviceSize)g_sc_ext.width * g_sc_ext.height * 4; if (g_cap_buf && g_cap_size == need) return 1; free_capture(); VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = need, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT}; VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST, .flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT}; VmaAllocationInfo info; if (vmaCreateBuffer(c->vma, &bi, &ai, &g_cap_buf, &g_cap_alloc, &info) != VK_SUCCESS) { g_cap_buf = VK_NULL_HANDLE; return 0; } g_cap_ptr = info.pMappedData; g_cap_size = need; return g_cap_ptr != NULL; } void vk_thumbs_enable(int n) { plat_rmutex_lock(&g_pl); if (!g_th_n && n > 0 && (g_th_tag = calloc((size_t)n, sizeof *g_th_tag))) g_th_n = n; plat_rmutex_unlock(&g_pl); } void vk_thumbs_frame(uint64_t frame) { plat_rmutex_lock(&g_pl); g_th_frame = frame; plat_rmutex_unlock(&g_pl); } static void free_thumbs(void) { VkCtx *c = vk_ctx(); if (g_th_img && c) vmaDestroyImage(c->vma, g_th_img, g_th_img_alloc); if (g_th_buf && c) vmaDestroyBuffer(c->vma, g_th_buf, g_th_alloc); g_th_img = VK_NULL_HANDLE, g_th_buf = VK_NULL_HANDLE, g_th_ptr = NULL; if (g_th_tag) memset(g_th_tag, 0, sizeof *g_th_tag * (size_t)g_th_n); memset(g_th_pending, 0, sizeof g_th_pending); } // The ring and the blit target in the swapchain's format; allocated on the first present, the image again when the // swapchain format changes. A failure turns the thumbnails off for the session. static int ensure_thumbs(void) { if (!g_th_n || g_th_failed || !g_th_frame) return 0; VkCtx *c = vk_ctx(); if (!g_th_buf) { VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = TH_BYTES * (VkDeviceSize)g_th_n, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT}; VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST, .flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT}; VmaAllocationInfo info; if (vmaCreateBuffer(c->vma, &bi, &ai, &g_th_buf, &g_th_alloc, &info) != VK_SUCCESS || !info.pMappedData) { if (g_th_buf) vmaDestroyBuffer(c->vma, g_th_buf, g_th_alloc); g_th_buf = VK_NULL_HANDLE; g_th_failed = 1; rt_log("gfx: flight thumbnails off: no host-visible ring of %llu MB", (unsigned long long)(bi.size >> 20)); return 0; } g_th_ptr = info.pMappedData; } // an HDR10 swapchain blits into RGBA8: its PQ codes, flat-looking but readable as bytes VkFormat tf = g_sc_hdr ? VK_FORMAT_R8G8B8A8_UNORM : g_sc_fmt; if (g_th_img && g_th_fmt != tf) { vmaDestroyImage(c->vma, g_th_img, g_th_img_alloc); // the previous present's fence was waited for: idle g_th_img = VK_NULL_HANDLE; } if (!g_th_img) { VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = tf, .extent = {FLIGHT_THUMB_W, FLIGHT_THUMB_H, 1}, .mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL, .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED}; VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE}; if (vmaCreateImage(c->vma, &ci, &ai, &g_th_img, &g_th_img_alloc, NULL) != VK_SUCCESS) { g_th_img = VK_NULL_HANDLE; g_th_failed = 1; rt_log("gfx: flight thumbnails off: no %ux%u image", FLIGHT_THUMB_W, FLIGHT_THUMB_H); return 0; } g_th_fmt = tf; } return 1; } // dst (TRANSFER_SRC, the swapchain image just written) -> thumbnail image -> ring slot of g_th_frame. static void record_thumb(VkCommandBuffer cb, VkImage dst, unsigned fence_slot) { uint64_t f = g_th_frame; unsigned ring = (unsigned)(f % (uint64_t)g_th_n); g_th_tag[ring] = 0; // under g_pl: vk_thumbs_read no longer hands this slot out sync_barrier(cb, g_th_img, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); VkImageBlit region = { .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .srcOffsets = {{0, 0, 0}, {(int)g_sc_ext.width, (int)g_sc_ext.height, 1}}, .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{0, 0, 0}, {FLIGHT_THUMB_W, FLIGHT_THUMB_H, 1}}}; vkCmdBlitImage(cb, dst, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, g_th_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, VK_FILTER_LINEAR); sync_barrier(cb, g_th_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); VkBufferImageCopy bc = {.bufferOffset = TH_BYTES * ring, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .imageExtent = {FLIGHT_THUMB_W, FLIGHT_THUMB_H, 1}}; vkCmdCopyImageToBuffer(cb, g_th_img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, g_th_buf, 1, &bc); VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT, .dstAccessMask = VK_ACCESS_2_HOST_READ_BIT}; VkDependencyInfo d = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb}; vkCmdPipelineBarrier2(cb, &d); g_th_pending[fence_slot] = f; g_th_frame = 0; } int vk_thumbs_read(uint64_t frame, uint8_t *rgba) { int ok = 0; plat_rmutex_lock(&g_pl); if (g_th_n && g_th_ptr && frame) { unsigned ring = (unsigned)(frame % (uint64_t)g_th_n); if (g_th_tag[ring] == frame) { VkCtx *c = vk_ctx(); vmaInvalidateAllocation(c->vma, g_th_alloc, TH_BYTES * ring, TH_BYTES); memcpy(rgba, g_th_ptr + TH_BYTES * ring, TH_BYTES); ok = g_th_fmt == VK_FORMAT_B8G8R8A8_UNORM || g_th_fmt == VK_FORMAT_B8G8R8A8_SRGB ? 2 : 1; } } plat_rmutex_unlock(&g_pl); if (ok == 2) for (size_t i = 0; i < TH_BYTES; i += 4) { uint8_t t = rgba[i]; rgba[i] = rgba[i + 2], rgba[i + 2] = t; } return ok != 0; } void vk_present_capture(int on) { plat_rmutex_lock(&g_pl); g_capture = on; if (!on) g_cap_valid = 0; plat_rmutex_unlock(&g_pl); } int vk_present_captured(const uint8_t **px, int *w, int *h, VkFormat *fmt) { plat_rmutex_lock(&g_pl); int ok = g_cap_valid; if (ok) { *px = g_cap_ptr; *w = g_cap_w; *h = g_cap_h; *fmt = g_sc_fmt; } plat_rmutex_unlock(&g_pl); return ok; } // The captured swapchain image as a PNG (the menu's test hook), RGBA with opaque alpha. static void save_capture(const char *path) { size_t n = (size_t)g_cap_w * (size_t)g_cap_h * 4; uint8_t *px = malloc(n); if (!px) return; int bgra = g_sc_fmt == VK_FORMAT_B8G8R8A8_UNORM || g_sc_fmt == VK_FORMAT_B8G8R8A8_SRGB; if (g_sc_hdr) vk_hdr_to_srgb8(g_cap_ptr, px, n / 4, g_sc_fmt == VK_FORMAT_A2R10G10B10_UNORM_PACK32); else for (size_t i = 0; i < n; i += 4) { px[i] = g_cap_ptr[i + (bgra ? 2 : 0)]; px[i + 1] = g_cap_ptr[i + 1]; px[i + 2] = g_cap_ptr[i + (bgra ? 0 : 2)]; px[i + 3] = 255; } rt_write_png(path, px, (uint32_t)g_cap_w, (uint32_t)g_cap_h); free(px); rt_log("gfx: menu test shot %s (%dx%d)", path, g_cap_w, g_cap_h); } // The rectangle {x, y, w, h} of a dw x dh swapchain image the cw x ch picture goes into: the whole image with // TOTK_ASPECT=stretch, else (fit, the default) the largest one of the picture's own aspect ratio, centred (a 16:9 picture // on the Steam Deck's 1280x800 screen gets bars of 40 rows above and below; a 16:9 picture in a 21:9 window, bars at the // sides). // Where the last present put the picture, as fractions of the swapchain (x, y, w, h): menu_cursor.c's pointer static float g_pic[4]; static volatile int32_t g_pic_ok; int gfx_picture_rect(float out[4]) { if (!__atomic_load_n(&g_pic_ok, __ATOMIC_ACQUIRE)) return 0; memcpy(out, g_pic, sizeof g_pic); return 1; } void gfx_fit_rect(int cw, int ch, int dw, int dh, int out[4]) { out[0] = out[1] = 0, out[2] = dw, out[3] = dh; if (cw <= 0 || ch <= 0 || !strcmp(cfg_value("TOTK_ASPECT"), "stretch")) return; if ((int64_t)dw * ch > (int64_t)dh * cw) out[2] = (int)(((int64_t)dh * cw + ch / 2) / ch); // window wider: pillarbox else out[3] = (int)(((int64_t)dw * ch + cw / 2) / cw); // taller: letterbox // A side of the window's parity keeps the two bars equal; a picture one pixel short of the window fills it out[2] += (dw - out[2]) & 1, out[3] += (dh - out[3]) & 1; out[0] = (dw - out[2]) / 2, out[1] = (dh - out[3]) / 2; } static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int h, const int crop[4], int defer) { int general = layout == VK_IMAGE_LAYOUT_GENERAL; gfx_window_start(); VkCtx *c = vk_ctx(); plat_rmutex_lock(&g_pl); flip_locked(); if (!g_sc || plat_cas32(&g_dirty, 0, 0)) { if (!wsys_minimized() && !create_swapchain() && !g_sc) goto out; } if (wsys_minimized() || !g_sc) goto out; unsigned slot = g_frame % FRAMES; if (vkWaitForFences(c->device, 1, &g_fence[slot], VK_TRUE, 5000000000ull) != VK_SUCCESS) { rt_log("gfx: present fence wait timed out"); goto out; } uint32_t idx = 0; VkResult r = vkAcquireNextImageKHR(c->device, g_sc, 1000000000ull, g_acquire[slot], VK_NULL_HANDLE, &idx); if (r == VK_ERROR_OUT_OF_DATE_KHR) { plat_xchg32(&g_dirty, 1); if (!create_swapchain()) goto out; r = vkAcquireNextImageKHR(c->device, g_sc, 1000000000ull, g_acquire[slot], VK_NULL_HANDLE, &idx); } vk_device_fault_report(r); if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) goto out; // lost, not ready: skip the frame if (r == VK_SUBOPTIMAL_KHR) plat_xchg32(&g_dirty, 1); vkResetFences(c->device, 1, &g_fence[slot]); if (g_sc_hdr) vk_hdr_metadata(g_sc, 0); // a changed peak or white goes to the display if (g_th_pending[slot]) { // that fence has signalled: the thumbnail its submission wrote is complete g_th_tag[g_th_pending[slot] % (uint64_t)g_th_n] = g_th_pending[slot]; g_th_pending[slot] = 0; } int cx = 0, cy = 0, cw = w, ch = h; if (crop && crop[2] > 0 && crop[3] > 0) { cx = crop[0]; cy = crop[1]; cw = crop[2]; ch = crop[3]; } if (cx < 0) cx = 0; if (cy < 0) cy = 0; if (cx + cw > w) cw = w - cx; if (cy + ch > h) ch = h - cy; window_settings_live(); // The menu overlay: laid out before recording (its first frame starts the ImGui back end and uploads the font) const char *menu_shot = NULL; MenuTarget mt = {c->instance, c->phys, c->device, c->gfx_family, c->gfx_queue, g_sc_hdr ? VK_FORMAT_R8G8B8A8_UNORM : g_sc_fmt, g_sc_ext.width, g_sc_ext.height, cw, ch, g_scaler}; int overlay = g_sc_view[idx] && menu_overlay_frame(__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED), &mt, &menu_shot); int cap = (g_capture || menu_shot) && ensure_capture_buffer(); int thumb = g_th_n && ensure_thumbs(); VkCommandBuffer cb = g_cb[slot]; vkResetCommandBuffer(cb, 0); VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT}; vkBeginCommandBuffer(cb, &bi); uint64_t crumb = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED); vk_crumb(cb, VK_CRUMB_PRESENT, crumb); VkImage dst = g_sc_img[idx]; sync_barrier(cb, dst, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); const int rect[4] = {cx, cy, cw, ch}; int to[4]; // where the picture goes in the swapchain image: {x, y, w, h} gfx_fit_rect(cw, ch, (int)g_sc_ext.width, (int)g_sc_ext.height, to); if (g_sc_ext.width && g_sc_ext.height) { for (int i = 0; i < 4; i++) g_pic[i] = (float)to[i] / (float)(i & 1 ? g_sc_ext.height : g_sc_ext.width); __atomic_store_n(&g_pic_ok, 1, __ATOMIC_RELEASE); } if (to[2] != (int)g_sc_ext.width || to[3] != (int)g_sc_ext.height) { // black bars beside or above and below it VkClearColorValue black = {.float32 = {0, 0, 0, 1}}; VkImageSubresourceRange all = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; vkCmdClearColorImage(cb, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &all); sync_barrier(cb, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); } // Colour adjustment (TOTK_COLOR_*, vk_color.c), or on an HDR10 swapchain the PQ encode (vk_hdr.c; the adjustment is // for SDR): a copy of the crop takes the game image's place below VkImage graded = !general ? VK_NULL_HANDLE : g_sc_hdr ? vk_hdr_encode(cb, slot, src, fmt, rect) : vk_color_record(cb, slot, src, fmt, rect); const int grect[4] = {0, 0, cw, ch}; VkImage psrc = graded ? graded : src; VkFormat pfmt = graded ? VK_FORMAT_R16G16B16A16_SFLOAT : fmt; const int *prect = graded ? grect : rect; // FSR (TOTK_UPSCALER) when the picture's rectangle is larger than the crop; else the bilinear blit int fsr = general && vk_fsr_record(cb, slot, psrc, pfmt, graded ? cw : w, graded ? ch : h, prect, dst, to); g_scaler = fsr ? 2 : cw != to[2] || ch != to[3]; if (!fsr) { if (general) // a GENERAL source (vk_texture.c images): earlier submissions' writes become visible to the blit here sync_barrier(cb, psrc, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); VkImageBlit region = { .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .srcOffsets = {{prect[0], prect[1], 0}, {prect[0] + cw, prect[1] + ch, 1}}, .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{to[0], to[1], 0}, {to[0] + to[2], to[1] + to[3], 1}}}; vkCmdBlitImage(cb, psrc, general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, VK_FILTER_LINEAR); if (general) // later submissions that write the source wait for the blit's read sync_barrier(cb, psrc, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, 0); } // last: the image's layout, and the stage and access of its last write, as each step leaves it VkImageLayout last = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; VkPipelineStageFlags2 last_stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT; VkAccessFlags2 last_write = VK_ACCESS_2_TRANSFER_WRITE_BIT; // HDR10: the menu goes into vk_hdr.c's RGBA8 image, which is then blended onto the swapchain image at the HUD's white VkImageView menu_view = overlay && g_sc_hdr ? vk_hdr_ui_target(cb, g_sc_ext.width, g_sc_ext.height) : VK_NULL_HANDLE; if (overlay && g_sc_hdr && !menu_view) overlay = 0; if (overlay) { sync_barrier(cb, dst, last, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, last_stage, last_write, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); VkRenderingAttachmentInfo ca = {.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, .imageView = menu_view ? menu_view : g_sc_view[idx], .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, .loadOp = menu_view ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD, .storeOp = VK_ATTACHMENT_STORE_OP_STORE}; VkRenderingInfo ri = {.sType = VK_STRUCTURE_TYPE_RENDERING_INFO, .renderArea = {{0, 0}, g_sc_ext}, .layerCount = 1, .colorAttachmentCount = 1, .pColorAttachments = &ca}; vkCmdBeginRendering(cb, &ri); menu_overlay_draw(cb); vkCmdEndRendering(cb); if (menu_view) vk_hdr_ui_compose(cb, g_sc_view[idx], g_sc_fmt, g_sc_ext.width, g_sc_ext.height); last = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; last_stage = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; last_write = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT; } if (cap) { sync_barrier(cb, dst, last, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, last_stage, last_write, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); VkBufferImageCopy bc = {.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .imageExtent = {g_sc_ext.width, g_sc_ext.height, 1}}; vkCmdCopyImageToBuffer(cb, dst, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, g_cap_buf, 1, &bc); last = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; last_stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT; last_write = 0; } if (thumb) { if (last != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) sync_barrier(cb, dst, last, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, last_stage, last_write, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); last = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; last_stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT; last_write = 0; record_thumb(cb, dst, slot); } sync_barrier(cb, dst, last, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, last_stage, last_write, VK_PIPELINE_STAGE_2_NONE, 0); vk_crumb(cb, VK_CRUMB_PRESENT_END, crumb); vkEndCommandBuffer(cb); VkSemaphoreSubmitInfo wait = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, .semaphore = g_acquire[slot], .stageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT}; VkSemaphoreSubmitInfo sig = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, .semaphore = g_render_done[idx], .stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT}; VkCommandBufferSubmitInfo cbi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer = cb}; VkSubmitInfo2 si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .waitSemaphoreInfoCount = 1, .pWaitSemaphoreInfos = &wait, .commandBufferInfoCount = 1, .pCommandBufferInfos = &cbi, .signalSemaphoreInfoCount = 1, .pSignalSemaphoreInfos = &sig}; vk_queue_lock(); r = vkQueueSubmit2(c->gfx_queue, 1, &si, g_fence[slot]); vk_queue_unlock(); if (r == VK_SUCCESS) { g_flip_on = 1; g_flip_idx = idx; g_flip_pid = g_blit_pid; // the frame this blit shows (vk_reflex_present_frame) if (!defer) flip_locked(); } else { vk_device_fault_report(r); rt_log("gfx: present submit failed (%d)", (int)r); } g_frame++; if (cap) { vkWaitForFences(c->device, 1, &g_fence[slot], VK_TRUE, 5000000000ull); vmaInvalidateAllocation(c->vma, g_cap_alloc, 0, VK_WHOLE_SIZE); g_cap_w = (int)g_sc_ext.width; g_cap_h = (int)g_sc_ext.height; g_cap_valid = 1; if (menu_shot) save_capture(menu_shot); } out: plat_rmutex_unlock(&g_pl); } void vk_present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int h, const int crop[4]) { present_image(src, layout, fmt, w, h, crop, 0); } void vk_present_blit(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int h, const int crop[4]) { present_image(src, layout, fmt, w, h, crop, 1); } void vk_present_flip(void) { plat_rmutex_lock(&g_pl); flip_locked(); plat_rmutex_unlock(&g_pl); } void gfx_window_resize(int cw, int chh) { gfx_window_start(); wsys_resize(cw, chh); plat_rmutex_lock(&g_pl); create_swapchain(); plat_rmutex_unlock(&g_pl); } // Called by vk_shutdown before the device goes away; the window and its thread stay. void gfx_window_release_vk(void) { VkCtx *c = vk_ctx(); plat_rmutex_lock(&g_pl); if (c && g_started) { vk_queue_lock(); vkDeviceWaitIdle(c->device); vk_queue_unlock(); g_flip_on = 0; // its blit is done (device idle); the swapchain goes away unpresented menu_vk_release(); vk_fsr_release(); vk_color_release(); vk_hdr_release(); vk_contact_release(); if (g_reflex_sem) { vkDestroySemaphore(c->device, g_reflex_sem, NULL); g_reflex_sem = VK_NULL_HANDLE; } g_reflex_mode = -1; free_capture(); free_thumbs(); for (int i = 0; i < FRAMES; i++) { vkDestroySemaphore(c->device, g_acquire[i], NULL); vkDestroyFence(c->device, g_fence[i], NULL); } for (uint32_t i = 0; i < g_sc_n; i++) { vkDestroySemaphore(c->device, g_render_done[i], NULL); if (g_sc_view[i]) vkDestroyImageView(c->device, g_sc_view[i], NULL); g_sc_view[i] = VK_NULL_HANDLE; } if (g_pool) vkDestroyCommandPool(c->device, g_pool, NULL); if (g_sc) vkDestroySwapchainKHR(c->device, g_sc, NULL); if (g_surface) vkDestroySurfaceKHR(c->instance, g_surface, NULL); g_sc = VK_NULL_HANDLE; g_surface = VK_NULL_HANDLE; g_pool = VK_NULL_HANDLE; g_sc_n = 0; g_started = 0; // the window and its thread stay; the next gfx_window_start makes a new surface } plat_rmutex_unlock(&g_pl); } void gfx_window_start(void) { plat_rmutex_lock(&g_start_lock); if (!g_started) { if (!vk_init()) rt_fatal("gfx: no usable Vulkan device (see the vk: lines above)"); VkCtx *c = vk_ctx(); static int opened; if (!opened && !(opened = wsys_open())) rt_fatal("gfx: window creation failed"); g_surface = wsys_surface(c->instance); if (!g_surface) rt_fatal("gfx: no Vulkan surface for the window"); VkCommandPoolCreateInfo pci = {.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, .queueFamilyIndex = c->gfx_family}; vkCreateCommandPool(c->device, &pci, NULL, &g_pool); VkCommandBufferAllocateInfo ai = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .commandPool = g_pool, .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = FRAMES}; vkAllocateCommandBuffers(c->device, &ai, g_cb); VkFenceCreateInfo fi = {.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .flags = VK_FENCE_CREATE_SIGNALED_BIT}; VkSemaphoreCreateInfo semi = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; for (int i = 0; i < FRAMES; i++) { vkCreateFence(c->device, &fi, NULL, &g_fence[i]); vkCreateSemaphore(c->device, &semi, NULL, &g_acquire[i]); } g_frame = 0; g_started = 1; plat_rmutex_lock(&g_pl); create_swapchain(); plat_rmutex_unlock(&g_pl); int cw = 0, ch = 0; wsys_client_size(&cw, &ch); rt_log("gfx: window %dx%d at DPI %u, swapchain %ux%u format %d", cw, ch, gfx_window_dpi(), g_sc_ext.width, g_sc_ext.height, (int)g_sc_fmt); } plat_rmutex_unlock(&g_start_lock); vsync_ensure(); }