Files
NX2navi/runtime/gfx_present.c
chrg6 a42648e27a Add the game's native HDR, render at the monitor's real size, and refresh movie frames on the GPU.
HDR is now a menu row: Off, Enhanced (the runtime's own HDR tonemap) or Native. Native reports an HDR10 display to
the game with the settings' peak and paper white; the game then renders its own HDR picture and presents it as PQ in
A2B10G10R10, which goes to the HDR10 swapchain as it is. The peak, game, HUD and (for Enhanced) hue rows follow it.

The build kit's link left out runtime/nx2navi.rc, so the game had no manifest and no icon: without per-monitor DPI
awareness, a 5120x2160 monitor at 150 % gave a 3413x1440 window that Windows stretched. It is compiled with windres
and linked again.

The movie decoder's frames are copied into the game's buffer by the host; the back end is now told of that write, so
the GPU's copy of the buffer does not keep older or empty bytes (green stripes in NV12).

A longjmp out of the game's JPEG decoder now logs the guest functions that called it, to trace a later crash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 13:31:07 -05:00

1139 lines
59 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).
// Contains code Copyright (c) 2026 Peiikai, Copyright (c) 2026 MacNativeGames contributors (MIT; see
// THIRD_PARTY_NOTICES.md).
//
// 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 NX2NAVI_WINDOW_SIZE and NX2NAVI_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"
#include "vk_fg.h"
#include "vk_dlaa.h"
#include "vk_dlssg.h"
#define FRAMES 2
#define MAX_IMAGES 16
#define VSYNC_NS 1000000000ull
#define VSYNC_HZ 60
// ---- queue lock
static PlatRMutex g_ql;
void vk_queue_lock(void) { plat_rmutex_lock(&g_ql); }
void vk_queue_unlock(void) { 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 NX2NAVI_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, 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();
prof_write(); // plat_exit runs no atexit handlers
if (restart) {
plat_setenv("NX2NAVI_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();
}
// NX2NAVI_FULLSCREEN=1: borderless window over the monitor. NX2NAVI_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("NX2NAVI_FULLSCREEN"); }
void gfx_cfg_window_size(int *w, int *h) {
int x = 0, y = 0;
if (sscanf(cfg_value("NX2NAVI_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 (NX2NAVI_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 (NX2NAVI_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,
// 3 shrunk by the blit and sharpened by RCAS
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;
// ---- NX2NAVI_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 NX2NAVI_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 NX2NAVI_VK_PRESENT the swapchain was made with
unsigned v = cfg_version();
if (started && v == seen) return;
seen = v;
res_fov_live();
const char *e = cfg_value("NX2NAVI_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 NX2NAVI_VK_PRESENT value of the mode the swapchain got; "" before one
static VkPresentModeKHR pick_present_mode(VkPhysicalDevice pd) {
const char *e = cfg_value("NX2NAVI_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;
int relaxed = 0, immediate = 0;
for (uint32_t i = 0; i < n; i++) {
if (m[i] == want) got = want;
relaxed |= m[i] == VK_PRESENT_MODE_FIFO_RELAXED_KHR, immediate |= m[i] == VK_PRESENT_MODE_IMMEDIATE_KHR;
}
// Mailbox missing (several AMD drivers): plain FIFO holds a frame that misses a refresh to the next one, and the
// game, paced at 60 on a 60 Hz display, drops to 30 at every slow frame. Relaxed FIFO shows a late frame at once
// (a tear that once) and waits for the refresh otherwise; immediate where that is missing too.
if (want == VK_PRESENT_MODE_MAILBOX_KHR && got != want)
got = relaxed ? VK_PRESENT_MODE_FIFO_RELAXED_KHR : immediate ? VK_PRESENT_MODE_IMMEDIATE_KHR : got;
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 (NX2NAVI_VK_PRESENT=%s)",
got == VK_PRESENT_MODE_FIFO_RELAXED_KHR ? "fifo relaxed" : 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 = vk_dlssg_active() ? vk_dlssg_present(vk_ctx()->gfx_queue, &pi)
: vk_fg_active() ? vk_fg_present(&pi) : 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;
}
// Returns 1 with a usable swapchain, 0 when the window has no area (minimised). Called with g_pl held.
static int create_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). NX2NAVI_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};
// Frame generation (vk_fg.cpp): AMD's swapchain in this one's place. Its swapchain is replaced only after the old
// one is gone; a first creation that fails leaves the plain swapchain below.
VkResult r = VK_ERROR_FEATURE_NOT_PRESENT;
if (vk_fg_wanted()) {
if (old && vk_fg_active()) vk_fg_destroy_swapchain(old);
else if (old) vkDestroySwapchainKHR(c->device, old, NULL);
ci.oldSwapchain = VK_NULL_HANDLE;
old = g_sc = VK_NULL_HANDLE;
r = vk_fg_create_swapchain(&ci, &g_sc);
}
if (r != VK_SUCCESS && vk_dlssg_active()) r = vk_dlssg_create_swapchain(c->device, &ci, &g_sc); // Streamline's hook
else if (r != VK_SUCCESS) 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 && vk_dlssg_active()) vk_dlssg_destroy_swapchain(c->device, old);
else if (old) vkDestroySwapchainKHR(c->device, old, NULL);
g_sc_fmt = sf.format;
g_sc_hdr = hdr;
vk_hdr_set_active(hdr);
if (hdr && !vk_fg_active()) vk_hdr_metadata(g_sc, 1);
g_sc_ext = ext;
g_sc_n = MAX_IMAGES;
if (vk_fg_active()) { // AMD's swapchain: the count first (a larger capacity is not cut down to it)
uint32_t n = 0;
vk_fg_get_images(g_sc, &n, NULL);
g_sc_n = n < MAX_IMAGES ? n : MAX_IMAGES;
memset(g_sc_img, 0, sizeof g_sc_img);
vk_fg_get_images(g_sc, &g_sc_n, g_sc_img);
} else if (vk_dlssg_active()) vk_dlssg_get_images(c->device, g_sc, &g_sc_n, g_sc_img);
else 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();
plat_xchg32(&g_dirty, 0);
return 1;
}
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
// NX2NAVI_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("NX2NAVI_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;
}
// ---- Movie overlay (ov_movie2.c): the latest decoded movie frame goes over the game image, fit to the window
// with black bars, while frames keep coming (GFX_MOVIE_HOLD_NS after the last one it disappears). It made movies
// visible before the game's own shaders translated; now the game draws them itself (its YUV compute pass, then the
// upscaler fallback of vk_exec_draw.c), under its menus, so the overlay is off unless NX2NAVI_MOVIE_OVERLAY=1. One staging buffer per frame in flight: the CPU writes slot i only after
// that slot's fence (the present waits it before recording), the GPU copies it into one RGBA8 image.
#define GFX_MOVIE_HOLD_NS 400000000ull
static PlatMutex g_mv_lock = PLAT_MUTEX_INIT;
static uint8_t *g_mv_rgba;
static int g_mv_w, g_mv_h;
static uint64_t g_mv_ns, g_mv_serial;
static VkBuffer g_mvb[FRAMES];
static VmaAllocation g_mva[FRAMES];
static void *g_mvp[FRAMES];
static VkDeviceSize g_mvsz[FRAMES];
static uint64_t g_mv_slot_serial[FRAMES];
static VkImage g_mvi;
static VmaAllocation g_mvia;
static int g_mvi_w, g_mvi_h;
void gfx_movie_frame(const uint8_t *rgba, int w, int h) {
if (w <= 0 || h <= 0) return;
plat_mutex_lock(&g_mv_lock);
size_t need = (size_t)w * h * 4;
if (w != g_mv_w || h != g_mv_h || !g_mv_rgba) {
free(g_mv_rgba);
g_mv_rgba = malloc(need);
g_mv_w = g_mv_rgba ? w : 0, g_mv_h = g_mv_rgba ? h : 0;
}
if (g_mv_rgba) {
memcpy(g_mv_rgba, rgba, need);
g_mv_ns = plat_now_ns();
g_mv_serial++;
}
plat_mutex_unlock(&g_mv_lock);
}
void gfx_movie_frame_own(uint8_t *rgba, int w, int h) {
if (w <= 0 || h <= 0 || !rgba) {
free(rgba);
return;
}
plat_mutex_lock(&g_mv_lock);
uint8_t *old = g_mv_rgba;
g_mv_rgba = rgba;
g_mv_w = w, g_mv_h = h;
g_mv_ns = plat_now_ns();
g_mv_serial++;
plat_mutex_unlock(&g_mv_lock);
free(old);
}
void gfx_movie_stop(void) {
plat_mutex_lock(&g_mv_lock);
g_mv_ns = 0;
plat_mutex_unlock(&g_mv_lock);
}
// Records the overlay into cb for swapchain image dst (in TRANSFER_DST_OPTIMAL, left so). 1 when drawn.
static int movie_overlay(VkCommandBuffer cb, int slot, VkImage dst) {
static int off = -1;
if (off < 0) {
const char *v = cfg_get("NX2NAVI_MOVIE_OVERLAY");
off = !(v && strcmp(v, "0"));
}
if (off) return 0;
VkCtx *c = vk_ctx();
plat_mutex_lock(&g_mv_lock);
if (!g_mv_ns || plat_now_ns() - g_mv_ns > GFX_MOVIE_HOLD_NS || !g_mv_rgba) {
plat_mutex_unlock(&g_mv_lock);
return 0;
}
int w = g_mv_w, h = g_mv_h;
VkDeviceSize need = (VkDeviceSize)w * h * 4;
if (g_mvsz[slot] != need) {
if (g_mvb[slot]) vmaDestroyBuffer(c->vma, g_mvb[slot], g_mva[slot]);
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = need,
.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
VMA_ALLOCATION_CREATE_MAPPED_BIT};
VmaAllocationInfo info;
if (vmaCreateBuffer(c->vma, &bi, &ai, &g_mvb[slot], &g_mva[slot], &info) != VK_SUCCESS) {
g_mvb[slot] = VK_NULL_HANDLE, g_mvsz[slot] = 0;
plat_mutex_unlock(&g_mv_lock);
return 0;
}
g_mvp[slot] = info.pMappedData;
g_mvsz[slot] = need;
g_mv_slot_serial[slot] = 0;
}
if (g_mv_slot_serial[slot] != g_mv_serial) {
memcpy(g_mvp[slot], g_mv_rgba, (size_t)need);
g_mv_slot_serial[slot] = g_mv_serial;
}
plat_mutex_unlock(&g_mv_lock);
if (g_mvi_w != w || g_mvi_h != h) {
if (g_mvi) {
vkDeviceWaitIdle(c->device);
vmaDestroyImage(c->vma, g_mvi, g_mvia);
g_mvi = VK_NULL_HANDLE;
}
VkImageCreateInfo ii = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
.format = VK_FORMAT_R8G8B8A8_UNORM, .extent = {(uint32_t)w, (uint32_t)h, 1},
.mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT,
.tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
if (vmaCreateImage(c->vma, &ii, &ai, &g_mvi, &g_mvia, NULL) != VK_SUCCESS) {
g_mvi = VK_NULL_HANDLE, g_mvi_w = g_mvi_h = 0;
return 0;
}
g_mvi_w = w, g_mvi_h = h;
}
sync_barrier(cb, g_mvi, 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);
VkBufferImageCopy bc = {.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
.imageExtent = {(uint32_t)w, (uint32_t)h, 1}};
vkCmdCopyBufferToImage(cb, g_mvb[slot], g_mvi, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &bc);
sync_barrier(cb, g_mvi, 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);
// the game image's blit wrote dst: black bars first, then the frame
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);
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);
int to[4];
gfx_fit_rect(w, h, (int)g_sc_ext.width, (int)g_sc_ext.height, to);
VkImageBlit region = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
.srcOffsets = {{0, 0, 0}, {w, h, 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, g_mvi, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region,
VK_FILTER_LINEAR);
return 1;
}
// The game image's crop into dst at `to`: colour adjustment (or the HDR10 encode), then FSR or the bilinear blit; dst in
// TRANSFER_DST_OPTIMAL. Returns vk_fsr_record's result (0: the blit scaled it).
static int put_picture(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt, int w, int h, const int rect[4], VkImage dst,
const int to[4], int general) {
int cw = rect[2], ch = rect[3];
// Colour adjustment (NX2NAVI_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 (NX2NAVI_UPSCALER): EASU when the picture's rectangle is larger than the crop, the shrink and RCAS when it is
// smaller; else the bilinear blit
int fsr = general ? vk_fsr_record(cb, slot, psrc, pfmt, graded ? cw : w, graded ? ch : h, prect, dst, to) : 0;
if (!fsr && src) {
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);
}
return fsr;
}
// The display's frame rate with frame generation: the swapchain's present count (generated frames included, vk_fg.cpp)
// over half a second, sampled at each of the game's presents (gfx_output_fps).
static volatile double g_out_fps;
static void output_rate(void) {
static uint64_t t0, n0;
uint64_t n = vk_fg_presents(g_sc), t = plat_now_ns();
if (!n) { g_out_fps = 0, t0 = 0; return; }
if (!t0 || n < n0) { t0 = t, n0 = n; return; }
if (t - t0 >= 500000000ull) g_out_fps = (double)(n - n0) * 1e9 / (double)(t - t0), t0 = t, n0 = n;
}
double gfx_output_fps(void) { return g_out_fps; }
static VkImage g_hl; // frame generation's HUD-less picture at the swapchain's size (hudless_record)
static VmaAllocation g_hl_mem;
static VkImageView g_hl_view;
static uint32_t g_hl_w, g_hl_h;
static VkFormat g_hl_fmt;
static VkImage hudless_record(VkCommandBuffer cb, unsigned slot, int w, int h, const int rect[4], const int to[4], int general) {
VkImage pic;
VkImageView pv;
uint32_t pw, ph;
if (!vk_fg_active() || !vk_dlaa_fg_hudless(&pic, &pv, &pw, &ph) || (int)pw != w || (int)ph != h) return VK_NULL_HANDLE;
VkCtx *c = vk_ctx();
if (g_hl && (g_hl_w != g_sc_ext.width || g_hl_h != g_sc_ext.height || g_hl_fmt != g_sc_fmt)) {
vkDeviceWaitIdle(c->device);
vkDestroyImageView(c->device, g_hl_view, NULL);
vmaDestroyImage(c->vma, g_hl, g_hl_mem);
g_hl = VK_NULL_HANDLE;
}
if (!g_hl) {
VkImageCreateInfo ii = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = g_sc_fmt,
.extent = {g_sc_ext.width, g_sc_ext.height, 1}, .mipLevels = 1, .arrayLayers = 1,
.samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
if (vmaCreateImage(c->vma, &ii, &ai, &g_hl, &g_hl_mem, NULL) != VK_SUCCESS) return g_hl = VK_NULL_HANDLE;
VkImageViewCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = g_hl,
.viewType = VK_IMAGE_VIEW_TYPE_2D, .format = g_sc_fmt,
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
if (vkCreateImageView(c->device, &vi, NULL, &g_hl_view) != VK_SUCCESS) g_hl_view = VK_NULL_HANDLE;
g_hl_w = g_sc_ext.width, g_hl_h = g_sc_ext.height, g_hl_fmt = g_sc_fmt;
rt_log("gfx: frame generation's HUD-less picture at %ux%u", g_hl_w, g_hl_h);
}
// the steps' work images were just read for the presented picture: they are written again only after that
sync_barrier(cb, g_hl, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT);
if (to[2] != (int)g_sc_ext.width || to[3] != (int)g_sc_ext.height) {
VkClearColorValue black = {.float32 = {0, 0, 0, 1}};
VkImageSubresourceRange all = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdClearColorImage(cb, g_hl, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &all);
sync_barrier(cb, g_hl, 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);
}
put_picture(cb, slot, pic, VK_FORMAT_R8G8B8A8_UNORM, w, h, rect, g_hl, to, general);
sync_barrier(cb, g_hl, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT);
return g_hl;
}
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;
{ // the presented image's format, when it changes (an HDR picture comes in another one)
static VkFormat last = VK_FORMAT_UNDEFINED;
if (src && fmt != last) last = fmt, rt_log("gfx: the game presents %dx%d images of format %d", w, h, (int)fmt);
}
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 = vk_dlssg_active() ? vk_dlssg_acquire(c->device, g_sc, 1000000000ull, g_acquire[slot], &idx)
: vk_fg_active() ? vk_fg_acquire(g_sc, 1000000000ull, g_acquire[slot], &idx)
: 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 = vk_dlssg_active() ? vk_dlssg_acquire(c->device, g_sc, 1000000000ull, g_acquire[slot], &idx)
: vk_fg_active() ? vk_fg_acquire(g_sc, 1000000000ull, g_acquire[slot], &idx)
: 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_fg_active()) 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};
// without a source image (gfx_present_boot): a black frame with the boot screen
int overlay = g_sc_view[idx] &&
(src ? menu_overlay_frame(__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED), &mt, &menu_shot)
: menu_boot_frame(&mt));
// NX2NAVI_SHOT_EVERY=<s> with NX2NAVI_SHOT_DIR=<dir>: the presented frame as shot_<n>.png every s seconds
// (a check of what the window shows, for runs nobody watches)
static char shot_path[1100];
{
static double every = -1;
static uint64_t next_ns;
static int n;
if (every < 0) {
const char *e = cfg_get("NX2NAVI_SHOT_EVERY");
every = e ? atof(e) : 0;
const char *after = cfg_get("NX2NAVI_SHOT_AFTER"); // seconds after the start before the first shot
next_ns = plat_now_ns() + (uint64_t)((every + (after ? atof(after) : 0)) * 1e9);
}
const char *dir = cfg_get("NX2NAVI_SHOT_DIR");
if (!menu_shot && every > 0 && dir && plat_now_ns() >= next_ns && n < 200) {
snprintf(shot_path, sizeof shot_path, "%s/shot_%03d.png", dir, n++);
menu_shot = shot_path;
next_ns = plat_now_ns() + (uint64_t)(every * 1e9);
}
}
int cap = (g_capture || menu_shot) && ensure_capture_buffer();
int thumb = src && 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 (src && 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 (!src || 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);
}
int fsr = put_picture(cb, slot, src, fmt, w, h, rect, dst, to, general);
if (src) g_scaler = fsr == 2 ? 3 : fsr ? 2 : cw != to[2] || ch != to[3];
// Frame generation without the HUD in its generated frames: the picture as it would be without the HUD (vk_dlaa.c,
// DLSS's or FSR 3.1's output before the HUD was drawn over it) goes through the same steps into an image of the
// swapchain's size, which frame generation compares with the presented one to find the HUD and leave it unwarped
VkImage hudless = src && !g_sc_hdr ? hudless_record(cb, slot, w, h, rect, to, general) : VK_NULL_HANDLE;
static int show_hl = -1; // NX2NAVI_FG_HUDLESS_SHOW=1 (test): the HUD-less picture on screen instead
if (show_hl < 0) show_hl = cfg_get("NX2NAVI_FG_HUDLESS_SHOW") && !strcmp(cfg_get("NX2NAVI_FG_HUDLESS_SHOW"), "1");
if (hudless && show_hl) {
VkImageCopy cp = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
.extent = {g_sc_ext.width, g_sc_ext.height, 1}};
vkCmdCopyImage(cb, hudless, VK_IMAGE_LAYOUT_GENERAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &cp);
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);
}
if (!g_sc_hdr && src) movie_overlay(cb, slot, dst); // a movie frame over the game image (SDR swapchains)
// 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);
}
if (src) vk_fg_frame(cb, g_sc, g_sc_fmt, g_sc_ext, hudless, g_hl_view); // frame generation: this frame's preparation
if (src) output_rate();
if (src) vk_dlssg_frame(cb, g_sc_ext); // DLSS Frame Generation: constants and tags (vk_dlssg.c)
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);
}
// The boot screen on a black frame (menu_boot_frame), without a game image: present_image's paths that read a source
// are skipped. The size only places the picture rectangle, which a frame without a picture leaves as it was.
void gfx_present_boot(void) { present_image(VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED, VK_FORMAT_UNDEFINED, 1920, 1080, NULL, 0); }
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 && vk_fg_active()) vk_fg_destroy_swapchain(g_sc), vk_fg_release();
else if (g_sc && vk_dlssg_active()) vk_dlssg_destroy_swapchain(c->device, g_sc);
else 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();
}