mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Where RTSS is installed and running, the frame limit from the menu goes into RTSS's profile for the game's executable through RTSS's hooks library, loaded at run time, and the game's own pacer stands aside while RTSS has hooked the game. Menu changes apply live, unlimited clears the limit, and closing the game (or any exit path) puts back what the profile held, or removes a profile the game created. Each change is read back from the saved profile: without administrator rights RTSS's profile folder cannot be written, and the game then keeps its own limiter. While RTSS limits, RTSS's own frame counter for the game is checked against the limit; well past it for three seconds, the game takes the limiting back for the run. The log says which limiter is in use and why, and a note under the frame-rate row says when RTSS limits, when its profile is read-only, and when it does not hold the game to the limit. rtss = 0 and frame generation keep RTSS out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1180 lines
61 KiB
C
1180 lines
61 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");
|
|
cfg_save(); // a menu change saves 400 ms later on the render thread (option_rows.cpp); closing sooner kept the old file
|
|
vk_pipeline_save();
|
|
prof_write(); // plat_exit runs no atexit handlers
|
|
rtss_limit_restore(); // RivaTuner Statistics Server's profile as it was
|
|
fs_save_flush(); // last: the game's threads still run until plat_exit, and a Commit after this one is refused
|
|
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) or the create failed, which leaves
|
|
// g_dirty set so the next present tries again. Called with g_pl held. g_dirty is cleared before the window's size is
|
|
// read: a resize that arrives while the swapchain is being made (AMD's frame generation swapchain takes tens of ms)
|
|
// sets it again, where clearing it at the end lost that resize and kept a swapchain of the old size, whose acquires
|
|
// then timed out frame after frame behind AMD's presenter (the picture stopped).
|
|
static int create_swapchain(void) {
|
|
flip_locked(); // the acquired image goes out before its swapchain is replaced
|
|
plat_xchg32(&g_dirty, 0);
|
|
VkCtx *c = vk_ctx();
|
|
VkSurfaceCapabilitiesKHR caps;
|
|
if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(c->phys, g_surface, &caps) != VK_SUCCESS) return plat_xchg32(&g_dirty, 1), 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 plat_xchg32(&g_dirty, 1), 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 plat_xchg32(&g_dirty, 1), 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);
|
|
// once AMD's swapchain context exists every swapchain call goes through it: a plain swapchain in its place would
|
|
// get AMD's acquire and present. Try again at the next present instead.
|
|
if (r != VK_SUCCESS && vk_fg_active()) {
|
|
static volatile int32_t n_fail;
|
|
if (plat_inc32(&n_fail) <= 5) rt_log("gfx: frame generation swapchain %ux%u failed (%d); trying again", ext.width, ext.height, (int)r);
|
|
g_sc = VK_NULL_HANDLE;
|
|
vk_queue_unlock();
|
|
plat_xchg32(&g_dirty, 1);
|
|
return 0;
|
|
}
|
|
}
|
|
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();
|
|
plat_xchg32(&g_dirty, 1);
|
|
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_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()) { // Streamline's swapchain too: the count first, then only that many images
|
|
uint32_t n = 0;
|
|
vk_dlssg_get_images(c->device, g_sc, &n, NULL);
|
|
g_sc_n = n < MAX_IMAGES ? n : MAX_IMAGES;
|
|
memset(g_sc_img, 0, sizeof g_sc_img);
|
|
if (vk_dlssg_get_images(c->device, g_sc, &g_sc_n, g_sc_img) < 0) g_sc_n = 0;
|
|
for (uint32_t i = 0; i < g_sc_n; i++)
|
|
if (!g_sc_img[i]) g_sc_n = i; // never a view of a null image
|
|
}
|
|
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();
|
|
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, ®ion,
|
|
VK_FILTER_LINEAR);
|
|
sync_barrier(cb, g_th_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
|
|
VK_ACCESS_2_TRANSFER_READ_BIT);
|
|
VkBufferImageCopy bc = {.bufferOffset = TH_BYTES * ring, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
|
.imageExtent = {FLIGHT_THUMB_W, FLIGHT_THUMB_H, 1}};
|
|
vkCmdCopyImageToBuffer(cb, g_th_img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, g_th_buf, 1, &bc);
|
|
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT,
|
|
.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT,
|
|
.dstAccessMask = VK_ACCESS_2_HOST_READ_BIT};
|
|
VkDependencyInfo d = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
|
|
vkCmdPipelineBarrier2(cb, &d);
|
|
g_th_pending[fence_slot] = f;
|
|
g_th_frame = 0;
|
|
}
|
|
|
|
int vk_thumbs_read(uint64_t frame, uint8_t *rgba) {
|
|
int ok = 0;
|
|
plat_rmutex_lock(&g_pl);
|
|
if (g_th_n && g_th_ptr && frame) {
|
|
unsigned ring = (unsigned)(frame % (uint64_t)g_th_n);
|
|
if (g_th_tag[ring] == frame) {
|
|
VkCtx *c = vk_ctx();
|
|
vmaInvalidateAllocation(c->vma, g_th_alloc, TH_BYTES * ring, TH_BYTES);
|
|
memcpy(rgba, g_th_ptr + TH_BYTES * ring, TH_BYTES);
|
|
ok = g_th_fmt == VK_FORMAT_B8G8R8A8_UNORM || g_th_fmt == VK_FORMAT_B8G8R8A8_SRGB ? 2 : 1;
|
|
}
|
|
}
|
|
plat_rmutex_unlock(&g_pl);
|
|
if (ok == 2)
|
|
for (size_t i = 0; i < TH_BYTES; i += 4) {
|
|
uint8_t t = rgba[i];
|
|
rgba[i] = rgba[i + 2], rgba[i + 2] = t;
|
|
}
|
|
return ok != 0;
|
|
}
|
|
|
|
void vk_present_capture(int on) {
|
|
plat_rmutex_lock(&g_pl);
|
|
g_capture = on;
|
|
if (!on) g_cap_valid = 0;
|
|
plat_rmutex_unlock(&g_pl);
|
|
}
|
|
|
|
int vk_present_captured(const uint8_t **px, int *w, int *h, VkFormat *fmt) {
|
|
plat_rmutex_lock(&g_pl);
|
|
int ok = g_cap_valid;
|
|
if (ok) { *px = g_cap_ptr; *w = g_cap_w; *h = g_cap_h; *fmt = g_sc_fmt; }
|
|
plat_rmutex_unlock(&g_pl);
|
|
return ok;
|
|
}
|
|
|
|
// The captured swapchain image as a PNG (the menu's test hook), RGBA with opaque alpha.
|
|
static void save_capture(const char *path) {
|
|
size_t n = (size_t)g_cap_w * (size_t)g_cap_h * 4;
|
|
uint8_t *px = malloc(n);
|
|
if (!px) return;
|
|
int bgra = g_sc_fmt == VK_FORMAT_B8G8R8A8_UNORM || g_sc_fmt == VK_FORMAT_B8G8R8A8_SRGB;
|
|
if (g_sc_hdr) vk_hdr_to_srgb8(g_cap_ptr, px, n / 4, g_sc_fmt == VK_FORMAT_A2R10G10B10_UNORM_PACK32);
|
|
else for (size_t i = 0; i < n; i += 4) {
|
|
px[i] = g_cap_ptr[i + (bgra ? 2 : 0)];
|
|
px[i + 1] = g_cap_ptr[i + 1];
|
|
px[i + 2] = g_cap_ptr[i + (bgra ? 0 : 2)];
|
|
px[i + 3] = 255;
|
|
}
|
|
rt_write_png(path, px, (uint32_t)g_cap_w, (uint32_t)g_cap_h);
|
|
free(px);
|
|
rt_log("gfx: menu test shot %s (%dx%d)", path, g_cap_w, g_cap_h);
|
|
}
|
|
|
|
// The rectangle {x, y, w, h} of a dw x dh swapchain image the cw x ch picture goes into: the whole image with
|
|
// 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) {
|
|
vk_queue_lock(), vkDeviceWaitIdle(c->device), vk_queue_unlock();
|
|
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, ®ion,
|
|
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, ®ion, VK_FILTER_LINEAR);
|
|
if (general) // later submissions that write the source wait for the blit's read
|
|
sync_barrier(cb, psrc, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0,
|
|
VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, 0);
|
|
}
|
|
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;
|
|
VkFormat pf;
|
|
if (!vk_fg_active() || !vk_dlaa_fg_hudless(&pic, &pv, &pw, &ph, &pf) || (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)) {
|
|
vk_queue_lock(), vkDeviceWaitIdle(c->device), vk_queue_unlock();
|
|
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);
|
|
}
|
|
// the steps' per-slot views of their source: a slot of its own (the presented picture's views stay valid for this
|
|
// command buffer); with HDR the PQ encode as for the presented float picture
|
|
put_picture(cb, slot + FRAMES, pic, pf, 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);
|
|
// lost, not ready: skip the frame. Acquires that keep timing out (1 s each) mean a swapchain nothing will free an
|
|
// image of again: three in a row rebuild it, where the picture used to stop for good.
|
|
static uint32_t n_timeouts;
|
|
if (r == VK_TIMEOUT || r == VK_NOT_READY) {
|
|
if (++n_timeouts >= 3) {
|
|
rt_log("gfx: no swapchain image for %u acquires (%d): the swapchain is made again", n_timeouts, (int)r);
|
|
n_timeouts = 0;
|
|
plat_xchg32(&g_dirty, 1);
|
|
}
|
|
} else {
|
|
n_timeouts = 0;
|
|
}
|
|
if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) goto out;
|
|
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};
|
|
// 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 ? 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, to); // 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();
|
|
vk_queue_lock(), vkDeviceWaitIdle(c->device), vk_queue_unlock();
|
|
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();
|
|
}
|