Files

858 lines
42 KiB
C

// 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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=<w>x<h>: 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 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();
started = 1;
}
// ---- 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;
VkPresentInfoKHR pi = {.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, .waitSemaphoreCount = 1,
.pWaitSemaphores = &g_render_done[g_flip_idx], .swapchainCount = 1, .pSwapchains = &g_sc,
.pImageIndices = &g_flip_idx};
vk_queue_lock();
VkResult r = vkQueuePresentKHR(vk_ctx()->gfx_queue, &pi);
vk_queue_unlock();
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;
VkSwapchainCreateInfoKHR ci = {
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, .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();
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, &region,
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, &region, 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;
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();
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();
}