Files
NX2navi/runtime/vk_pipeline.c
chrg6 677a69378f Fill the fragment inputs a mesh program does not write, with the point sprite's coordinate for point meshes.
A mesh pipeline whose fragment stage reads a location the mesh program never writes now gets a fragment module where
that input is a private variable set to (0, 0, 0, 1) at entry, or to (s, t, 0, 1) of gl_PointCoord when the mesh
program draws points. The fragment shaders recorded with two of the game's point-drawing mesh programs read location
5, which those programs never write; Vulkan leaves such an input undefined.

Checks: tests/fs_fill_inputs_check.py passes; 237 recorded fragment modules rewritten offline pass spirv-val; a
60 s Kakariko run with the change shows the area as before with no Vulkan errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:38:18 -05:00

7466 lines
408 KiB
C

// Graphics and compute pipelines and descriptor layouts of the Vulkan replay. The op-stream replay (vk_exec.c)
// collects NVN state into a VkPipeState and asks for the pipeline of each draw; this file maps that state to Vulkan,
// numbers the descriptor bindings and keeps the caches.
//
// Binding model. The translator (tools/xlate/Program.cs, a SPIR-V back end) decorates every resource of a
// shader with DescriptorSet 0 and numbers the bindings of each shader from 1 in the order it meets them (Accessor
// Create*Binding); binding 0 is its support buffer (ResourceManager: SupportBuffer.Binding, a std140 uniform
// buffer every shader declares). The numbering is per shader, so the vertex and fragment stages of one program collide.
// The scheme here:
// - one descriptor set, set 0, per pipeline; no push constants (the translator's SPIR-V uses none);
// - binding 0: the support buffer, one uniform buffer shared by all stages of the pipeline;
// - vertex stage: its own bindings 1..n unchanged; fragment stage: its bindings b >= 1 move to b + n (the SPIR-V
// Binding decorations are rewritten when the module is created); compute: its own bindings;
// - descriptor types from the reflection: constant buffers UNIFORM_BUFFER, storage buffers STORAGE_BUFFER, textures
// COMBINED_IMAGE_SAMPLER (UNIFORM_TEXEL_BUFFER for buffer textures, SAMPLED_IMAGE for separate ones), images
// STORAGE_IMAGE (STORAGE_TEXEL_BUFFER for buffer images), array lengths as descriptor counts;
// - set 0 is a push-descriptor set (VK_KHR_push_descriptor) when its descriptors fit maxPushDescriptors (32 on the RX
// 9070; a pipeline layout may hold one push set only, which rules out a set per stage); larger ones (a vertex and a
// fragment stage near the census maxima of 15 and 27) are ordinary sets the replay allocates (VkPipeInfo.push 0).
// A fixed layout per stage combination (a binding per constant buffer, storage buffer, texture
// and image slot per stage) would hold about 150 descriptors and could never be pushed, so layouts are made per distinct
// binding list and shared by every pipeline with that list; VkPipeInfo.bindings tells the replay which NVN slot feeds
// each binding.
//
// Fragment inputs the vertex stage does not write (stages are translated separately) get
// outputs added to the vertex module, as Vulkan rejects such pairs, initialised to (0, 0, 0, 1) as Maxwell reads an
// unwritten output (docs/maxwell.md 1.2). The unwritten components of an output the vertex stage does write
// start at (0, 0, 0, 1) in the translator (tools/xlate/patches/0006).
//
// Pipeline key. Everything Vulkan 1.3 can take as dynamic state is dynamic: viewports and scissors with count, depth
// bias, blend constants, stencil masks and references, cull mode, front face, topology (within its class), depth and
// stencil tests and ops, depth bias enable, rasterizer discard; vertex input (VK_EXT_vertex_input_dynamic_state, else
// binding strides only); and, where VK_EXT_extended_dynamic_state3 offers them, depth clamp, polygon mode, blend enables,
// equations, write masks and alpha to coverage. The key holds the rest: shaders, target formats, sample count, topology
// class, depth clip mode, logic op and what is not dynamic on the GPU. The VkPipelineCache persists to
// pipeline-cache.bin next to nx2navi.exe (NX2NAVI_PIPELINE_CACHE overrides the path).
//
// The vertex format table has a 3-component fallback. The pipeline key holds the program, target formats, blend and
// write masks, vertex layout and sample count. The binding conventions are those of the shader translator.
// Contains code Copyright (c) 2026 Peiikai, Copyright (c) 2026 jhaizhou (MIT; see THIRD_PARTY_NOTICES.md).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "gfx.h"
#include "host.h"
#include "nvn_hmap.h"
#include "platform/platform.h"
#include "prp_file.h"
#include "tex.h"
#include "vk_build_gate.h"
#include "vk_cache_file.h"
#include "vk_save_retry.h"
#include "vk_warm_hint.h"
#define LOG_ONCE(...) do { static volatile int32_t once_; if (!plat_xchg32(&once_, 1)) rt_log(__VA_ARGS__); } while (0)
enum { SPV_OP_ENTRY_POINT = 15, SPV_OP_TYPE_INT = 21, SPV_OP_TYPE_FLOAT = 22, SPV_OP_TYPE_VECTOR = 23,
SPV_OP_TYPE_POINTER = 32, SPV_OP_CONSTANT = 43, SPV_OP_CONSTANT_COMPOSITE = 44,
SPV_OP_CONSTANT_NULL = 46, SPV_OP_FUNCTION = 54, SPV_OP_VARIABLE = 59,
SPV_OP_DECORATE = 71, SPV_DEC_BUILTIN = 11, SPV_DEC_LOCATION = 30, SPV_DEC_INDEX = 32, SPV_DEC_BINDING = 33, SPV_DEC_DESCRIPTOR_SET = 34,
SPV_SC_OUTPUT = 3 };
#define MAX_BINDINGS 128
#define PIPE_BUCKETS 4096
#define LAYOUT_BUCKETS 256
// ---- device capabilities and the pipeline cache (set up on first use, under P.lock)
enum { NP_16 = 1, NP_32 = 2, NP_64 = 4 }; // float widths in P.nan_preserve and spirv_nan_preserve
static struct {
PlatMutex lock;
volatile int32_t ready;
VkDevice device;
VkPipelineCache cache;
uint32_t max_push;
int indep; // stage-independent layouts (vk_caps.gpl): vertex set 0, fragment set 1
int push_stage; // GFX_STAGE_* whose set is pushed with independent layouts
int gpl; // draws whose pipeline is building get a fast-linked one (vk_caps.gpl)
struct { uint64_t links, fb_links, wlinks, flinks, dlinks, pr_added, lib_slow[4], ready_n[6], ready_nofast, ready_mesh, ready_recorded, ready_fresh, ready_logged, lib_logged, hints, hint_libs, requeued, link_fail, defer, lib_n[4], lib_fail, swaps, slow, pre_used[4]; double pr_added_ms, lib_slow_ms[4], ready_ms, ready_max, link_ms, link_max, lib_ms[4], lib_max[4], dlink_ms, dlink_max; } g;
int vs_outs_all; // NX2NAVI_VS_OUTS: 0 union of paired fragment inputs, 1 (all) all 32 output locations
int dyn_vi, dyn_clamp, dyn_poly, dyn_blend_en, dyn_blend_eq, dyn_wmask, dyn_a2c;
int f_logic_op, f_depth_clamp, f_fill_non_solid, f_dual_src, f_full_subgroups;
int cache_control; // pipelineCreationCacheControl: a create can ask for the pipeline caches only
int feedback; // NX2NAVI_PIPE_FEEDBACK: 1 count pipeline cache hits, 2 also log each compute and graphics build
uint64_t cache_loaded; // bytes of pipeline-cache.bin the driver took at boot
VkPipelineCache play; // what this run compiles (pipeline-cache-play.bin); VK_NULL_HANDLE: all goes into cache
uint64_t play_loaded; // bytes of pipeline-cache-play.bin the driver took at boot
uint64_t saved[2]; // the data size of cache [0] and play [1] that their files hold; 0: not known
int fold; // the play file's content was merged into cache: saving cache removes the play file
volatile uint64_t play_made; // pipelines this run compiled into play
int pv_last; // provokingVertexLast enabled: NVN's default is the last vertex
VkShaderStageFlags sg32_stages; // stages that accept requiredSubgroupSize = 32 (0: not available)
int reconverge; // modules with subgroup ops get MaximallyReconvergesKHR (spirv_reconverge)
uint32_t nan_preserve; // float widths that get SignedZeroInfNanPreserve (spirv_nan_preserve): NP_16|NP_32|NP_64
int nan_bits; // NaN tests as integer bit tests (spirv_nan_bits)
volatile uint32_t nan_bits_tests, nan_bits_selects, nan_bits_modules; // rewritten so far (the first log line)
VkDynamicState dyn[32];
uint32_t n_dyn;
PFN_vkCmdPushDescriptorSetKHR push;
int tpl; // descriptor update templates on (make_tpl)
PFN_vkCreateDescriptorUpdateTemplate tpl_create;
PFN_vkDestroyDescriptorUpdateTemplate tpl_destroy;
PFN_vkUpdateDescriptorSetWithTemplate tpl_update;
PFN_vkCmdPushDescriptorSetWithTemplateKHR tpl_push;
PFN_vkCmdSetVertexInputEXT set_vi;
PFN_vkCmdSetDepthClampEnableEXT set_clamp;
PFN_vkCmdSetPolygonModeEXT set_poly;
PFN_vkCmdSetColorBlendEnableEXT set_blend_en;
PFN_vkCmdSetColorBlendEquationEXT set_blend_eq;
PFN_vkCmdSetColorWriteMaskEXT set_wmask;
PFN_vkCmdSetAlphaToCoverageEnableEXT set_a2c;
char cache_path[PLAT_PATH_MAX * 2];
char play_path[PLAT_PATH_MAX * 2 + 8]; // pipeline-cache-play.bin beside cache_path
VkPipeStats stats;
} P = {.lock = PLAT_MUTEX_INIT};
typedef struct Layout {
struct Layout *next;
uint64_t hash;
VkDescriptorSetLayout set;
VkPipelineLayout layout;
int push;
uint32_t n;
VkDescriptorSetLayout set1; // independent layouts: the fragment set (set 1); VK_NULL_HANDLE otherwise
int push1; // set 1 is a push-descriptor set
uint32_t n0; // independent layouts: b[0..n0) are set 0, the rest set 1
VkPipelineLayout half[2]; // GPL libraries: {set 0, null}, {null, set 1} (made on first use)
VkDescriptorUpdateTemplate tpl[2]; // per set (make_tpl); VK_NULL_HANDLE: written with write structs
VkDescriptorSetLayoutBinding b[];
} Layout;
typedef struct Pipe {
struct Pipe *next;
uint64_t hash;
VkPipeInfo info;
VkPipeBinding *bindings;
int building; // a background job owns info.pipeline (under P.lock)
struct Job *job; // that job while building
const struct Layout *lay; // graphics: the layout it was entered with
VkPipeline fast; // GPL: the fast-linked pipeline (kept until shutdown: recorded draws use it)
uint8_t fast_tried;
uint8_t link_queued; // a waiting draw queued its fast link to the workers (LibJob p), not yet run
uint8_t used; // compute: a dispatch of this run asked for it (vk_pipeline_compute)
uint8_t pre; // compute: built and kept by the boot prebuild (lb_compute) or bg_main
uint64_t t_miss; // graphics: when a draw first missed it (ns), 0 once it is usable (ready_note)
size_t key_size;
uint8_t key[];
} Pipe;
static Layout *layouts[LAYOUT_BUCKETS];
static Pipe *pipes[PIPE_BUCKETS];
static uint64_t fnv(uint64_t h, const void *p, size_t n) {
const uint8_t *b = p;
for (size_t i = 0; i < n; i++) h = (h ^ b[i]) * 0x100000001b3ull;
return h;
}
#define FNV0 0xcbf29ce484222325ull
static __thread uint64_t mm_hash; // FNV-1a of the module make_module last created on this thread (PIPE_FEEDBACK 2)
// Hash of a pipeline table key, 8 bytes per step: a draw hashes its whole GKey, and byte-wise FNV over it cost more
// than the lookup. Only the in-memory table uses it; the key file keeps fnv.
static uint64_t key_hash(const void *k, size_t n) { return nvn_hm_hash(k, (uint32_t)n); }
// The cache file lives next to nx2navi.exe, so every build dir (the main build and each private fork) has its own:
// forks that shared build\v121's cache broke each other's runs (2026-10-22).
static void cache_path_init(void) {
const char *cfg_path = cfg_get("NX2NAVI_PIPELINE_CACHE");
if (cfg_path && *cfg_path && strlen(cfg_path) < sizeof P.cache_path) {
strcpy(P.cache_path, cfg_path);
return;
}
P.cache_path[0] = 0;
char exe[PLAT_PATH_MAX * 2];
int n = plat_exe_path(exe, sizeof exe);
if (!n || n >= sizeof exe) return;
char *cut = plat_last_sep(exe);
if (!cut) return;
*cut = 0;
snprintf(P.cache_path, sizeof P.cache_path, "%s/pipeline-cache.bin", exe);
}
// ---- pipeline binaries (VK_KHR_pipeline_binary; NX2NAVI_PIPE_BINARIES, default 0 while it is being tested; 1 on)
// pipeline-cache.bin does not carry compiled pipelines from one executable to another on NVIDIA 621.01: two
// byte-identical exes under different names, every compute module hash-identical, and the second compiled 317 of the
// 323 compute pipelines the first had read from the file (2026-11-09). A pipeline binary made from one exe's pipeline
// creates the same pipeline in another in 0.5 ms instead of 26 ms. So where the driver has the extension, every
// pipeline, stage library and compute pipeline is created through pipe_create: its driver pipeline key
// (vkGetPipelineKeyKHR, computed from the create info) is looked up in pipeline-binaries.bin next to nx2navi.exe and
// the pipeline is made from the stored binaries; a key not there is compiled with its data captured, and its binaries
// are appended to the file (autosave, every 2 s). pipeline-cache.bin is then neither read nor written: the driver's
// own disk cache serves an unchanged exe, and the binaries serve a renamed or updated one.
// Each binary is stored once under its own binary key: on NVIDIA 621.01 nearly every graphics pipeline has, besides
// its own binaries of 20-200 KB, one 2 MiB binary that is byte-identical across pipelines (2,506 copies in a 4.7 GB
// first-version file).
// File: header {"NX2NPBIN", version, the driver's global key size and bytes} (another global key: another driver or
// GPU, so the file starts empty); then records {u32 bytes after this field, u32 type, u32 key size, 32 key bytes,
// payload}: type PB_BLOB, a binary under its binary key, payload its data; type PB_PIPE, a pipeline under its pipeline
// key, payload {u32 count, count x {u32 key size, 32 key bytes}} naming its binaries. Only an index of the records
// stays in memory; a pipeline's records are read from the file when it is created. NX2NAVI_PIPE_BINARIES_MAX_MB
// (default 1024, 0 no limit): a larger file starts empty.
#define PB_VERSION 2
// 64-bit file offsets: long is 32 bits on Windows, and a file over 2 GB read as cut short.
#ifdef _WIN32
#define pb_seek _fseeki64
#define pb_tell _ftelli64
#else
#define pb_seek fseeko
#define pb_tell ftello
#endif
#define PB_MAX_BIN 8
#define PB_KEY VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR
enum { PB_BLOB = 1, PB_PIPE = 2 };
#define PB_HEAD (4 + 4 + PB_KEY) // a record's bytes after its size field and before its payload
typedef struct { char magic[8]; uint32_t version, gkey_size; uint8_t gkey[PB_KEY]; } PbHeader;
typedef struct { uint64_t h, off; uint32_t size; } PbSlot; // off: file offset of the record's size field
static struct {
int on;
PFN_vkCreatePipelineBinariesKHR create;
PFN_vkDestroyPipelineBinaryKHR destroy;
PFN_vkGetPipelineKeyKHR key;
PFN_vkGetPipelineBinaryDataKHR data;
PFN_vkReleaseCapturedPipelineDataKHR release;
char path[PLAT_PATH_MAX * 2 + 32];
PlatMutex lock; // the index, the pending records and rf
FILE *rf; // reads records
PbSlot *slot;
uint32_t cap, n, pipes, blobs;
uint8_t *pend; // records not written yet; they follow the file's end in this order
size_t pend_n, pend_cap;
uint64_t file_size; // bytes on disk
int no_capture[3]; // kind (pipe_create) whose create refused the capture flag
uint64_t from_file, load_fail, compiled, captured, capture_fail, blob_new, blob_shared;
double from_ms, compile_ms;
} PB = {.lock = PLAT_MUTEX_INIT};
static __thread int pb_last; // 1: the last pipe_create on this thread made its pipeline from binaries
static __thread int cc_hit; // compute_create's last pipeline on this thread: 1 the pipeline cache held it
// (P.cache: read from pipeline-cache.bin, or built into it earlier in this
// run) or it was made from pipeline binaries, 0 not, -1 the driver did not say
static uint64_t pb_hash(uint32_t type, const uint8_t *key, uint32_t n) {
uint64_t h = fnv(fnv(FNV0, &type, 4), key, n);
return h ? h : 1;
}
static PbSlot *pb_find(uint64_t h) { // caller holds PB.lock
if (!PB.cap) return NULL;
for (uint32_t j = (uint32_t)h & (PB.cap - 1);; j = (j + 1) & (PB.cap - 1)) {
if (!PB.slot[j].h) return NULL;
if (PB.slot[j].h == h) return &PB.slot[j];
}
}
// Enters a record (a hash match is taken as the key: 64 bits). 0 when one with the key is there already or out of
// memory. Caller holds PB.lock.
static int pb_insert(uint64_t h, uint64_t off, uint32_t size) {
if ((PB.n + 1) * 2 > PB.cap) {
uint32_t cap = PB.cap ? PB.cap * 2 : 4096;
PbSlot *t = calloc(cap, sizeof *t);
if (!t) return 0;
for (uint32_t i = 0; i < PB.cap; i++) {
if (!PB.slot[i].h) continue;
uint32_t j = (uint32_t)PB.slot[i].h & (cap - 1);
while (t[j].h) j = (j + 1) & (cap - 1);
t[j] = PB.slot[i];
}
free(PB.slot);
PB.slot = t, PB.cap = cap;
}
if (pb_find(h)) return 0;
uint32_t j = (uint32_t)h & (PB.cap - 1);
while (PB.slot[j].h) j = (j + 1) & (PB.cap - 1);
PB.slot[j] = (PbSlot){h, off, size};
PB.n++;
return 1;
}
static void pb_rewrite(const PbHeader *hd) { // an empty file with this driver's header
FILE *f = fopen(PB.path, "wb");
if (f) fwrite(hd, sizeof *hd, 1, f), fclose(f);
PB.file_size = f ? sizeof *hd : 0;
}
// Opens pipeline-binaries.bin and indexes its records (cache_load, before P.cache).
static void pb_init(VkCtx *c) {
if (!c->has_pipeline_binary || !P.cache_path[0]) return;
#define PBPROC(field, name) (PB.field = (void *)vk_dpa(c->device, name))
if (!PBPROC(create, "vkCreatePipelineBinariesKHR") || !PBPROC(destroy, "vkDestroyPipelineBinaryKHR") ||
!PBPROC(key, "vkGetPipelineKeyKHR") || !PBPROC(data, "vkGetPipelineBinaryDataKHR") ||
!PBPROC(release, "vkReleaseCapturedPipelineDataKHR")) {
rt_log("vk_pipeline: pipeline binaries: the driver lacks a VK_KHR_pipeline_binary function; using pipeline-cache.bin");
return;
}
#undef PBPROC
PbHeader want = {{'N', 'X', '2', 'N', 'P', 'B', 'I', 'N'}, PB_VERSION, 0, {0}};
VkPipelineBinaryKeyKHR gk = {.sType = VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR};
if (PB.key(c->device, NULL, &gk) != VK_SUCCESS || !gk.keySize || gk.keySize > PB_KEY) {
rt_log("vk_pipeline: pipeline binaries: no global key from the driver; using pipeline-cache.bin");
return;
}
want.gkey_size = gk.keySize;
memcpy(want.gkey, gk.key, gk.keySize);
snprintf(PB.path, sizeof PB.path, "%s", P.cache_path);
char *cut = plat_last_sep(PB.path);
snprintf(cut ? cut + 1 : PB.path, sizeof PB.path - (size_t)(cut ? cut + 1 - PB.path : 0), "pipeline-binaries.bin");
uint64_t cap = (uint64_t)strtoull(cfg_value("NX2NAVI_PIPE_BINARIES_MAX_MB"), NULL, 10) << 20;
FILE *f = fopen(PB.path, "rb");
PbHeader hd;
const char *why = NULL;
uint64_t good = 0, len = 0;
if (f) {
pb_seek(f, 0, SEEK_END);
len = (uint64_t)pb_tell(f);
pb_seek(f, 0, SEEK_SET);
if (fread(&hd, sizeof hd, 1, f) != 1 || memcmp(&hd, &want, sizeof hd)) why = "written by another driver, GPU or version";
else if (cap && len > cap) why = "over NX2NAVI_PIPE_BINARIES_MAX_MB";
else {
good = sizeof hd;
for (;;) { // index: per record its type, key and offset
uint32_t head[3];
uint8_t key[PB_KEY];
if (fread(head, sizeof head, 1, f) != 1 || head[2] > PB_KEY || head[0] < PB_HEAD ||
(head[1] != PB_BLOB && head[1] != PB_PIPE) || fread(key, PB_KEY, 1, f) != 1 || good + 4 + head[0] > len)
break;
if (pb_insert(pb_hash(head[1], key, head[2]), good, head[0])) {
if (head[1] == PB_PIPE) PB.pipes++;
else PB.blobs++;
}
good += 4 + (uint64_t)head[0];
if (pb_seek(f, (int64_t)good, SEEK_SET)) break;
}
if (good != len) why = "cut short (a record half written)";
}
fclose(f);
}
if (why || !f) { // start empty: the index is dropped and the file rewritten
if (f) rt_log("vk_pipeline: pipeline binaries: %s %s (%.0f MB); starting empty", PB.path, why, len / 1048576.0);
free(PB.slot), PB.slot = NULL, PB.cap = PB.n = PB.pipes = PB.blobs = 0;
pb_rewrite(&want);
} else {
PB.file_size = len;
}
PB.rf = fopen(PB.path, "rb");
PB.on = PB.rf != NULL && PB.file_size >= sizeof want;
rt_log("vk_pipeline: pipeline binaries %s: %s, %u pipelines, %u binaries, %.1f MB (NX2NAVI_PIPE_BINARIES=0 goes back "
"to pipeline-cache.bin)", PB.on ? "on" : "off (cannot open the file)", PB.path, PB.pipes, PB.blobs,
PB.file_size / 1048576.0);
}
// The payload of the record of (type, key), malloc'd (*size bytes), or NULL. Caller holds PB.lock.
static uint8_t *pb_read(uint32_t type, const uint8_t *key, uint32_t ks, uint32_t *size) {
PbSlot *s = pb_find(pb_hash(type, key, ks));
if (!s || s->size < PB_HEAD) return NULL;
uint32_t n = s->size - PB_HEAD;
uint8_t *head = malloc(PB_HEAD + (size_t)n), *out = NULL;
if (!head) return NULL;
int ok;
if (s->off >= PB.file_size) { // still pending, at its place in pend
size_t at = (size_t)(s->off - PB.file_size) + 4;
ok = at + s->size <= PB.pend_n;
if (ok) memcpy(head, PB.pend + at, s->size);
} else {
ok = !pb_seek(PB.rf, (int64_t)(s->off + 4), SEEK_SET) && fread(head, s->size, 1, PB.rf) == 1;
}
uint32_t t, k;
if (ok) memcpy(&t, head, 4), memcpy(&k, head + 4, 4);
if (ok && t == type && k == ks && !memcmp(head + 8, key, ks)) { // the key itself, not only its hash
memmove(head, head + PB_HEAD, n);
out = head, *size = n;
} else {
free(head);
}
return out;
}
// The binaries stored for pipeline key k, created in bins (*n of them). 0 when none are stored or they fail.
static int pb_load(const VkPipelineBinaryKeyKHR *k, VkPipelineBinaryKHR bins[PB_MAX_BIN], uint32_t *n) {
VkPipelineBinaryKeyKHR keys[PB_MAX_BIN];
VkPipelineBinaryDataKHR data[PB_MAX_BIN];
uint8_t *blob[PB_MAX_BIN] = {0};
uint32_t nb = 0, size = 0;
plat_mutex_lock(&PB.lock);
uint8_t *pipe = pb_read(PB_PIPE, k->key, k->keySize, &size);
int ok = pipe && size >= 4;
if (ok) memcpy(&nb, pipe, 4);
if (!nb || nb > PB_MAX_BIN || size < 4 + nb * (4 + PB_KEY)) ok = 0;
for (uint32_t i = 0; ok && i < nb; i++) {
const uint8_t *e = pipe + 4 + i * (4 + PB_KEY);
uint32_t ks, ds = 0;
memcpy(&ks, e, 4);
keys[i] = (VkPipelineBinaryKeyKHR){.sType = VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR, .keySize = ks};
memcpy(keys[i].key, e + 4, PB_KEY);
if (ks > PB_KEY || !(blob[i] = pb_read(PB_BLOB, keys[i].key, ks, &ds))) ok = 0;
else data[i] = (VkPipelineBinaryDataKHR){.dataSize = ds, .pData = blob[i]};
}
plat_mutex_unlock(&PB.lock);
if (ok) {
VkPipelineBinaryKeysAndDataKHR kd = {.binaryCount = nb, .pPipelineBinaryKeys = keys, .pPipelineBinaryData = data};
VkPipelineBinaryCreateInfoKHR ci = {.sType = VK_STRUCTURE_TYPE_PIPELINE_BINARY_CREATE_INFO_KHR, .pKeysAndDataInfo = &kd};
VkPipelineBinaryHandlesInfoKHR hi = {.sType = VK_STRUCTURE_TYPE_PIPELINE_BINARY_HANDLES_INFO_KHR,
.pipelineBinaryCount = nb, .pPipelineBinaries = bins};
ok = PB.create(P.device, &ci, NULL, &hi) == VK_SUCCESS;
*n = nb;
}
for (uint32_t i = 0; i < PB_MAX_BIN; i++) free(blob[i]);
free(pipe);
return ok;
}
// Appends one record to pend and the index. Caller holds PB.lock.
static int pb_put(uint32_t type, const uint8_t *key, uint32_t ks, const void *a, size_t an, const void *b, size_t bn) {
size_t size = PB_HEAD + an + bn;
if (size >= (1u << 31)) return 0;
if (PB.pend_n + 4 + size > PB.pend_cap) {
size_t cap = PB.pend_cap ? PB.pend_cap * 2 : (size_t)4 << 20;
while (cap < PB.pend_n + 4 + size) cap *= 2;
uint8_t *t = realloc(PB.pend, cap);
if (!t) return 0;
PB.pend = t, PB.pend_cap = cap;
}
if (!pb_insert(pb_hash(type, key, ks), PB.file_size + PB.pend_n, (uint32_t)size)) return 0;
uint8_t *o = PB.pend + PB.pend_n;
uint32_t s32 = (uint32_t)size;
memcpy(o, &s32, 4), memcpy(o + 4, &type, 4), memcpy(o + 8, &ks, 4);
memset(o + 12, 0, PB_KEY), memcpy(o + 12, key, ks);
if (an) memcpy(o + 4 + PB_HEAD, a, an);
if (bn) memcpy(o + 4 + PB_HEAD + an, b, bn);
PB.pend_n += 4 + size;
if (type == PB_PIPE) PB.pipes++;
else PB.blobs++;
return 1;
}
static void pb_flush(void);
// Stores the binaries of pipeline p (created with its data captured) under pipeline key k, then frees the data. A
// binary whose key is stored already is not stored again.
static void pb_capture(VkPipeline p, const VkPipelineBinaryKeyKHR *k) {
VkPipelineBinaryCreateInfoKHR ci = {.sType = VK_STRUCTURE_TYPE_PIPELINE_BINARY_CREATE_INFO_KHR, .pipeline = p};
VkPipelineBinaryKHR bins[PB_MAX_BIN];
VkPipelineBinaryHandlesInfoKHR hi = {.sType = VK_STRUCTURE_TYPE_PIPELINE_BINARY_HANDLES_INFO_KHR};
uint32_t nb = 0;
int ok = PB.create(P.device, &ci, NULL, &hi) == VK_SUCCESS && hi.pipelineBinaryCount &&
hi.pipelineBinaryCount <= PB_MAX_BIN;
if (ok) {
hi.pPipelineBinaries = bins;
ok = PB.create(P.device, &ci, NULL, &hi) == VK_SUCCESS;
nb = ok ? hi.pipelineBinaryCount : 0;
}
VkPipelineBinaryKeyKHR keys[PB_MAX_BIN];
uint8_t list[4 + PB_MAX_BIN * (4 + PB_KEY)] = {0};
memcpy(list, &nb, 4);
for (uint32_t i = 0; ok && i < nb; i++) { // each binary's key, and its data unless stored already
VkPipelineBinaryDataInfoKHR di = {.sType = VK_STRUCTURE_TYPE_PIPELINE_BINARY_DATA_INFO_KHR, .pipelineBinary = bins[i]};
keys[i] = (VkPipelineBinaryKeyKHR){.sType = VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR};
size_t ds = 0;
ok = PB.data(P.device, &di, &keys[i], &ds, NULL) == VK_SUCCESS && keys[i].keySize && keys[i].keySize <= PB_KEY;
if (!ok) break;
memcpy(list + 4 + i * (4 + PB_KEY), &keys[i].keySize, 4), memcpy(list + 8 + i * (4 + PB_KEY), keys[i].key, keys[i].keySize);
plat_mutex_lock(&PB.lock);
int have = pb_find(pb_hash(PB_BLOB, keys[i].key, keys[i].keySize)) != NULL;
plat_mutex_unlock(&PB.lock);
if (have) {
PB.blob_shared++;
continue;
}
uint8_t *d = malloc(ds ? ds : 1);
ok = d && PB.data(P.device, &di, &keys[i], &ds, d) == VK_SUCCESS;
if (ok) {
plat_mutex_lock(&PB.lock);
if (pb_put(PB_BLOB, keys[i].key, keys[i].keySize, d, ds, NULL, 0)) PB.blob_new++;
plat_mutex_unlock(&PB.lock); // a 0 from pb_put: another thread stored the same binary meanwhile
}
free(d);
}
for (uint32_t i = 0; i < nb; i++) PB.destroy(P.device, bins[i], NULL);
VkReleaseCapturedPipelineDataInfoKHR ri = {.sType = VK_STRUCTURE_TYPE_RELEASE_CAPTURED_PIPELINE_DATA_INFO_KHR, .pipeline = p};
PB.release(P.device, &ri, NULL);
plat_mutex_lock(&PB.lock);
if (ok && pb_put(PB_PIPE, k->key, k->keySize, list, 4 + nb * (4 + PB_KEY), NULL, 0)) PB.captured++;
else if (!ok) PB.capture_fail++;
int full = PB.pend_n >= ((size_t)64 << 20);
plat_mutex_unlock(&PB.lock);
if (full) pb_flush(); // a cold boot captures thousands before the autosave thread runs: keep the buffer small
}
// Appends the pending records (the autosave thread every 2 s, and the save at exit).
static void pb_flush(void) {
if (!PB.on) return;
plat_mutex_lock(&PB.lock);
if (PB.pend_n) {
FILE *f = fopen(PB.path, "ab");
int ok = f && fwrite(PB.pend, PB.pend_n, 1, f) == 1;
if (f) ok = !fclose(f) && ok;
if (ok) PB.file_size += PB.pend_n;
else rt_log("vk_pipeline: pipeline binaries: cannot append to %s; this run's binaries stay in memory", PB.path);
if (ok) PB.pend_n = 0;
}
plat_mutex_unlock(&PB.lock);
}
static void pb_log(const char *when) {
if (!PB.on) return;
plat_mutex_lock(&PB.lock);
rt_log("vk_pipeline: pipeline binaries %s: %llu made from pipeline-binaries.bin (avg %.2f ms), %llu compiled (avg %.1f "
"ms), %llu captured, %llu could not be stored, %llu stored ones failed; binaries %llu new, %llu shared; %u pipelines and "
"%u binaries in the file, %.1f MB", when,
(unsigned long long)PB.from_file, PB.from_file ? PB.from_ms / PB.from_file : 0.0,
(unsigned long long)PB.compiled, PB.compiled ? PB.compile_ms / PB.compiled : 0.0,
(unsigned long long)PB.captured, (unsigned long long)PB.capture_fail, (unsigned long long)PB.load_fail,
(unsigned long long)PB.blob_new, (unsigned long long)PB.blob_shared, PB.pipes, PB.blobs,
(PB.file_size + PB.pend_n) / 1048576.0);
plat_mutex_unlock(&PB.lock);
}
// Creates one pipeline: kind 0 a graphics pipeline, 1 a graphics pipeline library, 2 a compute pipeline; ci its
// Vk*PipelineCreateInfo. With binaries on: from the stored binaries of its pipeline key when there are any, else
// compiled with P.cache and its binaries captured. The create info is restored before returning.
typedef struct { VkStructureType sType; const void *pNext; VkPipelineCreateFlags flags; } PipeCiHead;
// Builds into P.cache against reads of its data. Every create with P.cache holds this shared (pipe_create_in); a save
// holds it exclusively for the copy of the data and only takes it when no build is running. The Vulkan rules allow
// reading a cache created without VK_PIPELINE_CACHE_CREATE_EXTERNALLY_SYNCHRONIZED_BIT while pipelines are built into
// it, but Intel's Windows driver (101.8991 on an Arc 140V) crashed inside vkGetPipelineCacheData during the copy,
// reading through a null entry, with compute and library builds running beside the autosave. So no copy is made while
// a build runs, on any driver.
static PlatMutex cache_rw = PLAT_MUTEX_INIT;
// Set while the save at exit waits for cache_rw: the builds already in the driver finish, and new ones wait in
// cache_build_enter until the copy is made. Without it the boot's prewarm workers started builds back to back, no moment
// without one came within the save's 5 s, and the pipelines built in that run were not saved (a window closed during
// the prewarm, 2026-11-11). The autosave never sets it, so it never holds a build back.
static volatile int32_t cache_saving_at_exit;
// Takes cache_rw shared for a build into P.cache; while the save at exit waits for its copy, first waits for that.
static void cache_build_enter(void) {
for (;;) {
while (__atomic_load_n(&cache_saving_at_exit, __ATOMIC_ACQUIRE)) plat_sleep_ms(1);
plat_mutex_lock_shared(&cache_rw);
if (!__atomic_load_n(&cache_saving_at_exit, __ATOMIC_ACQUIRE)) return;
plat_mutex_unlock_shared(&cache_rw);
}
}
// A pipeline cache of this thread's own that its creates build into in place of P.cache; VK_NULL_HANDLE (every thread
// but the background builder of large compute shaders) for P.cache itself. A create into it does not hold cache_rw, so
// a build of minutes does not keep pipeline-cache.bin from being saved; bg_merge moves its content into P.cache.
static __thread VkPipelineCache own_cache;
// 1 on a thread whose creates take no pipeline cache of ours at all (the library warm in play, lw_worker and lw_probe):
// what it builds is wanted in the driver's own disk cache only. Built into P.cache, the 37,572 stage libraries of a
// warm grew it by 1.6 GB (2026-11-11: a save of 2026.7 MB in play, +1612 MB since the boot); the file was then over
// NX2NAVI_PIPELINE_CACHE_MAX_MB, so the next start dropped it whole and compiled the pipelines it had held.
static __thread int no_cache;
// New content in P.cache that P.stats does not count, for autosave_once to save: bg_merge's merges, and the compiles
// of pipelines built for the cache only (lb_compute keep 0).
static volatile uint32_t cache_merges;
// ---- the play cache (NX2NAVI_PIPELINE_CACHE_PLAY, default 1; needs pipelineCreationCacheControl)
// pipeline-cache.bin holds what the preparation built and what earlier runs made: 300 MB and more on an install. One
// file can only be written whole, so a single new pipeline in play had the whole of it copied out of the driver and
// written again (2026-11-11: saves of 358.1 MB for 0.3 MB of new data, and 302 to 306 MB written in each of seven
// sessions that added about 0.5 MB each). So what a run compiles goes into a second cache, P.play, with a file of its
// own, pipeline-cache-play.bin beside the first: a create asks P.cache with
// VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT, which gives the pipeline when that cache or the driver's
// own has it and VK_PIPELINE_COMPILE_REQUIRED when not, and only then builds into P.play (pipe_create_in). In play
// only the play file is saved; pipeline-cache.bin is not written.
// The play file is read back at the next start, into P.play and merged into P.cache in memory (cache_load), so the
// first create finds its pipelines. Once
// it has grown to NX2NAVI_PIPELINE_CACHE_PLAY_MB (default 64) it is merged into P.cache at the start, before play: the
// autosave thread writes pipeline-cache.bin once and removes the play file (cache_save). The preparation modes
// (--prepare-shaders, --warm-shaders, --prepare-pipelines) merge it at any size and build into P.cache alone, as before.
// A pipeline the driver's own cache gives to the first create enters P.cache in memory only: it is in a file again
// when the play file is next folded in, or when the driver no longer has it and it is compiled.
// Without the device feature, with pipeline binaries on, or with the setting 0, P.play stays VK_NULL_HANDLE and
// everything goes into P.cache as before.
static int prep_mode; // a preparation mode (set before ensure_init): no play cache
static VkResult pipe_create_one(int kind, void *ci, VkPipeline *pipe, VkPipelineCache cache) {
return kind == 2 ? vkCreateComputePipelines(P.device, cache, 1, ci, NULL, pipe)
: vkCreateGraphicsPipelines(P.device, cache, 1, ci, NULL, pipe);
}
static VkResult pipe_create_in(int kind, void *ci, VkPipeline *pipe, VkPipelineCache cache) {
if (no_cache) cache = VK_NULL_HANDLE;
int shared = cache && !own_cache;
if (cache && own_cache) cache = own_cache;
uint64_t t0 = perf_mark_begin(); // NX2NAVI_TRACE=perf: the create on any thread, its wait for cache_rw included
if (shared) cache_build_enter();
VkResult r;
if (shared && P.play) {
PipeCiHead *hd = ci;
VkPipelineCreateFlags flags = hd->flags;
hd->flags = flags | VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT;
r = pipe_create_one(kind, ci, pipe, cache);
hd->flags = flags;
if (r == VK_PIPELINE_COMPILE_REQUIRED) {
// Not in P.cache nor in the driver's own cache: from P.play, or compiled into it. A caller that asked for
// cached pipelines only (its own FAIL_ON_PIPELINE_COMPILE_REQUIRED) gets the play cache's answer.
r = pipe_create_one(kind, ci, pipe, P.play);
if (r == VK_SUCCESS && *pipe) __atomic_fetch_add(&P.play_made, 1, __ATOMIC_RELAXED);
}
} else {
r = pipe_create_one(kind, ci, pipe, cache);
}
if (shared) plat_mutex_unlock_shared(&cache_rw);
perf_mark_end(PERF_M_PIPE, t0, 0);
return r;
}
// ---- pipelines of a library that draws on our device (FSR 3.1: vk_ffx.cpp vk_ffx_proc_addr)
// Such a library creates its pipelines itself and passes no pipeline cache, so nothing of ours kept them: every
// context it made (one per render and output size, and one for frame generation) compiled its passes again unless the
// driver's own disk cache still had them, which a renamed or updated exe on NVIDIA and a driver without a disk cache
// do not. vk_ffx.cpp hands the library these creates in the place of vkCreateComputePipelines and
// vkCreateGraphicsPipelines; a create that names no cache goes through ours as pipe_create_in does: asked of P.cache
// first without compiling, and compiled into the play cache when neither it nor the driver has the pipeline, so the
// file a run writes (pipeline-cache-play.bin, or pipeline-cache.bin without a play cache) holds it for the next start.
// Before the first pipeline of ours (P.ready 0) and with pipeline binaries on there is no cache to give: the create
// goes to the driver as the library asked. infos: n VkComputePipelineCreateInfo (compute 1) or
// VkGraphicsPipelineCreateInfo. Any thread.
static struct { volatile uint64_t made, from_cache, compiled, uncached; } FP;
void vk_pipeline_foreign_counts(VkForeignPipes *o) {
o->made = __atomic_load_n(&FP.made, __ATOMIC_RELAXED), o->from_cache = __atomic_load_n(&FP.from_cache, __ATOMIC_RELAXED);
o->compiled = __atomic_load_n(&FP.compiled, __ATOMIC_RELAXED), o->uncached = __atomic_load_n(&FP.uncached, __ATOMIC_RELAXED);
}
static VkResult foreign_one(VkDevice dev, int compute, VkPipelineCache cache, uint32_t n, const void *ci,
const VkAllocationCallbacks *alloc, VkPipeline *out) {
return compute ? vkCreateComputePipelines(dev, cache, n, ci, alloc, out)
: vkCreateGraphicsPipelines(dev, cache, n, ci, alloc, out);
}
VkResult vk_pipeline_create_foreign(VkDevice dev, int compute, uint32_t n, const void *infos,
const VkAllocationCallbacks *alloc, VkPipeline *out) {
VkPipelineCache cache = __atomic_load_n(&P.ready, __ATOMIC_ACQUIRE) && dev == P.device ? P.cache : VK_NULL_HANDLE;
if (!cache) {
VkResult r = foreign_one(dev, compute, VK_NULL_HANDLE, n, infos, alloc, out);
if (r == VK_SUCCESS) __atomic_fetch_add(&FP.made, n, __ATOMIC_RELAXED), __atomic_fetch_add(&FP.uncached, n, __ATOMIC_RELAXED);
return r;
}
VkResult r = VK_SUCCESS;
cache_build_enter();
if (!P.play) { // one cache: whether it held them is not known here; its save looks at the data's size
r = foreign_one(dev, compute, cache, n, infos, alloc, out);
if (r == VK_SUCCESS) __atomic_fetch_add(&FP.made, n, __ATOMIC_RELAXED), __atomic_fetch_add(&cache_merges, n, __ATOMIC_RELAXED);
}
for (uint32_t i = 0; P.play && i < n; i++) {
// a copy of the create info: the flags are ours to change, the library's struct is not
VkComputePipelineCreateInfo cc;
VkGraphicsPipelineCreateInfo gc;
void *ci;
if (compute) cc = ((const VkComputePipelineCreateInfo *)infos)[i], ci = &cc;
else gc = ((const VkGraphicsPipelineCreateInfo *)infos)[i], ci = &gc;
PipeCiHead *hd = ci;
// Flags given in a VkPipelineCreateFlags2CreateInfo replace the struct's own, so the question "without
// compiling" cannot be put through hd->flags: such a create goes to the play cache at once.
int flags2 = 0;
for (const VkBaseInStructure *b = hd->pNext; b && !flags2; b = b->pNext)
flags2 = b->sType == VK_STRUCTURE_TYPE_PIPELINE_CREATE_FLAGS_2_CREATE_INFO_KHR;
VkPipelineCreateFlags flags = hd->flags;
VkResult ri = VK_PIPELINE_COMPILE_REQUIRED;
out[i] = VK_NULL_HANDLE;
if (!flags2) {
hd->flags = flags | VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT;
ri = foreign_one(dev, compute, cache, 1, ci, alloc, &out[i]);
hd->flags = flags;
if (ri == VK_SUCCESS) __atomic_fetch_add(&FP.from_cache, 1, __ATOMIC_RELAXED);
}
if (ri == VK_PIPELINE_COMPILE_REQUIRED) {
ri = foreign_one(dev, compute, P.play, 1, ci, alloc, &out[i]);
if (ri == VK_SUCCESS) {
__atomic_fetch_add(&FP.compiled, 1, __ATOMIC_RELAXED);
__atomic_fetch_add(&P.play_made, 1, __ATOMIC_RELAXED);
__atomic_fetch_add(&cache_merges, 1, __ATOMIC_RELAXED); // autosave_once: the play file is due
}
}
if (ri == VK_SUCCESS) __atomic_fetch_add(&FP.made, 1, __ATOMIC_RELAXED);
else if (r == VK_SUCCESS) r = ri; // the first that failed, as a create of several reports one result
if (ri != VK_SUCCESS) out[i] = VK_NULL_HANDLE;
}
plat_mutex_unlock_shared(&cache_rw);
return r;
}
static VkResult pipe_create(int kind, void *ci, VkPipeline *pipe) {
PipeCiHead *hd = ci;
VkResult r;
pb_last = 0;
#define PIPE_CREATE(cache) pipe_create_in(kind, ci, pipe, cache)
if (!PB.on) return PIPE_CREATE(P.cache);
VkPipelineCreateInfoKHR kci = {.sType = VK_STRUCTURE_TYPE_PIPELINE_CREATE_INFO_KHR, .pNext = ci};
VkPipelineBinaryKeyKHR key = {.sType = VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR};
int keyed = PB.key(P.device, &kci, &key) == VK_SUCCESS && key.keySize && key.keySize <= PB_KEY;
const void *next = hd->pNext;
if (keyed) {
VkPipelineBinaryKHR bins[PB_MAX_BIN];
uint32_t nb = 0;
uint64_t t0 = plat_now_ns();
if (pb_load(&key, bins, &nb)) {
VkPipelineBinaryInfoKHR bi = {.sType = VK_STRUCTURE_TYPE_PIPELINE_BINARY_INFO_KHR, .pNext = next,
.binaryCount = nb, .pPipelineBinaries = bins};
hd->pNext = &bi;
r = PIPE_CREATE(VK_NULL_HANDLE); // a pipeline made from binaries takes no pipeline cache
hd->pNext = next;
for (uint32_t i = 0; i < nb; i++) PB.destroy(P.device, bins[i], NULL);
plat_mutex_lock(&PB.lock);
if (r == VK_SUCCESS) PB.from_file++, PB.from_ms += (plat_now_ns() - t0) / 1e6;
else PB.load_fail++;
plat_mutex_unlock(&PB.lock);
if (r == VK_SUCCESS) return pb_last = 1, r;
}
}
int capture = keyed && !PB.no_capture[kind];
VkPipelineCreateFlags2CreateInfoKHR f2 = {.sType = VK_STRUCTURE_TYPE_PIPELINE_CREATE_FLAGS_2_CREATE_INFO_KHR, .pNext = next,
.flags = (VkPipelineCreateFlags2KHR)hd->flags | VK_PIPELINE_CREATE_2_CAPTURE_DATA_BIT_KHR};
uint64_t t0 = plat_now_ns();
if (capture) hd->pNext = &f2;
r = PIPE_CREATE(P.cache);
hd->pNext = next;
if (capture && r < 0) { // the driver refuses the capture for this kind: create without it from now on
PB.no_capture[kind] = 1;
rt_log("vk_pipeline: pipeline binaries: a %s with its data captured failed (%d); such pipelines are not stored",
kind == 2 ? "compute pipeline" : kind ? "pipeline library" : "graphics pipeline", (int)r);
capture = 0;
r = PIPE_CREATE(P.cache);
}
#undef PIPE_CREATE
if (r == VK_SUCCESS && *pipe) {
plat_mutex_lock(&PB.lock);
PB.compiled++, PB.compile_ms += (plat_now_ns() - t0) / 1e6;
plat_mutex_unlock(&PB.lock);
if (capture) pb_capture(*pipe, &key);
}
return r;
}
// The data of the pipeline cache file at path for vkCreatePipelineCache (malloc'd, its size in *size), NULL when the
// run starts without it, which the log says with the file's size. Cache data whose header names another device or
// driver is dropped before Vulkan sees it.
// NX2NAVI_PIPELINE_CACHE_MAX_MB (default 512; 0 no limit): a larger file starts the run empty, and the run's
// saves replace it. The file only grows: entries of earlier translators and builds stay, and on NVIDIA
// (621.01) an entry another executable wrote is neither used nor written again, so a renamed or updated exe
// compiles while the file keeps its size (2026-11-09: 650 MB, 15-18 % of the pipelines read from it). The
// whole file sits in process memory twice at the boot (this read and the driver's copy) and is copied out
// at every save of it, which a 16 GB machine near its commit limit cannot spare. The size is checked before the
// read, so a file that is dropped never takes its size in memory. Sizes are 64-bit all the way
// (vk_cache_file.h cache_file_read): a file past 2 GB is read with the limit raised or off, and refused by
// its real size otherwise.
static int cache_over_cap; // the last file cache_data_read was asked for is over the limit
static void *cache_data_read(VkCtx *c, const char *path, size_t *size) {
*size = 0;
cache_over_cap = 0;
if (!path[0]) return NULL;
uint64_t cap = (uint64_t)strtoull(cfg_value("NX2NAVI_PIPELINE_CACHE_MAX_MB"), NULL, 10) << 20, len = 0;
int why = CACHE_FILE_NONE;
void *data = cache_file_read(path, sizeof(VkPipelineCacheHeaderVersionOne), cap, &len, &why);
if (!data) {
if (why == CACHE_FILE_OVER_CAP)
cache_over_cap = 1,
rt_log("vk_pipeline: %s holds %.0f MB, over NX2NAVI_PIPELINE_CACHE_MAX_MB %llu: starting empty; it is "
"replaced when the play file is next folded in, at the next start that finds one", path, len / 1048576.0, (unsigned long long)(cap >> 20));
else if (why != CACHE_FILE_NONE)
rt_log("vk_pipeline: %s (%llu bytes, %.1f MB) is not read: %s; starting empty", path,
(unsigned long long)len, len / 1048576.0, cache_file_why(why));
return NULL;
}
VkPipelineCacheHeaderVersionOne h;
memcpy(&h, data, sizeof h);
if (h.headerVersion != VK_PIPELINE_CACHE_HEADER_VERSION_ONE || h.vendorID != c->props.vendorID ||
h.deviceID != c->props.deviceID || memcmp(h.pipelineCacheUUID, c->props.pipelineCacheUUID, VK_UUID_SIZE)) {
rt_log("vk_pipeline: %s (%.1f MB) was written by another device or driver; starting empty", path,
len / 1048576.0);
free(data);
return NULL;
}
*size = (size_t)len;
return data;
}
// A pipeline cache from data (may be NULL: empty); VK_NULL_HANDLE when the driver makes none. *kept: the size of
// the data it holds afterwards. A driver may drop initial data it does not accept without failing the create, which
// leaves every build of the run compiling while the cache file looks valid.
static VkPipelineCache cache_make(VkCtx *c, const void *data, size_t size, size_t *kept) {
VkPipelineCache cache = VK_NULL_HANDLE;
VkPipelineCacheCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO, .initialDataSize = size,
.pInitialData = size ? data : NULL};
if (vkCreatePipelineCache(c->device, &ci, NULL, &cache) != VK_SUCCESS) {
ci.initialDataSize = 0, ci.pInitialData = NULL;
if (vkCreatePipelineCache(c->device, &ci, NULL, &cache) != VK_SUCCESS) cache = VK_NULL_HANDLE;
}
*kept = 0;
if (cache && vkGetPipelineCacheData(c->device, cache, kept, NULL) != VK_SUCCESS) *kept = 0;
return cache;
}
static void cache_load(VkCtx *c) {
cache_path_init();
pb_init(c);
if (PB.on) { // the binaries carry compiled pipelines across runs; P.cache stays VK_NULL_HANDLE
rt_log("vk_pipeline: %s is not read or written while pipeline binaries are on", P.cache_path);
return;
}
size_t size = 0, kept = 0;
void *data = cache_data_read(c, P.cache_path, &size);
int base_over = cache_over_cap; // an oversized file is never read again: the play file takes its place at once
P.cache = cache_make(c, data, size, &kept);
free(data);
P.cache_loaded = kept;
// The file holds this data when the driver took all of it: a save is due only once the cache has grown (cache_save).
P.saved[0] = size && kept == size ? kept : 0;
if (size)
rt_log("vk_pipeline: %s: %.1f MB read, the driver holds %.1f MB of it", P.cache_path, size / 1048576.0,
kept / 1048576.0);
if (!P.cache || !P.cache_path[0]) return;
// The play cache (see above pipe_create_in).
snprintf(P.play_path, sizeof P.play_path, "%s", P.cache_path);
size_t n = strlen(P.play_path);
if (n > 4 && !strcmp(P.play_path + n - 4, ".bin")) n -= 4;
snprintf(P.play_path + n, sizeof P.play_path - n, "-play.bin");
int want = cfg_bool("NX2NAVI_PIPELINE_CACHE_PLAY") && !prep_mode, on = want && P.cache_control;
size_t psize = 0, pkept = 0;
void *pdata = cache_data_read(c, P.play_path, &psize);
VkPipelineCache play = psize || on ? cache_make(c, pdata, psize, &pkept) : VK_NULL_HANDLE;
free(pdata);
uint64_t fold_at = (uint64_t)strtoull(cfg_value("NX2NAVI_PIPELINE_CACHE_PLAY_MB"), NULL, 10) << 20;
int fold = play && psize && (!on || psize >= fold_at || base_over);
if (fold) {
// Into P.cache now; the file is written by the first save, which then removes the play file. A run that ends
// before that save finds both files as they were and merges again.
VkResult r = vkMergePipelineCaches(c->device, P.cache, 1, &play);
vkDestroyPipelineCache(c->device, play, NULL);
play = VK_NULL_HANDLE;
if (r == VK_SUCCESS) {
P.fold = 1, P.saved[0] = 0;
rt_log("vk_pipeline: %s: %.1f MB read, the driver holds %.1f MB of it; merged into the cache of %s, which is "
"written once now (%s)", P.play_path, psize / 1048576.0, pkept / 1048576.0, P.cache_path,
prep_mode ? "a preparation builds into that file alone"
: !want ? "NX2NAVI_PIPELINE_CACHE_PLAY=0"
: !P.cache_control ? "no pipelineCreationCacheControl on this device"
: "it reached the size at which it is folded in, before play");
if (on) play = cache_make(c, NULL, 0, &pkept), psize = 0;
} else {
rt_log("vk_pipeline: %s did not merge into the cache of %s (%d); it is left as it is and not used in this run",
P.play_path, P.cache_path, (int)r);
on = 0;
}
} else if (psize) {
// Kept as the play cache, so its file goes on holding what earlier runs made, and merged into P.cache in
// memory, where the first create of pipe_create_in finds its pipelines. Without the merge that create took
// them from the driver's own cache when it had them, at 8 ms a pipeline against 0.4 ms from ours.
VkResult r = vkMergePipelineCaches(c->device, P.cache, 1, &play);
rt_log("vk_pipeline: %s: %.1f MB read, the driver holds %.1f MB of it%s", P.play_path, psize / 1048576.0,
pkept / 1048576.0, r == VK_SUCCESS ? "" : "; it did not merge into the other cache in memory");
}
P.play = on ? play : VK_NULL_HANDLE;
P.play_loaded = P.play ? pkept : 0;
// An empty play cache is not written (its data is a header): its size counts as saved, as a whole file's does.
P.saved[1] = P.play && (!psize || pkept == psize) ? pkept : 0;
if (P.play)
rt_log("vk_pipeline: what this run compiles goes into %s; %s is not written in play", P.play_path, P.cache_path);
else if (want && !P.cache_control)
rt_log("vk_pipeline: no play cache (no pipelineCreationCacheControl on this device): a new pipeline has %s "
"written whole", P.cache_path);
}
// The float widths spirv_nan_preserve declares on this device (P.nan_preserve), from NX2NAVI_VK_NAN_PRESERVE: 0 off,
// 1 on every driver; unset, on every driver except NVIDIA's own, on which the game draws right without it. Only widths
// the driver can preserve (VkPhysicalDeviceFloatControlsProperties) are declared, and nothing without 32-bit floats.
static void nan_preserve_init(VkCtx *c) {
VkPhysicalDeviceDriverProperties drv = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
VkPhysicalDeviceFloatControlsProperties fc = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FLOAT_CONTROLS_PROPERTIES,
.pNext = &drv};
VkPhysicalDeviceProperties2 p2 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &fc};
vkGetPhysicalDeviceProperties2(c->phys, &p2);
const char *e = cfg_get("NX2NAVI_VK_NAN_PRESERVE"); // no fixed default: it depends on the driver, see above
int set = e && *e, want = set ? strcmp(e, "0") != 0 : drv.driverID != VK_DRIVER_ID_NVIDIA_PROPRIETARY;
uint32_t can = (fc.shaderSignedZeroInfNanPreserveFloat16 ? NP_16 : 0) |
(fc.shaderSignedZeroInfNanPreserveFloat32 ? NP_32 : 0) |
(fc.shaderSignedZeroInfNanPreserveFloat64 ? NP_64 : 0);
P.nan_preserve = want && (can & NP_32) ? can : 0;
// NX2NAVI_VK_NAN_BITS: 1 every driver, 0 none; unset: AMD's driver for RDNA 1 and 2 (spirv_nan_bits)
const char *nb = cfg_get("NX2NAVI_VK_NAN_BITS");
P.nan_bits = nb && *nb ? strcmp(nb, "0") != 0 : vk_caps.amd_sc;
if (P.nan_bits)
rt_log("vk_pipeline: game shaders test for NaN on the bits of the number, not as a float (%s)",
nb && *nb ? "NX2NAVI_VK_NAN_BITS=1" : "AMD's driver for RDNA 1 and 2");
if (P.nan_preserve)
rt_log("vk_pipeline: game shaders keep NaN, infinity and signed zero (SignedZeroInfNanPreserve for %s32%s-bit "
"floats%s)", can & NP_16 ? "16, " : "", can & NP_64 ? ", 64" : "", set ? ", NX2NAVI_VK_NAN_PRESERVE=1" : "");
else
rt_log("vk_pipeline: game shaders without SignedZeroInfNanPreserve (%s)",
!want ? (set ? "NX2NAVI_VK_NAN_PRESERVE=0" : "NVIDIA's driver") : "the driver cannot keep them in 32-bit floats");
}
static void keys_open(void);
static void cu_open(void);
static void prebuild_start(void);
static int ensure_init(void) {
if (P.ready) return 1;
if (!vk_init()) return 0;
plat_mutex_lock(&P.lock);
if (P.ready) {
plat_mutex_unlock(&P.lock);
return 1;
}
VkCtx *c = vk_ctx();
P.device = c->device;
VkPhysicalDevicePushDescriptorPropertiesKHR pd = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PUSH_DESCRIPTOR_PROPERTIES_KHR};
VkPhysicalDeviceProperties2 p2 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &pd};
vkGetPhysicalDeviceProperties2(c->phys, &p2);
P.max_push = pd.maxPushDescriptors;
P.indep = vk_caps.gpl;
P.gpl = vk_caps.gpl;
rt_log(P.gpl ? "vk_pipeline: graphics pipeline library on: a draw whose pipeline is building gets a fast-linked one, "
"the optimised build replaces it when done"
: "vk_pipeline: graphics pipeline library off: draws wait for or skip pipelines that are building");
const char *ps = cfg_get("NX2NAVI_PUSH_STAGE"), *vo = cfg_get("NX2NAVI_VS_OUTS");
P.push_stage = ps && !strcmp(ps, "fs") ? GFX_STAGE_FS : GFX_STAGE_VS;
P.vs_outs_all = vo && !strcmp(vo, "all");
if (P.indep) rt_log("vk_pipeline: stage-independent layouts: vertex set 0, fragment set 1, %s set pushed; vertex outputs: %s",
P.push_stage == GFX_STAGE_VS ? "vertex" : "fragment", P.vs_outs_all ? "all 32 locations" : "union of paired fragment inputs");
// vk_device.c enables every feature the GPU reports in this chain, so the query tells what is enabled.
VkPhysicalDeviceExtendedDynamicState3FeaturesEXT e3 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_3_FEATURES_EXT};
VkPhysicalDeviceProvokingVertexFeaturesEXT pvf = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_FEATURES_EXT,
.pNext = c->has_eds3 ? &e3 : NULL};
VkPhysicalDeviceVulkan13Features v13 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES,
.pNext = c->has_provoking_vertex ? (void *)&pvf : (c->has_eds3 ? (void *)&e3 : NULL)};
VkPhysicalDeviceFeatures2 f2 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &v13};
vkGetPhysicalDeviceFeatures2(c->phys, &f2);
P.pv_last = c->has_provoking_vertex && pvf.provokingVertexLast;
if (!P.pv_last) rt_log("vk_pipeline: provokingVertexLast unavailable: flat outputs come from the first vertex (NVN: last)");
// Maxwell warps have 32 lanes; the translator assumes it (maxwell.md 3.4). Pin 32 where the driver allows it.
if (v13.subgroupSizeControl) {
VkPhysicalDeviceVulkan13Properties p13 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_PROPERTIES};
VkPhysicalDeviceProperties2 q2 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &p13};
vkGetPhysicalDeviceProperties2(c->phys, &q2);
P.sg32_stages = vk_caps.sg_pin_stages; // vk_caps.c: 32 unless NX2NAVI_VK_SUBGROUP; 0 without the pin
rt_log("vk_pipeline: subgroup size %u-%u, requiredSubgroupSizeStages %#x, pinning %u on %#x", p13.minSubgroupSize,
p13.maxSubgroupSize, (unsigned)p13.requiredSubgroupSizeStages, vk_caps.sg_size, (unsigned)P.sg32_stages);
}
// A warp reconverges where the guest compiler put it back together; Vulkan guarantees that only with maximal
// reconvergence (spirv_reconverge). NX2NAVI_VK_RECONVERGE=0 leaves the modules as translated.
P.reconverge = c->has_maximal_reconvergence && rt_knob("VK_RECONVERGE", "1") == 1;
rt_log("vk_pipeline: maximal reconvergence for shaders with subgroup ops: %s",
P.reconverge ? "on" : c->has_maximal_reconvergence ? "off (NX2NAVI_VK_RECONVERGE=0)" : "not supported by the driver");
nan_preserve_init(c);
P.f_full_subgroups = v13.computeFullSubgroups;
P.cache_control = v13.pipelineCreationCacheControl;
// Pipeline creation feedback is core in Vulkan 1.3; the driver reports per pipeline whether P.cache held it.
P.feedback = c->props.apiVersion >= VK_API_VERSION_1_3 ? atoi(cfg_value("NX2NAVI_PIPE_FEEDBACK")) : 0;
P.f_logic_op = f2.features.logicOp;
P.f_depth_clamp = f2.features.depthClamp;
P.f_fill_non_solid = f2.features.fillModeNonSolid;
P.f_dual_src = f2.features.dualSrcBlend;
#define PROC(field, name) (P.field = (void *)vk_dpa(c->device, name))
PROC(push, "vkCmdPushDescriptorSetKHR");
// Descriptor update templates (core 1.1; the push variant comes with VK_KHR_push_descriptor on 1.1): the driver
// walks a layout prepared when the layout is made instead of parsing write structs every draw.
// NX2NAVI_DESC_TEMPLATE=0 keeps the write structs.
if (!cfg_bool("NX2NAVI_DESC_TEMPLATE")) {
rt_log("vk_pipeline: descriptor update templates off (NX2NAVI_DESC_TEMPLATE=0)");
} else if (c->props.apiVersion >= VK_API_VERSION_1_1 && PROC(tpl_create, "vkCreateDescriptorUpdateTemplate") &&
PROC(tpl_destroy, "vkDestroyDescriptorUpdateTemplate") && PROC(tpl_update, "vkUpdateDescriptorSetWithTemplate")) {
P.tpl = 1;
if (!PROC(tpl_push, "vkCmdPushDescriptorSetWithTemplateKHR"))
rt_log("vk_pipeline: vkCmdPushDescriptorSetWithTemplateKHR missing: push sets are written with write structs");
} else {
rt_log("vk_pipeline: descriptor update templates unavailable: sets are written with write structs");
}
// NX2NAVI_VK_DYN_VERTEX=0/1 forces dynamic vertex input off or on. Default off on two drivers:
// - Intel's frozen Windows driver for its older integrated GPUs (UHD 630, Ice Lake: Vulkan 1.3.215 and older), which
// draws nothing with it.
// - AMD's driver for RDNA 3 and newer (vk_caps.amd_llpc), which compiles a pipeline with dynamic vertex input again
// at every boot: neither pipeline-cache.bin nor the driver's own disk cache gives it back. Measured on an
// RX 7900 XT (26.10.1) with NX2NAVI_PIPE_FEEDBACK=2, booting one save twice from an empty cache: the second boot
// compiled all 622 recorded non-mesh pipelines again at 83 ms each, with the module and state hashes of the first
// boot (the same with NX2NAVI_GPL=0, so it is not the pipeline libraries or their layouts), while the mesh
// pipelines, which have no vertex input, came from the file. With the vertex layout in the pipeline the second
// boot built 787 pipelines in 170 ms and the third 898 of 898 from the file in 233 ms, with no build left for the
// scene. A player's file of 3,979 records used the whole boot budget (PREWARM_PIPE_MS, 20 s) at every boot; with
// the vertex layout in the pipeline its third boot built 4,273 pipelines in 0.9 s. The price is more pipelines,
// one per vertex layout (787 records against 692 for that boot); records written with dynamic vertex input still
// prebuild, each for the layout of the draw that recorded it. The RDNA 1 and 2 branch (vk_caps.amd_sc) is not
// measured and keeps dynamic vertex input.
const char *dv = cfg_get("NX2NAVI_VK_DYN_VERTEX"); // no fixed default: it depends on the GPU, see above
int intel_legacy = c->props.vendorID == 0x8086 && c->props.apiVersion <= VK_MAKE_API_VERSION(0, 1, 3, 215);
int dv_on = dv && *dv ? strcmp(dv, "0") != 0 : !intel_legacy && !vk_caps.amd_llpc;
P.dyn_vi = dv_on && c->has_vertex_input_dynamic_state && PROC(set_vi, "vkCmdSetVertexInputEXT");
if (!dv_on && c->has_vertex_input_dynamic_state)
rt_log("vk_pipeline: dynamic vertex input off (%s)",
dv && *dv ? "NX2NAVI_VK_DYN_VERTEX=0"
: intel_legacy ? "Intel's older Windows driver draws nothing with it"
: "AMD's driver compiles pipelines that have it again at every boot");
if (c->has_eds3) {
P.dyn_clamp = e3.extendedDynamicState3DepthClampEnable && P.f_depth_clamp && PROC(set_clamp, "vkCmdSetDepthClampEnableEXT");
P.dyn_poly = e3.extendedDynamicState3PolygonMode && PROC(set_poly, "vkCmdSetPolygonModeEXT");
P.dyn_blend_en = e3.extendedDynamicState3ColorBlendEnable && PROC(set_blend_en, "vkCmdSetColorBlendEnableEXT");
P.dyn_blend_eq = e3.extendedDynamicState3ColorBlendEquation && PROC(set_blend_eq, "vkCmdSetColorBlendEquationEXT");
P.dyn_wmask = e3.extendedDynamicState3ColorWriteMask && PROC(set_wmask, "vkCmdSetColorWriteMaskEXT");
P.dyn_a2c = e3.extendedDynamicState3AlphaToCoverageEnable && PROC(set_a2c, "vkCmdSetAlphaToCoverageEnableEXT");
}
// NX2NAVI_VK_DYN_A2C=0/1 forces dynamic alpha to coverage off or on. Default off on AMD's Windows driver branch for
// RDNA 2 and older (vk_caps.amd_sc), which applies alpha to coverage while the dynamic state says off: on an RX 590
// (24.9.1) NX2navi, which never turns it on, drew Link and the NPCs with depth but no colour (black silhouettes) and
// dithered other surfaces into checkerboards (issue #67, RX 470/570/580). Taken out of the dynamic state, it goes
// into the pipeline key and the characters draw. With the texture pool marks it stays out of the dynamic state
// whatever the setting: a marked fragment shader gets early fragment tests only in a pipeline that has alpha to
// coverage off (spirv_pool_mark), so the pipeline has to know.
const char *da = cfg_get("NX2NAVI_VK_DYN_A2C"); // no fixed default: it depends on the driver, see above
int marks = vk_pool_marks_enabled();
int da_on = !marks && (da && *da ? strcmp(da, "0") != 0 : !vk_caps.amd_sc);
if (!da_on && P.dyn_a2c) {
P.dyn_a2c = 0;
rt_log("vk_pipeline: dynamic alpha to coverage off (%s)", marks ? "the texture pool marks, NX2NAVI_TEX_DISUSE"
: da && *da ? "NX2NAVI_VK_DYN_A2C=0"
: "AMD's driver for RDNA 2 and older");
}
#undef PROC
static const VkDynamicState core[] = {
VK_DYNAMIC_STATE_VIEWPORT_WITH_COUNT, VK_DYNAMIC_STATE_SCISSOR_WITH_COUNT, VK_DYNAMIC_STATE_DEPTH_BIAS,
VK_DYNAMIC_STATE_BLEND_CONSTANTS, VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK, VK_DYNAMIC_STATE_STENCIL_WRITE_MASK,
VK_DYNAMIC_STATE_STENCIL_REFERENCE, VK_DYNAMIC_STATE_CULL_MODE, VK_DYNAMIC_STATE_FRONT_FACE,
VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY, VK_DYNAMIC_STATE_DEPTH_TEST_ENABLE, VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE,
VK_DYNAMIC_STATE_DEPTH_COMPARE_OP, VK_DYNAMIC_STATE_STENCIL_TEST_ENABLE, VK_DYNAMIC_STATE_STENCIL_OP,
VK_DYNAMIC_STATE_DEPTH_BIAS_ENABLE, VK_DYNAMIC_STATE_RASTERIZER_DISCARD_ENABLE,
VK_DYNAMIC_STATE_PRIMITIVE_RESTART_ENABLE};
P.n_dyn = 0;
for (size_t i = 0; i < sizeof core / sizeof *core; i++) P.dyn[P.n_dyn++] = core[i];
P.dyn[P.n_dyn++] = P.dyn_vi ? VK_DYNAMIC_STATE_VERTEX_INPUT_EXT : VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE;
if (P.dyn_clamp) P.dyn[P.n_dyn++] = VK_DYNAMIC_STATE_DEPTH_CLAMP_ENABLE_EXT;
if (P.dyn_poly) P.dyn[P.n_dyn++] = VK_DYNAMIC_STATE_POLYGON_MODE_EXT;
if (P.dyn_blend_en) P.dyn[P.n_dyn++] = VK_DYNAMIC_STATE_COLOR_BLEND_ENABLE_EXT;
if (P.dyn_blend_eq) P.dyn[P.n_dyn++] = VK_DYNAMIC_STATE_COLOR_BLEND_EQUATION_EXT;
if (P.dyn_wmask) P.dyn[P.n_dyn++] = VK_DYNAMIC_STATE_COLOR_WRITE_MASK_EXT;
if (P.dyn_a2c) P.dyn[P.n_dyn++] = VK_DYNAMIC_STATE_ALPHA_TO_COVERAGE_ENABLE_EXT;
vk_mem_stage_log("device creation");
cache_load(c);
vk_mem_stage_log("the pipeline cache load");
rt_log("vk_pipeline: max push descriptors %u; dynamic vertex input %d, depth clamp %d, polygon mode %d, blend enable %d, "
"blend equation %d, write mask %d, alpha to coverage %d; cache %s",
P.max_push, P.dyn_vi, P.dyn_clamp, P.dyn_poly, P.dyn_blend_en, P.dyn_blend_eq, P.dyn_wmask, P.dyn_a2c,
P.cache_path[0] ? P.cache_path : "<memory only>");
keys_open();
cu_open();
plat_xchg32(&P.ready, 1);
plat_mutex_unlock(&P.lock);
prebuild_start();
return 1;
}
// ---- NVN -> Vulkan mappings
static VkBlendFactor blend_factor(unsigned f) {
switch (f) {
case 1: return VK_BLEND_FACTOR_ZERO;
case 2: return VK_BLEND_FACTOR_ONE;
case 3: return VK_BLEND_FACTOR_SRC_COLOR;
case 4: return VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR;
case 5: return VK_BLEND_FACTOR_SRC_ALPHA;
case 6: return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
case 7: return VK_BLEND_FACTOR_DST_ALPHA;
case 8: return VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA;
case 9: return VK_BLEND_FACTOR_DST_COLOR;
case 10: return VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR;
case 11: return VK_BLEND_FACTOR_SRC_ALPHA_SATURATE;
case 0x10: return VK_BLEND_FACTOR_SRC1_COLOR;
case 0x11: return VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR;
case 0x12: return VK_BLEND_FACTOR_SRC1_ALPHA;
case 0x13: return VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA;
case 0x61: return VK_BLEND_FACTOR_CONSTANT_COLOR;
case 0x62: return VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR;
case 0x63: return VK_BLEND_FACTOR_CONSTANT_ALPHA;
case 0x64: return VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA;
default: LOG_ONCE("vk_pipeline: unknown NVN blend factor %#x, using ONE", f); return VK_BLEND_FACTOR_ONE;
}
}
static VkBlendOp blend_op(unsigned e) {
switch (e) {
case 2: return VK_BLEND_OP_SUBTRACT;
case 3: return VK_BLEND_OP_REVERSE_SUBTRACT;
case 4: return VK_BLEND_OP_MIN;
case 5: return VK_BLEND_OP_MAX;
default: return VK_BLEND_OP_ADD;
}
}
// NVN_DEPTH_FUNC_* / NVN_STENCIL_FUNC_* are NEVER = 1 .. ALWAYS = 8 in VkCompareOp order.
static VkCompareOp compare_op(unsigned f) { return f >= 1 && f <= 8 ? (VkCompareOp)(f - 1) : VK_COMPARE_OP_ALWAYS; }
// NVN_STENCIL_OP_* are KEEP = 1 .. DECR_WRAP = 8 in VkStencilOp order.
static VkStencilOp stencil_op(unsigned o) { return o >= 1 && o <= 8 ? (VkStencilOp)(o - 1) : VK_STENCIL_OP_KEEP; }
// NVN_POLYGON_MODE_POINT/LINE/FILL = 0/1/2, VkPolygonMode FILL/LINE/POINT = 0/1/2.
static VkPolygonMode polygon_mode(unsigned m) {
if (!P.f_fill_non_solid || m > 2) return VK_POLYGON_MODE_FILL;
return (VkPolygonMode)(2 - m);
}
VkPrimitiveTopology vk_pipeline_topology(int prim) {
switch (prim) {
case NVN_DRAW_PRIMITIVE_POINTS: return VK_PRIMITIVE_TOPOLOGY_POINT_LIST;
case NVN_DRAW_PRIMITIVE_LINES: return VK_PRIMITIVE_TOPOLOGY_LINE_LIST;
case NVN_DRAW_PRIMITIVE_LINE_LOOP:
case NVN_DRAW_PRIMITIVE_LINE_STRIP: return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
case NVN_DRAW_PRIMITIVE_TRIANGLE_STRIP:
case NVN_DRAW_PRIMITIVE_QUAD_STRIP: return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; // same triangles, same coverage
case NVN_DRAW_PRIMITIVE_TRIANGLE_FAN:
case NVN_DRAW_PRIMITIVE_POLYGON: return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN;
case NVN_DRAW_PRIMITIVE_LINES_ADJACENCY: return VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY;
case NVN_DRAW_PRIMITIVE_LINE_STRIP_ADJACENCY: return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY;
case NVN_DRAW_PRIMITIVE_TRIANGLES_ADJACENCY: return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY;
case NVN_DRAW_PRIMITIVE_TRIANGLE_STRIP_ADJACENCY: return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY;
case NVN_DRAW_PRIMITIVE_PATCHES: return VK_PRIMITIVE_TOPOLOGY_PATCH_LIST;
default: return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; // TRIANGLES; QUADS after the replay's index rewrite
}
}
// Topology class (a pipeline's static topology only fixes the class): 0 point, 1 line, 2 triangle, 3 patch.
static int topology_class(VkPrimitiveTopology t) {
switch (t) {
case VK_PRIMITIVE_TOPOLOGY_POINT_LIST: return 0;
case VK_PRIMITIVE_TOPOLOGY_LINE_LIST:
case VK_PRIMITIVE_TOPOLOGY_LINE_STRIP:
case VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY:
case VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY: return 1;
case VK_PRIMITIVE_TOPOLOGY_PATCH_LIST: return 3;
default: return 2;
}
}
int vk_pipeline_tex_type(int target) {
switch (target) {
case NVN_TEXTURE_TARGET_1D: return GFX_TEX_1D;
case NVN_TEXTURE_TARGET_3D: return GFX_TEX_3D;
case NVN_TEXTURE_TARGET_1D_ARRAY: return GFX_TEX_1D_ARRAY;
case NVN_TEXTURE_TARGET_2D_ARRAY: return GFX_TEX_2D_ARRAY;
case NVN_TEXTURE_TARGET_2D_MULTISAMPLE: return GFX_TEX_2D_MS;
case NVN_TEXTURE_TARGET_2D_MULTISAMPLE_ARRAY: return GFX_TEX_2D_MS_ARRAY;
case NVN_TEXTURE_TARGET_CUBEMAP: return GFX_TEX_CUBE;
case NVN_TEXTURE_TARGET_CUBEMAP_ARRAY: return GFX_TEX_CUBE_ARRAY;
case NVN_TEXTURE_TARGET_BUFFER: return GFX_TEX_BUFFER;
default: return GFX_TEX_2D; // 2D, RECTANGLE
}
}
// Numeric type of a colour format as a shader sees it: GFX_TYPE_*.
static int format_type(VkFormat f) {
switch (f) {
case VK_FORMAT_R8_UINT: case VK_FORMAT_R8G8_UINT: case VK_FORMAT_R8G8B8_UINT: case VK_FORMAT_R8G8B8A8_UINT:
case VK_FORMAT_R16_UINT: case VK_FORMAT_R16G16_UINT: case VK_FORMAT_R16G16B16_UINT: case VK_FORMAT_R16G16B16A16_UINT:
case VK_FORMAT_R32_UINT: case VK_FORMAT_R32G32_UINT: case VK_FORMAT_R32G32B32_UINT: case VK_FORMAT_R32G32B32A32_UINT:
case VK_FORMAT_A2B10G10R10_UINT_PACK32: case VK_FORMAT_B8G8R8A8_UINT: case VK_FORMAT_S8_UINT:
return GFX_TYPE_UINT;
case VK_FORMAT_R8_SINT: case VK_FORMAT_R8G8_SINT: case VK_FORMAT_R8G8B8_SINT: case VK_FORMAT_R8G8B8A8_SINT:
case VK_FORMAT_R16_SINT: case VK_FORMAT_R16G16_SINT: case VK_FORMAT_R16G16B16_SINT: case VK_FORMAT_R16G16B16A16_SINT:
case VK_FORMAT_R32_SINT: case VK_FORMAT_R32G32_SINT: case VK_FORMAT_R32G32B32_SINT: case VK_FORMAT_R32G32B32A32_SINT:
case VK_FORMAT_A2B10G10R10_SINT_PACK32: case VK_FORMAT_B8G8R8A8_SINT:
return GFX_TYPE_SINT;
default: return GFX_TYPE_FLOAT;
}
}
// Optimal-tiling (o = 1) or buffer (o = 0) features of a core format, cached.
static VkFormatFeatureFlags format_features(VkFormat f, int optimal) {
static VkFormatFeatureFlags known[2][256];
static uint8_t have[2][256];
VkCtx *c = vk_ctx();
if (!c) return 0;
if ((unsigned)f < 256 && have[optimal][f]) return known[optimal][f];
VkFormatProperties p;
vkGetPhysicalDeviceFormatProperties(c->phys, f, &p);
VkFormatFeatureFlags r = optimal ? p.optimalTilingFeatures : p.bufferFeatures;
if ((unsigned)f < 256) known[optimal][f] = r, have[optimal][f] = 1;
return r;
}
// Vertex attribute formats by NVNformat.
typedef struct { VkFormat vk; uint8_t type; } VtxFmt;
#define F(v) {VK_FORMAT_##v, GFX_TYPE_FLOAT}
#define U(v) {VK_FORMAT_##v, GFX_TYPE_UINT}
#define I(v) {VK_FORMAT_##v, GFX_TYPE_SINT}
static const VtxFmt vtx_formats[0x9B] = {
[0x01] = F(R8_UNORM), [0x02] = F(R8_SNORM), [0x03] = U(R8_UINT), [0x04] = I(R8_SINT), [0x05] = F(R16_SFLOAT),
[0x06] = F(R16_UNORM), [0x07] = F(R16_SNORM), [0x08] = U(R16_UINT), [0x09] = I(R16_SINT), [0x0A] = F(R32_SFLOAT),
[0x0B] = U(R32_UINT), [0x0C] = I(R32_SINT), [0x0D] = F(R8G8_UNORM), [0x0E] = F(R8G8_SNORM), [0x0F] = U(R8G8_UINT),
[0x10] = I(R8G8_SINT), [0x11] = F(R16G16_SFLOAT), [0x12] = F(R16G16_UNORM), [0x13] = F(R16G16_SNORM),
[0x14] = U(R16G16_UINT), [0x15] = I(R16G16_SINT), [0x16] = F(R32G32_SFLOAT), [0x17] = U(R32G32_UINT),
[0x18] = I(R32G32_SINT), [0x19] = F(R8G8B8_UNORM), [0x1A] = F(R8G8B8_SNORM), [0x1B] = U(R8G8B8_UINT),
[0x1C] = I(R8G8B8_SINT), [0x1D] = F(R16G16B16_SFLOAT), [0x1E] = F(R16G16B16_UNORM), [0x1F] = F(R16G16B16_SNORM),
[0x20] = U(R16G16B16_UINT), [0x21] = I(R16G16B16_SINT), [0x22] = F(R32G32B32_SFLOAT), [0x23] = U(R32G32B32_UINT),
[0x24] = I(R32G32B32_SINT), [0x25] = F(R8G8B8A8_UNORM), [0x26] = F(R8G8B8A8_SNORM), [0x27] = U(R8G8B8A8_UINT),
[0x28] = I(R8G8B8A8_SINT), [0x29] = F(R16G16B16A16_SFLOAT), [0x2A] = F(R16G16B16A16_UNORM),
[0x2B] = F(R16G16B16A16_SNORM), [0x2C] = U(R16G16B16A16_UINT), [0x2D] = I(R16G16B16A16_SINT),
[0x2E] = F(R32G32B32A32_SFLOAT), [0x2F] = U(R32G32B32A32_UINT), [0x30] = I(R32G32B32A32_SINT),
[0x3D] = F(A2B10G10R10_UNORM_PACK32), [0x3E] = U(A2B10G10R10_UINT_PACK32), [0x3F] = F(B10G11R11_UFLOAT_PACK32),
// *_UI2F / *_I2F: integer data read as float; the 8- and 16-bit ones are Vulkan's SCALED formats
[0x51] = F(R8_USCALED), [0x52] = F(R8_SSCALED), [0x53] = F(R16_USCALED), [0x54] = F(R16_SSCALED),
[0x57] = F(R8G8_USCALED), [0x58] = F(R8G8_SSCALED), [0x59] = F(R16G16_USCALED), [0x5A] = F(R16G16_SSCALED),
[0x5D] = F(R8G8B8_USCALED), [0x5E] = F(R8G8B8_SSCALED), [0x5F] = F(R16G16B16_USCALED), [0x60] = F(R16G16B16_SSCALED),
[0x63] = F(R8G8B8A8_USCALED), [0x64] = F(R8G8B8A8_SSCALED), [0x65] = F(R16G16B16A16_USCALED),
[0x66] = F(R16G16B16A16_SSCALED),
// 32-bit ones have no SCALED format: read as integers (values are not converted; logged when used)
[0x55] = U(R32_UINT), [0x56] = I(R32_SINT), [0x5B] = U(R32G32_UINT), [0x5C] = I(R32G32_SINT),
[0x61] = U(R32G32B32_UINT), [0x62] = I(R32G32B32_SINT), [0x67] = U(R32G32B32A32_UINT), [0x68] = I(R32G32B32A32_SINT),
[0x69] = F(A2B10G10R10_SNORM_PACK32), [0x6A] = I(A2B10G10R10_SINT_PACK32), [0x6B] = F(A2B10G10R10_USCALED_PACK32),
[0x6C] = F(A2B10G10R10_SSCALED_PACK32), [0x9A] = F(B8G8R8A8_UNORM),
};
#undef F
#undef U
#undef I
// 3-component 8/16-bit formats are not vertex formats on every GPU: read 4 components (LAS vertex_format_supported).
static VkFormat widen(VkFormat f) {
switch (f) {
case VK_FORMAT_R8G8B8_UNORM: return VK_FORMAT_R8G8B8A8_UNORM;
case VK_FORMAT_R8G8B8_SNORM: return VK_FORMAT_R8G8B8A8_SNORM;
case VK_FORMAT_R8G8B8_UINT: return VK_FORMAT_R8G8B8A8_UINT;
case VK_FORMAT_R8G8B8_SINT: return VK_FORMAT_R8G8B8A8_SINT;
case VK_FORMAT_R8G8B8_USCALED: return VK_FORMAT_R8G8B8A8_USCALED;
case VK_FORMAT_R8G8B8_SSCALED: return VK_FORMAT_R8G8B8A8_SSCALED;
case VK_FORMAT_R16G16B16_SFLOAT: return VK_FORMAT_R16G16B16A16_SFLOAT;
case VK_FORMAT_R16G16B16_UNORM: return VK_FORMAT_R16G16B16A16_UNORM;
case VK_FORMAT_R16G16B16_SNORM: return VK_FORMAT_R16G16B16A16_SNORM;
case VK_FORMAT_R16G16B16_UINT: return VK_FORMAT_R16G16B16A16_UINT;
case VK_FORMAT_R16G16B16_SINT: return VK_FORMAT_R16G16B16A16_SINT;
case VK_FORMAT_R16G16B16_USCALED: return VK_FORMAT_R16G16B16A16_USCALED;
case VK_FORMAT_R16G16B16_SSCALED: return VK_FORMAT_R16G16B16A16_SSCALED;
default: return VK_FORMAT_UNDEFINED;
}
}
VkFormat vk_vertex_format(int nvn_format, int *type) {
if (type) *type = GFX_TYPE_FLOAT;
if (nvn_format <= 0 || nvn_format >= (int)(sizeof vtx_formats / sizeof *vtx_formats)) return VK_FORMAT_UNDEFINED;
const VtxFmt *v = &vtx_formats[nvn_format];
if (v->vk == VK_FORMAT_UNDEFINED) return VK_FORMAT_UNDEFINED;
if (type) *type = v->type;
if ((nvn_format >= 0x55 && nvn_format <= 0x68) && v->type != GFX_TYPE_FLOAT)
LOG_ONCE("vk_pipeline: 32-bit *_UI2F/*_I2F vertex formats are read as integers (first: %#x)", nvn_format);
VkFormat f = v->vk;
if (vk_ctx() && !(format_features(f, 0) & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT)) {
VkFormat w = widen(f);
if (w == VK_FORMAT_UNDEFINED || !(format_features(w, 0) & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT)) {
LOG_ONCE("vk_pipeline: vertex format %d (NVN %#x) unsupported", (int)f, nvn_format);
return VK_FORMAT_UNDEFINED;
}
LOG_ONCE("vk_pipeline: vertex format %d unsupported, read as 4 components (first: NVN %#x)", (int)f, nvn_format);
f = w;
}
return f;
}
// Attachment format of an NVN format (tex_vk_format: the format the images are created with).
static VkFormat target_format(int nvn_format, int *depth, int *stencil) {
TexVkFormat t;
if (depth) *depth = 0;
if (stencil) *stencil = 0;
if (!nvn_format || !tex_vk_format(nvn_format, &t)) return VK_FORMAT_UNDEFINED;
if (depth) *depth = t.depth;
if (stencil) *stencil = t.stencil;
return t.vk;
}
void vk_pipe_state_defaults(VkPipeState *s) {
memset(s, 0, sizeof *s);
s->samples = 1;
s->prim = NVN_DRAW_PRIMITIVE_TRIANGLES;
for (int i = 0; i < 8; i++)
s->blend[i] = (blend_st){(uint64_t)i, NVN_BLEND_FUNC_ONE, NVN_BLEND_FUNC_ZERO, NVN_BLEND_FUNC_ONE,
NVN_BLEND_FUNC_ZERO, NVN_BLEND_EQUATION_ADD, NVN_BLEND_EQUATION_ADD};
s->color = (color_st){0, NVN_LOGIC_OP_COPY, NVN_ALPHA_FUNC_ALWAYS};
s->chmask.mask = 0xffffffffu;
s->ms.enable = 1;
s->poly = (poly_st){NVN_FACE_NONE, NVN_FRONT_FACE_CCW, NVN_POLYGON_MODE_FILL, 0};
s->ds = (ds_st){0, 1, NVN_DEPTH_FUNC_LESS, 0, NVN_STENCIL_FUNC_ALWAYS, NVN_STENCIL_FUNC_ALWAYS,
NVN_STENCIL_OP_KEEP, NVN_STENCIL_OP_KEEP, NVN_STENCIL_OP_KEEP,
NVN_STENCIL_OP_KEEP, NVN_STENCIL_OP_KEEP, NVN_STENCIL_OP_KEEP};
}
void vk_pipeline_answers(const VkPipeState *s, GfxShaderAnswers *vs, GfxShaderAnswers *fs) {
if (vs)
for (int i = 0; i < 16; i++) {
va_st a;
memcpy(&a, &s->vattr[i], sizeof a);
int type = GFX_TYPE_FLOAT;
if (a.fmt) vk_vertex_format((int)a.fmt, &type);
vs->attr_type[i] = (uint8_t)type;
}
if (fs)
for (int i = 0; i < 8; i++) {
VkFormat f = target_format(s->color_fmt[i], NULL, NULL);
fs->frag_out_type[i] = (uint8_t)(f == VK_FORMAT_UNDEFINED ? GFX_TYPE_FLOAT : format_type(f));
}
}
// ---- vertex input (pipeline state, or vkCmdSetVertexInputEXT)
typedef VkVtxInput VtxInput;
// An attribute the game left without a format is a fixed one on Maxwell, a single 32-bit component of 0 (an unused
// attribute is bound as IsFixed 1x32 float): it reads (0, 0, 0, 1), as Vulkan expands a
// one-component format. NX2NAVI's foliage shaders scale wind sway by 1 - colour.w; shapes without a vertex colour
// (Brightbloom stem and leaves) must not sway, or they bend away from the parts that have one (the bulb).
static VkFormat zero_format(int type) {
return type == GFX_TYPE_UINT ? VK_FORMAT_R32_UINT : type == GFX_TYPE_SINT ? VK_FORMAT_R32_SINT : VK_FORMAT_R32_SFLOAT;
}
// Attributes for the Locations the vertex shader reads; one without a usable NVN attribute reads VK_PIPE_ZERO_BINDING.
static void vertex_input(const VkPipeState *s, VtxInput *v) {
GfxShaderIo io[32];
uint32_t used = gfx_shader_inputs(s->vs, io), streams = 0;
int zero = 0;
memset(v, 0, sizeof *v);
for (int l = 0; l < 32; l++) {
if (!(used >> l & 1)) continue;
VkVertexInputAttributeDescription2EXT *a = &v->a[v->n_attr++];
*a = (VkVertexInputAttributeDescription2EXT){.sType = VK_STRUCTURE_TYPE_VERTEX_INPUT_ATTRIBUTE_DESCRIPTION_2_EXT,
.location = (uint32_t)l};
va_st va = {0};
if (l < 16) memcpy(&va, &s->vattr[l], sizeof va);
VkFormat f = va.fmt ? vk_vertex_format((int)va.fmt, NULL) : VK_FORMAT_UNDEFINED;
if (f != VK_FORMAT_UNDEFINED && va.stream < 16) {
a->binding = va.stream, a->format = f, a->offset = va.offset;
streams |= 1u << va.stream;
} else {
a->binding = VK_PIPE_ZERO_BINDING, a->format = zero_format(io[l].type);
zero = 1;
}
}
for (uint32_t b = 0; b < 16; b++) {
if (!(streams >> b & 1)) continue;
uint32_t div = s->vstream[b].divisor;
if (div > 1) LOG_ONCE("vk_pipeline: instance divisor %u treated as 1 (no VK_EXT_vertex_attribute_divisor)", div);
v->b[v->n_bind++] = (VkVertexInputBindingDescription2EXT){
.sType = VK_STRUCTURE_TYPE_VERTEX_INPUT_BINDING_DESCRIPTION_2_EXT, .binding = b, .stride = s->vstream[b].stride,
.inputRate = div ? VK_VERTEX_INPUT_RATE_INSTANCE : VK_VERTEX_INPUT_RATE_VERTEX, .divisor = 1};
}
if (zero)
v->b[v->n_bind++] = (VkVertexInputBindingDescription2EXT){
.sType = VK_STRUCTURE_TYPE_VERTEX_INPUT_BINDING_DESCRIPTION_2_EXT, .binding = VK_PIPE_ZERO_BINDING,
.stride = 0, .inputRate = VK_VERTEX_INPUT_RATE_VERTEX, .divisor = 1};
}
int vk_pipeline_vertex_input_dynamic(void) { return ensure_init() && P.dyn_vi; }
PFN_vkCmdPushDescriptorSetKHR vk_pipeline_push_fn(void) { return ensure_init() ? P.push : NULL; }
PFN_vkCmdPushDescriptorSetWithTemplateKHR vk_pipeline_push_tpl_fn(void) { return ensure_init() && P.tpl ? P.tpl_push : NULL; }
PFN_vkUpdateDescriptorSetWithTemplate vk_pipeline_update_tpl_fn(void) { return ensure_init() && P.tpl ? P.tpl_update : NULL; }
int vk_pipeline_set_indep(int on) {
if (!ensure_init()) return 0;
plat_mutex_lock(&P.lock);
int was = P.indep;
P.indep = on && vk_caps.gpl;
plat_mutex_unlock(&P.lock);
return was;
}
// ---- SPIR-V rewrites
// Binding decorations b >= 1 become b + off (the fragment stage's share of set 0).
static uint32_t *spirv_rebind(const uint32_t *w, uint64_t n, uint32_t off) {
uint32_t *o = malloc(n * 4);
if (!o) return NULL;
memcpy(o, w, n * 4);
for (uint64_t i = 5; i < n;) {
uint32_t op = o[i] & 0xffff, len = o[i] >> 16;
if (!len || i + len > n || op == SPV_OP_FUNCTION) break;
if (op == SPV_OP_DECORATE && len >= 4 && o[i + 2] == SPV_DEC_BINDING && o[i + 3] >= 1) o[i + 3] += off;
i += len;
}
return o;
}
// DescriptorSet decorations become set (the fragment stage's resources move to set 1 with independent layouts).
static uint32_t *spirv_set(const uint32_t *w, uint64_t n, uint32_t set) {
uint32_t *o = malloc(n * 4);
if (!o) return NULL;
memcpy(o, w, n * 4);
for (uint64_t i = 5; i < n;) {
uint32_t op = o[i] & 0xffff, len = o[i] >> 16;
if (!len || i + len > n || op == SPV_OP_FUNCTION) break;
if (op == SPV_OP_DECORATE && len >= 4 && o[i + 2] == SPV_DEC_DESCRIPTOR_SET) o[i + 3] = set;
i += len;
}
return o;
}
// Dual-source blending (docs/maxwell.md 5.6): SRC1 blend factors read the fragment shader's colour output 1,
// which Vulkan takes from Location 0, Index 1. The Output variable at Location 1 moves there, as the translator declares it
// with DualSourceBlendEnable (CodeGen/Spirv/Declarations.cs); the pipeline is keyed by GKey.dsb, so only pipelines
// that blend with SRC1 get this module. NULL when the module has no Output at Location 1.
static uint32_t *spirv_dual_source(const uint32_t *w, uint64_t n, uint64_t *out_n) {
uint32_t outs[64];
int nout = 0;
uint64_t at = 0;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len || i + len > n) return NULL;
if (op == SPV_OP_VARIABLE && len >= 4 && w[i + 3] == SPV_SC_OUTPUT && nout < 64) outs[nout++] = w[i + 2];
i += len;
}
for (uint64_t i = 5; i < n && !at;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (op == SPV_OP_DECORATE && len == 4 && w[i + 2] == SPV_DEC_LOCATION && w[i + 3] == 1)
for (int k = 0; k < nout; k++)
if (outs[k] == w[i + 1]) at = i;
i += len;
}
if (!at) return NULL;
uint32_t *o = malloc((n + 4) * 4);
if (!o) return NULL;
memcpy(o, w, (at + 4) * 4);
o[at + 3] = 0;
o[at + 4] = 4u << 16 | SPV_OP_DECORATE, o[at + 5] = w[at + 1], o[at + 6] = SPV_DEC_INDEX, o[at + 7] = 1;
memcpy(o + at + 8, w + at + 4, (n - at - 4) * 4);
*out_n = n + 4;
return o;
}
// Adds an Output variable for each Location of `missing` (type and width from io) to a module, declared in the entry
// point interface and initialised as Maxwell reads an unwritten output: (0, 0, 0, 1), the w as the bits of 1.0f whatever
// the type (attribute memory is untyped); fewer than 4 components are all zero. Existing scalar, vector and pointer types
// are reused. NULL when the module does not have the expected layout.
static uint32_t *spirv_add_outputs(const uint32_t *w, uint64_t n, uint32_t missing, const GfxShaderIo io[32],
uint64_t *out_n) {
uint64_t ep_at = 0, ann_end = 0, fn_at = 0;
uint32_t bound = w[3], scalar[3] = {0, 0, 0}; // GFX_TYPE_FLOAT, SINT, UINT
struct { uint32_t id, comp, count; } vec[64];
struct { uint32_t id, type; } ptr[64];
int nvec = 0, nptr = 0;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len || i + len > n) return NULL;
const uint32_t *a = w + i + 1;
if (op == SPV_OP_ENTRY_POINT && !ep_at) ep_at = i;
else if (op == SPV_OP_DECORATE || op == 72 || op == 73 || op == 74 || op == 75 || op == 332 || op == 5632 || op == 5633)
ann_end = i + len;
else if (op == SPV_OP_TYPE_FLOAT && len >= 3 && a[1] == 32) scalar[GFX_TYPE_FLOAT] = a[0];
else if (op == SPV_OP_TYPE_INT && len >= 4 && a[1] == 32) scalar[a[2] ? GFX_TYPE_SINT : GFX_TYPE_UINT] = a[0];
else if (op == SPV_OP_TYPE_VECTOR && len >= 4 && nvec < 64) vec[nvec].id = a[0], vec[nvec].comp = a[1], vec[nvec++].count = a[2];
else if (op == SPV_OP_TYPE_POINTER && len >= 4 && a[1] == SPV_SC_OUTPUT && nptr < 64) ptr[nptr].id = a[0], ptr[nptr++].type = a[2];
else if (op == SPV_OP_FUNCTION) { fn_at = i; break; }
i += len;
}
if (!ep_at || !ann_end || !fn_at || ann_end < ep_at || fn_at < ann_end) return NULL;
uint32_t types[32 * 40], decs[32 * 4], vars[32], zero[3] = {0, 0, 0}, one[3] = {0, 0, 0};
int nt = 0, nd = 0, nv = 0;
for (int l = 0; l < 32; l++) {
if (!(missing >> l & 1)) continue;
int t = io[l].type <= GFX_TYPE_UINT ? io[l].type : GFX_TYPE_FLOAT;
uint32_t comps = io[l].comps ? io[l].comps : 4;
if (!scalar[t]) {
scalar[t] = bound++;
if (t == GFX_TYPE_FLOAT) types[nt++] = 3u << 16 | SPV_OP_TYPE_FLOAT, types[nt++] = scalar[t], types[nt++] = 32;
else types[nt++] = 4u << 16 | SPV_OP_TYPE_INT, types[nt++] = scalar[t], types[nt++] = 32, types[nt++] = t == GFX_TYPE_SINT;
}
uint32_t type = scalar[t];
if (comps > 1) {
int k = 0;
while (k < nvec && !(vec[k].comp == type && vec[k].count == comps)) k++;
if (k == nvec) {
if (nvec == 64) return NULL;
vec[nvec].id = bound++, vec[nvec].comp = type, vec[nvec++].count = comps;
types[nt++] = 4u << 16 | SPV_OP_TYPE_VECTOR, types[nt++] = vec[k].id, types[nt++] = type, types[nt++] = comps;
}
type = vec[k].id;
}
int k = 0;
while (k < nptr && ptr[k].type != type) k++;
if (k == nptr) {
if (nptr == 64) return NULL;
ptr[nptr].id = bound++, ptr[nptr++].type = type;
types[nt++] = 4u << 16 | SPV_OP_TYPE_POINTER, types[nt++] = ptr[k].id, types[nt++] = SPV_SC_OUTPUT, types[nt++] = type;
}
uint32_t init = bound++, var = bound++;
if (comps == 4) {
if (!zero[t]) {
zero[t] = bound++, one[t] = bound++;
types[nt++] = 4u << 16 | SPV_OP_CONSTANT, types[nt++] = scalar[t], types[nt++] = zero[t], types[nt++] = 0;
types[nt++] = 4u << 16 | SPV_OP_CONSTANT, types[nt++] = scalar[t], types[nt++] = one[t], types[nt++] = 0x3f800000;
}
types[nt++] = 7u << 16 | SPV_OP_CONSTANT_COMPOSITE, types[nt++] = type, types[nt++] = init;
types[nt++] = zero[t], types[nt++] = zero[t], types[nt++] = zero[t], types[nt++] = one[t];
} else {
types[nt++] = 3u << 16 | SPV_OP_CONSTANT_NULL, types[nt++] = type, types[nt++] = init;
}
types[nt++] = 5u << 16 | SPV_OP_VARIABLE, types[nt++] = ptr[k].id, types[nt++] = var, types[nt++] = SPV_SC_OUTPUT,
types[nt++] = init;
decs[nd++] = 4u << 16 | SPV_OP_DECORATE, decs[nd++] = var, decs[nd++] = SPV_DEC_LOCATION, decs[nd++] = (uint32_t)l;
vars[nv++] = var;
}
uint64_t total = n + (uint64_t)(nt + nd + nv);
uint32_t *o = malloc(total * 4), *p = o;
if (!o) return NULL;
uint32_t ep_len = w[ep_at] >> 16;
memcpy(p, w, 5 * 4), p[3] = bound, p += 5;
memcpy(p, w + 5, (ep_at - 5) * 4), p += ep_at - 5;
*p++ = (ep_len + (uint32_t)nv) << 16 | SPV_OP_ENTRY_POINT;
memcpy(p, w + ep_at + 1, (ep_len - 1) * 4), p += ep_len - 1;
memcpy(p, vars, (size_t)nv * 4), p += nv;
memcpy(p, w + ep_at + ep_len, (ann_end - ep_at - ep_len) * 4), p += ann_end - ep_at - ep_len;
memcpy(p, decs, (size_t)nd * 4), p += nd;
memcpy(p, w + ann_end, (fn_at - ann_end) * 4), p += fn_at - ann_end;
memcpy(p, types, (size_t)nt * 4), p += nt;
memcpy(p, w + fn_at, (n - fn_at) * 4), p += n - fn_at;
*out_n = (uint64_t)(p - o);
return o;
}
// The locations a mesh module writes (its Output variables with a Location), and whether it draws points (execution
// mode OutputPoints). 0 when the module cannot be read.
static uint32_t spirv_mesh_outputs(const uint32_t *w, uint64_t n, int *points) {
enum { OP_EXECUTION_MODE = 16, MODE_OUTPUT_POINTS = 27 };
uint32_t bound = n >= 5 && w[3] < (1u << 22) ? w[3] : 0, mask = 0;
uint8_t *out = bound ? calloc(bound, 1) : NULL;
int8_t *loc = bound ? malloc(bound) : NULL;
*points = 0;
if (!out || !loc) return free(out), free(loc), 0;
memset(loc, -1, bound);
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len || i + len > n) break;
const uint32_t *a = w + i + 1;
if (op == OP_EXECUTION_MODE && len >= 3 && a[1] == MODE_OUTPUT_POINTS) *points = 1;
else if (op == SPV_OP_DECORATE && len >= 4 && a[1] == SPV_DEC_LOCATION && a[0] < bound && a[2] < 32) loc[a[0]] = (int8_t)a[2];
else if (op == SPV_OP_VARIABLE && len >= 4 && a[2] == SPV_SC_OUTPUT && a[1] < bound) out[a[1]] = 1;
else if (op == SPV_OP_FUNCTION) break;
i += len;
}
for (uint32_t id = 0; id < bound; id++)
if (out[id] && loc[id] >= 0) mask |= 1u << loc[id];
free(out), free(loc);
return mask;
}
// A fragment module whose inputs at the locations of `missing` read what Maxwell gives a fragment input no earlier
// stage writes: (0, 0, 0, 1), or with `points` the point sprite's coordinate (s, t, 0, 1). Each such Input variable
// (a float or a float vector) becomes a Private one, the access chains through it Private pointers, its decorations
// go, and the entry function stores the value into it before anything else. NULL when nothing is to be done or the
// module does not have the expected layout.
static uint32_t *spirv_fs_fill_inputs(const uint32_t *w, uint64_t n, uint32_t missing, int points, uint64_t *out_n) {
enum { SC_INPUT = 1, SC_PRIVATE = 6, OP_EXECUTION_MODE = 16, OP_TYPE_POINTER = 32, OP_LABEL = 248, OP_LOAD = 61,
OP_STORE = 62, OP_ACCESS_CHAIN = 65, OP_IN_BOUNDS_ACCESS_CHAIN = 66, OP_COMPOSITE_CONSTRUCT = 80,
OP_COMPOSITE_EXTRACT = 81, OP_DECORATE_ID = 332, BUILTIN_POINT_COORD = 16, MAX_FILL = 32, MAX_PRIV = 64 };
if (!missing || n < 5 || w[3] >= (1u << 22)) return NULL;
uint32_t old_bound = w[3], bound = old_bound;
uint32_t *pt_type = calloc(old_bound, 4), *vec_comp = calloc(old_bound, 4), *vec_n = calloc(old_bound, 4);
int8_t *loc = malloc(old_bound);
uint8_t *fill = calloc(old_bound, 1); // 1: a filled variable, 2: an access chain through one
uint32_t *o = NULL;
if (!pt_type || !vec_comp || !vec_n || !loc || !fill) goto done;
memset(loc, -1, old_bound);
uint32_t f32 = 0, v2 = 0, entry = 0;
uint64_t ep_at = 0, ann_end = 0, fn_at = 0;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len || i + len > n) goto done;
const uint32_t *a = w + i + 1;
if (op == SPV_OP_ENTRY_POINT && !ep_at) ep_at = i, entry = a[1];
else if (op == SPV_OP_DECORATE || op == 72 || op == OP_DECORATE_ID) {
ann_end = i + len;
if (op == SPV_OP_DECORATE && len >= 4 && a[1] == SPV_DEC_LOCATION && a[0] < old_bound && a[2] < 32)
loc[a[0]] = (int8_t)a[2];
} else if (op == SPV_OP_TYPE_FLOAT && len >= 3 && a[1] == 32) f32 = a[0];
else if (op == SPV_OP_TYPE_VECTOR && len >= 4 && a[0] < old_bound) {
vec_comp[a[0]] = a[1], vec_n[a[0]] = a[2];
if (a[1] == f32 && a[2] == 2) v2 = a[0];
} else if (op == OP_TYPE_POINTER && len >= 4 && a[0] < old_bound) pt_type[a[0]] = a[2];
else if (op == SPV_OP_FUNCTION) { fn_at = i; break; }
i += len;
}
if (!ep_at || !ann_end || !fn_at || !f32 || !entry || ann_end > fn_at) goto done;
// the variables to fill, and the Private pointer types they and the chains through them need (pass 1)
uint32_t vars[MAX_FILL], vtype[MAX_FILL], nvars = 0, priv[MAX_PRIV][2], npriv = 0; // priv: Input pointer type -> Private
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
const uint32_t *a = w + i + 1;
uint32_t ptr = 0;
if (op == SPV_OP_VARIABLE && len >= 4 && a[2] == SC_INPUT && a[1] < old_bound && a[0] < old_bound && i < fn_at) {
int l = loc[a[1]];
uint32_t t = pt_type[a[0]];
int floaty = t == f32 || (t < old_bound && vec_comp[t] == f32 && vec_n[t] >= 2 && vec_n[t] <= 4);
if (l >= 0 && (missing >> l & 1) && floaty && nvars < MAX_FILL) {
fill[a[1]] = 1, vars[nvars] = a[1], vtype[nvars++] = t, ptr = a[0];
}
} else if ((op == OP_ACCESS_CHAIN || op == OP_IN_BOUNDS_ACCESS_CHAIN) && len >= 4 && a[2] < old_bound && fill[a[2]] &&
a[1] < old_bound && a[0] < old_bound) {
fill[a[1]] = 2, ptr = a[0];
}
if (ptr) {
uint32_t k = 0;
while (k < npriv && priv[k][0] != ptr) k++;
if (k == npriv) {
if (npriv == MAX_PRIV) goto done;
priv[npriv][0] = ptr, priv[npriv++][1] = bound++;
}
}
i += len;
}
if (!nvars) goto done;
// the globals added before the first function: two constants, gl_PointCoord, and the Private pointer types not yet
// declared (a filled variable's goes right before the variable, below; an access chain's is only used in a function)
uint32_t extra[MAX_PRIV * 4 + 32], ne = 0, c0 = bound++, c1 = bound++, pc_ptr = 0, pc_var = 0;
uint8_t declared[MAX_PRIV] = {0};
extra[ne++] = 4u << 16 | SPV_OP_CONSTANT, extra[ne++] = f32, extra[ne++] = c0, extra[ne++] = 0;
extra[ne++] = 4u << 16 | SPV_OP_CONSTANT, extra[ne++] = f32, extra[ne++] = c1, extra[ne++] = 0x3f800000;
if (points) {
if (!v2) v2 = bound++, extra[ne++] = 4u << 16 | SPV_OP_TYPE_VECTOR, extra[ne++] = v2, extra[ne++] = f32, extra[ne++] = 2;
pc_ptr = bound++, pc_var = bound++;
extra[ne++] = 4u << 16 | OP_TYPE_POINTER, extra[ne++] = pc_ptr, extra[ne++] = SC_INPUT, extra[ne++] = v2;
extra[ne++] = 4u << 16 | SPV_OP_VARIABLE, extra[ne++] = pc_ptr, extra[ne++] = pc_var, extra[ne++] = SC_INPUT;
}
o = malloc((n + ne + (uint64_t)npriv * 4 + 16 + (uint64_t)nvars * 12 + 32) * 4);
if (!o) goto done;
uint32_t *p = o;
memcpy(p, w, 5 * 4), p += 5;
int in_entry = 0, stored = 0;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
const uint32_t *a = w + i + 1;
if (i == fn_at) {
for (uint32_t k = 0; k < npriv; k++)
if (!declared[k]) *p++ = 4u << 16 | OP_TYPE_POINTER, *p++ = priv[k][1], *p++ = SC_PRIVATE, *p++ = pt_type[priv[k][0]];
memcpy(p, extra, ne * 4), p += ne;
}
if (i == ep_at) { // the interface: filled variables out before SPIR-V 1.4 (Private is not listed), gl_PointCoord in
uint32_t *head = p, name_words = (uint32_t)(strnlen((const char *)(a + 2), (len - 3) * 4) / 4 + 1);
for (uint32_t k = 0; k < len; k++) {
uint32_t v = w[i + k];
if (k >= 3 + name_words && v < old_bound && fill[v] == 1 && w[1] < 0x00010400) continue;
*p++ = v;
}
if (pc_var) *p++ = pc_var;
*head = (uint32_t)(p - head) << 16 | SPV_OP_ENTRY_POINT;
i += len;
continue;
}
if ((op == SPV_OP_DECORATE || op == OP_DECORATE_ID) && len >= 3 && a[0] < old_bound && fill[a[0]] == 1) {
i += len; // Location, Flat and the like belong to Input variables
if (i == ann_end && pc_var)
*p++ = 4u << 16 | SPV_OP_DECORATE, *p++ = pc_var, *p++ = SPV_DEC_BUILTIN, *p++ = BUILTIN_POINT_COORD;
continue;
}
if (op == SPV_OP_VARIABLE && len >= 4 && a[1] < old_bound && fill[a[1]] == 1) // its pointer type first
for (uint32_t k = 0; k < npriv; k++)
if (priv[k][0] == a[0] && !declared[k])
declared[k] = 1, *p++ = 4u << 16 | OP_TYPE_POINTER, *p++ = priv[k][1], *p++ = SC_PRIVATE, *p++ = pt_type[priv[k][0]];
memcpy(p, w + i, len * 4);
if ((op == SPV_OP_VARIABLE || op == OP_ACCESS_CHAIN || op == OP_IN_BOUNDS_ACCESS_CHAIN) && len >= 4 &&
a[1] < old_bound && fill[a[1]]) {
for (uint32_t k = 0; k < npriv; k++)
if (priv[k][0] == a[0]) p[1] = priv[k][1];
if (op == SPV_OP_VARIABLE) p[3] = SC_PRIVATE;
}
p += len;
if (i + len == ann_end && pc_var) // gl_PointCoord's BuiltIn after the last decoration
*p++ = 4u << 16 | SPV_OP_DECORATE, *p++ = pc_var, *p++ = SPV_DEC_BUILTIN, *p++ = BUILTIN_POINT_COORD;
if (op == SPV_OP_FUNCTION && len >= 5) in_entry = a[1] == entry;
i += len;
if (op == OP_LABEL && in_entry && !stored) { // the entry's first block: its OpVariables, then the stores
while (i < n && (w[i] & 0xffff) == SPV_OP_VARIABLE) memcpy(p, w + i, (w[i] >> 16) * 4), p += w[i] >> 16, i += w[i] >> 16;
uint32_t sx = c0, sy = c0;
if (pc_var) {
uint32_t pc = bound++;
sx = bound++, sy = bound++;
*p++ = 4u << 16 | OP_LOAD, *p++ = v2, *p++ = pc, *p++ = pc_var;
*p++ = 5u << 16 | OP_COMPOSITE_EXTRACT, *p++ = f32, *p++ = sx, *p++ = pc, *p++ = 0;
*p++ = 5u << 16 | OP_COMPOSITE_EXTRACT, *p++ = f32, *p++ = sy, *p++ = pc, *p++ = 1;
}
for (uint32_t k = 0; k < nvars; k++) {
uint32_t t = vtype[k], c = t == f32 ? 1 : vec_n[t], comp[4] = {sx, sy, c0, c1}, val = comp[0];
if (c > 1) {
val = bound++;
*p++ = (3 + c) << 16 | OP_COMPOSITE_CONSTRUCT, *p++ = t, *p++ = val;
for (uint32_t q = 0; q < c; q++) *p++ = comp[q]; // x y z w in order: (s, t, 0, 1) or (0, 0, 0, 1)
}
*p++ = 3u << 16 | OP_STORE, *p++ = vars[k], *p++ = val;
}
stored = 1;
}
}
if (!stored) { free(o), o = NULL; goto done; }
o[3] = bound;
*out_n = (uint64_t)(p - o);
done:
free(pt_type), free(vec_comp), free(vec_n), free(loc), free(fill);
return o;
}
// Two fixes for what the translator declares but a shader does not use (NULL: nothing to change):
// - the fragment shader interlock capability and extension, which the translator adds to every fragment shader when the host
// reports interlock support (SpirvGenerator.cs) and vk_device.c does not enable; kept when the shader interlocks;
// - the CullDistance[1] member of gl_PerVertex next to ClipDistance[8] (Declarations.cs CreatePerVertexStructType):
// 9 distances exceed maxCombinedClipAndCullDistances (8 on the RX 9070). The translator never accesses CullDistance; the
// member goes when it is the struct's last and no access chain selects it.
static uint32_t *spirv_fixup(const uint32_t *w, uint64_t n, uint64_t *out_n) {
enum { OP_EXTENSION = 10, OP_CAPABILITY = 17, OP_MEMBER_NAME = 6, OP_EXECUTION_MODE = 16, OP_TYPE_ARRAY = 28,
OP_TYPE_STRUCT = 30, OP_CONSTANT = 43, OP_MEMBER_DECORATE = 72, OP_ACCESS_CHAIN = 65, OP_IN_BOUNDS_ACCESS_CHAIN = 66,
OP_BEGIN_INTERLOCK = 5364, OP_END_INTERLOCK = 5365, CAP_SAMPLE_INTERLOCK = 5363, CAP_PIXEL_INTERLOCK = 5378,
CAP_RATE_INTERLOCK = 5372, BUILTIN_CLIP = 3, BUILTIN_CULL = 4 };
if (n < 5) return NULL;
uint32_t bound = w[3] < (1u << 22) ? w[3] : (1u << 22);
uint32_t *val = calloc(bound, sizeof *val), *elem = calloc(bound, sizeof *elem), *len = calloc(bound, sizeof *len),
*pointee = calloc(bound, sizeof *pointee), *var_type = calloc(bound, sizeof *var_type);
uint32_t *o = NULL;
int interlock_used = 0, interlock_declared = 0, cull_ok = 0;
uint32_t cull_struct = 0, cull_member = 0, clip_member = ~0u, members = 0, member_types[8] = {0};
if (!val || !elem || !len || !pointee || !var_type) goto out;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, l = w[i] >> 16;
if (!l || i + l > n) goto out;
const uint32_t *a = w + i + 1;
if (op == OP_BEGIN_INTERLOCK || op == OP_END_INTERLOCK) interlock_used = 1;
else if (op == OP_EXECUTION_MODE && l >= 3 && a[1] >= 5366 && a[1] <= 5371) interlock_used = 1;
else if (op == OP_CAPABILITY && l >= 2 && (a[0] == CAP_PIXEL_INTERLOCK || a[0] == CAP_SAMPLE_INTERLOCK || a[0] == CAP_RATE_INTERLOCK))
interlock_declared = 1;
else if (op == OP_CONSTANT && l == 4 && a[1] < bound) val[a[1]] = a[2];
else if (op == OP_TYPE_ARRAY && l >= 4 && a[0] < bound) elem[a[0]] = a[1], len[a[0]] = a[2] < bound ? val[a[2]] : 0;
else if (op == SPV_OP_TYPE_POINTER && l >= 4 && a[0] < bound) pointee[a[0]] = a[2];
else if (op == SPV_OP_VARIABLE && l >= 4 && a[1] < bound) var_type[a[1]] = a[0] < bound ? pointee[a[0]] : 0;
else if (op == OP_MEMBER_DECORATE && l >= 5 && a[2] == SPV_DEC_BUILTIN && a[3] == BUILTIN_CULL) cull_struct = a[0], cull_member = a[1];
else if (op == OP_MEMBER_DECORATE && l >= 5 && a[2] == SPV_DEC_BUILTIN && a[3] == BUILTIN_CLIP) clip_member = a[1];
else if (op == OP_TYPE_STRUCT && cull_struct && a[0] == cull_struct && l >= 2) {
members = l - 2;
for (uint32_t m = 0; m < members && m < 8; m++) member_types[m] = a[1 + m];
} else if ((op == OP_ACCESS_CHAIN || op == OP_IN_BOUNDS_ACCESS_CHAIN) && l >= 5 && cull_struct && a[2] < bound) {
uint32_t t = var_type[a[2]], k = 3;
if (t < bound && t && elem[t] == cull_struct) k = 4; // arrayed per-vertex block
if ((t == cull_struct || k == 4) && k + 1 < l && a[k] < bound && val[a[k]] == cull_member) cull_struct = 0;
}
i += l;
}
VkCtx *c = vk_ctx();
uint32_t limit = c ? c->props.limits.maxCombinedClipAndCullDistances : 8;
if (cull_struct && members && members <= 8 && cull_member == members - 1 && clip_member < members) {
uint32_t clip_t = member_types[clip_member], cull_t = member_types[cull_member];
uint32_t clip_n = clip_t < bound ? len[clip_t] : 0, cull_n = cull_t < bound ? len[cull_t] : 0;
cull_ok = clip_n + cull_n > limit && clip_n <= limit;
}
int strip = interlock_declared && !interlock_used;
if (!strip && !cull_ok) goto out;
o = malloc(n * 4);
if (!o) goto out;
uint64_t on = 5;
memcpy(o, w, 5 * 4);
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, l = w[i] >> 16;
const uint32_t *a = w + i + 1;
int drop = 0;
if (strip && op == OP_CAPABILITY && (a[0] == CAP_PIXEL_INTERLOCK || a[0] == CAP_SAMPLE_INTERLOCK || a[0] == CAP_RATE_INTERLOCK)) drop = 1;
if (strip && op == OP_EXTENSION && l >= 2 && !strncmp((const char *)a, "SPV_EXT_fragment_shader_interlock", (l - 1) * 4)) drop = 1;
if (cull_ok && (op == OP_MEMBER_DECORATE || op == OP_MEMBER_NAME) && l >= 3 && a[0] == cull_struct && a[1] == cull_member) drop = 1;
if (!drop) {
memcpy(o + on, w + i, l * 4);
if (cull_ok && op == OP_TYPE_STRUCT && a[0] == cull_struct) o[on] = (l - 1) << 16 | op; // last member gone
on += op == OP_TYPE_STRUCT && cull_ok && a[0] == cull_struct ? l - 1 : l;
}
i += l;
}
*out_n = on;
out:
free(val), free(elem), free(len), free(pointee), free(var_type);
return o;
}
// True when the module declares a GroupNonUniform* or SubgroupBallot-style capability (lane masks assume 32 lanes).
static int uses_subgroup_ops(const uint32_t *w, uint64_t words) {
for (uint64_t i = 5; i < words;) {
uint32_t op = w[i] & 0xffff, n = w[i] >> 16;
if (!n || i + n > words) return 0;
if (op != 17) return 0; // OpCapability; capabilities come first, stop at the first other instruction
uint32_t cap = w[i + 1];
if ((cap >= 61 && cap <= 68) || cap == 4423 || cap == 4431) return 1; // NonUniform*, SubgroupBallotKHR, SubgroupVoteKHR
i += n;
}
return 0;
}
// Guest code runs warp-wide operations (ballot, shuffle, vote) where the guest compiler had the warp reconverged, for
// example right after a loop that each lane leaves at a different iteration. Vulkan only promises the same lanes at the
// same point with the MaximallyReconvergesKHR execution mode. Without it a driver may run the lanes that left the loop
// early through the operations on their own, with a ballot or scan that sees only some of the lanes: a mesh program
// that hands out output slots from one could give two primitives the same slot. This adds the extension and the mode
// for each entry point of a module that declares subgroup capabilities. NULL: nothing to change (or no memory).
static uint32_t *spirv_reconverge(const uint32_t *w, uint64_t n, uint64_t *out_n) {
enum { OP_EXTENSION = 10, OP_EXECUTION_MODE = 16, OP_CAPABILITY = 17, MODE_MAXIMALLY_RECONVERGES = 6023 };
static const char ext[] = "SPV_KHR_maximal_reconvergence";
const uint32_t ext_words = (uint32_t)(sizeof ext + 3) / 4; // the string with its terminating zero, padded
if (n < 5 || !uses_subgroup_ops(w, n)) return NULL;
uint64_t caps_end = 0, eps_end = 0;
uint32_t eps[16], neps = 0;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, l = w[i] >> 16;
if (!l || i + l > n) return NULL;
if (op == OP_CAPABILITY) caps_end = i + l;
else if (op == SPV_OP_ENTRY_POINT && l >= 3) {
if (neps == 16) return NULL;
eps[neps++] = w[i + 2], eps_end = i + l;
} else if (op == OP_EXECUTION_MODE && l >= 3 && w[i + 2] == MODE_MAXIMALLY_RECONVERGES) return NULL;
else if (op == SPV_OP_FUNCTION) break;
i += l;
}
if (!caps_end || !neps || eps_end < caps_end) return NULL;
uint64_t on = n + 1 + ext_words + 3ull * neps;
uint32_t *o = malloc(on * 4);
if (!o) return NULL;
uint64_t p = 0;
memcpy(o, w, caps_end * 4), p = caps_end;
o[p++] = (1 + ext_words) << 16 | OP_EXTENSION;
memset(o + p, 0, ext_words * 4);
memcpy(o + p, ext, sizeof ext), p += ext_words;
memcpy(o + p, w + caps_end, (eps_end - caps_end) * 4), p += eps_end - caps_end;
for (uint32_t k = 0; k < neps; k++) o[p++] = 3u << 16 | OP_EXECUTION_MODE, o[p++] = eps[k], o[p++] = MODE_MAXIMALLY_RECONVERGES;
memcpy(o + p, w + eps_end, (n - eps_end) * 4), p += n - eps_end;
*out_n = p;
return o;
}
// Maxwell computes in IEEE 754, and the game's shaders rely on it: its compiler writes an unordered comparison as
// `x > c || isnan(x)`, a normalisation of a zero-length vector gives an infinity that a later comparison sorts out, and
// integer masks such as 0xffffffff travel through float moves and selects. The translator's own guards are isnan
// tests as well (fragment outputs, min, max and clamp). Without the SignedZeroInfNanPreserve execution mode, SPIR-V
// lets the driver assume that no float is NaN or infinite, and AMD's compilers act on it: on an RX 9070 XT a select
// between 1 and the bits 0xffffffff, converted to float as a signed integer, gave 2^32 instead of -1 and blew the
// shadow casters up into huge triangles (issue #156); RX 6000 cards drew Navi, and Link's skin next to her, with black
// and white blocks (#41, #110); and AMD GPUs drew Navi's sparkles but not her orb, whose light sits at her own centre.
// NVIDIA's driver keeps the IEEE results without the mode. This adds the capability, the extension (the translator
// writes SPIR-V 1.3) and the mode for each entry point: 32-bit floats always, 16-bit and 64-bit ones where the module
// declares Float16 or Float64 and `widths` (NP_*) allows them. NULL: nothing to change (the module has the mode, or no
// 32-bit width is allowed, or no memory).
static uint32_t *spirv_nan_preserve(const uint32_t *w, uint64_t n, uint32_t widths, uint64_t *out_n) {
enum { OP_EXTENSION = 10, OP_EXECUTION_MODE = 16, OP_CAPABILITY = 17, CAP_FLOAT16 = 9, CAP_FLOAT64 = 10,
CAP_PRESERVE = 4466, MODE_PRESERVE = 4461 };
static const char ext[] = "SPV_KHR_float_controls";
const uint32_t ext_words = (uint32_t)(sizeof ext + 3) / 4; // the string with its terminating zero, padded
if (n < 5 || !(widths & NP_32)) return NULL;
uint64_t caps_end = 0, eps_end = 0;
uint32_t eps[16], neps = 0, use = NP_32;
int has_cap = 0, has_ext = 0;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, l = w[i] >> 16;
if (!l || i + l > n) return NULL;
if (op == OP_CAPABILITY && l >= 2) {
caps_end = i + l;
if (w[i + 1] == CAP_FLOAT16) use |= NP_16;
else if (w[i + 1] == CAP_FLOAT64) use |= NP_64;
else if (w[i + 1] == CAP_PRESERVE) has_cap = 1;
} else if (op == OP_EXTENSION && (l - 1) * 4 >= sizeof ext && !memcmp(w + i + 1, ext, sizeof ext)) {
has_ext = 1;
} else if (op == SPV_OP_ENTRY_POINT && l >= 3) {
if (neps == 16) return NULL;
eps[neps++] = w[i + 2], eps_end = i + l;
} else if (op == OP_EXECUTION_MODE && l >= 3 && w[i + 2] == MODE_PRESERVE) {
return NULL;
} else if (op == SPV_OP_FUNCTION) {
break;
}
i += l;
}
if (!caps_end || !neps || eps_end < caps_end) return NULL;
use &= widths;
uint32_t nw = 0, width[3];
if (use & NP_16) width[nw++] = 16;
width[nw++] = 32;
if (use & NP_64) width[nw++] = 64;
uint64_t on = n + (has_cap ? 0 : 2) + (has_ext ? 0 : 1 + ext_words) + 4ull * nw * neps;
uint32_t *o = malloc(on * 4);
if (!o) return NULL;
uint64_t p = 0;
memcpy(o, w, caps_end * 4), p = caps_end;
if (!has_cap) o[p++] = 2u << 16 | OP_CAPABILITY, o[p++] = CAP_PRESERVE;
if (!has_ext) {
o[p++] = (1 + ext_words) << 16 | OP_EXTENSION;
memset(o + p, 0, ext_words * 4);
memcpy(o + p, ext, sizeof ext), p += ext_words;
}
memcpy(o + p, w + caps_end, (eps_end - caps_end) * 4), p += eps_end - caps_end;
for (uint32_t k = 0; k < neps; k++)
for (uint32_t j = 0; j < nw; j++)
o[p++] = 4u << 16 | OP_EXECUTION_MODE, o[p++] = eps[k], o[p++] = MODE_PRESERVE, o[p++] = width[j];
memcpy(o + p, w + eps_end, (n - eps_end) * 4), p += n - eps_end;
*out_n = p;
return o;
}
// NaN tests that no compiler can drop (P.nan_bits). The game's shaders and the translator's own guards test for NaN with
// OpIsNan: 48,717 of an install's 54,959 translated modules, 26 tests each on average (the game's unordered
// comparisons, and the guard that keeps NaN out of every float colour output, patch 0027, so that one NaN texel does not
// spread through bloom and blur into blocks of black and white). SignedZeroInfNanPreserve asks the driver to keep NaN,
// but whether a compiler that reasons about floats keeps an isnan through every optimisation is up to it. The reports
// of black and white blocks, on Navi, on Link next to her and on bright surfaces seen from inside a house, come from
// RX 5000 and 6000 cards (#41, #110, #117), whose Windows driver compiles with "AMD proprietary shader compiler"
// (vk_caps.amd_sc); RDNA 3 and 4 (LLPC) draw the same scenes clean. It is not known whether that compiler keeps the
// mode: on it each float32 test becomes integer arithmetic on the bits, which means the same under any float
// assumption:
// isnan(x) -> (bitcast<uint>(x) & 0x7fffffff) > 0x7f800000
// for scalars and vectors of 32-bit floats; tests of 16- and 64-bit floats stay. A float32 select with a constant
// operand whose bits are a NaN (an integer mask such as 0xffffffff that the translator types as float) becomes a
// select of the bits (issue #156: on an RX 9070 XT without the mode such a select between 1 and -1 gave 2^32).
// NULL: nothing to change (or no memory); *tests and *selects count what changed.
static uint32_t *spirv_nan_bits(const uint32_t *w, uint64_t n, uint64_t *out_n, uint32_t *tests, uint32_t *selects) {
enum { OP_UNDEF = 1, OP_TYPE_BOOL = 20, OP_SPEC_CONSTANT = 50, OP_SPEC_CONSTANT_COMPOSITE = 51, OP_STORE = 62,
OP_BITCAST = 124, OP_IS_NAN = 156, OP_SELECT = 169, OP_UGREATER = 172, OP_BITWISE_AND = 199 };
*tests = *selects = 0;
if (n < 5 || w[3] >= (1u << 22)) return NULL;
uint32_t bound = w[3];
uint32_t *rtype = calloc(bound, sizeof *rtype), *vec_comp = calloc(bound, sizeof *vec_comp);
uint8_t *f32 = calloc(bound, 1), *vec_n = calloc(bound, 1), *nan_const = calloc(bound, 1);
uint32_t *o = NULL;
if (!rtype || !vec_comp || !f32 || !vec_n || !nan_const) goto out;
uint32_t tu = 0, uvec[5] = {0}; // uint32 and its vectors by component count
uint64_t fn_at = 0;
int vec_used[5] = {0};
// Ids that are not results of the instruction's second word, inside functions: first assignment wins anyway, as
// a definition comes before its uses (OpPhi operands aside, which this never reads).
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, l = w[i] >> 16;
if (!l || i + l > n) goto out;
const uint32_t *a = w + i + 1;
if (!fn_at) {
if (op == SPV_OP_FUNCTION) fn_at = i;
else if (op == SPV_OP_TYPE_FLOAT && l >= 3 && a[0] < bound && a[1] == 32) f32[a[0]] = 1;
else if (op == SPV_OP_TYPE_INT && l >= 4 && a[1] == 32 && a[2] == 0 && !tu) tu = a[0];
else if (op == SPV_OP_TYPE_VECTOR && l >= 4 && a[0] < bound && a[2] >= 2 && a[2] <= 4) {
vec_comp[a[0]] = a[1], vec_n[a[0]] = (uint8_t)a[2];
if (tu && a[1] == tu && !uvec[a[2]]) uvec[a[2]] = a[0];
} else if ((op == SPV_OP_CONSTANT || op == SPV_OP_CONSTANT_COMPOSITE || op == SPV_OP_CONSTANT_NULL ||
op == OP_UNDEF || op == SPV_OP_VARIABLE || op == OP_SPEC_CONSTANT ||
op == OP_SPEC_CONSTANT_COMPOSITE) && l >= 3 && a[1] < bound) {
rtype[a[1]] = a[0];
if (op == SPV_OP_CONSTANT && l == 4 && a[0] < bound && f32[a[0]] && (a[2] & 0x7fffffffu) > 0x7f800000u)
nan_const[a[1]] = 1;
}
}
if (fn_at) {
int no_result = op == OP_STORE || op == 63 || op == 64 || op == 8 || op == 317 || (op >= 246 && op <= 255) ||
op == 99 || op == 224 || op == 225 || op == 228 || op == 56 || op == 218 || op == 219 ||
op == 5294 || op == 5295 || op == 4416 || op == 5380 || op == 5364 || op == 5365;
if (!no_result && op != SPV_OP_FUNCTION && l >= 3 && a[1] < bound && !rtype[a[1]]) rtype[a[1]] = a[0];
if (op == OP_IS_NAN && l == 4 && a[2] < bound) {
uint32_t t = rtype[a[2]];
if (t < bound && f32[t]) ++*tests;
else if (t < bound && vec_n[t] && vec_comp[t] < bound && f32[vec_comp[t]]) ++*tests, vec_used[vec_n[t]] = 1;
} else if (op == OP_SELECT && l == 6 && a[0] < bound && f32[a[0]] && a[3] < bound && a[4] < bound &&
(nan_const[a[3]] || nan_const[a[4]])) {
++*selects;
}
}
i += l;
}
if (!fn_at || (!*tests && !*selects)) goto out;
// the declarations go right before the first function: uint32 and its vectors where missing, then the constants
uint32_t next = bound, decl[64], nd = 0;
if (!tu) tu = next++, decl[nd++] = 4u << 16 | SPV_OP_TYPE_INT, decl[nd++] = tu, decl[nd++] = 32, decl[nd++] = 0;
uint32_t c_mag = next++, c_inf = next++, cv_mag[5] = {0}, cv_inf[5] = {0};
for (int k = 2; k <= 4; k++)
if (vec_used[k] && !uvec[k])
uvec[k] = next++, decl[nd++] = 4u << 16 | SPV_OP_TYPE_VECTOR, decl[nd++] = uvec[k], decl[nd++] = tu, decl[nd++] = (uint32_t)k;
decl[nd++] = 4u << 16 | SPV_OP_CONSTANT, decl[nd++] = tu, decl[nd++] = c_mag, decl[nd++] = 0x7fffffffu;
decl[nd++] = 4u << 16 | SPV_OP_CONSTANT, decl[nd++] = tu, decl[nd++] = c_inf, decl[nd++] = 0x7f800000u;
for (int k = 2; k <= 4; k++) {
if (!vec_used[k]) continue;
cv_mag[k] = next++, cv_inf[k] = next++;
decl[nd++] = (uint32_t)(3 + k) << 16 | SPV_OP_CONSTANT_COMPOSITE, decl[nd++] = uvec[k], decl[nd++] = cv_mag[k];
for (int j = 0; j < k; j++) decl[nd++] = c_mag;
decl[nd++] = (uint32_t)(3 + k) << 16 | SPV_OP_CONSTANT_COMPOSITE, decl[nd++] = uvec[k], decl[nd++] = cv_inf[k];
for (int j = 0; j < k; j++) decl[nd++] = c_inf;
}
uint64_t cap = n + nd + 10ull * *tests + 12ull * *selects;
o = malloc(cap * 4);
if (!o) goto out;
memcpy(o, w, fn_at * 4);
uint64_t p = fn_at;
memcpy(o + p, decl, nd * 4), p += nd;
for (uint64_t i = fn_at; i < n;) {
uint32_t op = w[i] & 0xffff, l = w[i] >> 16;
const uint32_t *a = w + i + 1;
uint32_t t = op == OP_IS_NAN && l == 4 && a[2] < bound ? rtype[a[2]] : 0;
int k = t && t < bound ? (f32[t] ? 1 : vec_n[t] && vec_comp[t] < bound && f32[vec_comp[t]] ? vec_n[t] : 0) : 0;
if (k) {
uint32_t ut = k == 1 ? tu : uvec[k], mag = k == 1 ? c_mag : cv_mag[k], inf = k == 1 ? c_inf : cv_inf[k];
uint32_t bits = next++, low = next++;
o[p++] = 4u << 16 | OP_BITCAST, o[p++] = ut, o[p++] = bits, o[p++] = a[2];
o[p++] = 5u << 16 | OP_BITWISE_AND, o[p++] = ut, o[p++] = low, o[p++] = bits, o[p++] = mag;
o[p++] = 5u << 16 | OP_UGREATER, o[p++] = a[0], o[p++] = a[1], o[p++] = low, o[p++] = inf;
} else if (op == OP_SELECT && l == 6 && a[0] < bound && f32[a[0]] && a[3] < bound && a[4] < bound &&
(nan_const[a[3]] || nan_const[a[4]])) {
uint32_t x = next++, y = next++, s = next++;
o[p++] = 4u << 16 | OP_BITCAST, o[p++] = tu, o[p++] = x, o[p++] = a[3];
o[p++] = 4u << 16 | OP_BITCAST, o[p++] = tu, o[p++] = y, o[p++] = a[4];
o[p++] = 6u << 16 | OP_SELECT, o[p++] = tu, o[p++] = s, o[p++] = a[2], o[p++] = x, o[p++] = y;
o[p++] = 4u << 16 | OP_BITCAST, o[p++] = a[0], o[p++] = a[1], o[p++] = s;
} else {
memcpy(o + p, w + i, l * 4), p += l;
}
i += l;
}
o[3] = next;
*out_n = p;
out:
free(rtype), free(vec_comp), free(f32), free(vec_n), free(nan_const);
return o;
}
static int spirv_mark_list_test(const char *list); // texture pool marks, below
// --selftest (main.c): spirv_nan_preserve on a small fragment module, without a device. Returns the number of failures.
int vk_pipeline_spirv_selftest(void) {
enum { CAP = 2u << 16 | 17, MODE = 4u << 16 | 16 };
static const uint32_t head[] = {0x07230203, 0x00010300, 0, 5, 0, CAP, 1, CAP, 10}; // Shader, Float64
static const uint32_t body[] = {
3u << 16 | 14, 0, 1, // OpMemoryModel Logical GLSL450
5u << 16 | 15, 4, 1, 0x6e69616d, 0, // OpEntryPoint Fragment %1 "main"
3u << 16 | 16, 1, 7, // OpExecutionMode %1 OriginUpperLeft
2u << 16 | 19, 2, // %2 = OpTypeVoid
3u << 16 | 33, 3, 2, // %3 = OpTypeFunction %2
5u << 16 | 54, 2, 1, 0, 3, // %1 = OpFunction %2 None %3
2u << 16 | 248, 4, 1u << 16 | 253, 1u << 16 | 56}; // OpLabel, OpReturn, OpFunctionEnd
enum { NH = sizeof head / 4, NB = sizeof body / 4, EP_END = 8 }; // EP_END: body words up to the entry point's end
uint32_t in[NH + NB], want[NH + NB + 32];
memcpy(in, head, sizeof head), memcpy(in + NH, body, sizeof body);
int failures = 0;
// widths: what the device allows; f64: the module declares Float64; w64: a 64-bit mode is expected
for (int t = 0; t < 3; t++) {
uint32_t widths = t == 0 ? NP_16 | NP_32 | NP_64 : NP_32, n = NH + NB, p = 0;
int f64 = t != 2, w64 = t == 0;
if (!f64) in[NH - 1] = 0; // Matrix in place of Float64
memcpy(want, in, NH * 4), p = NH;
want[p++] = CAP, want[p++] = 4466;
want[p++] = 7u << 16 | 10;
memset(want + p, 0, 24), memcpy(want + p, "SPV_KHR_float_controls", 23), p += 6;
memcpy(want + p, body, EP_END * 4), p += EP_END;
want[p++] = MODE, want[p++] = 1, want[p++] = 4461, want[p++] = 32;
if (w64) want[p++] = MODE, want[p++] = 1, want[p++] = 4461, want[p++] = 64;
memcpy(want + p, body + EP_END, (NB - EP_END) * 4), p += NB - EP_END;
uint64_t on = 0, again_n = 0;
uint32_t *o = spirv_nan_preserve(in, n, widths, &on);
uint32_t *again = o ? spirv_nan_preserve(o, on, widths, &again_n) : NULL;
if (!o || on != p || memcmp(o, want, p * 4) || again) {
printf("spirv: SignedZeroInfNanPreserve rewrite, case %d: wrong output\n", t);
failures++;
}
free(o), free(again);
}
if (spirv_nan_preserve(in, NH + NB, NP_16 | NP_64, &(uint64_t){0})) {
printf("spirv: SignedZeroInfNanPreserve rewrite: a module changed without the 32-bit width\n");
failures++;
}
printf("spirv: SignedZeroInfNanPreserve rewrite: %s\n", failures ? "FAIL" : "ok");
{ // spirv_nan_bits: a scalar and a vec2 isnan, a select on a NaN constant; no uint32 type in the module
static const uint32_t m[] = {
0x07230203, 0x00010300, 0, 20, 0,
2u << 16 | 17, 1, // OpCapability Shader
3u << 16 | 14, 0, 1, // OpMemoryModel Logical GLSL450
5u << 16 | 15, 5, 1, 0x6e69616d, 0, // OpEntryPoint GLCompute %1 "main"
2u << 16 | 19, 2, // %2 void
3u << 16 | 33, 3, 2, // %3 fn
3u << 16 | 22, 4, 32, // %4 float
2u << 16 | 20, 5, // %5 bool
4u << 16 | 23, 6, 4, 2, // %6 vec2
4u << 16 | 23, 7, 5, 2, // %7 bvec2
4u << 16 | 43, 4, 8, 0xffffffffu, // %8 = NaN bits
4u << 16 | 43, 4, 9, 0x3f800000u, // %9 = 1.0
5u << 16 | 44, 6, 10, 8, 9, // %10 = vec2(%8, %9)
3u << 16 | 41, 5, 11, // %11 = OpConstantTrue
5u << 16 | 54, 2, 1, 0, 3, // %1 = OpFunction
2u << 16 | 248, 12, // OpLabel
4u << 16 | 156, 5, 13, 9, // %13 = OpIsNan bool %9
4u << 16 | 156, 7, 14, 10, // %14 = OpIsNan bvec2 %10
6u << 16 | 169, 4, 15, 11, 9, 8, // %15 = OpSelect float %11 %9 %8
1u << 16 | 253, 1u << 16 | 56}; // OpReturn, OpFunctionEnd
uint64_t on = 0;
uint32_t tests = 0, sels = 0, isnan_left = 0, ugt = 0, int32 = 0, bitcasts = 0;
uint32_t *o = spirv_nan_bits(m, sizeof m / 4, &on, &tests, &sels);
for (uint64_t i = 5; o && i < on;) {
uint32_t op = o[i] & 0xffff, l = o[i] >> 16;
if (!l) break;
isnan_left += op == 156, ugt += op == 172, int32 += op == 21 && o[i + 2] == 32, bitcasts += op == 124;
i += l;
}
int bad = !o || tests != 2 || sels != 1 || isnan_left || ugt != 2 || int32 != 1 || bitcasts != 5 || o[3] <= 20;
printf("spirv: integer NaN tests rewrite: %s\n", bad ? "FAIL" : "ok");
failures += bad;
free(o);
}
const char *list = cfg_get("NX2NAVI_SPV_MARK_TEST");
if (list && *list) failures += spirv_mark_list_test(list);
return failures;
}
// Chain {size 32} into a stage create info when the stage uses subgroup ops and the driver allows the pin.
static void pin_subgroup32(VkPipelineShaderStageCreateInfo *st, VkPipelineShaderStageRequiredSubgroupSizeCreateInfo *rq,
const uint32_t *w, uint64_t words) {
if (!(P.sg32_stages & st->stage) || !w || !uses_subgroup_ops(w, words)) return;
*rq = (VkPipelineShaderStageRequiredSubgroupSizeCreateInfo){
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO, .requiredSubgroupSize = vk_caps.sg_size};
st->pNext = rq;
}
// The x of a compute module's LocalSize (OpExecutionMode 17), 0 when it has none.
static uint32_t local_size_x(const uint32_t *w, uint64_t words) {
for (uint64_t i = 5; w && i < words;) {
uint32_t op = w[i] & 0xffff, n = w[i] >> 16;
if (!n || i + n > words) return 0;
if (op == 16 && n >= 6 && w[i + 2] == 17) return w[i + 3];
if (op == 54) return 0; // OpFunction: the execution modes are all before it
i += n;
}
return 0;
}
// Pool arrays (the translator's bindless texture and sampler arrays of VK_POOL_TEX / VK_POOL_SAMP elements, found by
// their types) move to set 2, at the binding of their view kind (vk.h VK_POOL_KINDS) or VK_POOL_KINDS for samplers;
// the kinds used are noted in vk_pool_kinds_used so the replay fills them. NULL: nothing to move (or no memory).
static uint32_t *spirv_pool_rebind(const uint32_t *w, uint64_t n) {
enum { OP_TYPE_IMAGE = 25, OP_TYPE_SAMPLER = 26, OP_TYPE_ARRAY_ = 28 };
uint32_t bound = n >= 5 && w[3] < (1u << 22) ? w[3] : 0;
if (!bound) return NULL;
int32_t *kind = malloc(bound * sizeof *kind); // per id: image kind, 100 sampler, -1 other (types, then variables)
uint32_t *cval = calloc(bound, sizeof *cval);
if (!kind || !cval) return free(kind), free(cval), NULL;
memset(kind, 0xff, bound * sizeof *kind);
// Pool variables and their binding decorations are module declarations, all before the first function.
int any = 0;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len || i + len > n || op == SPV_OP_FUNCTION) break;
if (op == OP_TYPE_IMAGE && len >= 9 && w[i + 1] < bound && w[i + 7] == 1) { // sampled image
uint32_t dim = w[i + 3], depth = w[i + 4], arrayed = w[i + 5], ms = w[i + 6];
if (dim <= 3 && !ms) kind[w[i + 1]] = (int32_t)((depth == 1 ? 8 : 0) + dim * 2 + (arrayed ? 1 : 0));
} else if (op == OP_TYPE_SAMPLER && len >= 2 && w[i + 1] < bound) {
kind[w[i + 1]] = 100;
} else if (op == SPV_OP_CONSTANT && len >= 4 && w[i + 2] < bound) {
cval[w[i + 2]] = w[i + 3];
} else if (op == OP_TYPE_ARRAY_ && len >= 4 && w[i + 1] < bound && w[i + 2] < bound && w[i + 3] < bound) {
int32_t k = kind[w[i + 2]];
uint32_t want = k == 100 ? VK_POOL_SAMP : VK_POOL_TEX;
kind[w[i + 1]] = k >= 0 && cval[w[i + 3]] == want ? k + 1000 : -1; // + 1000: a pool array
} else if (op == SPV_OP_TYPE_POINTER && len >= 4 && w[i + 1] < bound && w[i + 3] < bound) {
kind[w[i + 1]] = kind[w[i + 3]] >= 1000 ? kind[w[i + 3]] : -1;
} else if (op == SPV_OP_VARIABLE && len >= 4 && w[i + 1] < bound && w[i + 2] < bound) {
kind[w[i + 2]] = kind[w[i + 1]] >= 1000 ? kind[w[i + 1]] + 1000 : -1; // + 2000: a pool variable
if (kind[w[i + 2]] >= 2000) any = 1;
}
i += len;
}
uint32_t *o = NULL;
if (any && (o = malloc(n * 4))) {
memcpy(o, w, n * 4);
uint32_t used = 0;
for (uint64_t i = 5; i < n;) {
uint32_t op = o[i] & 0xffff, len = o[i] >> 16;
if (!len || i + len > n || op == SPV_OP_FUNCTION) break;
if (op == SPV_OP_DECORATE && len >= 4 && o[i + 1] < bound && kind[o[i + 1]] >= 2000) {
int32_t k = kind[o[i + 1]] - 2000;
if (o[i + 2] == SPV_DEC_DESCRIPTOR_SET) o[i + 3] = 2;
else if (o[i + 2] == SPV_DEC_BINDING) o[i + 3] = k == 100 ? VK_POOL_KINDS : (uint32_t)k;
if (k != 100) used |= 1u << k;
}
i += len;
}
__atomic_fetch_or(&vk_pool_kinds_used, used, __ATOMIC_RELAXED);
}
free(kind), free(cval);
return o;
}
// ---- texture pool marks (NX2NAVI_TEX_DISUSE, default on; vk_texture.c "disuse" uses them)
// Ocarina samples nearly every texture through the bindless pool: a shader reads a handle from memory and indexes the
// pool array with it, so the CPU never learns which of the registered textures a frame samples, and every registered
// texture counts as in use (vk_texture.c idle). The game keeps the textures of the areas it has loaded registered, so
// none of them could go, and their memory, which the driver charges to the system's commit, grew to 6 GiB in a session.
// This rewrite gives each module that indexes the texture pool a storage buffer at set 2, binding VK_POOL_MARK_BINDING
// (a uint per pool slot, vk_exec_draw.c "pool marks") and, after every access chain into a pool texture array, a store
// of 1 into the slot it indexes. The stores of one warp to one slot are one memory transaction. The replay copies the
// buffer out and clears it every few submissions; slots marked mean textures sampled. A module that indexes the pool
// and cannot be marked (SPIR-V before 1.3 or after 1.6, a stage whose stores the device does not allow, no memory) is
// counted in vk_pool_marks_missed, which keeps vk_texture.c from evicting by disuse: an unmarked shader could sample a
// texture the marks call unused. make_module logs the first few by stage, version, size, hash and reason.
volatile int32_t vk_pool_marks_missed;
static int marks_on = -1, marks_vs_ok, marks_fs_ok;
int vk_pool_marks_enabled(void) {
if (marks_on < 0) {
marks_on = rt_knob("TEX_DISUSE", "1") >= 1; // 2: marks and stand-ins, nothing freed (vk_texture.c "disuse")
VkCtx *c = vk_ctx();
if (c) {
VkPhysicalDeviceFeatures f;
vkGetPhysicalDeviceFeatures(c->phys, &f); // vk_device.c enables every feature the GPU reports
marks_vs_ok = f.vertexPipelineStoresAndAtomics, marks_fs_ok = f.fragmentStoresAndAtomics;
}
}
return marks_on;
}
// A fragment shader that stores to memory runs before the depth test unless it declares EarlyFragmentTests: the GPU
// must run it for fragments the test then rejects, as the stores are visible. The marks would so take the early depth
// rejection from every marked fragment shader and make overdraw cost full shading. With early_ok, a fragment shader
// whose result is the same under early tests gets EarlyFragmentTests along with its marks: one that discards, writes
// its depth, sample mask or stencil reference, or already writes memory does not, as early tests would change what it
// does. The caller passes early_ok only for a pipeline without alpha to coverage, which also drops samples after the
// shader (ensure_init keeps alpha to coverage out of the dynamic state while marks are on, so a pipeline knows).
// *status: 1 marked, 0 no pool texture access to mark, -1 one that could not be marked (*why says what stopped it).
// NULL unless marked.
static uint32_t *spirv_pool_mark(const uint32_t *w, uint64_t n, int early_ok, uint64_t *out_n, int *status,
const char **why) {
enum { OP_TYPE_RUNTIME_ARRAY = 29, OP_TYPE_STRUCT_ = 30, OP_ACCESS_CHAIN_ = 65, OP_IN_BOUNDS_ACCESS_CHAIN = 66,
OP_STORE_ = 62, OP_MEMBER_DECORATE_ = 72, OP_DECORATION_GROUP = 73, OP_GROUP_DECORATE = 74,
OP_GROUP_MEMBER_DECORATE = 75, OP_DECORATE_ID = 332, OP_DECORATE_STRING = 5632, OP_MEMBER_DECORATE_STRING = 5633,
OP_EXECUTION_MODE = 16, OP_EXECUTION_MODE_ID = 331, OP_IMAGE_WRITE = 99, OP_ATOMIC_FIRST = 227,
OP_ATOMIC_LAST = 242, OP_KILL = 252, OP_TERMINATE_INVOCATION = 4416, OP_DEMOTE_TO_HELPER = 5380,
EM_EARLY_FRAGMENT_TESTS = 9, BI_SAMPLE_MASK = 20, BI_FRAG_DEPTH = 22, BI_FRAG_STENCIL_REF = 5014,
SC_STORAGE_BUFFER = 12, DEC_BLOCK = 2, DEC_ARRAY_STRIDE = 6, DEC_OFFSET_ = 35 };
*status = 0, *why = NULL;
if (n < 5) return NULL;
uint32_t bound = w[3];
if (!bound || bound > (1u << 22)) return NULL;
uint32_t *bind = calloc(bound, sizeof *bind); // per id: its Binding + 1 when its DescriptorSet is 2, else 0
uint8_t *set2 = calloc(bound, 1);
if (!bind || !set2) return free(bind), free(set2), *status = -1, *why = "no memory", NULL;
uint64_t ann_end = 0, fn_start = 0, em_end = 0; // em_end: past the entry points and execution modes
uint32_t uint_t = 0, c0 = 0, c1 = 0, ptr_u = 0, fs_entry = 0;
int vertex_stage = 0, fragment_stage = 0, entries = 0, early = early_ok;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, l = w[i] >> 16;
if (!l || i + l > n) return free(bind), free(set2), *status = -1, *why = "an instruction past the module's end", NULL;
if (op == SPV_OP_FUNCTION) { fn_start = i; break; }
if (op == SPV_OP_ENTRY_POINT || op == OP_EXECUTION_MODE || op == OP_EXECUTION_MODE_ID) em_end = i + l;
if (op == SPV_OP_ENTRY_POINT && l >= 3) {
entries++;
if (w[i + 1] <= 3) vertex_stage = 1; // Vertex, TessellationControl, TessellationEvaluation, Geometry
else if (w[i + 1] == 4) fragment_stage = 1, fs_entry = w[i + 2];
} else if (op == OP_EXECUTION_MODE && l >= 3 && w[i + 2] == EM_EARLY_FRAGMENT_TESTS) {
early = 0; // has them already
} else if (op == SPV_OP_DECORATE && l >= 4 && w[i + 1] < bound) {
if (w[i + 2] == SPV_DEC_DESCRIPTOR_SET && w[i + 3] == 2) set2[w[i + 1]] = 1;
else if (w[i + 2] == SPV_DEC_BINDING) bind[w[i + 1]] = w[i + 3] + 1;
else if (w[i + 2] == SPV_DEC_BUILTIN &&
(w[i + 3] == BI_SAMPLE_MASK || w[i + 3] == BI_FRAG_DEPTH || w[i + 3] == BI_FRAG_STENCIL_REF))
early = 0;
} else if (op == SPV_OP_VARIABLE && l >= 4 && w[i + 3] == SC_STORAGE_BUFFER) {
early = 0; // the guest's own storage buffers: its stores would change under early tests
} else if (op == SPV_OP_TYPE_INT && l == 4 && w[i + 2] == 32 && w[i + 3] == 0) {
uint_t = w[i + 1];
} else if (op == SPV_OP_CONSTANT && l == 4 && uint_t && w[i + 1] == uint_t) {
if (w[i + 3] == 0 && !c0) c0 = w[i + 2];
if (w[i + 3] == 1 && !c1) c1 = w[i + 2];
} else if (op == SPV_OP_TYPE_POINTER && l == 4 && w[i + 2] == SC_STORAGE_BUFFER && uint_t && w[i + 3] == uint_t) {
ptr_u = w[i + 1];
}
if (op == SPV_OP_DECORATE || op == OP_MEMBER_DECORATE_ || op == OP_DECORATION_GROUP || op == OP_GROUP_DECORATE ||
op == OP_GROUP_MEMBER_DECORATE || op == OP_DECORATE_ID || op == OP_DECORATE_STRING || op == OP_MEMBER_DECORATE_STRING)
ann_end = i + l;
i += l;
}
// pool texture variables: set 2, a binding below VK_POOL_KINDS (spirv_pool_rebind); one already at the marks
// binding means the module was marked before
uint64_t sites = 0;
int marked = 0;
for (uint32_t id = 0; id < bound; id++)
if (set2[id] && bind[id] == VK_POOL_MARK_BINDING + 1) marked = 1;
for (uint64_t i = fn_start; fn_start && !marked && i < n;) {
uint32_t op = w[i] & 0xffff, l = w[i] >> 16;
if (!l || i + l > n) break;
if ((op == OP_ACCESS_CHAIN_ || op == OP_IN_BOUNDS_ACCESS_CHAIN) && l >= 5 && w[i + 3] < bound && set2[w[i + 3]] &&
bind[w[i + 3]] && bind[w[i + 3]] - 1 < VK_POOL_KINDS)
sites++;
else if (op == OP_KILL || op == OP_TERMINATE_INVOCATION || op == OP_DEMOTE_TO_HELPER || op == OP_IMAGE_WRITE ||
(op >= OP_ATOMIC_FIRST && op <= OP_ATOMIC_LAST))
early = 0;
i += l;
}
if (marked || !sites) return free(bind), free(set2), NULL;
*why = w[1] < 0x00010300 || w[1] > 0x00010600 ? "a SPIR-V version before 1.3 or after 1.6"
: !ann_end || !fn_start ? "no decorations or no function"
: vertex_stage && !marks_vs_ok ? "the GPU allows no stores in vertex-pipeline shaders"
: fragment_stage && !marks_fs_ok ? "the GPU allows no stores in fragment shaders"
: NULL;
if (*why) return free(bind), free(set2), *status = -1, NULL;
early = early && fragment_stage && entries == 1 && em_end && em_end < ann_end;
// From SPIR-V 1.4 on an entry point lists every global variable its functions use, not only the inputs and outputs
// (the translator's mesh shaders are 1.4): each entry point gets the marks variable, a word more.
int iface = w[1] >= 0x00010400;
// the mode, a word per entry point, decorations, at most 30 words of new types and the variable, 9 a site
uint64_t on = n + 3 + (uint64_t)entries + 20 + 30 + 9 * sites;
uint32_t *o = malloc(on * 4);
if (!o) return free(bind), free(set2), *status = -1, *why = "no memory", NULL;
uint32_t next = bound;
uint32_t new_uint = uint_t ? 0 : (uint_t = next++), new_c0 = c0 ? 0 : (c0 = next++), new_c1 = c1 ? 0 : (c1 = next++);
uint32_t rta = next++, st = next++, ptr_st = next++, var = next++, new_ptr_u = ptr_u ? 0 : (ptr_u = next++);
uint64_t p = 5;
memcpy(o, w, 5 * 4);
for (uint64_t i = 5; i < ann_end;) { // the header up to the annotations' end: entry points widened, the mode added
uint32_t op = w[i] & 0xffff, l = w[i] >> 16;
if (early && i == em_end) o[p++] = 3u << 16 | OP_EXECUTION_MODE, o[p++] = fs_entry, o[p++] = EM_EARLY_FRAGMENT_TESTS;
memcpy(o + p, w + i, l * 4);
if (iface && op == SPV_OP_ENTRY_POINT && l >= 3) o[p] = (l + 1) << 16 | op, o[p + l] = var, p++;
p += l;
i += l;
}
o[p++] = 4u << 16 | SPV_OP_DECORATE, o[p++] = rta, o[p++] = DEC_ARRAY_STRIDE, o[p++] = 4;
o[p++] = 5u << 16 | OP_MEMBER_DECORATE_, o[p++] = st, o[p++] = 0, o[p++] = DEC_OFFSET_, o[p++] = 0;
o[p++] = 3u << 16 | SPV_OP_DECORATE, o[p++] = st, o[p++] = DEC_BLOCK;
o[p++] = 4u << 16 | SPV_OP_DECORATE, o[p++] = var, o[p++] = SPV_DEC_DESCRIPTOR_SET, o[p++] = 2;
o[p++] = 4u << 16 | SPV_OP_DECORATE, o[p++] = var, o[p++] = SPV_DEC_BINDING, o[p++] = VK_POOL_MARK_BINDING;
memcpy(o + p, w + ann_end, (fn_start - ann_end) * 4), p += fn_start - ann_end;
if (new_uint) o[p++] = 4u << 16 | SPV_OP_TYPE_INT, o[p++] = uint_t, o[p++] = 32, o[p++] = 0;
if (new_c0) o[p++] = 4u << 16 | SPV_OP_CONSTANT, o[p++] = uint_t, o[p++] = c0, o[p++] = 0;
if (new_c1) o[p++] = 4u << 16 | SPV_OP_CONSTANT, o[p++] = uint_t, o[p++] = c1, o[p++] = 1;
o[p++] = 3u << 16 | OP_TYPE_RUNTIME_ARRAY, o[p++] = rta, o[p++] = uint_t;
o[p++] = 3u << 16 | OP_TYPE_STRUCT_, o[p++] = st, o[p++] = rta;
o[p++] = 4u << 16 | SPV_OP_TYPE_POINTER, o[p++] = ptr_st, o[p++] = SC_STORAGE_BUFFER, o[p++] = st;
if (new_ptr_u) o[p++] = 4u << 16 | SPV_OP_TYPE_POINTER, o[p++] = ptr_u, o[p++] = SC_STORAGE_BUFFER, o[p++] = uint_t;
o[p++] = 4u << 16 | SPV_OP_VARIABLE, o[p++] = ptr_st, o[p++] = var, o[p++] = SC_STORAGE_BUFFER;
for (uint64_t i = fn_start; i < n;) {
uint32_t op = w[i] & 0xffff, l = w[i] >> 16;
memcpy(o + p, w + i, l * 4), p += l;
if ((op == OP_ACCESS_CHAIN_ || op == OP_IN_BOUNDS_ACCESS_CHAIN) && l >= 5 && w[i + 3] < bound && set2[w[i + 3]] &&
bind[w[i + 3]] && bind[w[i + 3]] - 1 < VK_POOL_KINDS) {
uint32_t at = next++; // marks[slot] = 1, slot being the pool index of this access
o[p++] = 6u << 16 | OP_ACCESS_CHAIN_, o[p++] = ptr_u, o[p++] = at, o[p++] = var, o[p++] = c0, o[p++] = w[i + 4];
o[p++] = 3u << 16 | OP_STORE_, o[p++] = at, o[p++] = c1;
}
i += l;
}
o[3] = next;
free(bind), free(set2);
*out_n = p;
*status = 1;
return o;
}
// --selftest with NX2NAVI_SPV_MARK_TEST=<list>: each line "<in.spv> <out.spv>"; the module is moved to the pools
// (spirv_pool_rebind) and marked, and written to <out> when marked, for spirv-val. Prints the counts; 0 failures unless
// a file cannot be read or written.
static int spirv_mark_list_test(const char *list) {
marks_vs_ok = marks_fs_ok = 1;
FILE *f = fopen(list, "rb");
if (!f) return printf("spirv: pool marks test: %s cannot be read\n", list), 1;
char line[2048];
int marked = 0, none = 0, missed = 0, bad = 0;
while (fgets(line, sizeof line, f)) {
char in[1024], out[1024];
if (sscanf(line, "%1023s %1023s", in, out) != 2) continue;
FILE *g = fopen(in, "rb");
if (!g) { bad++; continue; }
fseek(g, 0, SEEK_END);
long sz = ftell(g);
fseek(g, 0, SEEK_SET);
uint32_t *w = sz > 20 ? malloc((size_t)sz) : NULL;
if (!w || fread(w, 1, (size_t)sz, g) != (size_t)sz) { fclose(g), free(w), bad++; continue; }
fclose(g);
uint64_t n = (uint64_t)sz / 4, mn = 0;
uint32_t *pooled = spirv_pool_rebind(w, n);
int st;
const char *why;
uint32_t *m = spirv_pool_mark(pooled ? pooled : w, n, 1, &mn, &st, &why);
if (st > 0) {
marked++;
if (!plat_file_write_atomic(out, m, (size_t)mn * 4)) {
if (!bad) printf("spirv: pool marks test: %s cannot be written\n", out);
bad++;
}
} else if (st < 0) {
missed++;
printf("spirv: pool marks test: %s not markable: %s\n", in, why);
} else {
none++;
}
free(m), free(pooled), free(w);
}
fclose(f);
printf("spirv: pool marks test: %d marked, %d without pool texture accesses, %d not markable, %d file errors\n", marked,
none, missed, bad);
return bad;
}
// early_ok: a fragment shader whose pipeline has no alpha to coverage (spirv_pool_mark)
static VkShaderModule make_module(const uint32_t *w, uint64_t words, int early_ok) {
const uint32_t *orig_w = w; // for the log of a module the marks cannot take
uint64_t orig_words = words, fixed_n = 0;
uint32_t *fixed = spirv_fixup(w, words, &fixed_n);
if (fixed) w = fixed, words = fixed_n;
uint32_t *pooled = spirv_pool_rebind(w, words);
if (pooled) {
free(fixed);
w = fixed = pooled;
if (vk_pool_marks_enabled()) {
uint64_t mk_n = 0;
int st;
const char *why;
uint32_t *mk = spirv_pool_mark(w, words, early_ok, &mk_n, &st, &why);
if (mk) {
free(fixed);
w = fixed = mk, words = mk_n;
} else if (st < 0) {
int nth = plat_inc32(&vk_pool_marks_missed);
// the hash is FNV-1a of the words make_module got, as vk_dump hashes a shader's SPIR-V; it differs
// from the translation's only where the pipeline patched the module first (added vertex outputs)
if (nth <= 8) {
uint32_t model = ~0u;
for (uint64_t i = 5; i < words;) {
uint32_t op = w[i] & 0xffff, l = w[i] >> 16;
if (!l) break;
if (op == SPV_OP_ENTRY_POINT && l >= 2) { model = w[i + 1]; break; }
i += l;
}
const char *stage = model == 0 ? "vertex" : model <= 3 ? "tessellation or geometry" : model == 4 ? "fragment"
: model == 5 ? "compute" : model == 5364 ? "task" : model == 5365 ? "mesh" : "other";
rt_log("vk_pipeline: a %s shader that indexes the texture pool could not be marked (SPIR-V %u.%u, %llu "
"words, hash %016llx): %s%s", stage, w[1] >> 16 & 0xff, w[1] >> 8 & 0xff,
(unsigned long long)words, (unsigned long long)fnv(FNV0, orig_w, (size_t)orig_words * 4), why,
nth == 1 ? "; textures are not evicted by disuse this run (NX2NAVI_TEX_DISUSE)" : "");
}
}
}
}
uint64_t rc_n = 0;
uint32_t *rc = P.reconverge ? spirv_reconverge(w, words, &rc_n) : NULL;
if (rc) {
free(fixed);
w = fixed = rc, words = rc_n;
}
uint64_t np_n = 0;
uint32_t *np = P.nan_preserve ? spirv_nan_preserve(w, words, P.nan_preserve, &np_n) : NULL;
if (np) {
free(fixed);
w = fixed = np, words = np_n;
}
uint64_t nb_n = 0;
uint32_t nb_tests = 0, nb_sels = 0;
uint32_t *nb = P.nan_bits ? spirv_nan_bits(w, words, &nb_n, &nb_tests, &nb_sels) : NULL;
if (nb) {
free(fixed);
w = fixed = nb, words = nb_n;
__atomic_fetch_add(&P.nan_bits_tests, nb_tests, __ATOMIC_RELAXED);
__atomic_fetch_add(&P.nan_bits_selects, nb_sels, __ATOMIC_RELAXED);
if (__atomic_add_fetch(&P.nan_bits_modules, 1, __ATOMIC_RELAXED) == 1)
rt_log("vk_pipeline: first module with NaN tests made integer: %u tests, %u selects of NaN bits", nb_tests,
nb_sels);
}
if (P.feedback >= 2) mm_hash = fnv(FNV0, w, (size_t)words * 4);
VkShaderModuleCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = (size_t)words * 4, .pCode = w};
VkShaderModule m = VK_NULL_HANDLE;
VkResult r = vkCreateShaderModule(P.device, &ci, NULL, &m);
free(fixed);
return r == VK_SUCCESS ? m : VK_NULL_HANDLE;
}
// ---- descriptor layouts
typedef struct {
uint32_t n;
VkDescriptorSetLayoutBinding b[MAX_BINDINGS];
VkPipeBinding pb[MAX_BINDINGS];
int pool; // a stage indexes the texture or sampler pool (set 2, not in b / pb)
} Bindings;
static VkDescriptorType descriptor_type(int kind, const GfxShaderRes *r) {
int buffer = (r->type & 0xff) == 2;
switch (kind) {
case GFX_RES_UBO: return VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
case GFX_RES_SSBO: return VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
case GFX_RES_TEX:
if (buffer) return VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
if (r->separate) LOG_ONCE("vk_pipeline: a separate texture handle: bound as SAMPLED_IMAGE");
return r->separate ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
default: return buffer ? VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER : VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
}
}
static VkShaderStageFlags stage_bit(int stage) {
return stage == GFX_STAGE_VS ? VK_SHADER_STAGE_VERTEX_BIT : stage == GFX_STAGE_FS ? VK_SHADER_STAGE_FRAGMENT_BIT
: stage == GFX_STAGE_TS ? VK_SHADER_STAGE_TASK_BIT_EXT : stage == GFX_STAGE_MS ? VK_SHADER_STAGE_MESH_BIT_EXT
: VK_SHADER_STAGE_COMPUTE_BIT;
}
static int is_mesh(const VkPipeState *s) { return s->vs && gfx_shader_stage(s->vs) == GFX_STAGE_MS; }
// Where a shader's translation came from (gfx_shader_origin), for the log.
static const char *origin_name(const GfxShader *s) {
static const char *const name[3] = {"from the shader cache", "a previous translator's", "translated in this session"};
return s ? name[gfx_shader_origin(s)] : "none";
}
// The shaders of the pipeline this thread builds, for the crash notes (platform.h plat_crash_context): the first 8
// bytes of each cache key, as gfx_shader.c names its cache files.
static void crash_note(const char *what, const GfxShader *a, const GfxShader *b, const GfxShader *c) {
static __thread char buf[160];
char *o = buf;
o += snprintf(o, 40, "building %s", what);
const GfxShader *sh[3] = {a, b, c};
for (int i = 0; i < 3; i++)
if (sh[i]) {
const uint8_t *k = gfx_shader_key(sh[i]);
o += snprintf(o, 40, " stage %d %02x%02x%02x%02x%02x%02x%02x%02x", gfx_shader_stage(sh[i]), k[0], k[1], k[2], k[3],
k[4], k[5], k[6], k[7]);
}
plat_crash_context(buf);
}
// One past the highest binding a shader uses (1 with no resources: binding 0 is the support buffer).
static uint32_t binding_span(const GfxShader *s) {
uint32_t span = 1;
for (int k = 0; k < GFX_RES_KINDS; k++) {
const GfxShaderRes *r;
int n = gfx_shader_resources(s, k, &r);
for (int i = 0; i < n; i++)
if (r[i].binding + (uint32_t)(r[i].count ? r[i].count : 1) > span) span = r[i].binding + (r[i].count ? r[i].count : 1);
}
return span;
}
// Adds a stage's resources with bindings b >= 1 moved by off; binding 0 (support buffer) is shared.
static int add_stage(Bindings *B, const GfxShader *s, uint32_t off) {
int stage = gfx_shader_stage(s);
VkShaderStageFlags bit = stage_bit(stage);
if (!B->n) {
B->b[0] = (VkDescriptorSetLayoutBinding){0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, 0, NULL};
B->pb[0] = (VkPipeBinding){0, (uint8_t)stage, VK_PIPE_SUPPORT, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, 0, NULL};
B->n = 1;
}
B->b[0].stageFlags |= bit;
B->pb[0].stages |= bit;
for (int k = 0; k < GFX_RES_KINDS; k++) {
const GfxShaderRes *r;
int n = gfx_shader_resources(s, k, &r);
for (int i = 0; i < n; i++) {
if (r[i].pool) { // set 2 (spirv_pool_rebind), bound by the replay from the pools
B->pool = 1;
continue;
}
if (B->n == MAX_BINDINGS || !r[i].binding) return 0;
VkDescriptorType t = descriptor_type(k, &r[i]);
uint32_t binding = r[i].binding + off, count = r[i].count ? r[i].count : 1;
B->b[B->n] = (VkDescriptorSetLayoutBinding){binding, t, count, bit, NULL};
B->pb[B->n] = (VkPipeBinding){(uint16_t)binding, (uint8_t)stage, (uint8_t)k, t, (uint16_t)count, bit, &r[i]};
B->n++;
}
}
return 1;
}
// The first binding a module's resource variables use that the binding list b[0..n) lacks (module bindings >= 1 moved
// by off), or -1. A reflection that misses a resource would otherwise hand the driver an invalid layout.
static int module_binding_missing(const GfxShader *s, const VkDescriptorSetLayoutBinding *b, uint32_t n, uint32_t off) {
uint64_t words = 0;
const uint32_t *w = gfx_shader_spirv(s, &words);
const GfxShaderRes *tr;
int nt = gfx_shader_resources(s, GFX_RES_TEX, &tr);
for (uint64_t i = 5; w && i < words;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len || i + len > words) break;
int pooled = 0; // a pool array: set 2, not in this layout (spirv_pool_rebind)
for (int t = 0; t < nt && op == SPV_OP_DECORATE && len >= 4; t++)
if (tr[t].pool && tr[t].binding == (w[i + 3] & 0xffff)) pooled = 1; // GfxShaderRes.binding is 16 bits
if (op == SPV_OP_DECORATE && len >= 4 && w[i + 2] == SPV_DEC_BINDING && !pooled) {
uint32_t want = w[i + 3] + (w[i + 3] >= 1 ? off : 0), j = 0;
while (j < n && b[j].binding != want) j++;
if (j == n) return (int)w[i + 3];
}
i += len;
}
return -1;
}
// Checks both stages of a graphics pipeline against its layout; 0 with why filled when one uses a missing binding.
static int layout_covers(const VkPipeState *s, const Layout *l, uint32_t fs_off, char *why, size_t cap) {
uint32_t nv = l->set1 ? l->n0 : l->n;
int m = module_binding_missing(s->vs, l->b, nv, 0);
const char *stage = "vertex";
if (m < 0 && s->fs) {
m = l->set1 ? module_binding_missing(s->fs, l->b + l->n0, l->n - l->n0, 0) : module_binding_missing(s->fs, l->b, l->n, fs_off);
stage = "fragment";
}
if (m >= 0) snprintf(why, cap, "the %s shader uses binding %d, which its reflection and layout lack", stage, m);
return m < 0;
}
// The update template of a set whose bindings are b[0..n): one entry per binding, in order, over the packed array the
// replay fills from the set's first descriptor, a VkDescSlot per descriptor. The bind point follows the stages (compute
// layouts hold the compute stage only). VK_NULL_HANDLE when templates are off, for an empty set, for a push set
// without vkCmdPushDescriptorSetWithTemplateKHR or when creation fails: the replay then writes the set with write
// structs.
static VkDescriptorUpdateTemplate make_tpl(const VkDescriptorSetLayoutBinding *b, uint32_t n, VkDescriptorSetLayout sl,
int push, VkPipelineLayout pl, uint32_t set) {
if (!P.tpl || !n || (push && !P.tpl_push)) return VK_NULL_HANDLE;
VkDescriptorUpdateTemplateEntry e[MAX_BINDINGS];
size_t off = 0;
VkShaderStageFlags stages = 0;
for (uint32_t i = 0; i < n; i++) {
e[i] = (VkDescriptorUpdateTemplateEntry){b[i].binding, 0, b[i].descriptorCount, b[i].descriptorType, off, sizeof(VkDescSlot)};
off += sizeof(VkDescSlot) * b[i].descriptorCount;
stages |= b[i].stageFlags;
}
VkDescriptorUpdateTemplateCreateInfo ci = {
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_UPDATE_TEMPLATE_CREATE_INFO, .descriptorUpdateEntryCount = n,
.pDescriptorUpdateEntries = e,
.templateType = push ? VK_DESCRIPTOR_UPDATE_TEMPLATE_TYPE_PUSH_DESCRIPTORS_KHR : VK_DESCRIPTOR_UPDATE_TEMPLATE_TYPE_DESCRIPTOR_SET,
.descriptorSetLayout = sl,
.pipelineBindPoint = stages & VK_SHADER_STAGE_COMPUTE_BIT ? VK_PIPELINE_BIND_POINT_COMPUTE : VK_PIPELINE_BIND_POINT_GRAPHICS,
.pipelineLayout = pl, .set = set};
VkDescriptorUpdateTemplate t;
if (P.tpl_create(P.device, &ci, NULL, &t) != VK_SUCCESS) {
LOG_ONCE("vk_pipeline: descriptor update template creation failed: that set is written with write structs");
return VK_NULL_HANDLE;
}
return t;
}
// Finds or creates the layout of a binding list (sorted here); NULL on failure. Caller holds P.lock.
// Sorts a binding list by binding and hashes it into *h; the descriptor total, or -1 when a binding repeats.
static int64_t bindings_sort(Bindings *B, uint64_t *h) {
for (uint32_t i = 1; i < B->n; i++) // insertion sort by binding; the lists are short
for (uint32_t j = i; j > 0 && B->b[j - 1].binding > B->b[j].binding; j--) {
VkDescriptorSetLayoutBinding t = B->b[j]; B->b[j] = B->b[j - 1]; B->b[j - 1] = t;
VkPipeBinding u = B->pb[j]; B->pb[j] = B->pb[j - 1]; B->pb[j - 1] = u;
}
int64_t total = 0;
for (uint32_t i = 0; i < B->n; i++) {
if (i && B->b[i].binding == B->b[i - 1].binding) return -1;
uint32_t k[4] = {B->b[i].binding, B->b[i].descriptorType, B->b[i].descriptorCount, B->b[i].stageFlags};
*h = fnv(*h, k, sizeof k);
total += B->b[i].descriptorCount;
}
return total;
}
static Layout *layout_get(Bindings *B) {
uint64_t h = FNV0;
int64_t total = bindings_sort(B, &h);
if (total < 0) return NULL;
Layout **head = &layouts[h % LAYOUT_BUCKETS];
for (Layout *l = *head; l; l = l->next)
if (l->hash == h && l->n == B->n && !memcmp(l->b, B->b, B->n * sizeof *B->b)) return l;
Layout *l = calloc(1, sizeof *l + B->n * sizeof *B->b);
if (!l) return NULL;
l->hash = h, l->n = B->n;
memcpy(l->b, B->b, B->n * sizeof *B->b);
l->push = total <= (int64_t)P.max_push;
VkDescriptorSetLayoutCreateInfo si = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.flags = l->push ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR : 0,
.bindingCount = l->n, .pBindings = l->b};
if (vkCreateDescriptorSetLayout(P.device, &si, NULL, &l->set) != VK_SUCCESS) {
free(l);
return NULL;
}
VkDescriptorSetLayout sets3[3] = {l->set, vk_empty_set_layout(), vk_pool_set_layout()}; // set 2: the pools
VkPipelineLayoutCreateInfo pi = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 3,
.pSetLayouts = sets3};
if (vkCreatePipelineLayout(P.device, &pi, NULL, &l->layout) != VK_SUCCESS) {
vkDestroyDescriptorSetLayout(P.device, l->set, NULL);
free(l);
return NULL;
}
l->tpl[0] = make_tpl(l->b, l->n, l->set, l->push, l->layout, 0);
l->next = *head;
*head = l;
P.stats.layouts++;
P.stats.push_layouts += (uint64_t)l->push;
return l;
}
// Independent layouts (P.indep): the vertex stage's bindings in set 0, the fragment stage's in set 1 (empty without a
// fragment shader), each with its own support buffer at binding 0, the pipeline layout created with
// VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT so stage libraries can use it. Only the P.push_stage set is a
// push-descriptor set (a layout holds one), when it fits maxPushDescriptors; the other is allocated per draw from the
// command buffer's pools. On success *B holds both lists, set 0 first, VkPipeBinding.set filled in. Caller holds P.lock.
static Layout *layout_pair(Bindings *B, Bindings *F) {
uint64_t h = fnv(FNV0, "indep", 5);
int64_t tv = bindings_sort(B, &h);
h = fnv(h, "|", 1);
int64_t tf = bindings_sort(F, &h);
if (tv < 0 || tf < 0 || B->n + F->n > MAX_BINDINGS) return NULL;
uint32_t n0 = B->n, n = B->n + F->n;
memcpy(B->b + n0, F->b, F->n * sizeof *F->b);
memcpy(B->pb + n0, F->pb, F->n * sizeof *F->pb);
for (uint32_t i = n0; i < n; i++) B->pb[i].set = 1;
B->n = n;
Layout **head = &layouts[h % LAYOUT_BUCKETS];
for (Layout *l = *head; l; l = l->next)
if (l->hash == h && l->set1 && l->n == n && l->n0 == n0 && !memcmp(l->b, B->b, n * sizeof *B->b)) return l;
Layout *l = calloc(1, sizeof *l + n * sizeof *B->b);
if (!l) return NULL;
l->hash = h, l->n = n, l->n0 = n0;
memcpy(l->b, B->b, n * sizeof *B->b);
l->push = P.push_stage == GFX_STAGE_VS && tv <= (int64_t)P.max_push;
l->push1 = P.push_stage == GFX_STAGE_FS && F->n && tf <= (int64_t)P.max_push;
VkDescriptorSetLayoutCreateInfo s0 = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.flags = l->push ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR : 0,
.bindingCount = n0, .pBindings = l->b};
VkDescriptorSetLayoutCreateInfo s1 = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.flags = l->push1 ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR : 0,
.bindingCount = n - n0, .pBindings = l->b + n0};
if (vkCreateDescriptorSetLayout(P.device, &s0, NULL, &l->set) != VK_SUCCESS) {
free(l);
return NULL;
}
if (vkCreateDescriptorSetLayout(P.device, &s1, NULL, &l->set1) != VK_SUCCESS) {
vkDestroyDescriptorSetLayout(P.device, l->set, NULL);
free(l);
return NULL;
}
VkDescriptorSetLayout sets[3] = {l->set, l->set1, vk_pool_set_layout()};
VkPipelineLayoutCreateInfo pi = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.flags = VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT, .setLayoutCount = 3,
.pSetLayouts = sets};
if (vkCreatePipelineLayout(P.device, &pi, NULL, &l->layout) != VK_SUCCESS) {
vkDestroyDescriptorSetLayout(P.device, l->set, NULL);
vkDestroyDescriptorSetLayout(P.device, l->set1, NULL);
free(l);
return NULL;
}
l->tpl[0] = make_tpl(l->b, n0, l->set, l->push, l->layout, 0);
l->tpl[1] = make_tpl(l->b + n0, n - n0, l->set1, l->push1, l->layout, 1);
l->next = *head;
*head = l;
P.stats.layouts++;
P.stats.push_layouts += (uint64_t)(l->push | l->push1);
return l;
}
// Vertex outputs with independent layouts: each vertex shader declares the union of the inputs of every fragment
// shader met with it (pipeline-keys.bin at the prewarm, then each pipeline built), so its module does not depend on the
// fragment shader of one pipeline. Unwritten ones read (0, 0, 0, 1) as before. NX2NAVI_VS_OUTS=all declares all 32.
#define VS_OUTS_CAP 8192
static struct {
PlatMutex lock;
const GfxShader *vs[VS_OUTS_CAP];
uint32_t mask[VS_OUTS_CAP];
GfxShaderIo io[VS_OUTS_CAP][32];
uint32_t n, full;
} VO = {.lock = PLAT_MUTEX_INIT};
// Adds fs's inputs (packed ones excepted) to vs's union; returns the union, types in io (the first type met wins).
static uint32_t vs_outs(const GfxShader *vs, const GfxShader *fs, GfxShaderIo io[32]) {
GfxShaderIo fin[32];
uint32_t need = fs ? gfx_shader_inputs(fs, fin) : 0, mask;
for (int i = 0; i < 32; i++)
if (need >> i & 1 && fin[i].packed) need &= ~(1u << i);
plat_mutex_lock(&VO.lock);
uint32_t j = (uint32_t)(((uintptr_t)vs >> 4) * 0x9e3779b1u) & (VS_OUTS_CAP - 1);
while (VO.vs[j] && VO.vs[j] != vs) j = (j + 1) & (VS_OUTS_CAP - 1);
if (!VO.vs[j] && VO.n + 1 >= VS_OUTS_CAP * 3 / 4) { // full: this pair alone
VO.full++;
plat_mutex_unlock(&VO.lock);
LOG_ONCE("vk_pipeline: vertex output union table full: outputs from the pair");
memset(io, 0, 32 * sizeof *io);
for (int i = 0; i < 32; i++)
if (need >> i & 1) io[i] = fin[i];
return need;
}
if (!VO.vs[j]) VO.vs[j] = vs, VO.n++;
for (int i = 0; i < 32; i++)
if (!(need >> i & 1)) continue;
else if (!(VO.mask[j] >> i & 1)) VO.io[j][i] = fin[i], VO.mask[j] |= 1u << i;
else if (VO.io[j][i].type == fin[i].type && VO.io[j][i].comps < fin[i].comps) VO.io[j][i].comps = fin[i].comps;
mask = VO.mask[j];
memcpy(io, VO.io[j], 32 * sizeof *io);
plat_mutex_unlock(&VO.lock);
return mask;
}
// ---- pipeline cache table
static Pipe *pipe_find(const void *key, size_t size, uint64_t h) {
for (Pipe *p = pipes[h % PIPE_BUCKETS]; p; p = p->next)
if (p->hash == h && p->key_size == size && !memcmp(p->key, key, size)) return p;
return NULL;
}
// Stores a result (a failed one too, so it is not retried every draw). Caller holds P.lock.
static Pipe *pipe_add(const void *key, size_t size, uint64_t h, VkPipeline pipeline, const Layout *l, const Bindings *B,
VkPipelineBindPoint bp, uint32_t vs_inputs) {
Pipe *p = calloc(1, sizeof *p + size);
if (!p) return NULL;
p->hash = h, p->key_size = size;
memcpy(p->key, key, size);
if (l && B) {
p->bindings = malloc(B->n * sizeof *p->bindings);
if (p->bindings) memcpy(p->bindings, B->pb, B->n * sizeof *p->bindings);
}
p->info = (VkPipeInfo){pipeline, l ? l->layout : VK_NULL_HANDLE, l ? l->set : VK_NULL_HANDLE, l ? l->push : 0, bp,
p->bindings ? B->n : 0, p->bindings, vs_inputs};
p->info.set_layout1 = l ? l->set1 : VK_NULL_HANDLE;
p->info.push1 = l ? l->push1 : 0;
p->info.n_sets = l && l->set1 ? 2 : 1;
p->info.pool = B ? B->pool : 0;
if (l) p->info.tpl[0] = l->tpl[0], p->info.tpl[1] = l->tpl[1];
p->lay = l;
Pipe **head = &pipes[h % PIPE_BUCKETS];
p->next = *head;
*head = p;
return p;
}
// ---- graphics pipelines
typedef struct { // normalized: zero where the state is dynamic or unused
const GfxShader *vs, *fs, *ts; // ts: the task stage of a mesh draw
uint32_t color[8], depth, stencil; // VkFormat
uint8_t n_color, samples, topo_class, minus_w;
uint8_t clamp, poly_mode, a2c, logic_op; // logic_op: VkLogicOp + 1 when enabled
uint8_t blend_en, dsb, fbl, pad; // dsb: an enabled target blends with a SRC1 factor; fbl: VkPipeState.feedback
uint32_t wmask;
uint8_t blend[8][6]; // src, dst, op, src alpha, dst alpha, alpha op (VkBlendFactor / VkBlendOp)
uint32_t attr_fmt[32], attr_off[32];
uint8_t attr_bind[32];
uint8_t rate[17]; // per binding: 0 unused, 1 vertex, 2 instance
uint8_t indep; // P.indep when built: the probe draws through both layouts
uint8_t pad2[2];
} GKey;
static int blendable(VkFormat f) {
return f != VK_FORMAT_UNDEFINED && (format_features(f, 1) & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT);
}
static int target_count(const VkPipeState *s) {
int n = 0;
for (int i = 0; i < 8; i++)
if (s->color_fmt[i]) n = i + 1;
return n;
}
// SRC1 blend factors read the fragment shader's second output (dual-source blending); the pipeline then needs a
// fragment module whose output 1 is Location 0, Index 1 (spirv_dual_source).
static int is_src1(unsigned f) { return f >= NVN_BLEND_FUNC_SRC1_COLOR && f <= NVN_BLEND_FUNC_ONE_MINUS_SRC1_ALPHA; }
// Runs per draw (gkey_fill, target_wmask): only the blend factors of blend-enabled targets are read until one is SRC1,
// which NX2navi never sets (0 of 876 pipelines in the field and fight saves).
// NX2NAVI_VK_NOOPT=1: pipelines built without the driver's optimiser, for a driver whose shader compiler crashes on
// one (the test switch for such a report); slower shaders, the same picture. mesh: only pipelines with task and mesh
// stages, the foliage's, whose optimiser is the suspect in device losses some minutes into a forest on AMD GPUs.
static VkPipelineCreateFlags noopt_flag_for(int mesh) {
static int on = -1;
if (on < 0) {
const char *v = cfg_value("NX2NAVI_VK_NOOPT");
on = !strcmp(v, "mesh") ? 2 : strcmp(v, "0") != 0;
if (on) rt_log("vk_pipeline: %s built without the driver's optimiser (NX2NAVI_VK_NOOPT)",
on == 2 ? "pipelines with task and mesh stages" : "pipelines");
}
return on == 1 || (on == 2 && mesh) ? VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BIT : 0;
}
static VkPipelineCreateFlags noopt_flag(void) { return noopt_flag_for(0); }
// ---- pipeline creation feedback (NX2NAVI_PIPE_FEEDBACK, default 1; 0 off)
// Each pipeline, library and compute build asks the driver whether pipeline-cache.bin (P.cache) held it
// (VK_PIPELINE_CREATION_FEEDBACK_APPLICATION_PIPELINE_CACHE_HIT_BIT) and how long the create took. The counts appear
// with the prewarm, the stage library prebuild and every cache save, so a log tells whether a slow boot compiled or
// read its pipelines: on NVIDIA the driver's own disk cache is kept per executable, so a renamed or new exe gets nothing
// from it, and only pipeline-cache.bin can make its first boot quick. =2 also logs every compute build with a hash of
// its final module, and every graphics pipeline build with the hashes of its final modules and of its fixed state, so
// two runs can be compared for a module or a state that changes between them.
enum { FB_GRAPHICS, FB_LIBRARY, FB_COMPUTE, FB_KINDS };
static struct {
PlatMutex lock;
uint64_t n[FB_KINDS], hit[FB_KINDS];
double hit_ms[FB_KINDS], miss_ms[FB_KINDS];
} FB = {.lock = PLAT_MUTEX_INIT};
// Chains fb into a create info's pNext (next: its current pNext). Returns what the create info's pNext becomes.
static const void *fb_chain(VkPipelineCreationFeedbackCreateInfo *ci, VkPipelineCreationFeedback *fb, const void *next) {
if (!P.feedback) return next;
memset(fb, 0, sizeof *fb);
*ci = (VkPipelineCreationFeedbackCreateInfo){.sType = VK_STRUCTURE_TYPE_PIPELINE_CREATION_FEEDBACK_CREATE_INFO,
.pNext = next, .pPipelineCreationFeedback = fb};
return ci;
}
static void fb_note(int kind, const VkPipelineCreationFeedback *fb) {
if (!P.feedback || !P.cache || !(fb->flags & VK_PIPELINE_CREATION_FEEDBACK_VALID_BIT)) return;
int hit = (fb->flags & VK_PIPELINE_CREATION_FEEDBACK_APPLICATION_PIPELINE_CACHE_HIT_BIT) != 0;
double ms = (double)fb->duration / 1e6;
plat_mutex_lock(&FB.lock);
FB.n[kind]++, FB.hit[kind] += (uint64_t)hit;
if (hit) FB.hit_ms[kind] += ms;
else FB.miss_ms[kind] += ms;
plat_mutex_unlock(&FB.lock);
}
// One line: per kind, builds that came from pipeline-cache.bin of all builds so far, with the average time of each.
static void fb_log(const char *when) {
pb_log(when);
if (!P.feedback || !P.cache) return;
static const char *const kn[FB_KINDS] = {"pipelines", "libraries", "compute"};
char line[512];
int o = snprintf(line, sizeof line, "vk_pipeline: pipeline-cache.bin hits %s:", when);
plat_mutex_lock(&FB.lock);
for (int k = 0; k < FB_KINDS && o < (int)sizeof line - 96; k++) {
uint64_t miss = FB.n[k] - FB.hit[k];
o += snprintf(line + o, sizeof line - o, " %s %llu of %llu (%.0f%%; avg %.2f ms from the file, %.1f ms otherwise)%s", kn[k],
(unsigned long long)FB.hit[k], (unsigned long long)FB.n[k], FB.n[k] ? 100.0 * FB.hit[k] / FB.n[k] : 0.0,
FB.hit[k] ? FB.hit_ms[k] / FB.hit[k] : 0.0, miss ? FB.miss_ms[k] / miss : 0.0, k + 1 < FB_KINDS ? ";" : "");
}
plat_mutex_unlock(&FB.lock);
rt_log("%s", line);
}
static int uses_src1(const VkPipeState *s) {
for (uint32_t m = s->color.blend_en & 0xffu; m; m &= m - 1) {
int i = __builtin_ctz(m);
const blend_st *b = &s->blend[i];
if ((is_src1(b->src_rgb) || is_src1(b->dst_rgb) || is_src1(b->src_a) || is_src1(b->dst_a)) &&
i < target_count(s) && blendable(target_format(s->color_fmt[i], NULL, NULL)))
return 1;
}
return 0;
}
// Colour write mask of target i: NVN's channel mask, zero for a target outside the fragment shader's output map (SPH
// omap). Maxwell leaves such a target untouched;
// Vulkan writes undefined values, and on AMD they lit grass in front of Link as flat lime. Zeros are wrong
// too: NX2NAVI's half-res transparency pass binds its linear depth (R32F) as target 7, samples it in the same draws and
// writes only targets 0-2, 4 and 5; zeroed, the depth that later passes rebuild from it was 0 in tiles, and the fog
// over distant geometry showed rectangles of clear sky.
// The same holds per component: a shader that writes a target's RGB and not its alpha leaves the alpha alone on
// Maxwell, while Vulkan writes undefined values, and AMD's driver writes whatever its export registers held (NaN and
// saturated pixels the bloom then spreads). Components outside the shader's omap are masked out, where it is known.
static uint32_t target_wmask(const VkPipeState *s, int i) {
uint32_t m = s->chmask.mask >> (4 * i) & 0xf;
if (s->fs && !(gfx_shader_outputs(s->fs, NULL) >> i & 1)) m = 0;
if (s->fs) {
uint32_t c = gfx_shader_omap(s->fs) >> (4 * i) & 0xf;
if (c) m &= c;
}
if (i == 1 && s->fs && P.f_dual_src && uses_src1(s)) m = 0; // output 1 is the second blend source (spirv_dual_source)
return m;
}
uint32_t vk_pipeline_target_wmask(const VkPipeState *s, int i) { return target_wmask(s, i); }
static void gkey_fill(const VkPipeState *s, GKey *k) {
memset(k, 0, sizeof *k);
k->vs = s->vs, k->fs = s->fs, k->ts = s->ts;
k->indep = (uint8_t)P.indep;
k->n_color = (uint8_t)target_count(s);
for (int i = 0; i < k->n_color; i++) k->color[i] = target_format(s->color_fmt[i], NULL, NULL);
int d, st;
VkFormat df = target_format(s->depth_fmt, &d, &st);
k->depth = d ? df : VK_FORMAT_UNDEFINED;
k->stencil = st ? df : VK_FORMAT_UNDEFINED;
k->samples = s->samples > 1 ? s->samples : 1;
k->topo_class = (uint8_t)topology_class(vk_pipeline_topology(s->prim));
k->minus_w = s->depth_minus_w != 0;
k->dsb = s->fs && P.f_dual_src && uses_src1(s);
k->fbl = s->fs ? s->feedback : 0;
if (!P.dyn_clamp) k->clamp = s->depth_clamp && P.f_depth_clamp;
if (!P.dyn_poly) k->poly_mode = (uint8_t)polygon_mode(s->poly.mode);
if (!P.dyn_a2c) k->a2c = s->ms.a2c != 0;
if (P.f_logic_op && s->color.logic_op != NVN_LOGIC_OP_COPY) k->logic_op = (uint8_t)(s->color.logic_op + 1);
for (int i = 0; i < k->n_color; i++) {
int on = (s->color.blend_en >> i & 1) && blendable(k->color[i]);
if (!P.dyn_blend_en && on) k->blend_en |= (uint8_t)(1u << i);
if (!P.dyn_wmask) k->wmask |= target_wmask(s, i) << (4 * i);
if (!P.dyn_blend_eq && on) {
const blend_st *b = &s->blend[i];
uint8_t *o = k->blend[i];
o[0] = (uint8_t)blend_factor(b->src_rgb), o[1] = (uint8_t)blend_factor(b->dst_rgb), o[2] = (uint8_t)blend_op(b->eq_rgb);
o[3] = (uint8_t)blend_factor(b->src_a), o[4] = (uint8_t)blend_factor(b->dst_a), o[5] = (uint8_t)blend_op(b->eq_a);
}
}
if (is_mesh(s)) k->topo_class = 2; // mesh draws: no vertex input, the mesh stage declares its primitives
if (!P.dyn_vi && !is_mesh(s)) {
VtxInput v;
vertex_input(s, &v);
for (uint32_t i = 0; i < v.n_attr; i++) {
uint32_t l = v.a[i].location;
k->attr_fmt[l] = v.a[i].format, k->attr_off[l] = v.a[i].offset, k->attr_bind[l] = (uint8_t)v.a[i].binding;
}
for (uint32_t i = 0; i < v.n_bind; i++)
k->rate[v.b[i].binding] = v.b[i].inputRate == VK_VERTEX_INPUT_RATE_INSTANCE ? 2 : 1;
}
}
// Independent layouts: the outputs the vertex shader lacks from its union with s->fs's inputs added (vs_outs), types in
// fin. The vertex module (and its pre-rasterization library) depends on the vertex shader and this mask only.
static uint32_t vs_union_missing(const VkPipeState *s, GfxShaderIo fin[32]) {
GfxShaderIo vout[32];
uint32_t need = vs_outs(s->vs, s->fs, fin), have = gfx_shader_outputs(s->vs, vout), missing = 0;
if (s->fs) { // a location another fragment shader reads as another type: this pair's type (rare)
GfxShaderIo fi[32];
uint32_t fneed = gfx_shader_inputs(s->fs, fi);
for (int i = 0; i < 32; i++)
if (fneed >> i & 1 && !fi[i].packed &&
(!(need >> i & 1) || fin[i].type != fi[i].type || fin[i].comps < fi[i].comps)) {
if (need >> i & 1 && fin[i].type != fi[i].type)
LOG_ONCE("vk_pipeline: varying %d read as type %d and %d by fragment shaders of one vertex shader", i, fin[i].type, fi[i].type);
fin[i] = fi[i], need |= 1u << i;
}
}
if (P.vs_outs_all)
for (int i = 0; i < 32; i++)
if (!(need >> i & 1)) fin[i] = (GfxShaderIo){GFX_TYPE_FLOAT, 4, 0}, need |= 1u << i;
for (int i = 0; i < 32; i++)
if (need >> i & 1 && !(have >> i & 1)) missing |= 1u << i;
return missing;
}
static VkPipeline create_graphics(const VkPipeState *s, const GKey *k, const Layout *l, uint32_t fs_off) {
VkShaderModule vm = VK_NULL_HANDLE, fm = VK_NULL_HANDLE;
uint64_t vw, fw, vm_hash = 0, fm_hash = 0; // the hashes: of the final modules, for the PIPE_FEEDBACK 2 line
const uint32_t *vcode = gfx_shader_spirv(s->vs, &vw);
uint32_t *vpatched = NULL, *fpatched = NULL;
int mesh = is_mesh(s);
VkShaderModule tm = VK_NULL_HANDLE;
if (mesh && s->ts) { // the task stage's bindings follow the fragment stage's (pipe_miss)
uint64_t tw;
const uint32_t *tcode = gfx_shader_spirv(s->ts, &tw);
uint32_t toff = fs_off + (s->fs ? binding_span(s->fs) - 1 : 0);
uint32_t *t = tcode && toff ? spirv_rebind(tcode, tw, toff) : NULL;
tm = make_module(t ? t : tcode, tw, 0);
free(t);
}
if (l->set1 && !mesh) { // independent layouts: outputs from the vertex shader's union, not from this fragment shader
GfxShaderIo fin[32];
uint32_t missing = vs_union_missing(s, fin);
if (missing) {
uint64_t n2 = 0;
vpatched = spirv_add_outputs(vcode, vw, missing, fin, &n2);
if (!vpatched) LOG_ONCE("vk_pipeline: cannot add missing vertex outputs (mask %#x)", missing);
else vcode = vpatched, vw = n2;
}
}
if (s->fs) {
GfxShaderIo fin[32], vout[32];
// a mesh stage's outputs from its module (spirv_mesh_outputs): the translation reflects none
int points = 0;
uint32_t need = gfx_shader_inputs(s->fs, fin), missing = 0,
have = mesh ? (vcode ? spirv_mesh_outputs(vcode, vw, &points) : ~0u) : gfx_shader_outputs(s->vs, vout);
for (int i = 0; i < 32; i++) {
if (!(need >> i & 1) || fin[i].packed) continue;
if (!(have >> i & 1)) missing |= 1u << i;
else if (!mesh && vout[i].type != fin[i].type)
LOG_ONCE("vk_pipeline: varying %d is type %d in the vertex stage, %d in the fragment stage", i, vout[i].type, fin[i].type);
}
if (missing && !l->set1 && !mesh) {
uint64_t n2 = 0;
vpatched = spirv_add_outputs(vcode, vw, missing, fin, &n2);
if (!vpatched) LOG_ONCE("vk_pipeline: cannot add missing vertex outputs (mask %#x)", missing);
else vcode = vpatched, vw = n2;
}
const uint32_t *fcode = gfx_shader_spirv(s->fs, &fw);
if (missing && mesh && fcode) { // inputs no mesh output feeds read as Maxwell gives them (spirv_fs_fill_inputs)
uint64_t n2 = 0;
uint32_t *f = spirv_fs_fill_inputs(fcode, fw, missing, points, &n2);
if (f) fcode = fpatched = f, fw = n2;
else LOG_ONCE("vk_pipeline: cannot fill fragment inputs the mesh stage lacks (mask %#x)", missing);
}
if (k->dsb) {
uint64_t n2 = 0;
uint32_t *d = spirv_dual_source(fcode, fw, &n2);
if (d) free(fpatched), fcode = fpatched = d, fw = n2;
else LOG_ONCE("vk_pipeline: SRC1 blend factors but the fragment shader writes no colour output 1");
}
if (l->set1 || fs_off) {
uint32_t *r = !fcode ? NULL : l->set1 ? spirv_set(fcode, fw, 1) : spirv_rebind(fcode, fw, fs_off);
free(fpatched);
fcode = fpatched = r;
}
// A bound colour target outside the shader's output map gets write mask 0 (target_wmask)
if (fcode) fm = make_module(fcode, fw, !k->a2c), fm_hash = mm_hash;
}
vm = make_module(vcode, vw, 0), vm_hash = mm_hash;
free(vpatched), free(fpatched);
VkPipeline pipe = VK_NULL_HANDLE;
if (!vm || (s->fs && !fm) || (mesh && s->ts && !tm)) goto out;
VkPipelineShaderStageCreateInfo stages[3] = {
{.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = mesh ? VK_SHADER_STAGE_MESH_BIT_EXT : VK_SHADER_STAGE_VERTEX_BIT,
.module = vm, .pName = "main"},
{.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_FRAGMENT_BIT, .module = fm, .pName = "main"},
{.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_TASK_BIT_EXT, .module = tm, .pName = "main"}};
uint32_t n_stages = 1;
if (s->fs) n_stages = 2;
if (tm) {
if (!s->fs) stages[1] = stages[2];
n_stages++;
}
VkPipelineShaderStageRequiredSubgroupSizeCreateInfo rqv, rqf;
uint64_t sw;
const uint32_t *scode = gfx_shader_spirv(s->vs, &sw); // vcode may be a patched copy that is freed by now
pin_subgroup32(&stages[0], &rqv, scode, sw);
if (s->fs) pin_subgroup32(&stages[1], &rqf, gfx_shader_spirv(s->fs, &sw), sw);
VkVertexInputAttributeDescription va[32];
VkVertexInputBindingDescription vb[17];
uint32_t na = 0, nb = 0;
if (!P.dyn_vi) {
VtxInput v;
vertex_input(s, &v);
for (uint32_t i = 0; i < v.n_attr; i++) va[na++] = (VkVertexInputAttributeDescription){v.a[i].location, v.a[i].binding, v.a[i].format, v.a[i].offset};
for (uint32_t i = 0; i < v.n_bind; i++) vb[nb++] = (VkVertexInputBindingDescription){v.b[i].binding, v.b[i].stride, v.b[i].inputRate};
}
VkPipelineVertexInputStateCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
.vertexBindingDescriptionCount = nb, .pVertexBindingDescriptions = vb,
.vertexAttributeDescriptionCount = na, .pVertexAttributeDescriptions = va};
static const VkPrimitiveTopology class_base[4] = {VK_PRIMITIVE_TOPOLOGY_POINT_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_LIST,
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_PATCH_LIST};
VkPipelineInputAssemblyStateCreateInfo ia = {.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
.topology = class_base[k->topo_class]};
VkPipelineViewportDepthClipControlCreateInfoEXT clip = {.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_DEPTH_CLIP_CONTROL_CREATE_INFO_EXT,
.negativeOneToOne = k->minus_w};
VkPipelineViewportStateCreateInfo vp = {.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, .pNext = &clip};
// NVN's default is the last vertex (SetProvokingVertexMode, nvn_ext.c); Vulkan's is the first.
VkPipelineRasterizationProvokingVertexStateCreateInfoEXT pvs = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_PROVOKING_VERTEX_STATE_CREATE_INFO_EXT,
.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT};
VkPipelineRasterizationStateCreateInfo rs = {.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
.pNext = P.pv_last ? &pvs : NULL,
.depthClampEnable = k->clamp, .polygonMode = (VkPolygonMode)k->poly_mode,
.lineWidth = 1.0f};
VkPipelineMultisampleStateCreateInfo ms = {.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
.rasterizationSamples = (VkSampleCountFlagBits)k->samples,
.alphaToCoverageEnable = k->a2c};
VkPipelineDepthStencilStateCreateInfo ds = {.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO,
.maxDepthBounds = 1.0f};
VkPipelineColorBlendAttachmentState cba[8];
memset(cba, 0, sizeof cba);
for (int i = 0; i < k->n_color; i++) {
const uint8_t *b = k->blend[i];
cba[i].blendEnable = k->blend_en >> i & 1;
cba[i].srcColorBlendFactor = (VkBlendFactor)b[0], cba[i].dstColorBlendFactor = (VkBlendFactor)b[1];
cba[i].colorBlendOp = (VkBlendOp)b[2], cba[i].srcAlphaBlendFactor = (VkBlendFactor)b[3];
cba[i].dstAlphaBlendFactor = (VkBlendFactor)b[4], cba[i].alphaBlendOp = (VkBlendOp)b[5];
cba[i].colorWriteMask = k->wmask >> (4 * i) & 0xf;
}
VkPipelineColorBlendStateCreateInfo cb = {.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
.logicOpEnable = k->logic_op != 0,
.logicOp = k->logic_op ? (VkLogicOp)(k->logic_op - 1) : VK_LOGIC_OP_COPY,
.attachmentCount = k->n_color, .pAttachments = cba};
VkPipelineDynamicStateCreateInfo dy = {.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
.dynamicStateCount = P.n_dyn, .pDynamicStates = P.dyn};
VkDynamicState mdyn[sizeof P.dyn / sizeof P.dyn[0] + 1];
if (mesh) { // a mesh pipeline must not list vertex input, topology or primitive restart as dynamic
uint32_t n = 0;
for (uint32_t i = 0; i < P.n_dyn; i++)
if (P.dyn[i] != VK_DYNAMIC_STATE_VERTEX_INPUT_EXT && P.dyn[i] != VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE &&
P.dyn[i] != VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY && P.dyn[i] != VK_DYNAMIC_STATE_PRIMITIVE_RESTART_ENABLE)
mdyn[n++] = P.dyn[i];
if (vk_ctx()->has_vrs) mdyn[n++] = VK_DYNAMIC_STATE_FRAGMENT_SHADING_RATE_KHR; // vk_exec_draw.c mesh_shading_rate
dy.dynamicStateCount = n, dy.pDynamicStates = mdyn;
}
VkPipelineRenderingCreateInfo ri = {.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, .colorAttachmentCount = k->n_color,
.pColorAttachmentFormats = (const VkFormat *)k->color,
.depthAttachmentFormat = (VkFormat)k->depth, .stencilAttachmentFormat = (VkFormat)k->stencil};
VkGraphicsPipelineCreateInfo pi = {.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .pNext = &ri,
.stageCount = n_stages, .pStages = stages, .pVertexInputState = mesh ? NULL : &vi,
.pInputAssemblyState = mesh ? NULL : &ia, .pViewportState = &vp, .pRasterizationState = &rs,
.pMultisampleState = &ms, .pDepthStencilState = &ds, .pColorBlendState = &cb,
.pDynamicState = &dy, .layout = l->layout, .flags = noopt_flag_for(mesh)};
if (k->fbl & 1) pi.flags |= VK_PIPELINE_CREATE_COLOR_ATTACHMENT_FEEDBACK_LOOP_BIT_EXT;
if (k->fbl & 2) pi.flags |= VK_PIPELINE_CREATE_DEPTH_STENCIL_ATTACHMENT_FEEDBACK_LOOP_BIT_EXT;
VkPipelineCreationFeedbackCreateInfo fci;
VkPipelineCreationFeedback fb;
pi.pNext = fb_chain(&fci, &fb, pi.pNext);
uint64_t p0 = perf_begin();
crash_note("a graphics pipeline", s->vs, s->fs, s->ts);
VkResult r = pipe_create(0, &pi, &pipe);
if (r == VK_ERROR_UNKNOWN && !(pi.flags & VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BIT)) {
// A compiler that gives up on a pipeline (AMD's Windows driver answers VK_ERROR_UNKNOWN) may still build it
// without its optimiser: one more try that way, slower shaders for that pipeline instead of a draw left out.
static int told;
if (!told++) rt_log("vk_pipeline: the driver could not build a graphics pipeline (VK_ERROR_UNKNOWN): built again "
"without its optimiser (logged once)");
pi.flags |= VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BIT;
r = pipe_create(0, &pi, &pipe);
}
plat_crash_context(NULL);
perf_end(PERF_PIPE, p0); // counts on the render thread only, so background builds stay out of it
if (r != VK_SUCCESS) {
rt_log("vk_pipeline: vkCreateGraphicsPipelines failed (%d)", (int)r);
pipe = VK_NULL_HANDLE;
} else {
perf_count(PERF_CNT_PIPE_BUILT);
fb_note(FB_GRAPHICS, &fb);
if (P.feedback >= 2) { // the state hash: what the create info takes from the key besides the modules
uint64_t st = fnv(fnv(FNV0, cba, sizeof cba), k->color, sizeof k->color);
if (!P.dyn_vi) // the vertex layout is pipeline state then (zero in a mesh pipeline's key)
st = fnv(fnv(fnv(fnv(st, k->attr_fmt, sizeof k->attr_fmt), k->attr_off, sizeof k->attr_off),
k->attr_bind, sizeof k->attr_bind), k->rate, sizeof k->rate);
uint32_t bits[] = {(uint32_t)mesh, k->topo_class, k->minus_w, k->clamp, k->poly_mode, k->samples, k->a2c,
k->logic_op, k->n_color, k->fbl, k->dsb, l->set1 != 0, fs_off, pi.flags, k->depth, k->stencil};
rt_log("vk_pipeline: graphics: modules %016llx %016llx, state %016llx (mesh %d, topology class %u, samples %u, "
"colors %u, feedback loop %u, depth format %u, stage-independent layout %d, flags 0x%x), %s, %.1f ms",
(unsigned long long)vm_hash, (unsigned long long)fm_hash, (unsigned long long)fnv(st, bits, sizeof bits),
mesh, (unsigned)k->topo_class, (unsigned)k->samples, (unsigned)k->n_color, (unsigned)k->fbl,
(unsigned)k->depth, l->set1 != 0, (unsigned)pi.flags,
pb_last ? "from pipeline-binaries.bin" : fb.flags & VK_PIPELINE_CREATION_FEEDBACK_APPLICATION_PIPELINE_CACHE_HIT_BIT ? "from pipeline-cache.bin" : "compiled",
(double)fb.duration / 1e6);
}
}
out:
if (vm) vkDestroyShaderModule(P.device, vm, NULL);
if (fm) vkDestroyShaderModule(P.device, fm, NULL);
if (tm) vkDestroyShaderModule(P.device, tm, NULL);
return pipe;
}
// ---- graphics pipeline library (vk_caps.gpl)
// A pipeline is linked from four libraries, each cached by the part of the pipeline key it depends on: vertex input
// (topology class, vertex attributes without dynamic vertex input), pre-rasterization (vertex shader, the outputs its
// union adds, static rasterization state), fragment shader (fragment shader, dual-source rewrite, samples) and fragment
// output (targets, blend, samples). With independent layouts each stage's set depends on its shader alone, so the
// pre-rasterization library uses {set 0, null} and the fragment shader library {null, set 1}. The fast link runs on the
// thread of the draw that needs it (no link-time optimisation); the optimised pipeline is the job pipe_miss queued
// (a monolithic build with the same modules), which replaces the fast one in the Pipe when done. Libraries and fast
// pipelines live until vk_pipeline_shutdown.
// Background build signalling, both with P.lock: work_cv wakes the pipeline workers, done_cv wakes threads waiting for a
// pipeline or a stage library to be published.
static PlatCond work_cv = PLAT_COND_INIT, done_cv = PLAT_COND_INIT;
static PlatCond lib_cv = PLAT_COND_INIT; // wakes the library workers (lib_worker): work_cv's signal must reach a pool worker
#define LIB_BUCKETS 1024
enum { LIB_VI, LIB_PR, LIB_FS, LIB_FO };
typedef struct Lib { struct Lib *next; uint64_t hash; VkPipeline pipe; uint32_t size; uint8_t pre, used, building, pad;
struct LibJob *job; uint8_t key[]; } Lib;
// pre: built by the boot prebuild ("stage library prebuild" below); used: a draw's link found it; building: a thread
// builds it or job holds it queued (pipe not set yet); entries live until libs_destroy
static Lib *libs[LIB_BUCKETS];
typedef struct { uint32_t kind, topo_class, attr_fmt[32], attr_off[32]; uint8_t attr_bind[32], rate[17], pad[3]; } ViKey;
typedef struct { uint32_t kind, missing; const GfxShader *vs; uint8_t clamp, poly_mode, minus_w, pad; GfxShaderIo fin[32]; } PrKey;
typedef struct { uint32_t kind; uint8_t dsb, samples, a2c, pad; const GfxShader *fs; } FsKey;
typedef struct { uint32_t kind; uint8_t n_color, samples, a2c, logic_op, blend_en, fbl, pad[2]; uint32_t color[8], depth, stencil, wmask;
uint8_t blend[8][6]; } FoKey; // fbl: GKey.fbl, the feedback loop flags of the library (gpl_feedback)
static uint64_t qpc(void) { return plat_now_ns(); }
static double ms_since(uint64_t t0) { return (double)(qpc() - t0) / 1e6; }
static VkPipelineLayout half_layout(Layout *l, int h) {
if (l->half[h]) return l->half[h];
VkDescriptorSetLayout sets[3] = {h ? VK_NULL_HANDLE : l->set, h ? l->set1 : VK_NULL_HANDLE, vk_pool_set_layout()};
VkPipelineLayoutCreateInfo pi = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.flags = VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT, .setLayoutCount = 3,
.pSetLayouts = sets};
if (vkCreatePipelineLayout(P.device, &pi, NULL, &l->half[h]) != VK_SUCCESS) l->half[h] = VK_NULL_HANDLE;
return l->half[h];
}
// Builds one library from the pipeline's state; key is the library's key (PrKey: fin holds the added outputs).
static VkPipeline lib_create(int kind, const VkPipeState *s, const GKey *k, VkPipelineLayout lay, const void *key) {
static const VkGraphicsPipelineLibraryFlagsEXT part[4] = {
VK_GRAPHICS_PIPELINE_LIBRARY_VERTEX_INPUT_INTERFACE_BIT_EXT, VK_GRAPHICS_PIPELINE_LIBRARY_PRE_RASTERIZATION_SHADERS_BIT_EXT,
VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT, VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_OUTPUT_INTERFACE_BIT_EXT};
VkGraphicsPipelineLibraryCreateInfoEXT gl = {.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_LIBRARY_CREATE_INFO_EXT, .flags = part[kind]};
VkPipelineRenderingCreateInfo ri = {.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, .pNext = &gl};
VkPipelineDynamicStateCreateInfo dy = {.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
.dynamicStateCount = P.n_dyn, .pDynamicStates = P.dyn};
VkGraphicsPipelineCreateInfo pi = {.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .pNext = &ri,
.flags = VK_PIPELINE_CREATE_LIBRARY_BIT_KHR | noopt_flag(), .pDynamicState = &dy};
VkShaderModule m = VK_NULL_HANDLE;
VkPipelineShaderStageCreateInfo st = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .pName = "main"};
VkPipelineShaderStageRequiredSubgroupSizeCreateInfo rq;
VkVertexInputAttributeDescription va[32];
VkVertexInputBindingDescription vb[17];
VkPipelineVertexInputStateCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
static const VkPrimitiveTopology class_base[4] = {VK_PRIMITIVE_TOPOLOGY_POINT_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_LIST,
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_PATCH_LIST};
VkPipelineInputAssemblyStateCreateInfo ia = {.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
.topology = class_base[k->topo_class]};
VkPipelineViewportDepthClipControlCreateInfoEXT clip = {.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_DEPTH_CLIP_CONTROL_CREATE_INFO_EXT,
.negativeOneToOne = k->minus_w};
VkPipelineViewportStateCreateInfo vp = {.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, .pNext = &clip};
VkPipelineRasterizationProvokingVertexStateCreateInfoEXT pvs = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_PROVOKING_VERTEX_STATE_CREATE_INFO_EXT,
.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT};
VkPipelineRasterizationStateCreateInfo rs = {.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
.pNext = P.pv_last ? &pvs : NULL, .depthClampEnable = k->clamp,
.polygonMode = (VkPolygonMode)k->poly_mode, .lineWidth = 1.0f};
VkPipelineMultisampleStateCreateInfo ms = {.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
.rasterizationSamples = (VkSampleCountFlagBits)k->samples,
.alphaToCoverageEnable = k->a2c};
VkPipelineDepthStencilStateCreateInfo ds = {.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO,
.maxDepthBounds = 1.0f};
VkPipelineColorBlendAttachmentState cba[8];
VkPipelineColorBlendStateCreateInfo cb = {.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
uint32_t *patched = NULL;
VkPipeline pipe = VK_NULL_HANDLE;
if (kind == LIB_VI) {
if (!P.dyn_vi) {
VtxInput v;
vertex_input(s, &v);
for (uint32_t i = 0; i < v.n_attr; i++)
va[i] = (VkVertexInputAttributeDescription){v.a[i].location, v.a[i].binding, v.a[i].format, v.a[i].offset};
for (uint32_t i = 0; i < v.n_bind; i++)
vb[i] = (VkVertexInputBindingDescription){v.b[i].binding, v.b[i].stride, v.b[i].inputRate};
vi.vertexAttributeDescriptionCount = v.n_attr, vi.pVertexAttributeDescriptions = va;
vi.vertexBindingDescriptionCount = v.n_bind, vi.pVertexBindingDescriptions = vb;
}
pi.pVertexInputState = &vi, pi.pInputAssemblyState = &ia;
} else if (kind == LIB_PR) {
const PrKey *pk = key;
uint64_t vw, sw, n2 = 0;
const uint32_t *vcode = gfx_shader_spirv(s->vs, &vw), *scode = vcode;
sw = vw;
if (pk->missing && (patched = spirv_add_outputs(vcode, vw, pk->missing, pk->fin, &n2))) vcode = patched, vw = n2;
else if (pk->missing) LOG_ONCE("vk_pipeline: cannot add missing vertex outputs (mask %#x)", pk->missing);
if (!(m = make_module(vcode, vw, 0))) goto out;
st.stage = VK_SHADER_STAGE_VERTEX_BIT, st.module = m;
pin_subgroup32(&st, &rq, scode, sw);
pi.stageCount = 1, pi.pStages = &st, pi.pViewportState = &vp, pi.pRasterizationState = &rs;
pi.layout = lay;
} else if (kind == LIB_FS) {
if (s->fs) {
uint64_t fw, sw, n2 = 0;
const uint32_t *fcode = gfx_shader_spirv(s->fs, &fw), *scode = fcode;
sw = fw;
uint32_t *d = NULL;
if (k->dsb && fcode) {
if ((d = spirv_dual_source(fcode, fw, &n2))) fcode = d, fw = n2;
else LOG_ONCE("vk_pipeline: SRC1 blend factors but the fragment shader writes no colour output 1");
}
patched = fcode ? spirv_set(fcode, fw, 1) : NULL;
free(d);
if (!patched || !(m = make_module(patched, fw, !k->a2c))) goto out;
st.stage = VK_SHADER_STAGE_FRAGMENT_BIT, st.module = m;
pin_subgroup32(&st, &rq, scode, sw);
pi.stageCount = 1, pi.pStages = &st;
}
pi.pMultisampleState = &ms, pi.pDepthStencilState = &ds;
pi.layout = lay;
} else {
memset(cba, 0, sizeof cba);
for (int i = 0; i < k->n_color; i++) {
const uint8_t *b = k->blend[i];
cba[i].blendEnable = k->blend_en >> i & 1;
cba[i].srcColorBlendFactor = (VkBlendFactor)b[0], cba[i].dstColorBlendFactor = (VkBlendFactor)b[1];
cba[i].colorBlendOp = (VkBlendOp)b[2], cba[i].srcAlphaBlendFactor = (VkBlendFactor)b[3];
cba[i].dstAlphaBlendFactor = (VkBlendFactor)b[4], cba[i].alphaBlendOp = (VkBlendOp)b[5];
cba[i].colorWriteMask = k->wmask >> (4 * i) & 0xf;
}
cb.logicOpEnable = k->logic_op != 0, cb.logicOp = k->logic_op ? (VkLogicOp)(k->logic_op - 1) : VK_LOGIC_OP_COPY;
cb.attachmentCount = k->n_color, cb.pAttachments = cba;
ri.colorAttachmentCount = k->n_color, ri.pColorAttachmentFormats = (const VkFormat *)k->color;
ri.depthAttachmentFormat = (VkFormat)k->depth, ri.stencilAttachmentFormat = (VkFormat)k->stencil;
pi.pColorBlendState = &cb, pi.pMultisampleState = &ms;
// Whether the pipeline may sample an attachment it renders to is part of the fragment output state: the flags
// go on this library alone, and a pipeline linked from it has them (gpl_feedback).
const FoKey *ok = key;
if (ok->fbl & 1) pi.flags |= VK_PIPELINE_CREATE_COLOR_ATTACHMENT_FEEDBACK_LOOP_BIT_EXT;
if (ok->fbl & 2) pi.flags |= VK_PIPELINE_CREATE_DEPTH_STENCIL_ATTACHMENT_FEEDBACK_LOOP_BIT_EXT;
}
VkPipelineCreationFeedbackCreateInfo fci;
VkPipelineCreationFeedback fb;
pi.pNext = fb_chain(&fci, &fb, pi.pNext);
crash_note("a pipeline library", s->vs, s->fs, NULL);
if (pipe_create(1, &pi, &pipe) != VK_SUCCESS) pipe = VK_NULL_HANDLE;
plat_crash_context(NULL);
if (pipe && (kind == LIB_PR || kind == LIB_FS)) fb_note(FB_LIBRARY, &fb); // the shader libraries: the others compile nothing
out:
if (m) vkDestroyShaderModule(P.device, m, NULL);
free(patched);
return pipe;
}
// The cached library for key. Caller holds P.lock. build 0: lookup only (VK_NULL_HANDLE and *miss when absent);
// build 1: a missing one is built with P.lock released (a failed build is cached too).
// The library entry for key, NULL when absent. Caller holds P.lock.
static Lib *lib_find(uint64_t h, const void *key, uint32_t size) {
for (Lib *b = libs[h % LIB_BUCKETS]; b; b = b->next)
if (b->hash == h && b->size == size && !memcmp(b->key, key, size)) return b;
return NULL;
}
// Enters a library built with P.lock released; another thread's entry made meanwhile wins and pipe is destroyed.
// Returns the entered handle. Caller holds P.lock.
static VkPipeline lib_put(uint64_t h, const void *key, uint32_t size, VkPipeline pipe) {
Lib *b = lib_find(h, key, size);
if (b && b->building) { // another thread's build publishes the entry
if (pipe) vkDestroyPipeline(P.device, pipe, NULL);
return VK_NULL_HANDLE;
}
if (b) {
if (pipe) vkDestroyPipeline(P.device, pipe, NULL);
return b->pipe;
}
if (!(b = calloc(1, sizeof *b + size))) return pipe;
b->hash = h, b->size = size, b->pipe = pipe;
memcpy(b->key, key, size);
b->next = libs[h % LIB_BUCKETS];
libs[h % LIB_BUCKETS] = b;
return pipe;
}
// Shader libraries a waiting draw needs (lib_wait, link_queue): queued here ahead of every pipeline job, so an idle
// worker builds each at once and the vertex and fragment libraries of one draw build in parallel. A job no worker took
// is built by the first thread that needs its library (lib_get; lib_wait only without library workers). A job with p
// is a waiting draw's fast link of p (after its library jobs), run on a worker so the drawing thread only waits.
// Under P.lock.
typedef struct LibJob { struct LibJob *next; Lib *b; int kind; Layout *l; VkPipeState s; GKey k; Pipe *p; } LibJob;
static struct {
LibJob *head, *tail;
int idle; // workers asleep on work_cv
int running; // jobs workers are building
int on; // NX2NAVI_PIPE_WAIT: 1 workers (default), 0 the drawing thread builds them (draw);
// set by vk_pipeline_async_enable, before the replay runs
int lib_workers; // workers that take only these jobs (lib_worker), started with the pool
uint64_t waits, queued, by_worker, stolen, timeouts, links;
} U;
// A building entry for key, NULL when out of memory. Caller holds P.lock and found none.
static Lib *lib_new(uint64_t h, const void *key, uint32_t size) {
Lib *b = calloc(1, sizeof *b + size);
if (!b) return NULL;
b->hash = h, b->size = size, b->building = 1;
memcpy(b->key, key, size);
b->next = libs[h % LIB_BUCKETS];
libs[h % LIB_BUCKETS] = b;
return b;
}
// Builds b (building, not queued) with P.lock released and publishes it; a failed build is entered as VK_NULL_HANDLE.
static VkPipeline lib_build(Lib *b, int kind, const VkPipeState *s, const GKey *k, Layout *l) {
VkPipelineLayout lay = kind == LIB_PR ? half_layout(l, 0) : kind == LIB_FS ? half_layout(l, 1) : VK_NULL_HANDLE;
VkPipeline pipe = VK_NULL_HANDLE;
if (lay || kind == LIB_VI || kind == LIB_FO) {
plat_mutex_unlock(&P.lock);
uint64_t t0 = qpc();
pipe = lib_create(kind, s, k, lay, b->key);
double ms = ms_since(t0);
plat_mutex_lock(&P.lock);
P.g.lib_n[kind]++, P.g.lib_ms[kind] += ms;
if (ms > P.g.lib_max[kind]) P.g.lib_max[kind] = ms;
int added = kind == LIB_PR && ((const PrKey *)(const void *)b->key)->missing;
if (added) P.g.pr_added++, P.g.pr_added_ms += ms;
if (ms >= 25) P.g.lib_slow[kind]++, P.g.lib_slow_ms[kind] += ms;
// A shader library that took this long was not in the driver's cache; why not, for the first 32 of a run
if (ms >= 25 && (kind == LIB_PR || kind == LIB_FS) && P.g.lib_logged < 32) {
const GfxShader *sh = kind == LIB_PR ? s->vs : s->fs;
const uint8_t *id = sh ? gfx_shader_input_id(sh) : NULL;
P.g.lib_logged++;
rt_log("vk_pipeline: gpl: a %s library took %.0f ms: shader %02x%02x%02x%02x, %s%s",
kind == LIB_PR ? "pre-rasterization" : "fragment shader", ms, id ? id[0] : 0, id ? id[1] : 0,
id ? id[2] : 0, id ? id[3] : 0, origin_name(sh),
added ? "; outputs were added for its fragment shader, a module the preparation did not build"
: sh && gfx_shader_origin(sh) == 2 ? "; nothing was prepared from a shader first translated now"
: "; the driver's cache did not have it, or the build waited behind others in the driver");
}
}
if (!pipe && ++P.g.lib_fail <= 8) rt_log("vk_pipeline: gpl: stage library %d not built", kind);
b->pipe = pipe, b->building = 0;
plat_cond_broadcast(&done_cv);
return pipe;
}
// Takes b's queued job out of the queue and builds it on this thread. Caller holds P.lock.
static void lib_steal(Lib *b) {
LibJob *j = b->job, **pp = &U.head, *prev = NULL;
while (*pp && *pp != j) prev = *pp, pp = &(*pp)->next;
if (*pp) *pp = j->next;
if (U.tail == j) U.tail = prev;
b->job = NULL;
lib_build(b, j->kind, &j->s, &j->k, j->l);
free(j);
}
static VkPipeline lib_get(int kind, const VkPipeState *s, const GKey *k, Layout *l, const void *key, uint32_t size,
int build, int *miss) {
uint64_t h = key_hash(key, size);
Lib *have = lib_find(h, key, size);
while (have && have->building) { // being built (wait) or queued (build it here)
if (!build) {
*miss = 1;
return VK_NULL_HANDLE;
}
if (have->job) U.stolen++, lib_steal(have);
else plat_cond_wait(&done_cv, &P.lock);
}
if (have) {
if (have->pre && !have->used) have->used = 1, P.g.pre_used[kind]++;
return have->pipe;
}
VkPipelineLayout lay = kind == LIB_PR ? half_layout(l, 0) : kind == LIB_FS ? half_layout(l, 1) : VK_NULL_HANDLE;
if (!build || ((kind == LIB_PR || kind == LIB_FS) && !lay)) {
*miss = 1;
return VK_NULL_HANDLE;
}
Lib *b = lib_new(h, key, size); // entered first, so other threads wait for this build instead of repeating it
if (b) return lib_build(b, kind, s, k, l);
plat_mutex_unlock(&P.lock);
uint64_t t0 = qpc();
VkPipeline pipe = lib_create(kind, s, k, lay, key);
double ms = ms_since(t0);
plat_mutex_lock(&P.lock);
if ((have = lib_find(h, key, size)) && !have->building) { // another thread built it meanwhile
if (pipe) vkDestroyPipeline(P.device, pipe, NULL);
return have->pipe;
}
P.g.lib_n[kind]++, P.g.lib_ms[kind] += ms;
if (ms > P.g.lib_max[kind]) P.g.lib_max[kind] = ms;
if (!pipe && ++P.g.lib_fail <= 8) rt_log("vk_pipeline: gpl: stage library %d not built", kind);
return lib_put(h, key, size, pipe);
}
static void libs_destroy(void) {
for (int i = 0; i < LIB_BUCKETS; i++) {
for (Lib *b = libs[i], *n; b; b = n) {
n = b->next;
if (b->pipe) vkDestroyPipeline(P.device, b->pipe, NULL);
free(b);
}
libs[i] = NULL;
}
}
// The four library keys of pipeline s (key k).
static void lib_keys(const VkPipeState *s, const GKey *k, ViKey *v, PrKey *pk, FsKey *fk, FoKey *ok) {
memset(v, 0, sizeof *v);
v->kind = LIB_VI, v->topo_class = k->topo_class;
memcpy(v->attr_fmt, k->attr_fmt, sizeof v->attr_fmt), memcpy(v->attr_off, k->attr_off, sizeof v->attr_off);
memcpy(v->attr_bind, k->attr_bind, sizeof v->attr_bind), memcpy(v->rate, k->rate, sizeof v->rate);
memset(pk, 0, sizeof *pk);
GfxShaderIo fin[32];
pk->kind = LIB_PR, pk->vs = s->vs, pk->clamp = k->clamp, pk->poly_mode = k->poly_mode, pk->minus_w = k->minus_w;
pk->missing = vs_union_missing(s, fin);
for (int i = 0; i < 32; i++)
if (pk->missing >> i & 1) pk->fin[i] = fin[i];
memset(fk, 0, sizeof *fk);
fk->kind = LIB_FS, fk->fs = s->fs, fk->dsb = k->dsb, fk->samples = k->samples, fk->a2c = k->a2c;
memset(ok, 0, sizeof *ok);
ok->kind = LIB_FO, ok->n_color = k->n_color, ok->samples = k->samples, ok->a2c = k->a2c, ok->logic_op = k->logic_op;
ok->blend_en = k->blend_en, ok->fbl = k->fbl, ok->depth = k->depth, ok->stencil = k->stencil, ok->wmask = k->wmask;
memcpy(ok->color, k->color, sizeof ok->color), memcpy(ok->blend, k->blend, sizeof ok->blend);
}
// NX2NAVI_GPL_FEEDBACK (default 1; 0 = as before): a pipeline whose fragment shader samples an attachment of its own pass
// (VkPipeState.feedback: soft particles and water reading the depth buffer, decals, full-screen passes that read the
// target they write) gets a fast link like any other. The feedback loop flags belong to the fragment output state, so
// they are part of that library's key (FoKey.fbl) and of nothing else; the shader libraries are the ones the same
// shaders use without a loop. Before, such a pipeline had no fast link at all and its draws waited for, or were
// skipped during, the whole optimised build. On a first visit to Kokiri Forest (RTX 5090, every stage library in the
// driver's cache, DLSS Frame Generation on, so no frame is held back) full-screen draws of such pipelines waited
// 267 ms at the area's first frame and 410 ms in the middle of a walk; linked, 3 ms.
static int gpl_feedback(void) {
static int on = -1;
if (on < 0) {
on = cfg_bool("NX2NAVI_GPL_FEEDBACK");
if (!on) rt_log("vk_pipeline: gpl: pipelines with a feedback loop get no fast link (NX2NAVI_GPL_FEEDBACK=0)");
}
return on;
}
// A fast-linked pipeline for s from its stage libraries. Caller holds P.lock.
// build 0 (drawing thread): links only when the shader libraries exist (vertex input and fragment output libraries,
// which hold no shader, are built here); *final 0 = not yet, a worker builds them. build 1 (worker): builds the
// missing libraries and links with P.lock released. VK_NULL_HANDLE with *final 1 when GPL cannot express the pipeline
// (shared layout; a feedback loop with NX2NAVI_GPL_FEEDBACK=0) or a build failed.
static VkPipeline gpl_link(const VkPipeState *s, const GKey *k, const Layout *lc, int build, int *final) {
Layout *l = (Layout *)lc;
*final = 1;
if (!l->set1 || (k->fbl && !gpl_feedback())) return VK_NULL_HANDLE;
uint64_t t0 = qpc();
ViKey vk_;
PrKey pk;
FsKey fk;
FoKey ok;
lib_keys(s, k, &vk_, &pk, &fk, &ok);
int miss = 0;
VkPipeline lib[4];
lib[1] = lib_get(LIB_PR, s, k, l, &pk, sizeof pk, build, &miss); // shader libraries first: no work when absent
lib[2] = miss ? VK_NULL_HANDLE : lib_get(LIB_FS, s, k, l, &fk, sizeof fk, build, &miss);
if (miss) {
if (!build) *final = 0, P.g.defer++;
return VK_NULL_HANDLE;
}
lib[0] = lib_get(LIB_VI, s, k, l, &vk_, sizeof vk_, 1, &miss);
lib[3] = lib_get(LIB_FO, s, k, l, &ok, sizeof ok, 1, &miss);
VkPipeline pipe = VK_NULL_HANDLE;
if (lib[0] && lib[1] && lib[2] && lib[3]) {
VkPipelineLibraryCreateInfoKHR li = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR, .libraryCount = 4,
.pLibraries = lib};
VkGraphicsPipelineCreateInfo pi = {.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .pNext = &li,
.layout = l->layout};
if (build) plat_mutex_unlock(&P.lock);
uint64_t t1 = qpc();
crash_note("a library link", s->vs, s->fs, NULL);
if (vkCreateGraphicsPipelines(P.device, VK_NULL_HANDLE, 1, &pi, NULL, &pipe) != VK_SUCCESS) pipe = VK_NULL_HANDLE;
plat_crash_context(NULL);
double ms = ms_since(t1); // the link alone: library builds are counted apart
if (build) plat_mutex_lock(&P.lock);
P.g.link_ms += ms;
if (ms > P.g.link_max) P.g.link_max = ms;
}
if (pipe) P.g.links++, P.g.fb_links += k->fbl != 0;
else if (!P.g.link_fail++) rt_log("vk_pipeline: gpl: fast link failed; such draws wait or skip as without GPL");
double total = ms_since(t0);
if (!build && total > 5.0 && P.g.slow++ < 16)
rt_log("vk_pipeline: gpl: fast link on the drawing thread took %.1f ms", total);
return pipe;
}
// ---- background graphics pipeline builds (vk_pipeline_async_enable)
// A miss in vk_pipeline_graphics_async enters the Pipe as building and queues a job; the replay skips draws with it
// until a worker publishes the handle. The job holds copies of the state and the key. What they point to outlives it:
// shaders (gfx_shader_get results) live for the process (gfx_shader_drop_memory is the shader probe's, which builds no
// pipelines), layouts until vk_pipeline_shutdown, which drains the queue first. Workers call vkCreateGraphicsPipelines
// without P.lock: P.cache is created without VK_PIPELINE_CACHE_CREATE_EXTERNALLY_SYNCHRONIZED_BIT, so the driver
// synchronises builds into it; only the copy of its data in cache_save is kept apart from them (cache_rw). Compute
// pipelines stay synchronous: a skipped
// dispatch can change what the game computes.
//
// Two queues: draws' misses (A.head) and the boot prebuild (A.low, see "pipeline keys" below). Workers take the low
// queue only when the other is empty and run its jobs at PLAT_PRIO_BELOW_NORMAL, so guest threads win. A draw
// that finds a prebuild job still queued moves it to the draw queue; one that finds it running raises its worker.
typedef struct Job {
struct Job *next, *prev; // prev: low queue only
Pipe *p;
const Layout *l;
uint32_t fs_off;
uint8_t low; // in the low queue, or running at below-normal priority
uint8_t prebuild, demanded; // from the key file; a draw asked for it before it was ready
int worker; // running on A.thread[worker]; -1 queued; JOB_ON_DRAW on the thread that waits for it
VkPipeState s;
GKey k;
} Job;
#define MAX_WORKERS 4
#define JOB_ON_DRAW (-2)
static struct {
int on; // set once by vk_pipeline_async_enable, before the replay runs
int threads;
PlatThread *thread[MAX_WORKERS];
uint8_t prio_low[MAX_WORKERS]; // worker runs below normal (under P.lock)
Job *head, *tail; // draws' misses
Job *low, *low_tail; // prebuild
uint32_t inflight; // queued or running, both queues
int low_running; // pool workers running a job taken from the low queue
int gate_shut; // pool workers the memory gate has refused a low job since it last admitted one
uint64_t first, taken; // waited-for jobs moved to the head of the draw queue; built by the waiting thread
PlatCond idle; // with P.lock: no job queued or running
} A = {.idle = PLAT_COND_INIT};
// ---- pipeline keys (pipeline-keys.bin next to the pipeline cache)
// Every graphics pipeline built in a game run appends a KeyRec: the VkPipeState it was built from with the shader
// pointers zeroed and the shaders named by their gfx_shader input ids instead, which do not depend on the translator,
// so the record rebuilds the pipeline without guest memory. At the next boot vk_pipeline_prewarm (rt_boot, before the
// guest runs; NX2NAVI_PIPE_PREBUILD=0 skips it) loads each record's shaders, translating the ones not cached under the
// current translator from their stored inputs (gfx_shader_prewarm; a record whose shader has neither is dropped),
// enters each Pipe as building under the key a draw would compute, queues it to the low queue and builds the queue on
// every core. Records are deduplicated by a hash of their bytes; they are collected under P.lock by the worker that
// built the pipeline and appended by the present thread (vk_pipeline_autosave) or at exit (vk_pipeline_save). A file
// with another magic, version or record size is ignored and rewritten; a version 3 file (no task stage) is converted
// and rewritten. Bump KEYS_VERSION when VkPipeState or the record changes meaning without changing size.
#define KEYS_VERSION 4 // 3: shaders named by input id (gfx_shader.c); 4: mesh pipelines, with their task stage
typedef struct { char magic[8]; uint32_t version, rec_size; } KeysHeader;
typedef struct {
uint8_t vs[32], fs[32];
uint32_t has_fs, pad;
VkPipeState s;
} KeyRecV3;
typedef struct {
uint8_t vs[32], fs[32], ts[32]; // gfx_shader_input_id of each stage (vs: the mesh stage of a mesh pipeline); fs all
// zero with has_fs 0, ts with has_ts 0
uint32_t has_fs, has_ts;
VkPipeState s; // vs, fs, ts zero
} KeyRec;
static struct {
char path[PLAT_PATH_MAX * 2];
int header_ok; // the file exists with our header: append
uint64_t *seen; // open addressing set of record hashes (0 empty)
uint32_t seen_cap, seen_n;
KeyRec *pend; // records not written yet
uint32_t pend_n, pend_cap;
uint8_t *file; // the file as read at boot, until the prewarm is done with it
size_t file_n; // records in it
PlatMutex io; // one writer
// prebuild progress (under P.lock)
int loading; // the prewarm still queues records
int warm; // vk_pipeline_prewarm owns the file (prebuild_start leaves it)
uint32_t left; // prebuild jobs queued or running
uint32_t built, failed, promoted;
uint32_t gate_ended; // boot builders the memory gate ended (warm_builder)
double t0; // now_ms() when the prebuild started
double until; // now_ms() after which the boot builders stop (comment at prewarm_run); 0 none
} K = {.io = PLAT_MUTEX_INIT};
static uint64_t rec_hash(const KeyRec *r) {
uint64_t h = fnv(FNV0, r, sizeof *r);
return h ? h : 1;
}
// 1 if h was new. Caller holds P.lock.
static int seen_add(uint64_t h) {
if ((K.seen_n + 1) * 2 > K.seen_cap) {
uint32_t cap = K.seen_cap ? K.seen_cap * 2 : 4096;
uint64_t *t = calloc(cap, sizeof *t);
if (!t) return 1;
for (uint32_t i = 0; i < K.seen_cap; i++) {
if (!K.seen[i]) continue;
uint32_t j = (uint32_t)K.seen[i] & (cap - 1);
while (t[j]) j = (j + 1) & (cap - 1);
t[j] = K.seen[i];
}
free(K.seen);
K.seen = t, K.seen_cap = cap;
}
uint32_t j = (uint32_t)h & (K.seen_cap - 1);
while (K.seen[j]) {
if (K.seen[j] == h) return 0;
j = (j + 1) & (K.seen_cap - 1);
}
K.seen[j] = h;
K.seen_n++;
return 1;
}
static void rec_fill(KeyRec *r, const VkPipeState *s) {
memset(r, 0, sizeof *r);
memcpy(r->vs, gfx_shader_input_id(s->vs), 32);
if (s->fs) memcpy(r->fs, gfx_shader_input_id(s->fs), 32), r->has_fs = 1;
if (s->ts) memcpy(r->ts, gfx_shader_input_id(s->ts), 32), r->has_ts = 1;
r->s = *s;
r->s.vs = r->s.fs = r->s.ts = NULL;
r->s.prim_restart = 0; // dynamic state: the same pipeline either way
}
// A pipeline was built for a draw: queue its record for the file. Caller holds P.lock.
static void keys_note(const VkPipeState *s) {
if (!A.on || !K.path[0]) return;
static const uint8_t none[32];
if (s->fs && !memcmp(gfx_shader_input_id(s->fs), none, 32)) return; // the runtime's own shader (gfx_shader_own)
if (s->ts && !memcmp(gfx_shader_input_id(s->ts), none, 32)) return;
if (!memcmp(gfx_shader_input_id(s->vs), none, 32)) return; // the mesh fallback's vertex stage (vk_exec_draw.c mfb_vs_get)
KeyRec r;
rec_fill(&r, s);
if (!seen_add(rec_hash(&r))) return;
if (K.pend_n == K.pend_cap) {
uint32_t cap = K.pend_cap ? K.pend_cap * 2 : 64;
KeyRec *t = realloc(K.pend, cap * sizeof *t);
if (!t) return;
K.pend = t, K.pend_cap = cap;
}
K.pend[K.pend_n++] = r;
}
// Appends the pending records (present thread or exit; never the render thread).
static void keys_flush(void) {
if (!K.path[0]) return;
plat_mutex_lock(&K.io);
plat_mutex_lock(&P.lock);
KeyRec *r = K.pend;
uint32_t n = K.pend_n;
K.pend = NULL, K.pend_n = K.pend_cap = 0;
plat_mutex_unlock(&P.lock);
if (n) {
FILE *f = fopen(K.path, K.header_ok ? "ab" : "wb");
int ok = f != NULL;
if (ok && !K.header_ok) {
KeysHeader h = {{'N', 'X', '2', 'N', 'P', 'K', 'E', 'Y'}, KEYS_VERSION, sizeof(KeyRec)};
ok = fwrite(&h, sizeof h, 1, f) == 1;
}
if (ok) ok = fwrite(r, sizeof *r, n, f) == n;
if (ok) ok = plat_file_sync(f); // appended records on the disk before the next append
if (f) ok = !fclose(f) && ok;
if (ok) K.header_ok = 1;
else rt_log("vk_pipeline: cannot write %s", K.path);
}
free(r);
plat_mutex_unlock(&K.io);
}
// Reads the key file into K.file and its record hashes into the set. Caller holds P.lock (ensure_init).
static void keys_open(void) {
if (!A.on || !P.cache_path[0]) return;
snprintf(K.path, sizeof K.path, "%s", P.cache_path);
char *slash = strrchr(K.path, '\\'), *fslash = strrchr(K.path, '/');
if (fslash > slash) slash = fslash;
snprintf(slash ? slash + 1 : K.path, sizeof K.path - (size_t)(slash ? slash + 1 - K.path : 0), "pipeline-keys.bin");
gfx_shader_record_at(K.path); // shader-answers.bin next to it (gfx_shader.c)
FILE *f = fopen(K.path, "rb");
if (!f) return;
KeysHeader h;
fseek(f, 0, SEEK_END);
long len = ftell(f);
fseek(f, 0, SEEK_SET);
int head = len >= (long)sizeof h && fread(&h, sizeof h, 1, f) == 1 && !memcmp(h.magic, "NX2NPKEY", 8);
if (head && h.version == KEYS_VERSION && h.rec_size == sizeof(KeyRec)) {
K.header_ok = 1;
size_t n = ((size_t)len - sizeof h) / sizeof(KeyRec);
if (n && (K.file = malloc(n * sizeof(KeyRec))) && fread(K.file, sizeof(KeyRec), n, f) == n) K.file_n = n;
else free(K.file), K.file = NULL;
for (size_t i = 0; i < K.file_n; i++) seen_add(rec_hash((const KeyRec *)K.file + i));
if (len != (long)(sizeof h + n * sizeof(KeyRec))) // a torn append: rewrite the file from what was read
K.header_ok = 0;
} else if (head && h.version == 3 && h.rec_size == sizeof(KeyRecV3)) {
// the vertex pipelines of a version 3 file keep their prewarm: each record without a task stage, and the
// file rewritten in this version below (header_ok 0)
size_t n = ((size_t)len - sizeof h) / sizeof(KeyRecV3);
KeyRecV3 *old = n ? malloc(n * sizeof *old) : NULL;
if (old && fread(old, sizeof *old, n, f) == n && (K.file = calloc(n, sizeof(KeyRec)))) {
KeyRec *r = (KeyRec *)K.file;
for (size_t i = 0; i < n; i++) {
memcpy(r[i].vs, old[i].vs, 32), memcpy(r[i].fs, old[i].fs, 32);
r[i].has_fs = old[i].has_fs, r[i].s = old[i].s;
seen_add(rec_hash(&r[i]));
}
K.file_n = n;
rt_log("vk_pipeline: %s: %zu version 3 records converted; the file is rewritten", K.path, n);
}
free(old);
} else {
rt_log("vk_pipeline: %s has another format or version; it is rewritten", K.path);
}
fclose(f);
if (!K.header_ok && K.file_n) { // keep the whole records of a torn file
KeyRec *t = malloc(K.file_n * sizeof *t);
if (t) memcpy(t, K.file, K.file_n * sizeof *t), K.pend = t, K.pend_n = K.pend_cap = (uint32_t)K.file_n;
}
}
static double now_ms(void) { return (double)plat_now_ns() / 1e6; }
// Caller holds P.lock.
// p, which a draw missed, has its first usable pipeline: how long that took is how long its draws were skipped or
// waited. fast 0: it had no fast link and this is the optimised build. Caller holds P.lock.
// The summary (save_with) counts what a slow one can be: a mesh pipeline (no stage libraries, so its whole build), one
// the key file names whose prebuild had not come to it, one with a shader translated in this session (nothing of it
// was prepared, so its libraries compile cold). The first READY_LOGS that took READY_SLOW_MS or longer get a line
// each with those facts, so a log says which pipelines left their draws out of the picture and for which reason.
#define READY_SLOW_MS 50.0
#define READY_LOGS 48
static void ready_note(Pipe *p, int fast) {
if (!p->t_miss) return;
double ms = (double)(plat_now_ns() - p->t_miss) / 1e6;
p->t_miss = 0;
P.g.ready_n[ms < 4 ? 0 : ms < 8 ? 1 : ms < 17 ? 2 : ms < 50 ? 3 : ms < 150 ? 4 : 5]++;
P.g.ready_ms += ms;
if (ms > P.g.ready_max) P.g.ready_max = ms;
P.g.ready_nofast += !fast;
const Job *j = p->job;
if (!j) return;
int mesh = is_mesh(&j->s);
int fresh = gfx_shader_origin(j->s.vs) == 2 || (j->s.fs && gfx_shader_origin(j->s.fs) == 2) ||
(j->s.ts && gfx_shader_origin(j->s.ts) == 2);
P.g.ready_mesh += (uint64_t)mesh, P.g.ready_recorded += j->prebuild, P.g.ready_fresh += (uint64_t)fresh;
if (ms < READY_SLOW_MS || P.g.ready_logged >= READY_LOGS) return;
P.g.ready_logged++;
const uint8_t *vi = gfx_shader_input_id(j->s.vs), *fi = j->s.fs ? gfx_shader_input_id(j->s.fs) : NULL;
char fid[16] = "none";
if (fi) snprintf(fid, sizeof fid, "%02x%02x%02x%02x", fi[0], fi[1], fi[2], fi[3]);
rt_log("vk_pipeline: a pipeline a draw missed was usable after %.0f ms: a %s pipeline, %s; %s; %s shader "
"%02x%02x%02x%02x (%s), fragment shader %s (%s)%s%s",
ms, mesh ? "mesh" : "vertex",
fast ? "fast-linked, so the time went to its stage libraries (the lines \"a ... library took\" name the slow ones)"
: mesh ? "which has no stage libraries: this is its whole build"
: "which got no fast link: this is its whole build",
j->prebuild ? "pipeline-keys.bin names it and its prebuild had not come to it" : "not in pipeline-keys.bin before",
mesh ? "mesh" : "vertex", vi[0], vi[1], vi[2], vi[3], origin_name(j->s.vs), fid, origin_name(j->s.fs),
j->k.fbl ? "; feedback loop" : "", P.g.ready_logged == READY_LOGS ? " (the last such line of this run)" : "");
}
static void prebuild_report(void) {
rt_log("vk_pipeline: prebuild done: %u built, %u failed, %u asked for by a draw first; %.0f ms after the device was "
"ready", K.built, K.failed, K.promoted, now_ms() - K.t0);
}
static void job_lower(Job *j);
// Runs a waiting draw's job taken from U on a worker: a library build or, for a job with p, p's fast link (gpl_link
// build 1 builds a library still missing and waits for one another worker builds). Releases P.lock while building.
static void u_run(LibJob *lj) {
U.by_worker++, U.running++;
if (lj->p) {
Pipe *p = lj->p;
if (!p->info.pipeline) {
int final = 0;
VkPipeline f = gpl_link(&lj->s, &lj->k, lj->l, 1, &final); // releases P.lock while linking
if (f && !p->info.pipeline) {
p->fast = p->info.pipeline = f;
ready_note(p, 1);
job_lower(p->job); // its job now only does the optimised build
} else if (f) {
vkDestroyPipeline(P.device, f, NULL); // the job published one meanwhile
}
}
p->link_queued = 0;
U.links++;
plat_cond_broadcast(&done_cv);
} else {
lib_build(lj->b, lj->kind, &lj->s, &lj->k, lj->l);
}
U.running--;
free(lj);
}
// NX2NAVI_PIPE_FAST_WORKERS (default 1; 0 = as before): the library workers also take the fast link of a draw-queue job
// (fast_take). A draw that is skipped rather than waiting leaves its pipeline to the pool, where a job is first
// fast-linked and then goes back in the queue for its optimised build. The pool keeps one worker out of optimised
// builds, so with the others inside one (about 100 ms each with the driver's cache warm, 350 ms cold) that single
// worker linked every new pipeline of a burst, one after another at 5 to 10 ms each. On a first visit to Kokiri
// Forest (RTX 5090, libraries in the driver's cache) 180 of the 444 pipelines draws missed took over 50 ms to become
// usable, the longest 1.3 s, and their draws were missing from every frame until then; with the library workers
// linking, 23 did, 19 of them mesh pipelines, which have no fast link.
static int fast_workers(void) {
static int on = -1;
if (on < 0) on = cfg_bool("NX2NAVI_PIPE_FAST_WORKERS");
return on;
}
// The first draw-queue job whose pipeline still lacks its fast link, taken out of the queue; NULL when there is none.
// Caller holds P.lock.
static Job *fast_take(void) {
if (!P.gpl || !fast_workers()) return NULL;
for (Job *j = A.head, *prev = NULL; j; prev = j, j = j->next) {
Pipe *p = j->p;
if (p->info.pipeline || p->fast_tried || p->link_queued || !p->lay || is_mesh(&j->s)) continue;
if (prev) prev->next = j->next;
else A.head = j->next;
if (A.tail == j) A.tail = prev;
j->next = j->prev = NULL;
return j;
}
return NULL;
}
// A worker for stage libraries and fast links only: a waiting draw's jobs (U) first, so a draw that waits finds one
// free even while the pool builds pipelines and the drawing thread never builds a library itself; then the fast link
// of a draw-queue job (fast_take), whose optimised build goes on to the pool.
static void lib_worker(void *arg) {
(void)arg;
plat_mutex_lock(&P.lock);
for (;;) {
Job *j = NULL;
while (!U.head && !(j = fast_take())) plat_cond_wait(&lib_cv, &P.lock);
if (!j) {
LibJob *lj = U.head;
if (!(U.head = lj->next)) U.tail = NULL;
if (lj->b) lj->b->job = NULL;
u_run(lj);
continue;
}
Pipe *p = j->p;
int final;
p->fast_tried = 1; // no other thread links it meanwhile; a draw that waits for it waits for this link
VkPipeline f = gpl_link(&j->s, &j->k, p->lay, 1, &final); // releases P.lock while building
if (f && !p->info.pipeline) {
p->fast = p->info.pipeline = f, P.g.wlinks++, P.g.flinks++;
ready_note(p, 1);
plat_cond_broadcast(&done_cv);
} else if (f) {
vkDestroyPipeline(P.device, f, NULL);
}
if (p->info.pipeline) { // the optimised build: ahead of the prebuild, behind other draws' misses
j->low = 1, P.g.requeued++;
j->next = A.low;
if (A.low) A.low->prev = j;
else A.low_tail = j;
A.low = j;
} else { // no fast link for this one: its whole build, next in the draw queue
if (!(j->next = A.head)) A.tail = j;
A.head = j;
}
plat_cond_signal(&work_cv);
}
}
// ---- the memory gate of the background builds (vk_build_gate.h)
// Every pipeline built ahead of its first use passes here before it starts: the recorded pipelines the boot builds
// (warm_builder), the pool's low queue during play (pipe_worker: the rest of the recorded pipelines and the optimised
// builds behind a fast link), the compute pipelines and stage libraries of the boot (lb_worker), the library warm
// (lw_worker) and the large compute shaders (bg_main). A build a draw or a dispatch waits for never does: it is needed
// now.
// NX2NAVI_BUILD_FREE_MB (default 4096; 0 no check): the system commit headroom that stays free of them.
// NX2NAVI_BUILD_RESERVE_MB (default 64): what one build under way is taken to cost the driver's compiler while it
// runs; a large compute shader reserves GATE_BIG_MB. A build starts only when the headroom holds the part kept free,
// the reservations of the builds under way and its own.
// Measured on the machine this was written on (RTX 5090, 73,154 MB commit limit, warm driver cache): the boot's 3,898
// recorded pipelines on 16 threads raised the process commit by 3,054 MB in 2.2 s, and the 4,066 compute pipelines and
// libraries after them, together with the game's own start, by another 9,824 MB. A device was lost on that machine
// at 56,673 of 65,531 MB system commit right after such a prebuild.
// The commit is read at most every GATE_SAMPLE_MS: sixteen builders finishing a cached pipeline every half millisecond
// would otherwise ask the system thousands of times a second. What the builds took since the last reading is covered
// by their reservations.
#define GATE_BIG_MB 4096.0
#define GATE_SAMPLE_MS 100.0
#define GATE_WAIT_MS 30000.0 // how long a boot or warm builder waits for room before it leaves the rest to their first use
static struct {
PlatMutex lock;
BuildGate g;
double est, used, limit, at;
int init;
int off; // a preparation run (seed_pipelines): nothing is refused, there is no game to keep room for
uint64_t refused;
} GATE = {.lock = PLAT_MUTEX_INIT};
// 1 when a build estimated at est MB may start, and then est is reserved until gate_leave. est 0: an ordinary build
// (NX2NAVI_BUILD_RESERVE_MB), whose estimate *took returns for gate_leave.
static int gate_enter(double est, double *took) {
plat_mutex_lock(&GATE.lock);
if (!GATE.init) {
GATE.init = 1;
GATE.g.free_mb = strtod(cfg_value("NX2NAVI_BUILD_FREE_MB"), NULL);
GATE.est = strtod(cfg_value("NX2NAVI_BUILD_RESERVE_MB"), NULL);
if (GATE.est < 0) GATE.est = 0;
GATE.at = -GATE_SAMPLE_MS;
}
if (GATE.off) GATE.g.free_mb = 0;
double now = now_ms();
if (now - GATE.at >= GATE_SAMPLE_MS) {
double proc;
plat_commit_info(&proc, &GATE.used, &GATE.limit);
GATE.at = now;
}
if (est <= 0) est = GATE.est;
int ok = build_gate_enter(&GATE.g, GATE.used, GATE.limit, est);
if (!ok) GATE.refused++;
plat_mutex_unlock(&GATE.lock);
if (took) *took = est;
return ok;
}
static void gate_leave(double est) {
plat_mutex_lock(&GATE.lock);
build_gate_leave(&GATE.g, est);
plat_mutex_unlock(&GATE.lock);
}
// The gate's numbers for a log line that says why a builder stopped or waits.
static const char *gate_text(char *buf, size_t n) {
plat_mutex_lock(&GATE.lock);
snprintf(buf, n, "system commit %.0f of %.0f MB, %.0f MB reserved for builds under way, %.0f MB kept free "
"(NX2NAVI_BUILD_FREE_MB), %.0f MB a build (NX2NAVI_BUILD_RESERVE_MB)", GATE.used, GATE.limit,
GATE.g.reserved_mb, GATE.g.free_mb, GATE.est);
plat_mutex_unlock(&GATE.lock);
return buf;
}
// A boot or warm builder's wait for room: 1 with est reserved (*took), 0 when *stop was set or GATE_WAIT_MS passed
// without room, which the caller takes as the end of its work. who names the builder for the log's one line a wait.
static int gate_wait(const char *who, volatile int32_t *stop, double *took) {
double w0 = now_ms();
int waited = 0;
while (!gate_enter(0, took)) {
char g[256];
if (*stop) return 0;
if (!waited++) rt_log("vk_pipeline: %s: waiting for commit headroom (%s)", who, gate_text(g, sizeof g));
if (now_ms() - w0 > GATE_WAIT_MS) return 0;
for (int i = 0; i < 20 && !*stop; i++) plat_sleep_ms(100);
}
return 1;
}
// --probe-buildgate (host.h): the gate with this machine's commit and the settings in effect, without a device and
// without a build. It prints what the settings and the system give, asks for sixteen ordinary builds and one large
// one, and checks the answers against the same numbers: the builds admitted are those that fit with the earlier
// ones' reservations, a refused one reserves nothing, and a builder's wait ends at once when its stop flag is set.
// tests/build_gate_check.py runs it with build_free_mb at 0 (everything admitted) and at a value no machine has free
// (everything refused).
int vk_pipeline_probe_buildgate(void) {
double took = 0, big = 0;
int fails = 0, in = 0, want = 0;
int first = gate_enter(0, &took);
double free_mb = GATE.g.free_mb, used = GATE.used, limit = GATE.limit, est = took;
if (first) gate_leave(took);
printf("probe-buildgate: system commit %.0f of %.0f MB, %.0f MB kept free (NX2NAVI_BUILD_FREE_MB), %.0f MB a build "
"(NX2NAVI_BUILD_RESERVE_MB)\n", used, limit, free_mb, est);
double room = limit - used - free_mb;
for (int i = 0; i < 16; i++) {
want += free_mb <= 0 || limit < 0 || room - i * est >= est; // the commit sample stays for GATE_SAMPLE_MS
in += gate_enter(0, &took);
}
double reserved = GATE.g.reserved_mb;
int big_in = gate_enter(GATE_BIG_MB, &big);
int big_want = free_mb <= 0 || limit < 0 || room - reserved >= GATE_BIG_MB;
printf("probe-buildgate: %d of 16 ordinary builds admitted (%d fit), %.0f MB reserved; a large compute build beside "
"them %s\n", in, want, reserved, big_in ? "admitted" : "refused");
if (in != want) fails++, printf("probe-buildgate: FAIL: %d admitted, %d fit\n", in, want);
if (reserved != in * est) fails++, printf("probe-buildgate: FAIL: %.0f MB reserved for %d builds\n", reserved, in);
if (big_in != big_want) fails++, printf("probe-buildgate: FAIL: the large build was %s\n", big_in ? "admitted" : "refused");
if (big_in) gate_leave(big);
for (int i = 0; i < in; i++) gate_leave(est);
if (GATE.g.reserved_mb != 0) fails++, printf("probe-buildgate: FAIL: %.0f MB still reserved\n", GATE.g.reserved_mb);
volatile int32_t stop = 1;
double t0 = now_ms();
int got = gate_wait("probe", &stop, &took);
if (got) gate_leave(took);
if (got != first || now_ms() - t0 > 1000)
fails++, printf("probe-buildgate: FAIL: a stopped builder's wait gave %d after %.0f ms\n", got, now_ms() - t0);
printf("probe-buildgate: an ordinary build is %s now; %s\n", first ? "admitted" : "refused", fails ? "FAILED" : "passed");
return fails ? 1 : 0;
}
// The whole build of job j, which the caller took off its queue, and the job's end: its pipeline is entered, the
// threads waiting for it are woken and j is freed. lowrun: 1 on a pool worker that took j from the low queue. Caller
// holds P.lock, which is released for the build.
static void job_build(Job *j, int lowrun) {
plat_mutex_unlock(&P.lock);
VkPipeline pipe = create_graphics(&j->s, &j->k, j->l, j->fs_off);
plat_mutex_lock(&P.lock);
if (pipe || !j->p->fast) j->p->info.pipeline = pipe; // a failed optimised build keeps the fast-linked one
ready_note(j->p, 0);
if (pipe && j->p->fast) P.g.swaps++;
j->p->building = 0;
j->p->job = NULL;
if (pipe) {
P.stats.graphics++, P.stats.async_built++;
if (!j->prebuild) keys_note(&j->s);
} else {
P.stats.failures++;
rt_log("vk_pipeline: graphics pipeline not built: pipeline creation failed");
}
if (j->prebuild) {
if (pipe) K.built++;
else K.failed++;
K.promoted += j->demanded;
if (!--K.left && !K.loading) prebuild_report();
}
A.low_running -= lowrun;
if (!--A.inflight) plat_cond_broadcast(&A.idle);
plat_cond_broadcast(&done_cv);
if (pipe && (!j->prebuild || j->demanded)) perf_count(PERF_CNT_PIPE_ASYNC); // a draw waited for it
free(j);
}
static void pipe_worker(void *arg) {
int me = (int)(intptr_t)arg;
plat_mutex_lock(&P.lock);
for (;;) {
// With 3 or more workers one never takes low-queue work, so a waiting draw's library starts at once
// (not with NX2NAVI_PIPE_WAIT=draw).
while (!U.head && !A.head && !(A.low && (A.threads < 3 || !U.on || A.low_running < A.threads - 1))) {
U.idle++;
plat_cond_wait(&work_cv, &P.lock);
U.idle--;
}
if (U.head) { // a library a draw waits for
LibJob *lj = U.head;
if (!(U.head = lj->next)) U.tail = NULL;
if (lj->b) lj->b->job = NULL;
if (A.prio_low[me]) A.prio_low[me] = 0, plat_thread_priority(PLAT_PRIO_NORMAL);
u_run(lj); // releases P.lock while building
continue;
}
Job *j = A.head;
int lowrun = !j;
double held = 0; // the low job's reservation at the memory gate
if (!j && !gate_enter(0, &held)) {
// No room for a background build now (gate_enter). The job stays queued: a draw that needs its pipeline
// moves it to the draw queue (promote), which wakes this wait; otherwise the gate is asked again in 2 s.
if (!A.gate_shut++) {
char g[256];
rt_log("vk_pipeline: background pipeline builds wait for commit headroom (%s); a pipeline a draw needs "
"is still built at once", gate_text(g, sizeof g));
}
plat_cond_wait_ns(&work_cv, &P.lock, 2000000000ull);
continue;
}
if (!j && A.gate_shut) {
A.gate_shut = 0;
rt_log("vk_pipeline: background pipeline builds go on: the commit headroom is back");
}
if (j) {
if (!(A.head = j->next)) A.tail = NULL;
} else {
j = A.low;
if ((A.low = j->next)) A.low->prev = NULL;
else A.low_tail = NULL;
A.low_running++;
}
j->next = j->prev = NULL;
j->worker = me;
if (j->low != A.prio_low[me]) { // under P.lock, so a draw raising this thread (promote) is not undone
A.prio_low[me] = j->low;
plat_thread_priority(j->low ? PLAT_PRIO_BELOW_NORMAL : PLAT_PRIO_NORMAL);
}
// Step 1 for a job a draw waits for: its stage libraries and the fast link. Step 2, the optimised build, goes to
// the head of the low queue, so a burst of new pipelines gets every fast link before any optimised build.
if (P.gpl && (!j->prebuild || j->demanded) && !j->p->info.pipeline && !j->p->fast_tried && j->p->lay &&
!is_mesh(&j->s)) {
int final;
VkPipeline f = gpl_link(&j->s, &j->k, j->p->lay, 1, &final); // releases P.lock while building
j->p->fast_tried = 1;
if (f && !j->p->info.pipeline) {
j->p->fast = j->p->info.pipeline = f, P.g.wlinks++;
ready_note(j->p, 1);
plat_cond_broadcast(&done_cv);
j->low = 1, j->worker = -1, P.g.requeued++;
j->prev = NULL, j->next = A.low;
if (A.low) A.low->prev = j;
else A.low_tail = j;
A.low = j;
A.low_running -= lowrun;
if (lowrun) gate_leave(held);
continue;
} else if (f) {
vkDestroyPipeline(P.device, f, NULL);
}
}
job_build(j, lowrun);
if (lowrun) gate_leave(held);
}
return;
}
static int hint_on(void);
// Starts the pool on first use: a quarter of the logical processors, 2 to 4. Caller holds P.lock.
static int workers_start(void) {
if (A.threads) return 1;
uint32_t cpus = plat_cpu_usable();
int n = cpus / 4 < 2 ? 2 : cpus / 4 > MAX_WORKERS ? MAX_WORKERS : (int)(cpus / 4);
for (int i = 0; i < n; i++) {
PlatThread *t = plat_thread_new(pipe_worker, (void *)(intptr_t)A.threads, "pipe worker");
if (!t) break;
A.thread[A.threads++] = t; // kept: promote() raises a worker running a prebuild job
}
// Workers for stage libraries and fast links alone: two (the vertex and fragment libraries of one waiting draw in
// parallel), and as many as the pool has with the look-ahead, which queues the libraries of a whole submission's
// new pipelines at once (vk_pipeline_hint), or with the fast links of skipped draws (lib_worker). They sleep
// unless a library or a fast link is queued.
int nlib = hint_on() || fast_workers() ? n : 2;
for (int i = 0; A.threads && U.on && P.gpl && i < nlib; i++)
if (plat_thread_new(lib_worker, NULL, "pipe lib worker")) U.lib_workers++;
if (A.threads)
rt_log("vk_pipeline: %d background graphics pipeline workers, %d for waiting draws' libraries", A.threads,
U.lib_workers);
else rt_log("vk_pipeline: cannot start pipeline workers; building synchronously");
return A.threads != 0;
}
static void workers_drain(void) {
plat_mutex_lock(&P.lock);
K.loading = 0;
for (Job *j = A.low, *n; j; j = n) { // queued prebuild jobs are dropped, not built
n = j->next;
j->p->building = 0, j->p->job = NULL;
A.inflight--, K.left--;
free(j);
}
A.low = A.low_tail = NULL;
for (LibJob *j = U.head, *n; j; j = n) { // queued library jobs too: their entries read as failed builds
n = j->next;
j->b->job = NULL, j->b->building = 0;
free(j);
}
U.head = U.tail = NULL;
while (A.inflight) plat_cond_wait(&A.idle, &P.lock);
while (U.running) plat_cond_wait(&done_cv, &P.lock);
plat_mutex_unlock(&P.lock);
}
void vk_pipeline_async_enable(void) {
U.on = strcmp(cfg_value("NX2NAVI_PIPE_WAIT"), "draw") != 0;
const char *e = cfg_get("NX2NAVI_PIPE_SYNC");
if (e && *e && strcmp(e, "0")) return;
A.on = 1;
}
int vk_pipeline_async_on(void) { return A.on; }
// A draw needs a pipeline a prebuild job owns: the job moves to the draw queue, or its worker to normal priority.
// Caller holds P.lock.
static void promote(Pipe *p) {
Job *j = p->job;
if (!j || !j->low) return;
if (j->prebuild && !j->demanded && !p->info.pipeline) p->t_miss = plat_now_ns();
j->low = 0, j->demanded = 1;
if (j->worker == -1) {
if (j->prev) j->prev->next = j->next;
else A.low = j->next;
if (j->next) j->next->prev = j->prev;
else A.low_tail = j->prev;
j->next = j->prev = NULL;
if (A.tail) A.tail->next = j;
else A.head = j;
A.tail = j;
plat_cond_signal(&work_cv);
if (U.lib_workers) plat_cond_signal(&lib_cv); // its fast link (lib_worker)
} else if (j->worker >= 0 && A.prio_low[j->worker]) {
A.prio_low[j->worker] = 0;
plat_thread_set_priority(A.thread[j->worker], PLAT_PRIO_NORMAL);
}
}
// A draw waits up to wait_ms for the pipeline of job j, which has no fast link (a mesh pipeline, a pipeline without
// stage libraries, or one whose link failed) and so needs its whole build. While the job is still queued the wait
// buys nothing: the draw queue is first in, first out, and the jobs before it are the fast links and builds of other
// draws' misses, so on a burst of new pipelines the one a draw stood waiting for started last and the draw was
// skipped at the end of its wait all the same.
// - The job goes to the head of the draw queue and a worker is woken, so the next free worker takes it.
// - A wait longer than TAKE_WAIT_MS is not one of a frame's budgets (vk_exec_draw.c frame_wait is at most 500 ms): the
// caller wants this pipeline whatever it takes (a one-shot draw with NX2NAVI_ONESHOT_BUDGET=0). With no worker idle
// the job is then built here at once (job_build), as a library no worker took is (lib_wait). A wait inside a
// budget is never turned into a build on this thread: a compile takes 0.2 to 1.1 s for a mesh pipeline, and the
// budget exists so that the replay thread is not held that long.
// Caller holds P.lock; it is released while the job is built here.
#define TAKE_WAIT_MS 500u
static void job_wanted(Job *j, uint32_t wait_ms) {
if (!j || j->worker != -1 || j->low) return; // running already, or gone
Job *q = NULL;
if (A.head != j) {
for (q = A.head; q && q->next != j; q = q->next) {}
if (!q) return;
}
if (wait_ms > TAKE_WAIT_MS && !U.idle) {
if (q) {
if (!(q->next = j->next)) A.tail = q;
} else if (!(A.head = j->next)) {
A.tail = NULL;
}
j->next = j->prev = NULL;
j->worker = JOB_ON_DRAW;
A.taken++;
job_build(j, 0);
return;
}
if (q) { // to the head
if (!(q->next = j->next)) A.tail = q;
j->next = A.head, A.head = j;
A.first++;
}
plat_cond_signal(&work_cv);
}
// Enters a missing pipeline: built here (mode 0), or queued for a draw (1) or for the prebuild (2). Failures are
// entered too. *queued: 1 when a job owns it. Caller holds P.lock.
static Pipe *pipe_miss(const VkPipeState *s, const GKey *k, uint64_t h, int mode, int *queued) {
*queued = 0;
const char *why = NULL;
Bindings B = {0};
Layout *l = NULL;
uint32_t span = binding_span(s->vs);
int mesh = is_mesh(s);
if (mesh) {
// Mesh draws (VK_EXT_mesh_shader): one set holding the mesh stage's bindings, the fragment stage's after them and
// the task stage's after those (create_graphics renumbers both modules); no libraries (the GPL fast link is for
// vertex pipelines). They go to the workers like the others: built here, each new one stopped the frame for its
// whole compile (0.26-0.51 s in Kokiri Forest), and they are recorded for the boot prewarm (keys_note).
uint32_t fspan = s->fs ? binding_span(s->fs) : 1;
if (!vk_ctx()->has_mesh_shader) why = "the GPU has no mesh shaders";
else if ((s->fs && gfx_shader_stage(s->fs) != GFX_STAGE_FS) || (s->ts && gfx_shader_stage(s->ts) != GFX_STAGE_TS)) why = "stage mismatch";
else if (!add_stage(&B, s->vs, 0) || (s->fs && !add_stage(&B, s->fs, span - 1)) ||
(s->ts && !add_stage(&B, s->ts, span - 1 + fspan - 1))) why = "too many bindings";
else if (!(l = layout_get(&B))) why = "descriptor layout creation failed";
} else if (gfx_shader_stage(s->vs) != GFX_STAGE_VS || (s->fs && gfx_shader_stage(s->fs) != GFX_STAGE_FS)) why = "stage mismatch";
else if (k->topo_class == 3) why = "patches need tessellation stages, which are not translated";
else if (P.indep) {
Bindings F = {0};
if (!add_stage(&B, s->vs, 0) || (s->fs && !add_stage(&F, s->fs, 0))) why = "too many bindings";
else if (!(l = layout_pair(&B, &F))) why = "descriptor layout creation failed";
B.pool |= F.pool;
span = 1;
} else if (!add_stage(&B, s->vs, 0) || (s->fs && !add_stage(&B, s->fs, span - 1))) why = "too many bindings";
else if (!(l = layout_get(&B))) why = "descriptor layout creation failed";
char lack[128];
if (!why && !layout_covers(s, l, span - 1, lack, sizeof lack)) why = lack, l = NULL;
VkPipeline pipe = VK_NULL_HANDLE;
Job *j = NULL;
if (!why && mode && workers_start() && (j = calloc(1, sizeof *j))) {
*j = (Job){.l = l, .fs_off = span - 1, .low = mode == 2, .prebuild = mode == 2, .worker = -1, .s = *s, .k = *k};
} else if (!why && !(pipe = create_graphics(s, k, l, span - 1))) {
why = "pipeline creation failed";
}
if (why) {
P.stats.failures++;
rt_log("vk_pipeline: graphics pipeline not built: %s", why);
} else if (!j) {
P.stats.graphics++;
if (mode != 2) keys_note(s);
}
Pipe *p = pipe_add(k, sizeof *k, h, pipe, l, l ? &B : NULL, VK_PIPELINE_BIND_POINT_GRAPHICS, gfx_shader_inputs(s->vs, NULL));
if (!j || !p) {
free(j);
return p;
}
j->p = p;
p->building = 1, p->job = j;
if (mode != 2) p->t_miss = plat_now_ns();
if (mode == 2) {
j->prev = A.low_tail;
if (A.low_tail) A.low_tail->next = j;
else A.low = j;
A.low_tail = j;
K.left++;
} else {
if (A.tail) A.tail->next = j;
else A.head = j;
A.tail = j;
if (U.lib_workers && fast_workers()) plat_cond_signal(&lib_cv); // its fast link (lib_worker)
}
A.inflight++;
plat_cond_signal(&work_cv);
*queued = 1;
return p;
}
// A job still in the draw queue goes to the head of the low queue: its pipeline has (or is about to have) a fast-linked
// one, so the optimised build waits behind other draws' misses. Caller holds P.lock.
static void job_lower(Job *j) {
if (!j || j->worker != -1 || j->low) return;
if (A.head == j) {
if (!(A.head = j->next)) A.tail = NULL;
} else {
Job *q = A.head;
while (q && q->next != j) q = q->next;
if (!q) return;
if (!(q->next = j->next)) A.tail = q;
}
j->low = 1, P.g.requeued++;
j->prev = NULL, j->next = A.low;
if (A.low) A.low->prev = j;
else A.low_tail = j;
A.low = j;
}
// A draw waits for pipeline s: its missing pre-rasterization and fragment shader libraries go to the head of the
// worker queues (U), so they build in parallel on idle workers, and this thread sleeps until both are published or
// end (now_ms) passes. A job no worker took within 1 ms (all busy) is built here. Caller holds P.lock.
static void u_push(LibJob *j) {
if (U.tail) U.tail->next = j;
else U.head = j;
U.tail = j;
}
// Queues s's missing pre-rasterization and fragment shader libraries (b: both entries). 0 when one cannot be entered
// (no layout half, out of memory). Caller holds P.lock.
static int lib_queue(const VkPipeState *s, const GKey *k, Layout *l, Lib *b[2]) {
ViKey vk_;
PrKey pk;
FsKey fk;
FoKey ok;
lib_keys(s, k, &vk_, &pk, &fk, &ok);
U.waits++;
const void *key[2] = {&pk, &fk};
uint32_t size[2] = {sizeof pk, sizeof fk};
int queued = 0;
for (int i = 0; i < 2; i++) {
uint64_t h = key_hash(key[i], size[i]);
if ((b[i] = lib_find(h, key[i], size[i]))) continue;
if (!half_layout(l, i)) return 0; // gpl_link reports it
LibJob *j = calloc(1, sizeof *j);
if (!j || !(b[i] = lib_new(h, key[i], size[i]))) {
free(j);
return 0;
}
*j = (LibJob){.b = b[i], .kind = i ? LIB_FS : LIB_PR, .l = l, .s = *s, .k = *k};
b[i]->job = j;
u_push(j);
U.queued++, queued++;
}
if (queued) plat_cond_broadcast(&work_cv), plat_cond_broadcast(&lib_cv);
return 1;
}
// A waiting draw's whole fast link of p on the library workers: its library jobs, then the link job behind them.
// The caller then waits for p->info.pipeline. 0 when nothing was queued (the caller links as before). Under P.lock.
static int link_queue(Pipe *p, const VkPipeState *s, const GKey *k, Layout *l) {
Lib *b[2];
if (!U.lib_workers || !lib_queue(s, k, l, b)) return 0;
LibJob *j = calloc(1, sizeof *j);
if (!j) return 0;
*j = (LibJob){.l = l, .s = *s, .k = *k, .p = p};
p->link_queued = 1;
u_push(j);
plat_cond_broadcast(&work_cv), plat_cond_broadcast(&lib_cv);
return 1;
}
static void lib_wait(const VkPipeState *s, const GKey *k, Layout *l, double end) {
Lib *b[2];
if (!lib_queue(s, k, l, b)) return;
double steal_at = now_ms() + 1.0;
for (;;) {
if (!b[0]->building && !b[1]->building) return;
double now = now_ms();
if (now >= end) {
U.timeouts++;
return;
}
Lib *q = b[0]->job ? b[0] : b[1]->job ? b[1] : NULL;
if (q && !U.lib_workers && (now >= steal_at || !U.idle)) {
U.stolen++;
lib_steal(q); // releases P.lock while building
continue;
}
double until = q && steal_at < end ? steal_at : end;
plat_cond_wait_ms(&done_cv, &P.lock, (uint32_t)(until - now) + 1);
}
}
// ---- stage libraries ahead of their draws (NX2NAVI_PIPE_AHEAD, default 1; 0 off; vk_exec.c "looking ahead")
// The replay names the state of a draw it has not reached yet. When no pipeline is known for it, its pre-rasterization
// and fragment shader libraries are queued to the library workers now, so they build while the draws before it are
// recorded, in parallel with each other and with those of the other draws named: the draw's own lookup then finds both
// and links on its thread in microseconds instead of waiting for them or being skipped while they build. Nothing else
// is entered, no pipeline, no optimised build and no key record: a state the scan guessed wrong costs two libraries
// at most. The feedback loop bits, the topology and the targets do not matter here, as no shader library depends on
// them. Returns the libraries queued.
static int hint_on(void) {
static int on = -1;
if (on < 0) on = cfg_bool("NX2NAVI_PIPE_AHEAD");
return on;
}
int vk_pipeline_hint(const VkPipeState *s) {
if (!A.on || !U.on || !hint_on() || !s->vs || !ensure_init() || !P.gpl || !P.indep || is_mesh(s)) return 0;
if (gfx_shader_stage(s->vs) != GFX_STAGE_VS || (s->fs && gfx_shader_stage(s->fs) != GFX_STAGE_FS)) return 0;
GKey k;
gkey_fill(s, &k);
if (k.topo_class == 3) return 0;
uint64_t h = key_hash(&k, sizeof k);
int queued = 0;
plat_mutex_lock(&P.lock);
if (!pipe_find(&k, sizeof k, h)) {
Bindings B = {0}, F = {0};
Layout *l = add_stage(&B, s->vs, 0) && (!s->fs || add_stage(&F, s->fs, 0)) ? layout_pair(&B, &F) : NULL;
Lib *b[2];
uint64_t q0 = U.queued, w0 = U.waits;
if (l && l->set1 && workers_start() && lib_queue(s, &k, l, b)) queued = (int)(U.queued - q0);
U.waits = w0, U.queued = q0; // those count waiting draws' jobs
P.g.hints++, P.g.hint_libs += (uint64_t)queued;
}
plat_mutex_unlock(&P.lock);
return queued;
}
static VkPipeline graphics(const VkPipeState *s, const VkPipeInfo **info, int async, int *pending, uint32_t wait_ms) {
if (info) *info = NULL;
if (pending) *pending = 0;
if (!s->vs || !ensure_init()) return VK_NULL_HANDLE;
GKey k;
gkey_fill(s, &k);
uint64_t h = key_hash(&k, sizeof k);
plat_mutex_lock(&P.lock);
Pipe *p = pipe_find(&k, sizeof k, h);
int queued;
if (p) {
if (!p->building) P.stats.hits++;
} else {
p = pipe_miss(s, &k, h, async, &queued);
}
// A draw that would wait (wait_ms) gets its missing shader libraries built at once, the vertex and fragment ones in
// parallel on idle workers (lib_wait; about 0.12 ms per library from the driver's disk cache, 5-180 ms cold), and
// links them here: its own job may queue behind optimised builds of earlier pipelines. The job then only does the
// optimised build, from the low queue. Other draws leave the libraries to the job and are skipped meanwhile.
// NX2NAVI_PIPE_WAIT=draw builds them on this thread one after the other instead, as before; NX2NAVI_GPL_DRAW_BUILD=0
// leaves them to the job.
static int draw_build = -1;
if (draw_build < 0) {
const char *e = cfg_get("NX2NAVI_GPL_DRAW_BUILD");
draw_build = !(e && !strcmp(e, "0"));
rt_log("vk_pipeline: a draw that waits for its pipeline %s", !draw_build ? "leaves its libraries to its job"
: U.on ? "has its libraries built in parallel on the workers, ahead of other jobs"
: "builds its libraries itself (NX2NAVI_PIPE_WAIT=draw)");
}
double end = wait_ms ? now_ms() + wait_ms : 0;
if (p && p->building && P.gpl && !p->info.pipeline && !p->fast_tried && !p->link_queued && p->lay && !is_mesh(s) &&
wait_ms && draw_build && U.on && link_queue(p, s, &k, (Layout *)p->lay)) {
// the libraries and the link on the library workers; this thread waits below without building
p->fast_tried = 1;
job_lower(p->job);
} else if (p && p->building && P.gpl && !p->info.pipeline && !p->fast_tried && !p->link_queued && p->lay &&
!is_mesh(s)) {
int final = 0, build = wait_ms != 0 && draw_build;
uint64_t p0 = perf_begin(), t0 = qpc();
if (build && U.on) {
job_lower(p->job); // its worker would only wait for the same libraries; it links them if this draw cannot
lib_wait(s, &k, (Layout *)p->lay, end); // releases P.lock while waiting
build = 0; // links here when both libraries exist (gpl_link build 0)
} else if (build) {
p->fast_tried = 1; // the job, if a worker takes it meanwhile, goes straight to the optimised build
}
VkPipeline f = p->info.pipeline ? VK_NULL_HANDLE : gpl_link(s, &k, p->lay, build, &final); // build 1 releases P.lock
perf_end(PERF_PIPE, p0);
if (wait_ms && draw_build) {
double ms = ms_since(t0);
P.g.dlinks++, P.g.dlink_ms += ms;
if (ms > P.g.dlink_max) P.g.dlink_max = ms;
if (ms > 5.0 && P.g.slow++ < 16)
rt_log("vk_pipeline: gpl: a waiting draw %s its libraries in %.1f ms", U.on ? "got" : "built", ms);
}
if (final) p->fast_tried = 1;
if (f && !p->info.pipeline) {
p->fast = p->info.pipeline = f;
ready_note(p, 1);
job_lower(p->job);
plat_cond_broadcast(&done_cv);
} else if (f) {
vkDestroyPipeline(P.device, f, NULL); // a worker published one while the lock was free
}
}
if (p && p->building && !p->info.pipeline) {
promote(p);
uint64_t p0 = wait_ms ? perf_begin() : 0; // the wait counts as the render thread's pipeline time
double now;
if (wait_ms) job_wanted(p->job, wait_ms); // releases P.lock when it builds the job here
while (p->building && !p->info.pipeline && wait_ms && (now = now_ms()) < end)
plat_cond_wait_ms(&done_cv, &P.lock, (uint32_t)(end - now) + 1);
if (wait_ms) perf_end(PERF_PIPE, p0);
if (wait_ms && !p->building) perf_count(PERF_CNT_PIPE_WAIT);
}
int wait = p && p->building && !p->info.pipeline;
if (wait) P.stats.pending_skips++;
VkPipeline pipe = p ? p->info.pipeline : VK_NULL_HANDLE;
plat_mutex_unlock(&P.lock);
if (wait) {
if (pending) *pending = 1;
return VK_NULL_HANDLE;
}
if (!p) return VK_NULL_HANDLE;
if (info) *info = &p->info;
return pipe;
}
VkPipeline vk_pipeline_graphics(const VkPipeState *s, const VkPipeInfo **info) {
return graphics(s, info, 0, NULL, 0);
}
VkPipeline vk_pipeline_graphics_async(const VkPipeState *s, const VkPipeInfo **info, int *pending, uint32_t wait_ms) {
return graphics(s, info, A.on, pending, wait_ms);
}
double vk_pipeline_building_ms(const VkPipeState *s) {
if (!s->vs || !P.ready) return 0;
GKey k;
gkey_fill(s, &k);
uint64_t h = key_hash(&k, sizeof k);
plat_mutex_lock(&P.lock);
const Pipe *p = pipe_find(&k, sizeof k, h);
double ms = p && p->building && !p->info.pipeline && p->t_miss ? (double)(plat_now_ns() - p->t_miss) / 1e6 : 0;
plat_mutex_unlock(&P.lock);
return ms;
}
// ---- boot prewarm from the key file (comment at "pipeline keys")
static int id_cmp(const void *x, const void *y) { return memcmp(x, y, 32); }
// Builds queued prebuild jobs until the low queue is empty or K.until passes, beside the pipe_worker pool (same
// bookkeeping). Jobs left queued at K.until stay for the pool. So do those the memory gate refuses (gate_enter): a
// builder it refuses ends, which leaves fewer builders compiling side by side while the others' reservations stand,
// and none when the machine has no room; the pool builds what is left during play, when the gate lets it.
// arg non-NULL: one job at most. The builders start with one pipeline built alone on the starting thread:
// a driver's first graphics pipeline sets up its compiler, and NVIDIA's 621.14 was reported to fault in a sizable
// share of starts when every builder began its first pipeline at the same moment.
static void warm_builder(void *arg) {
int one = arg != NULL;
plat_mutex_lock(&P.lock);
while (A.low && !(K.until && now_ms() >= K.until)) {
double held;
if (!gate_enter(0, &held)) {
if (!K.gate_ended++) {
char g[256];
rt_log("vk_pipeline: prewarm: the boot builds fewer recorded pipelines side by side, or none: the commit "
"headroom is short (%s). The rest are built in the background when there is room, or at their "
"first draw", gate_text(g, sizeof g));
}
break;
}
Job *j = A.low;
if ((A.low = j->next)) A.low->prev = NULL;
else A.low_tail = NULL;
j->next = j->prev = NULL;
j->low = 0; // not on a pool thread: promote() has nothing to raise
plat_mutex_unlock(&P.lock);
VkPipeline pipe = create_graphics(&j->s, &j->k, j->l, j->fs_off);
plat_mutex_lock(&P.lock);
if (pipe || !j->p->fast) j->p->info.pipeline = pipe; // a failed optimised build keeps the fast-linked one
ready_note(j->p, 0);
if (pipe && j->p->fast) P.g.swaps++;
j->p->building = 0;
j->p->job = NULL;
if (pipe) {
P.stats.graphics++, P.stats.async_built++, K.built++;
} else {
P.stats.failures++, K.failed++;
rt_log("vk_pipeline: graphics pipeline not built: pipeline creation failed");
}
K.promoted += j->demanded;
K.left--;
if (!--A.inflight) plat_cond_broadcast(&A.idle);
plat_cond_broadcast(&done_cv);
free(j);
gate_leave(held);
if (one) break;
}
plat_mutex_unlock(&P.lock);
return;
}
static void lb_start(const uint32_t pr_hist[16], const uint32_t fs_hist[4]);
static int pm_skipped(void);
static void pm_set(void);
// vk_pipeline_prewarm_progress's numbers: the phase, and the pipelines finished (built or failed) of those queued, copied
// while prewarm_run waits for its builders.
static int32_t pw_phase;
static int64_t pw_pipe_done, pw_pipe_total;
void vk_pipeline_prewarm_progress(VkPrewarmProgress *p) {
p->phase = __atomic_load_n(&pw_phase, __ATOMIC_ACQUIRE);
gfx_shader_prewarm_progress(&p->sh_done, &p->sh_total, &p->sh_translated);
p->pipe_done = __atomic_load_n(&pw_pipe_done, __ATOMIC_RELAXED);
p->pipe_total = __atomic_load_n(&pw_pipe_total, __ATOMIC_RELAXED);
}
static void prewarm_run(void);
// The boot's share of the pipeline prebuild with the graphics pipeline library. The boot builds the recorded
// pipelines in record order, so those met first in play (the title screen, the first places) come first, for at most
// this long after the shaders are ready; the rest stay queued for the pool workers, which build them below normal
// priority while the game runs. A draw that meets one of them before it is built gets a pipeline linked from stage
// libraries at once (gpl_link), as for any new pipeline in play. After a driver update, when every pipeline is built
// from nothing, the boot thus waits this long instead of for all of them (15 minutes and more with a long play
// history). Without the library a draw would have to wait for a whole build, so the boot waits for all; so it does
// with NX2NAVI_PREWARM_FULL=1.
#define PREWARM_PIPE_MS 20000.0
void vk_pipeline_prewarm(void) {
prewarm_run();
__atomic_store_n(&pw_phase, 3, __ATOMIC_RELEASE);
}
static void prewarm_run(void) {
const char *gpu = rt_config()->gpu;
uint32_t pr_hist[16] = {0}, fs_hist[4] = {0}; // library state of the recorded pipelines (lb_start)
if (!A.on || (gpu && !strcmp(gpu, "null"))) return;
if (!cfg_bool("NX2NAVI_PIPE_PREBUILD")) { // prebuild_start logs the off case
if (ensure_init()) lb_start(pr_hist, fs_hist);
return;
}
double t0 = now_ms();
K.warm = 1;
if (!ensure_init() || !K.file_n) {
rt_log("vk_pipeline: prewarm: no pipeline keys (%s)", K.path[0] ? K.path : "<none>");
if (P.ready) lb_start(pr_hist, fs_hist);
return;
}
const KeyRec *r = (const KeyRec *)K.file;
size_t n = K.file_n, m = 0;
uint8_t (*ids)[32] = malloc(3 * n * 32);
const GfxShader **sh = calloc(3 * n, sizeof *sh);
if (!ids || !sh) {
free(ids), free(sh);
return;
}
for (size_t i = 0; i < n; i++) {
memcpy(ids[m++], r[i].vs, 32);
if (r[i].has_fs) memcpy(ids[m++], r[i].fs, 32);
if (r[i].has_ts) memcpy(ids[m++], r[i].ts, 32);
}
qsort(ids, m, 32, id_cmp);
size_t u = 0;
for (size_t i = 0; i < m; i++)
if (!u || memcmp(ids[u - 1], ids[i], 32)) memcpy(ids[u++], ids[i], 32);
int cpus = (int)plat_cpu_usable();
const char *pe = cfg_get("NX2NAVI_PREWARM_XLATE"); // no fixed default: half the CPUs, at most 16, when unset
int procs = pe && *pe ? atoi(pe) : cpus / 2 > 16 ? 16 : cpus / 2;
__atomic_store_n(&pw_phase, 1, __ATOMIC_RELEASE);
gfx_shader_prewarm((const uint8_t(*)[32])ids, u, sh, cpus, procs < 1 ? 1 : procs);
double t1 = now_ms();
size_t carried = 0, fresh = 0; // the recorded pipelines' shaders by where their translation came from
for (size_t i = 0; i < u; i++)
if (sh[i]) carried += gfx_shader_origin(sh[i]) == 1, fresh += gfx_shader_origin(sh[i]) == 2;
vk_mem_stage_log("the shader prewarm");
if (P.indep) { // vertex output unions first: workers may take a job before the queue is complete
uint64_t pair = 0, uni = 0, pairs = 0;
for (size_t i = 0; i < n; i++) {
const uint8_t (*v)[32] = bsearch(r[i].vs, ids, u, 32, id_cmp);
const uint8_t (*f)[32] = r[i].has_fs ? bsearch(r[i].fs, ids, u, 32, id_cmp) : NULL;
if (!v || !f || !sh[v - ids] || !sh[f - ids] || gfx_shader_stage(sh[v - ids]) != GFX_STAGE_VS) continue;
GfxShaderIo io[32];
vs_outs(sh[v - ids], sh[f - ids], io);
pair += (uint64_t)__builtin_popcount(gfx_shader_inputs(sh[f - ids], NULL)), pairs++;
}
for (uint32_t j = 0; j < VS_OUTS_CAP; j++) uni += VO.vs[j] ? (uint64_t)__builtin_popcount(VO.mask[j]) : 0;
rt_log("vk_pipeline: vertex output unions: %u vertex shaders, %.2f locations each; %llu pairs read %.2f each",
VO.n, VO.n ? (double)uni / VO.n : 0.0, (unsigned long long)pairs, pairs ? (double)pair / pairs : 0.0);
}
if (pm_skipped()) { // the boot's pipeline builds killed an earlier start on this GPU and driver
plat_mutex_lock(&P.lock);
free(K.file);
K.file = NULL;
plat_mutex_unlock(&P.lock);
free(ids), free(sh);
lb_start(pr_hist, fs_hist);
return;
}
pm_set();
uint32_t queued = 0, dropped = 0, known = 0;
plat_mutex_lock(&P.lock);
K.t0 = t1;
K.loading = 1;
for (size_t i = 0; i < n; i++) {
const uint8_t (*v)[32] = bsearch(r[i].vs, ids, u, 32, id_cmp);
const uint8_t (*f)[32] = r[i].has_fs ? bsearch(r[i].fs, ids, u, 32, id_cmp) : NULL;
const uint8_t (*t)[32] = r[i].has_ts ? bsearch(r[i].ts, ids, u, 32, id_cmp) : NULL;
const GfxShader *vs = v ? sh[v - ids] : NULL, *fs = f ? sh[f - ids] : NULL, *ts = t ? sh[t - ids] : NULL;
if (!vs || (r[i].has_fs && !fs) || (r[i].has_ts && !ts)) {
dropped++;
continue;
}
VkPipeState s = r[i].s;
s.vs = vs, s.fs = fs, s.ts = ts;
GKey k;
gkey_fill(&s, &k);
if (!is_mesh(&s)) { // the stage libraries lb_start prebuilds are for vertex pipelines
pr_hist[(k.clamp & 1) | (k.poly_mode & 3) << 1 | (k.minus_w & 1) << 3]++;
fs_hist[k.samples >= 8 ? 3 : k.samples >= 4 ? 2 : k.samples >= 2 ? 1 : 0]++;
}
uint64_t h = key_hash(&k, sizeof k);
int q = 0;
if (pipe_find(&k, sizeof k, h)) known++;
else pipe_miss(&s, &k, h, 2, &q), queued += (uint32_t)q;
}
K.loading = 0;
plat_mutex_unlock(&P.lock);
free(ids), free(sh);
__atomic_store_n(&pw_pipe_done, 0, __ATOMIC_RELAXED);
__atomic_store_n(&pw_pipe_total, (int64_t)queued, __ATOMIC_RELAXED);
__atomic_store_n(&pw_phase, 2, __ATOMIC_RELEASE);
const char *full = cfg_get("NX2NAVI_PREWARM_FULL");
K.until = P.gpl && !(full && !strcmp(full, "1")) ? now_ms() + PREWARM_PIPE_MS : 0; // before the builders start
PlatThread *t[64];
int nt = 0;
if (queued) warm_builder((void *)1); // the first alone (warm_builder)
for (int i = 0; i < cpus && i < 64 && queued; i++)
if ((t[nt] = plat_thread_new(warm_builder, NULL, "pipe prewarm"))) nt++;
if (!nt) warm_builder(NULL);
double logged = now_ms();
for (int i = 0; i < nt; i++)
while (!plat_thread_join_ns(t[i], 100000000u)) { // every 100 ms the numbers, every 2 s a log line
uint32_t fin = __atomic_load_n(&K.built, __ATOMIC_RELAXED) + __atomic_load_n(&K.failed, __ATOMIC_RELAXED);
__atomic_store_n(&pw_pipe_done, (int64_t)fin, __ATOMIC_RELAXED);
if (now_ms() - logged < 2000) continue;
logged = now_ms();
rt_log("vk_pipeline: prewarm: %u of %u pipelines, %.0f ms", fin, queued, now_ms() - t1);
}
__atomic_store_n(&pw_pipe_done, (int64_t)queued, __ATOMIC_RELAXED);
plat_mutex_lock(&P.lock);
uint32_t later = 0;
for (Job *j = A.low; j; j = j->next) later += j->prebuild;
if (!later) // all built: wait for those a pool worker took
while (K.left) plat_cond_wait(&done_cv, &P.lock);
else // the boot's time is up: the pool builds the rest while the game runs
plat_cond_broadcast(&work_cv);
rt_log("vk_pipeline: prewarm: %zu records in %s: %u pipelines built, %u left to build in the background, %u "
"failed, %u already built, %u dropped (shader neither cached nor stored); shaders %.0f ms, pipelines %.0f "
"ms on %d threads, total %.0f ms", n, K.path, K.built, later, K.failed, known, dropped, t1 - t0,
now_ms() - t1, nt, now_ms() - t0);
free(K.file);
K.file = NULL;
plat_mutex_unlock(&P.lock);
// After an update with another translator: a record names its shaders by input id, which no translator changes,
// so every record still finds its shaders, and the pipeline cache gives the pipeline back wherever the translation
// in use is the one it was built from. The line below this one says how many it gave back.
if (carried || fresh)
rt_log("vk_pipeline: prewarm: of the %zu shaders the recorded pipelines name, %zu are a previous translator's "
"translations and %zu were translated now; a recorded pipeline is compiled again only where its shaders' "
"translation changed, which the pipeline-cache.bin hits below show", u, carried, fresh);
fb_log("after the boot prewarm");
vk_mem_stage_log("the recorded pipelines' prebuild");
vk_pipeline_prewarm_marker_clear(); // the boot's share is done; what the pool builds later runs during play
lb_start(pr_hist, fs_hist);
}
// ---- boot prebuild crash marker
// The boot builds the recorded pipelines (warm_builder) before the game starts. A driver whose compiler crashes on one
// of them would kill every start at the same point, and the preparation journal above does not cover these builds.
// So <dir>/pipeline-prewarm-running.txt exists while they run: written before the first one, deleted when the boot's
// share ends (or when the boot stops waiting and lets the game start). A start that finds it left over records this
// GPU and driver in <dir>/pipeline-prewarm-skip.txt, and starts on them build no recorded pipelines ahead of play:
// each one is built at its first draw instead, as with NX2NAVI_PIPE_PREBUILD=0. Another driver version, or deleting
// the file, brings the prebuild back. Only the boot's own share is covered: a crash during play, where the pool also
// builds what the boot left, says nothing about these builds. An orderly end of the process during the boot (the
// window closed, the guest's ExitProcess, a --seconds run) deletes the marker as well: only a process that died leaves it.
static int cache_dir(char *dir, size_t n);
static void gpu_key(char *out, size_t n);
static char PM_run[PLAT_PATH_MAX * 2 + 48];
static volatile int32_t pm_on;
static int pm_skipped(void) {
char dir[PLAT_PATH_MAX * 2], sp[sizeof PM_run], key[32];
if (!cache_dir(dir, sizeof dir)) return 0;
snprintf(PM_run, sizeof PM_run, "%s/pipeline-prewarm-running.txt", dir);
snprintf(sp, sizeof sp, "%s/pipeline-prewarm-skip.txt", dir);
gpu_key(key, sizeof key);
uint64_t n = 0;
uint8_t *left = plat_file_read(PM_run, &n);
if (left) {
char text[256];
int len = snprintf(text, sizeof text,
"# The boot's pipeline prebuild did not finish on this GPU and driver (runtime/vk_pipeline.c "
"pm_skipped): starts skip it\n# while this line matches. Delete this file to build ahead of play "
"again.\n%s\n", key);
if (plat_file_write_atomic(sp, text, (size_t)len)) plat_file_delete(PM_run);
rt_log("vk_pipeline: the last start died while the boot built pipelines (%s was left); %s now skips that "
"prebuild on this GPU and driver (%s)", PM_run, sp, key);
free(left);
}
uint8_t *skip = plat_file_read(sp, &n);
int on = 0;
for (char *line = (char *)skip, *next; line && *line && !on; line = next) {
next = strchr(line, '\n');
if (next) *next++ = 0;
size_t l = strlen(line);
while (l && (line[l - 1] == '\r' || line[l - 1] == ' ')) line[--l] = 0;
on = line[0] != '#' && !strcmp(line, key);
}
free(skip);
if (on)
rt_log("vk_pipeline: prewarm: %zu recorded pipelines not built ahead of play: an earlier start died while building "
"them on this GPU and driver (%s lists %s); each builds at its first draw", K.file_n, sp, key);
return on;
}
static void pm_set(void) {
if (!PM_run[0]) return;
char key[32];
gpu_key(key, sizeof key);
// straight to the disk: the file has to be there if the driver takes the process down a moment later
if (plat_file_write_atomic(PM_run, key, strlen(key))) plat_xchg32(&pm_on, 1);
}
void vk_pipeline_prewarm_marker_clear(void) {
if (plat_xchg32(&pm_on, 0)) plat_file_delete(PM_run);
}
// ensure_init's hook: without a prewarm (NX2NAVI_PIPE_PREBUILD=0, or the device came up first elsewhere) the records
// are not built. Caller holds no lock.
static void prebuild_start(void) {
plat_mutex_lock(&P.lock);
if (K.file && !K.warm) {
rt_log("vk_pipeline: prebuild off%s; %zu records not built",
!cfg_bool("NX2NAVI_PIPE_PREBUILD") ? " (NX2NAVI_PIPE_PREBUILD=0)" : " (no boot prewarm)", K.file_n);
free(K.file);
K.file = NULL;
}
plat_mutex_unlock(&P.lock);
}
// NX2NAVI_SPV_OVERRIDE=<dir> (test): a compute shader whose SPIR-V hashes (FNV-1a over its bytes, the name vk_dump.c
// gives it) to a file <dir>/<hash>.spv runs that SPIR-V instead, an edited copy with the same interface, to see a
// shader's intermediate values in a frame dump. NULL: no override (malloc'd words otherwise).
static uint32_t *spv_override(const uint32_t *code, uint64_t words, uint64_t *out) {
const char *dir = cfg_get("NX2NAVI_SPV_OVERRIDE");
if (!dir || !*dir) return NULL;
uint64_t h = 0xcbf29ce484222325ull;
for (uint64_t i = 0; i < words * 4; i++) h = (h ^ ((const uint8_t *)code)[i]) * 0x100000001b3ull;
char path[600];
snprintf(path, sizeof path, "%s/%016llx.spv", dir, (unsigned long long)h);
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
uint32_t *w = n > 20 && n % 4 == 0 ? malloc((size_t)n) : NULL;
if (w && fread(w, 1, (size_t)n, f) != (size_t)n) free(w), w = NULL;
fclose(f);
if (w) *out = (uint64_t)n / 4, rt_log("vk_pipeline: compute shader %016llx replaced by %s", (unsigned long long)h, path);
return w;
}
// A skipped froxel's exit in the fog's first compute pass: the block at word at (an OpLabel) makes five image writes, four
// of (0,0,0,1) and last the mask value (1/255,0,0,0), and returns.
static int fog_skip_block(const uint32_t *w, uint64_t n, uint64_t at, const uint64_t *def, uint32_t bound) {
int writes = 0;
for (uint64_t i = at + (w[at] >> 16); i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len || i + len > n) return 0;
if (op == 253) return writes == 5; // OpReturn
if (op == 248 || op == 249 || op == 250) return 0; // OpLabel, OpBranch, OpBranchConditional
if (op == 99) { // OpImageWrite
if (len < 4 || w[i + 3] >= bound || ++writes > 5) return 0;
uint64_t c = def[w[i + 3]];
if (!c || (w[c] & 0xffff) != 80 || w[c] >> 16 != 7) return 0; // OpCompositeConstruct of four
float v[4];
for (int k = 0; k < 4; k++) {
uint32_t id = w[c + 3 + k];
uint64_t f = id < bound ? def[id] : 0;
if (!f || (w[f] & 0xffff) != 43 || w[f] >> 16 != 4) return 0; // OpConstant of one word
memcpy(&v[k], &w[f + 3], 4);
}
if (writes == 5 ? !(v[0] > 0.0039f && v[0] < 0.0040f && v[1] == 0 && v[2] == 0 && v[3] == 0)
: !(v[0] == 0 && v[1] == 0 && v[2] == 0 && v[3] == 1)) return 0;
}
i += len;
}
return 0;
}
// The volumetric fog's first compute pass skips a froxel behind a coarse depth table and writes it as empty, and the next
// pass's mask keeps it empty. That table comes from the frame before, so in the columns a near object has just left, every
// froxel past the object's old depth came out with no fog: a dark block of the fog grid beside the object, which the fog's
// history then smeared into a trail, and worst at the screen edges, where near objects move fastest. A froxel behind
// today's depth is never seen, so computing it only costs a little time: the comparison is made never true (x > x).
// Returns the patched copy of that shader, NULL for any other. Found by its shape rather than a hash, so it holds across
// translator updates: a branch whose other side is that exit, taken on !(distance > table) || flag.
// NX2NAVI_FOG_CULL=1 keeps the game's skip.
static uint32_t *spirv_fog_keep_froxels(const uint32_t *w, uint64_t n) {
const char *e = cfg_get("NX2NAVI_FOG_CULL");
if (n < 6 || (e && !strcmp(e, "1"))) return NULL;
uint32_t bound = w[3];
int writes = 0;
for (uint64_t i = 5; i < n;) {
uint32_t len = w[i] >> 16;
if (!len || i + len > n) return NULL;
writes += (w[i] & 0xffff) == 99;
i += len;
}
if (writes < 5) return NULL;
uint64_t *def = calloc(bound, sizeof *def); // word of each id's definition, for the instructions looked at below
if (!def) return NULL;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (op == 248 && len >= 2 && w[i + 1] < bound) def[w[i + 1]] = i; // OpLabel
else if ((op == 43 || op == 80 || op == 166 || op == 168 || op == 186) && len >= 3 && w[i + 2] < bound) def[w[i + 2]] = i;
i += len;
}
uint64_t cmp = 0, prev = 0;
int found = 0;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (op == 250 && len >= 4 && prev && (w[prev] & 0xffff) == 247 && w[prev + 1] == w[i + 3] && w[i + 1] < bound &&
w[i + 3] < bound && def[w[i + 3]] && fog_skip_block(w, n, def[w[i + 3]], def, bound)) { // OpSelectionMerge just before
uint64_t c = def[w[i + 1]];
for (int k = 0; c && (w[c] & 0xffff) == 166 && k < 2; k++) { // OpLogicalOr
uint32_t a = w[c + 3 + k];
uint64_t no = a < bound ? def[a] : 0;
if (!no || (w[no] & 0xffff) != 168 || w[no + 3] >= bound) continue; // OpLogicalNot
uint64_t gt = def[w[no + 3]];
if (gt && (w[gt] & 0xffff) == 186) cmp = gt, found++; // OpFOrdGreaterThan
}
}
prev = i;
i += len;
}
free(def);
uint32_t *o = found == 1 ? malloc(n * 4) : NULL;
if (!o) return NULL;
memcpy(o, w, n * 4);
o[cmp + 4] = o[cmp + 3];
LOG_ONCE("vk_pipeline: fog: froxels behind last frame's depth are computed too");
return o;
}
// A compute pipeline for cs in layout l; VK_NULL_HANDLE on failure. No lock needed (P.cache is driver-synchronised).
// cached_only (needs P.cache_control): from P.cache or the driver's own disk cache, never compiled; VK_NULL_HANDLE when
// neither holds it (the driver returns VK_PIPELINE_COMPILE_REQUIRED, a success code, with no pipeline).
static VkPipeline compute_create(const GfxShader *cs, const Layout *l, int cached_only) {
uint64_t words;
const uint32_t *code = gfx_shader_spirv(cs, &words);
uint64_t ow = 0;
uint32_t *over = spv_override(code, words, &ow);
if (over) code = over, words = ow;
uint32_t *fog = spirv_fog_keep_froxels(code, words);
if (fog) free(over), over = fog, code = fog;
VkShaderModule m = make_module(code, words, 0);
if (!m) return free(over), VK_NULL_HANDLE;
VkPipelineShaderStageRequiredSubgroupSizeCreateInfo rqc;
VkComputePipelineCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
.stage = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m, .pName = "main"},
.layout = l->layout, .flags = noopt_flag()};
if (cached_only) ci.flags |= VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT;
pin_subgroup32(&ci.stage, &rqc, code, words);
// Subgroup operations in a workgroup whose width is a whole number of subgroups need every subgroup full: the
// driver may otherwise run partial ones, and a ballot over the culling's candidates then counted differently from
// one frame to the next
uint32_t lx = local_size_x(code, words);
if (P.f_full_subgroups && vk_caps.sg_size && lx && lx % vk_caps.sg_size == 0 && uses_subgroup_ops(code, words))
ci.stage.flags |= VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT;
VkPipeline pipe = VK_NULL_HANDLE;
VkPipelineCreationFeedbackCreateInfo fci;
VkPipelineCreationFeedback fb;
ci.pNext = fb_chain(&fci, &fb, ci.pNext);
crash_note("a compute pipeline", cs, NULL, NULL);
if (pipe_create(2, &ci, &pipe) != VK_SUCCESS) pipe = VK_NULL_HANDLE;
plat_crash_context(NULL);
if (pipe) fb_note(FB_COMPUTE, &fb);
cc_hit = !pipe ? -1 : pb_last ? 1
: !P.feedback || !P.cache || !(fb.flags & VK_PIPELINE_CREATION_FEEDBACK_VALID_BIT) ? -1
: (fb.flags & VK_PIPELINE_CREATION_FEEDBACK_APPLICATION_PIPELINE_CACHE_HIT_BIT) != 0;
if (P.feedback >= 2 && (pipe || !cached_only))
rt_log("vk_pipeline: compute %016llx: %llu words, module %016llx, %s, %.1f ms", (unsigned long long)fnv(FNV0, code, words * 4),
(unsigned long long)words, (unsigned long long)mm_hash,
!pipe ? "failed" : pb_last ? "from pipeline-binaries.bin" : fb.flags & VK_PIPELINE_CREATION_FEEDBACK_APPLICATION_PIPELINE_CACHE_HIT_BIT ? "from pipeline-cache.bin" : "compiled",
(double)fb.duration / 1e6);
vkDestroyShaderModule(P.device, m, NULL);
free(over);
return pipe;
}
// The layout of a compute shader in *B; NULL with *why on failure. Caller holds P.lock.
static Layout *compute_layout(const GfxShader *cs, Bindings *B, const char **why) {
Layout *l = NULL;
if (gfx_shader_stage(cs) != GFX_STAGE_CS) *why = "not a compute shader";
else if (!add_stage(B, cs, 0)) *why = "too many bindings";
else if (!(l = layout_get(B))) *why = "descriptor layout creation failed";
return l;
}
// ---- stage library prebuild
// Two places build what a draw would otherwise build while the game waits:
// - nx2navi.exe --prepare-shaders (lb_warm_prepared), after the translation, on every core: for each prepared variant
// (shader-prepared.bin) the pre-rasterization library of a vertex shader, the fragment-shader library of a fragment
// shader and the pipeline of a compute shader, each built from a private shader copy and destroyed at once. What
// stays is the driver's own disk cache: on AMD a library built again in a later process takes 0.1 ms instead of
// 55 ms (vertex) / 28 ms (fragment). Our pipeline-cache.bin is not written there:
// it would grow by about 10 KB per library (~240 MB) and be read into memory at every boot.
// - boot (lb_worker), after the pipeline-key prewarm, on two threads (lb_start) at PLAT_PRIO_IDLE (the short P.lock
// sections and shader loads at normal priority, so a preempted thread never holds the lock the replay needs; a
// compute pipeline's compile below normal priority, lb_compute):
// compute pipelines of every recorded and prepared compute shader (vk_pipeline_compute builds compute
// synchronously on the replay thread; 163 prepared): those of the shaders dispatched most recently stay, the others
// are built into the pipeline cache and destroyed (comment at compute-used.bin). Then the libraries of the first
// NX2NAVI_LIB_PREBUILD_KEEP variants play recorded (priority 0), kept in libs[] with their shaders. KEEP defaults to
// 0: a kept library costs 140-460 KB of process memory (10,000 kept took 9 GB in run 1) to save a rebuild the driver
// cache makes 0.1 ms.
// Library state: the vertex output union known now (vs_union_peek; the prewarm fills it from pipeline-keys.bin, the
// preparation has none), the clamp, polygon mode and depth mode most recorded pipelines use (defaults without a key
// file: clamp 0, fill, -w..w), single-sampled, no dual source; the state dynamic on this GPU is 0 in every key.
// A draw that meets such a variant in a new pipeline then gets its libraries from libs[] or the driver cache and
// fast-links (gpl_link). NX2NAVI_LIB_PREBUILD=0 turns the boot part off, =N stops it after N variants;
// NX2NAVI_LIB_PREBUILD_THREADS sets its thread count, NX2NAVI_LIB_PREBUILD_BIG its compute size limit (LB_BIG_WORDS).
#define LB_MAX_THREADS 16
// The boot part builds a compute shader over LB_BIG_WORDS SPIR-V words only from the pipeline caches (compute_create
// cached_only); one that no cache holds is left to its first dispatch, like any shader outside the warm list. Play
// recorded four straight-line compute shaders of 0.87-4.4 million words (one function of up to 888K instructions; the
// median of 3,648 compute shaders is 7,351 words, p99 64,844). NVIDIA 621.01 on an RTX 4090 compiled them on four
// prebuild threads for minutes at 100% CPU each while the process grew from 12 GB to 28-31 GB, until RAM ran out and the
// game stuttered and went black a few minutes after boot. None finished, so no cache held one and every boot did it
// again. NX2NAVI_LIB_PREBUILD_BIG sets the limit in words; 0 builds every size.
#define LB_BIG_WORDS (512u * 1024)
static struct {
GfxWarmEnt *e;
size_t n;
size_t n_keep; // e[0..n_keep): compute shaders whose pipeline stays (cu_keep)
volatile int64_t next;
volatile int32_t stop, running;
volatile int32_t short_mem; // the memory gate refused a build for GATE_WAIT_MS: the workers ended
int threads;
PlatThread *th[LB_MAX_THREADS];
uint8_t clamp, poly, minus_w, samples; // library state (GKey fields)
double t0, ms[3]; // ms: build time per kind (vertex, fragment, compute)
// warmed: built for the caches only and destroyed; nolib: vertex and fragment variants skipped with the graphics
// pipeline library off
// skipped: variants of the skip list (SK below)
uint64_t built[3], have[3], fail[3], warmed[3], noshader, nolib, skipped, done, cs_draw;
uint64_t big_words, deferred; // LB_BIG_WORDS; compute shaders over it left to their dispatch
uint64_t big_unused; // compute shaders over it that no run dispatched: not built at all
double warm_ms[3];
volatile int32_t kept; // libraries entered in libs[] so far
int32_t keep_n;
int idle; // build at PLAT_PRIO_IDLE (boot), normal (preparation)
int tail_one; // automatic boot policy: only worker 0 takes the warming tail
PlatMutex stat;
} LB = {.stat = PLAT_MUTEX_INIT};
static __thread int lb_me = -1; // on a boot prebuild thread (lb_worker): its index in LB.th
// Claim a boot job without dropping one when the extra workers finish the recent-pipeline prefix. Recent pipelines
// retain parallel creation; a single worker continues the complete tail. Explicit thread counts override this
// policy, and installer preparation uses its own queue at full concurrency. The CAS makes the prefix boundary
// exact even when two workers reach it together. No pipeline creation or dispatch waits on this queue lock.
static size_t lb_next(int worker) {
int64_t next = __atomic_load_n(&LB.next, __ATOMIC_RELAXED);
for (;;) {
if (LB.stop || (size_t)next >= LB.n ||
(LB.idle && LB.tail_one && worker > 0 && (size_t)next >= LB.n_keep)) return LB.n;
if (__atomic_compare_exchange_n(&LB.next, &next, next + 1, 0, __ATOMIC_RELAXED, __ATOMIC_RELAXED))
return (size_t)next;
}
}
// ---- preparation that outlives a driver crash. A bug in the driver's shader compiler can kill the process inside
// vkCreate*Pipelines: NVIDIA 615.78.08 on an RTX 5080 crashed on the same vertex library at the same point of every
// preparation, natively and under Wine, so preparation never finished (issue #30 and a report on PR #60).
// While lb_warm_prepared builds, slot k of <dir>/shader-prepare-journal.bin holds the variant its worker k has handed to
// the driver (input id and stage; zeros between variants). The writes go straight to the OS, so they outlast the
// process, and a run that finishes deletes the file. The next preparation or boot (skip_init) moves the variants a
// leftover journal names into <dir>/shader-prepare-skip.txt with this GPU and driver version, and preparation and the
// boot prebuild (lb_worker) skip what that file lists for them. The variants the other workers were building at the
// crash are skipped along with the culprit; like it, they build at their first draw, where the driver can still crash.
// Another driver version builds them again, and so does deleting the file. tools/prepare_shaders.py runs the build
// again (--warm-shaders) after a run that left a journal.
#define JR_SLOT 64 // bytes per journal slot: input id, stage, zeros
#define JR_SLOTS 64 // lb_warm_prepared's thread cap
static struct {
PlatFile *journal; // while lb_warm_prepared builds
uint8_t (*iid)[32]; // the skip list's variants for this GPU and driver
size_t n;
int loaded;
} SK;
static int cache_dir(char *dir, size_t n) { // the folder of pipeline-cache.bin and shader-prepared.bin
snprintf(dir, n, "%s", P.cache_path);
char *cut = plat_last_sep(dir);
if (!cut) return 0;
*cut = 0;
return 1;
}
static void gpu_key(char *out, size_t n) { // vendor:device:driver version, as the skip list names a GPU and driver
const VkPhysicalDeviceProperties *p = &vk_ctx()->props;
snprintf(out, n, "%04x:%04x:%08x", p->vendorID, p->deviceID, p->driverVersion);
}
static const char *stage_name(int st) {
return st == GFX_STAGE_VS ? "vertex" : st == GFX_STAGE_FS ? "fragment" : st == GFX_STAGE_CS ? "compute" : "other";
}
static int hex_digit(int c) {
return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c >= 'A' && c <= 'F' ? c - 'A' + 10 : -1;
}
// A journal a crashed run left into the skip list, then the skip list's variants for this GPU and driver into SK.
static void skip_init(const char *dir) {
if (SK.loaded) return;
SK.loaded = 1;
char jp[PLAT_PATH_MAX * 2 + 48], sp[PLAT_PATH_MAX * 2 + 48], key[32];
snprintf(jp, sizeof jp, "%s/shader-prepare-journal.bin", dir);
snprintf(sp, sizeof sp, "%s/shader-prepare-skip.txt", dir);
gpu_key(key, sizeof key);
uint64_t jn = 0, sn = 0;
uint8_t *j = plat_file_read(jp, &jn);
char *s = (char *)plat_file_read(sp, &sn);
int keep_journal = 0;
if (j) {
static const char head[] =
"# Shader variants the graphics driver was building when the process died during preparation\n"
"# (runtime/vk_pipeline.c skip_init): input id, stage, GPU vendor:device:driver version. Preparation skips\n"
"# them on that GPU and driver. Delete this file to build them again.\n";
size_t cap = (s ? sn : sizeof head) + (jn / JR_SLOT + 1) * 128, len = 0;
char *out = malloc(cap);
int added = 0;
if (out) {
len = s ? sn : sizeof head - 1;
memcpy(out, s ? s : head, len);
if (len && out[len - 1] != '\n') out[len++] = '\n';
for (uint64_t o = 0; o + 33 <= jn; o += JR_SLOT) {
int set = 0;
for (int b = 0; b < 32; b++) set |= j[o + b];
if (!set) continue;
char hex[65];
for (int b = 0; b < 32; b++) snprintf(hex + 2 * b, 3, "%02x", j[o + b]);
len += (size_t)snprintf(out + len, cap - len, "%s %s %s\n", hex, stage_name(j[o + 32]), key);
added++;
}
out[len] = 0; // parsed below as the list's text
}
if (added && plat_file_write_atomic(sp, out, len)) {
rt_log("vk_pipeline: the last shader preparation died while the driver built %d variants; %s now lists them "
"and preparation skips them on this GPU and driver (%s)", added, sp, key);
free(s), s = out, sn = len, out = NULL;
} else if (added) {
rt_log("vk_pipeline: cannot write %s; %s stays for the next run", sp, jp);
keep_journal = 1;
}
free(out);
free(j);
}
if (!keep_journal) plat_file_delete(jp);
for (char *line = s, *next; line && *line; line = next) {
next = strchr(line, '\n');
if (next) *next++ = 0;
char hex[65], st[16], k[32];
uint8_t iid[32];
int ok = line[0] != '#' && sscanf(line, "%64s %15s %31s", hex, st, k) == 3 && strlen(hex) == 64 &&
!strcmp(k, key);
for (int b = 0; ok && b < 32; b++) {
int hi = hex_digit(hex[2 * b]), lo = hex_digit(hex[2 * b + 1]);
if (hi < 0 || lo < 0) ok = 0;
else iid[b] = (uint8_t)(hi << 4 | lo);
}
uint8_t (*grown)[32] = ok ? realloc(SK.iid, (SK.n + 1) * sizeof *SK.iid) : NULL;
if (grown) memcpy(grown[SK.n++], iid, 32), SK.iid = grown;
}
free(s);
if (SK.n) rt_log("vk_pipeline: %zu shader variants skipped ahead of play on this GPU and driver (%s)", SK.n, sp);
}
static int skip_listed(const uint8_t iid[32]) {
for (size_t i = 0; i < SK.n; i++)
if (!memcmp(SK.iid[i], iid, 32)) return 1;
return 0;
}
static void skip_count(void) {
plat_mutex_lock(&LB.stat);
LB.skipped++;
plat_mutex_unlock(&LB.stat);
}
// Journal slot `slot`: the variant e its worker hands to the driver next, or zeros (e NULL) when the build returned.
static void journal_mark(int slot, const GfxWarmEnt *e) {
if (!SK.journal || slot < 0 || slot >= JR_SLOTS) return;
uint8_t rec[JR_SLOT] = {0};
if (e) memcpy(rec, e->iid, 32), rec[32] = e->stage;
plat_file_pwrite(SK.journal, rec, sizeof rec, (uint64_t)slot * JR_SLOT);
}
// Compute shaders whose pipeline a thread is compiling now (P.lock): a cold driver takes about 2 s for one of Ocarina's
// large compute shaders. The replay used to compile under P.lock, which held every other pipeline lookup that long, and
// compiled a second copy of a pipeline the boot prebuild (lb_compute) was already compiling.
#define CS_FLIGHT 32
static const GfxShader *cs_flight[CS_FLIGHT];
static int cs_flight_has(const GfxShader *cs) {
for (int i = 0; i < CS_FLIGHT; i++)
if (cs_flight[i] == cs) return 1;
return 0;
}
static volatile uint8_t cs_flight_want[CS_FLIGHT]; // a dispatch waits for the slot's pipeline (vk_pipeline_compute)
static PlatThread *cs_flight_th[CS_FLIGHT]; // the boot prebuild thread compiling it below normal priority
// th: the thread a waiting dispatch raises to normal priority, NULL when it compiles at normal priority already.
static int cs_flight_add(const GfxShader *cs, PlatThread *th) { // 0 when the table is full (the caller compiles anyway)
for (int i = 0; i < CS_FLIGHT; i++)
if (!cs_flight[i]) return cs_flight[i] = cs, cs_flight_want[i] = 0, cs_flight_th[i] = th, 1;
return 0;
}
// A dispatch waits for cs's build: a builder that would only have filled the pipeline cache keeps the pipeline. Returns
// the thread to raise, the first time.
static PlatThread *cs_flight_wanted(const GfxShader *cs) {
PlatThread *th = NULL;
for (int i = 0; i < CS_FLIGHT; i++)
if (cs_flight[i] == cs && !cs_flight_want[i]) cs_flight_want[i] = 1, th = cs_flight_th[i];
return th;
}
// Without P.lock, for the thread that holds cs's slot: whether a dispatch came to wait.
static int cs_flight_is_wanted(const GfxShader *cs) {
for (int i = 0; i < CS_FLIGHT; i++)
if (__atomic_load_n(&cs_flight[i], __ATOMIC_RELAXED) == cs) return cs_flight_want[i];
return 0;
}
static int cs_flight_remove(const GfxShader *cs) { // 1 when a dispatch waited for it
int want = 0;
for (int i = 0; i < CS_FLIGHT; i++)
if (cs_flight[i] == cs) cs_flight[i] = NULL, want |= cs_flight_want[i];
return want;
}
// ---- compute pipelines the boot keeps (compute-used.bin next to pipeline-keys.bin; NX2NAVI_COMPUTE_KEEP)
// The boot prebuild (lb_compute) used to build the pipeline of every compute shader of the warm list (the ones play
// recorded and the prepared ones, shader-prepared.bin) and keep all of them for the process: 4,066 on an installed
// copy (42,344 variants in its list, 42,142 of them prepared), of which a boot into the first village dispatched 95.
// What that cost was twenty of them. A copy's list has about twenty compute shaders of 0.9 to 2.0 million SPIR-V
// words: the game's tensor-core upscaler programs, which the game never dispatches natively (vk_exec_draw.c
// native_tensor) and which no run of the usage record below ever dispatched. A pipeline of one holds about 100 MiB
// of device memory from the moment it exists, used or not, and the driver keeps that memory when the pipeline is
// destroyed. On an RTX 5090 (NVIDIA 621.01) a boot pass that read all 4,066 pipelines from pipeline-cache.bin took the
// device memory from 386 to 2,398 MiB although it destroyed 3,949 of them at once; the same pass before the twenty
// were in the cache left it at 386, and building only the twenty took it to 1,370 (--probe-prewarm, 2026-11-10; the
// game showed 608 to 2,743 MiB over the same stage). The 4,046 others are cheap: all of them kept cost 2 MiB of device
// memory and 0.7 GB of the process's commit. So a copy with prepared shaders and a filled pipeline cache held 2 GiB
// for nothing from the title screen on, on cards that need that memory for textures.
// What the boot does now:
// - A compute shader over LB.big_words is built ahead only when a run has dispatched it (lb_compute, its keep
// argument). One that no run dispatched is not created at all: not from the pipeline caches and not by the
// background builder (bg_main). If the game does dispatch one for the first time, that dispatch builds it, which
// holds the frame for the compile once (0.65 to 1.9 s measured for such a shader), and the record has it built ahead
// from the next boot on.
// - Of the others the boot keeps the pipelines of the NX2NAVI_COMPUTE_KEEP (default 512) dispatched most recently, and
// builds every other one into the pipeline cache and destroys it at once (lb_compute keep 0), from a copy of its
// shader that is released again (lb_worker): the cache then holds it, so its first dispatch creates it without
// compiling. On that machine 94 first dispatches in play took 0.24 ms on average and 1.6 ms at most, 14 of them in
// the busiest 33 ms; reading the shader from the disk at that moment adds 0.05 to 0.15 ms (--probe-prewarm).
// The record: one entry per compute shader a run dispatched (its input id) with the number of the last run that did;
// each run that dispatches anything takes the next number. Kept at boot are the keep_n entries with the highest
// numbers, so a short run does not push out what a long one used, and nothing is forgotten until newer shaders take
// its place. Without a record (the first run that has one) nothing is kept and the run pays the first dispatches once.
// NX2NAVI_COMPUTE_KEEP=all builds and keeps every compute shader of the warm list, the large ones too, as before; 0
// keeps none.
#define CU_VERSION 1
typedef struct { char magic[8]; uint32_t version, rec_size, run, pad; } CuHeader;
typedef struct { uint8_t iid[32]; uint32_t run, pad; } CuRec;
static struct {
PlatMutex lock;
int opened, dirty;
char path[PLAT_PATH_MAX * 2 + 32];
CuRec *rec;
uint8_t *keep; // per record read at boot: among the keep_n dispatched most recently
uint32_t n, cap, n_file; // n_file: the records read at boot (keep has one byte for each)
uint32_t *slot; // open addressing by the id's first bytes: record index + 1, 0 empty
uint32_t slot_cap;
uint32_t run; // this run's number
int quiet; // --probe-prewarm: its dispatches are not the game's, the file stays as it is
int32_t keep_n; // NX2NAVI_COMPUTE_KEEP; -1: all
uint32_t kept; // records flagged in keep
} CU = {.lock = PLAT_MUTEX_INIT};
// What the first dispatches of compute shaders cost this run (under P.lock): each creates its pipeline on the replay
// thread. burst: the creations that fell into one window of CF_WINDOW_MS, the worst such window kept, which is what a
// player would feel as one long frame when a new area starts.
#define CF_WINDOW_MS 33.0
static struct {
uint64_t n, hit, pre_used; // hit: the pipeline cache held it (cc_hit 1); pre_used: kept from the boot and dispatched
double ms, max_ms, hit_ms;
double win_t0, win_ms, worst_ms;
uint32_t win_n, worst_n;
} CF;
static uint32_t *cu_slot(const uint8_t iid[32]) { // the id's slot, or the empty one it would take. Caller holds CU.lock.
uint64_t h;
memcpy(&h, iid, 8);
for (uint32_t j = (uint32_t)(h * 0x9e3779b97f4a7c15ull >> 32) & (CU.slot_cap - 1);; j = (j + 1) & (CU.slot_cap - 1))
if (!CU.slot[j] || !memcmp(CU.rec[CU.slot[j] - 1].iid, iid, 32)) return &CU.slot[j];
}
// Room for one more record and a table at most half full. 0 when out of memory. Caller holds CU.lock.
static int cu_room(void) {
if (CU.n == CU.cap) {
uint32_t cap = CU.cap ? CU.cap * 2 : 1024;
CuRec *t = realloc(CU.rec, cap * sizeof *t);
if (!t) return 0;
CU.rec = t, CU.cap = cap;
}
if ((CU.n + 1) * 2 > CU.slot_cap) {
uint32_t cap = CU.slot_cap ? CU.slot_cap * 2 : 4096;
uint32_t *t = calloc(cap, sizeof *t), *old = CU.slot;
if (!t) return 0;
CU.slot = t, CU.slot_cap = cap;
for (uint32_t i = 0; i < CU.n; i++) *cu_slot(CU.rec[i].iid) = i + 1;
free(old);
}
return 1;
}
static int cu_run_cmp(const void *a, const void *b) { // {run, index} pairs, the highest run first
const uint32_t *x = a, *y = b;
return x[0] != y[0] ? (x[0] < y[0] ? 1 : -1) : (x[1] > y[1]) - (x[1] < y[1]);
}
// The number of records in a compute-used.bin image (f, size) with its header in *h, or -1 when it is not one.
static long cu_parse(const uint8_t *f, uint64_t size, CuHeader *h) {
if (!f || size < sizeof *h) return -1;
memcpy(h, f, sizeof *h);
if (memcmp(h->magic, "NX2NCUSE", 8) || h->version != CU_VERSION || h->rec_size != sizeof(CuRec)) return -1;
return (long)((size - sizeof *h) / sizeof(CuRec));
}
// Reads the record and marks what the boot keeps. Caller holds P.lock (ensure_init), after keys_open.
static void cu_open(void) {
if (!A.on || !K.path[0]) return;
const char *e = cfg_value("NX2NAVI_COMPUTE_KEEP");
plat_mutex_lock(&CU.lock);
int count = cfg_int("NX2NAVI_COMPUTE_KEEP"); // a value that is neither "all" nor a number reads as the default
CU.keep_n = !strcmp(e, "all") ? -1 : count < 0 ? 0 : count;
snprintf(CU.path, sizeof CU.path, "%s", K.path);
char *cut = plat_last_sep(CU.path);
snprintf(cut ? cut + 1 : CU.path, sizeof CU.path - (size_t)(cut ? cut + 1 - CU.path : 0), "compute-used.bin");
uint64_t size = 0;
uint8_t *f = plat_file_read(CU.path, &size);
CuHeader h;
long n = cu_parse(f, size, &h);
if (n >= 0) {
for (long i = 0; i < n && cu_room(); i++) {
CuRec r;
memcpy(&r, f + sizeof h + (size_t)i * sizeof r, sizeof r);
uint32_t *s = cu_slot(r.iid);
if (*s) continue; // an id twice: the first entry stands
CU.rec[CU.n] = r, *s = ++CU.n;
}
CU.run = h.run;
} else if (f) {
rt_log("vk_pipeline: %s has another format or version; it is rewritten", CU.path);
}
free(f);
CU.n_file = CU.n, CU.run++;
uint32_t (*order)[2] = CU.n ? malloc(CU.n * sizeof *order) : NULL;
CU.keep = CU.n ? calloc(CU.n, 1) : NULL;
if (order && CU.keep) {
for (uint32_t i = 0; i < CU.n; i++) order[i][0] = CU.rec[i].run, order[i][1] = i;
qsort(order, CU.n, sizeof *order, cu_run_cmp);
for (uint32_t i = 0; i < CU.n && (CU.keep_n < 0 || i < (uint32_t)CU.keep_n); i++) CU.keep[order[i][1]] = 1, CU.kept++;
}
free(order);
CU.opened = 1;
rt_log("vk_pipeline: %s: %u compute shaders dispatched in earlier runs; the boot keeps the pipelines of %s%u "
"dispatched most recently (NX2NAVI_COMPUTE_KEEP %s)", CU.path, CU.n,
CU.keep_n < 0 ? "every recorded compute shader, among them the " : "the ", CU.kept, e);
plat_mutex_unlock(&CU.lock);
}
// 1 when the boot keeps the pipeline of the compute shader with this input id.
static int cu_keep(const uint8_t iid[32]) {
plat_mutex_lock(&CU.lock);
int keep = CU.keep_n < 0 || !CU.opened; // without the record (no cache folder): as before, every one
if (!keep && CU.slot_cap) {
uint32_t s = *cu_slot(iid);
keep = s && s <= CU.n_file && CU.keep && CU.keep[s - 1];
}
plat_mutex_unlock(&CU.lock);
return keep;
}
// The first dispatch of cs in this run: its entry takes this run's number. The runtime's own shaders (no input id)
// are built at their first use every run and are not recorded.
static void cu_note(const GfxShader *cs) {
static const uint8_t none[32];
const uint8_t *iid = gfx_shader_input_id(cs);
if (!CU.opened || !memcmp(iid, none, 32)) return;
plat_mutex_lock(&CU.lock);
if (cu_room()) {
uint32_t *s = cu_slot(iid);
if (!*s) {
memset(&CU.rec[CU.n], 0, sizeof *CU.rec);
memcpy(CU.rec[CU.n].iid, iid, 32);
*s = ++CU.n;
}
if (CU.rec[*s - 1].run != CU.run) CU.rec[*s - 1].run = CU.run, CU.dirty = 1;
}
plat_mutex_unlock(&CU.lock);
}
// Writes the record to path. 1 when written.
// One writer at a time, from the copy of the record to the end of the file write: the autosave thread and a save at
// exit can both be here, and the older copy must not land on the newer file.
static PlatMutex cu_io = PLAT_MUTEX_INIT;
static int cu_write(const char *path) {
plat_mutex_lock(&cu_io);
plat_mutex_lock(&CU.lock);
uint32_t n = CU.n;
plat_mutex_unlock(&CU.lock);
// allocated outside CU.lock, which a dispatch takes (cu_note); room for the entries that come meanwhile
size_t cap = sizeof(CuHeader) + ((size_t)n + 256) * sizeof(CuRec), size = 0;
uint8_t *out = malloc(cap);
plat_mutex_lock(&CU.lock);
CuHeader h = {{'N', 'X', '2', 'N', 'C', 'U', 'S', 'E'}, CU_VERSION, sizeof(CuRec), CU.run, 0};
if (out && sizeof h + CU.n * sizeof(CuRec) <= cap) {
size = sizeof h + CU.n * sizeof(CuRec);
memcpy(out, &h, sizeof h);
if (CU.n) memcpy(out + sizeof h, CU.rec, CU.n * sizeof(CuRec));
CU.dirty = 0;
}
plat_mutex_unlock(&CU.lock);
int ok = size && plat_file_write_atomic(path, out, size);
if (!ok) { // still to be written: the next flush tries again
plat_mutex_lock(&CU.lock);
CU.dirty = 1;
plat_mutex_unlock(&CU.lock);
}
free(out);
plat_mutex_unlock(&cu_io);
return ok;
}
// Writes the record when a dispatch changed it (the autosave thread and the save at exit).
static void cu_flush(void) {
if (!CU.opened || CU.quiet || !*(volatile int *)&CU.dirty) return;
if (!cu_write(CU.path)) LOG_ONCE("vk_pipeline: cannot write %s; the next saves try again", CU.path);
}
// A compute pipeline created at a dispatch took ms. Caller holds P.lock.
static void cf_note(double ms, int hit) {
double now = now_ms();
CF.n++, CF.ms += ms;
if (ms > CF.max_ms) CF.max_ms = ms;
if (hit == 1) CF.hit++, CF.hit_ms += ms;
if (now - CF.win_t0 > CF_WINDOW_MS) CF.win_t0 = now, CF.win_n = 0, CF.win_ms = 0;
CF.win_n++, CF.win_ms += ms;
if (CF.win_ms > CF.worst_ms) CF.worst_ms = CF.win_ms, CF.worst_n = CF.win_n;
}
static void cf_log(void) {
plat_mutex_lock_shared(&P.lock);
typeof(CF) c = CF; // the line is written after the unlock: every dispatch needs P.lock, and this
plat_mutex_unlock_shared(&P.lock); // runs on the autosave thread below normal priority
if (c.n || c.pre_used)
rt_log("vk_pipeline: compute pipelines created at their first dispatch: %llu (avg %.2f ms, longest %.2f ms; the "
"pipeline cache held %llu of them, avg %.2f ms); the %.0f ms that held the most of it: %u pipelines, "
"%.1f ms; of the compute pipelines kept from the boot, %llu were dispatched",
(unsigned long long)c.n, c.n ? c.ms / c.n : 0.0, c.max_ms, (unsigned long long)c.hit,
c.hit ? c.hit_ms / c.hit : 0.0, CF_WINDOW_MS, c.worst_n, c.worst_ms, (unsigned long long)c.pre_used);
}
// The background builder of large compute shaders (comment at bg_main).
static struct {
const GfxShader **cs;
uint8_t (*iid)[32];
uint64_t *words;
size_t n, cap;
PlatThread *th;
const GfxShader *volatile building; // the shader on the driver now, NULL between builds
volatile int32_t raised; // a dispatch waited for it and raised the thread
uint64_t built, failed, there;
double ms;
} BG;
VkPipeline vk_pipeline_compute(const GfxShader *cs, const VkPipeInfo **info) {
if (info) *info = NULL;
if (!cs || !ensure_init()) return VK_NULL_HANDLE;
struct { const GfxShader *cs; uint64_t compute; } k = {cs, 1}; // no padding: the key is hashed and compared as bytes
uint64_t h = key_hash(&k, sizeof k);
plat_mutex_lock(&P.lock);
Pipe *p = pipe_find(&k, sizeof k, h);
double w0 = !p && cs_flight_has(cs) ? now_ms() : 0;
int bg = 0;
while (!p && cs_flight_has(cs)) { // another thread is compiling it: its result, not a second compile
// a build that only fills the pipeline cache keeps its pipeline for this dispatch, and a boot prebuild thread
// compiling it below normal priority goes to normal
PlatThread *builder = cs_flight_wanted(cs);
if (builder) plat_thread_set_priority(builder, PLAT_PRIO_NORMAL);
if (BG.building == cs && BG.th && !plat_xchg32(&BG.raised, 1)) // the background builder: normal priority now
plat_thread_set_priority(BG.th, PLAT_PRIO_NORMAL), bg = 1;
plat_mutex_unlock(&P.lock);
plat_sleep_ms(1);
plat_mutex_lock(&P.lock);
p = pipe_find(&k, sizeof k, h);
}
if (w0 && now_ms() - w0 >= 2.0) {
static int logs;
if (logs++ < 16)
rt_log("vk_exec: a dispatch waited %.0f ms for its compute pipeline (%s)", now_ms() - w0,
bg ? "built on the background thread" : "built by another thread");
}
if (p) {
P.stats.hits++;
} else {
const char *why = NULL;
Bindings B = {0};
Layout *l = compute_layout(cs, &B, &why);
VkPipeline pipe = VK_NULL_HANDLE;
if (!why) {
int flight = cs_flight_add(cs, NULL);
plat_mutex_unlock(&P.lock); // the compile runs without the lock
uint64_t p0 = perf_begin();
double c0 = now_ms();
pipe = compute_create(cs, l, 0);
double cms = now_ms() - c0;
int hit = cc_hit;
perf_end(PERF_PIPE, p0);
plat_mutex_lock(&P.lock);
if (flight) cs_flight_remove(cs);
if (!pipe) why = "pipeline creation failed";
else cf_note(cms, hit);
}
if ((p = pipe_find(&k, sizeof k, h))) { // another thread added it meanwhile (no flight slot was free)
if (pipe) vkDestroyPipeline(P.device, pipe, NULL);
} else {
if (why) {
P.stats.failures++;
rt_log("vk_pipeline: compute pipeline not built: %s", why);
} else {
P.stats.compute++;
LB.cs_draw++;
}
p = pipe_add(&k, sizeof k, h, pipe, l, l ? &B : NULL, VK_PIPELINE_BIND_POINT_COMPUTE, 0);
}
}
if (p && !p->used) { // the first dispatch of this run: the boot of the next runs keeps this shader's pipeline
p->used = 1;
CF.pre_used += p->pre;
if (p->info.pipeline) cu_note(cs);
}
plat_mutex_unlock(&P.lock);
if (!p) return VK_NULL_HANDLE;
if (info) *info = &p->info;
return p->info.pipeline;
}
static void lb_lock(void) {
plat_thread_priority(PLAT_PRIO_NORMAL);
plat_mutex_lock(&P.lock);
}
static void lb_unlock(void) {
plat_mutex_unlock(&P.lock);
if (LB.idle) plat_thread_priority(PLAT_PRIO_IDLE);
}
static void lb_count(int kind, int what, double ms) { // what: 0 built, 1 already there, 2 failed, 3 cache only
plat_mutex_lock(&LB.stat);
if (what == 3) LB.warmed[kind]++, LB.warm_ms[kind] += ms;
else (what == 0 ? LB.built : what == 1 ? LB.have : LB.fail)[kind]++, LB.ms[kind] += ms;
plat_mutex_unlock(&LB.stat);
}
// vs_union_missing for vs alone, without entering vs into the union table (it holds VS_OUTS_CAP shaders).
static uint32_t vs_union_peek(const GfxShader *vs, GfxShaderIo fin[32]) {
GfxShaderIo vout[32];
uint32_t need = 0, have = gfx_shader_outputs(vs, vout), missing = 0;
memset(fin, 0, 32 * sizeof *fin);
plat_mutex_lock_shared(&VO.lock);
uint32_t j = (uint32_t)(((uintptr_t)vs >> 4) * 0x9e3779b1u) & (VS_OUTS_CAP - 1);
while (VO.vs[j] && VO.vs[j] != vs) j = (j + 1) & (VS_OUTS_CAP - 1);
if (VO.vs[j]) need = VO.mask[j], memcpy(fin, VO.io[j], 32 * sizeof *fin);
plat_mutex_unlock_shared(&VO.lock);
if (P.vs_outs_all)
for (int i = 0; i < 32; i++)
if (!(need >> i & 1)) fin[i] = (GfxShaderIo){GFX_TYPE_FLOAT, 4, 0}, need |= 1u << i;
for (int i = 0; i < 32; i++)
if (need >> i & 1 && !(have >> i & 1)) missing |= 1u << i;
return missing;
}
// A compute shader over LB.big_words that no pipeline cache holds: its first dispatch builds it (vk_pipeline_compute).
static void lb_defer(const uint8_t iid[32], uint64_t words) {
plat_mutex_lock(&LB.stat);
LB.deferred++;
plat_mutex_unlock(&LB.stat);
rt_log("vk_pipeline: stage library prebuild: compute shader %02x%02x%02x%02x%02x%02x%02x%02x (%llu SPIR-V words) %s: "
"left to its first dispatch (NX2NAVI_LIB_PREBUILD_BIG %llu)", iid[0], iid[1], iid[2], iid[3], iid[4], iid[5],
iid[6], iid[7], (unsigned long long)words,
P.cache_control ? "not in the pipeline caches" : "too large to build without a cache probe (no pipelineCreationCacheControl)",
(unsigned long long)LB.big_words);
}
// ---- large compute shaders on a background thread (NX2NAVI_LIB_PREBUILD_BIG_BG, default 1; 0 leaves them to their
// first dispatch as before)
// The boot prebuild leaves a compute shader over LB.big_words that no pipeline cache holds (lb_compute). Its first
// dispatch then compiled it on the replay thread: frames of 0.65-1.9 s in Kokiri Forest on an RTX 5090 after a
// translator change. One thread builds them instead, once the boot prebuild is done, one at a time and the smallest
// first: four compiled side by side grew the process by 16-19 GB on an RTX 4090 (comment at LB_BIG_WORDS). The shader
// being built is in cs_flight, so a dispatch that needs it waits for this build (vk_pipeline_compute raises the
// thread to normal priority then) instead of compiling a second copy. What is built is entered for the process, as
// lb_compute's pipelines are, and lands in pipeline-cache.bin for the next boot.
static int bg_on(void) {
static int on = -1;
if (on < 0) on = cfg_bool("NX2NAVI_LIB_PREBUILD_BIG_BG");
return on;
}
// A large compute shader no cache holds goes to the background builder (lb_worker threads; under LB.stat).
static void bg_queue(const GfxShader *cs, const uint8_t iid[32], uint64_t words) {
plat_mutex_lock(&LB.stat);
if (BG.n == BG.cap) {
size_t cap = BG.cap ? BG.cap * 2 : 16;
const GfxShader **c = realloc(BG.cs, cap * sizeof *c);
if (c) BG.cs = c;
uint8_t (*i)[32] = realloc(BG.iid, cap * sizeof *i);
if (i) BG.iid = i;
uint64_t *w = realloc(BG.words, cap * sizeof *w);
if (w) BG.words = w;
if (c && i && w) BG.cap = cap;
}
int ok = BG.n < BG.cap;
if (ok) BG.cs[BG.n] = cs, memcpy(BG.iid[BG.n], iid, 32), BG.words[BG.n] = words, BG.n++;
plat_mutex_unlock(&LB.stat);
if (!ok) {
lb_defer(iid, words);
return;
}
rt_log("vk_pipeline: stage library prebuild: compute shader %02x%02x%02x%02x%02x%02x%02x%02x (%llu SPIR-V words) not in "
"the pipeline caches: built on the background thread after the prebuild (NX2NAVI_LIB_PREBUILD_BIG_BG)",
iid[0], iid[1], iid[2], iid[3], iid[4], iid[5], iid[6], iid[7], (unsigned long long)words);
}
// NX2NAVI_BIG_BUILD_FREE_MB (default 8192; 0 no check): the system commit headroom a large compute build waits for.
// A build of one of these shaders took the process up by 1-3 GB on an RTX 5090 and four at once by 16-19 GB on an
// RTX 4090; started on a machine near its commit limit it can end the game. While the headroom is short the shader
// waits, and if the game dispatches it first it is built there, as without the background builder.
// The builds under way elsewhere count through their reservations at the memory gate (gate_enter), where this build
// then reserves GATE_BIG_MB of its own for as long as it runs, so the other background builds leave it its room.
static int bg_room(double *used, double *limit, double *held) {
static double need = -1;
if (need < 0) need = strtod(cfg_value("NX2NAVI_BIG_BUILD_FREE_MB"), NULL);
double proc;
plat_commit_info(&proc, used, limit);
if (!(need <= 0 || *limit < 0 || *limit - *used >= need)) return 0;
return gate_enter(GATE_BIG_MB, held);
}
// NX2NAVI_BIG_BUILD_MAX_MB (default 4096; 0 no cap): how far the builder may raise the process commit before it stops
// and leaves the rest to their first dispatch. One of these shaders costs the driver 1-4 GB: on a 31 GB machine four of
// them took the process from 15 to 29 GB and system commit to 60 of 65 GB, which is where a player feels the game
// stutter. The builder exists to remove hitches, not to trade them for paging, so a machine that cannot hold them all
// builds only what it can and pays one hitch per shader later.
static double bg_growth_cap(void) {
static double cap = -1;
if (cap < 0) cap = strtod(cfg_value("NX2NAVI_BIG_BUILD_MAX_MB"), NULL);
return cap;
}
// The builder compiles into a pipeline cache of its own (own_cache at pipe_create_in), not into P.cache. A create into
// P.cache holds cache_rw shared from start to end, and the cache is only saved at a moment when nobody holds it: on an
// RX 7900 XT (26.10.1) the largest of these shaders (4,874,502 SPIR-V words) was still compiling 590 s after the boot,
// pipeline-cache.bin was not written once in that time, and the game was closed before the build ended, so the next
// boot found neither that shader nor the pipelines built in play and started the same build again. bg_merge moves the
// result into P.cache when the build is done, at a moment with no build into P.cache running: the lock is tried as
// cache_save tries it, because a waiting exclusive request would hold back every new build.
static void bg_merge(VkPipelineCache own, int built) {
int locked = 0;
while (built && !LB.stop && !(locked = plat_mutex_trylock(&cache_rw))) plat_sleep_ms(2);
if (locked) {
// what play compiled: into the play cache when there is one, so its file gets it and not pipeline-cache.bin
VkResult r = vkMergePipelineCaches(P.device, P.play ? P.play : P.cache, 1, &own);
plat_mutex_unlock(&cache_rw);
if (r == VK_SUCCESS) __atomic_fetch_add(&cache_merges, 1, __ATOMIC_RELAXED);
else rt_log("vk_pipeline: background compute build: its cache did not merge into %s (%d)",
P.play ? P.play_path : P.cache_path, (int)r);
}
vkDestroyPipelineCache(P.device, own, NULL);
}
static void bg_main(void *arg) {
(void)arg;
plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
// The queue can grow while this thread builds (bg_queue runs on the replay thread and reallocs these arrays), so
// the sort and every read below happen under LB.stat; an entry is copied out before its compile so no pointer into
// the arrays is held across it.
plat_mutex_lock(&LB.stat);
for (size_t i = 1; i < BG.n; i++) // smallest first: insertion sort of a handful
for (size_t j = i; j > 0 && BG.words[j] < BG.words[j - 1]; j--) {
const GfxShader *c = BG.cs[j];
BG.cs[j] = BG.cs[j - 1], BG.cs[j - 1] = c;
uint64_t w = BG.words[j];
BG.words[j] = BG.words[j - 1], BG.words[j - 1] = w;
uint8_t t[32];
memcpy(t, BG.iid[j], 32), memcpy(BG.iid[j], BG.iid[j - 1], 32), memcpy(BG.iid[j - 1], t, 32);
}
plat_mutex_unlock(&LB.stat);
double t_all = now_ms();
double c_start, c_used0, c_limit0;
plat_commit_info(&c_start, &c_used0, &c_limit0);
for (size_t i = 0; !LB.stop; i++) {
uint8_t idbuf[32];
uint64_t words = 0;
const GfxShader *cs;
plat_mutex_lock(&LB.stat);
int have = i < BG.n;
if (have) {
cs = BG.cs[i];
memcpy(idbuf, BG.iid[i], 32);
words = BG.words[i];
}
plat_mutex_unlock(&LB.stat);
if (!have) break;
const uint8_t *iid = idbuf;
struct { const GfxShader *cs; uint64_t compute; } k = {cs, 1}; // no padding: the key is hashed and compared as bytes
uint64_t h = key_hash(&k, sizeof k);
Bindings B = {0};
const char *why = NULL;
double used, limit, c_now;
plat_commit_info(&c_now, &used, &limit);
double cap = bg_growth_cap();
if (cap > 0 && c_now - c_start >= cap) {
rt_log("vk_pipeline: background compute build: stopping, the process grew %.0f MB in all (NX2NAVI_BIG_BUILD_MAX_MB "
"%.0f); the shaders left build at their first dispatch", c_now - c_start, cap);
break;
}
int waited = 0, no_room = 0;
double w0 = now_ms(), held = -1; // held: this build's reservation at the memory gate, -1 none
while (!LB.stop && !bg_room(&used, &limit, &held)) { // not under P.lock: a dispatch builds it meanwhile
held = -1;
if (!waited++)
rt_log("vk_pipeline: background compute build: waiting for commit headroom (system %.0f of %.0f MB, "
"NX2NAVI_BIG_BUILD_FREE_MB %s)", used, limit, cfg_value("NX2NAVI_BIG_BUILD_FREE_MB"));
if (now_ms() - w0 > 30000.0) { // waiting is only useful when the headroom will come back
rt_log("vk_pipeline: background compute build: no commit headroom after %.0f s; the shaders left build at "
"their first dispatch", (now_ms() - w0) / 1000.0);
no_room = 1;
break;
}
plat_sleep_ms(2000);
}
if (LB.stop || no_room) {
if (held >= 0 && !no_room) gate_leave(held); // stopped right after the gate let it in
break;
}
plat_mutex_lock(&P.lock);
int there = cs_flight_has(cs) || pipe_find(&k, sizeof k, h); // a dispatch got to it first
Layout *l = there ? NULL : compute_layout(cs, &B, &why);
int flight = l ? cs_flight_add(cs, NULL) : 0; // vk_pipeline_compute raises this thread through BG.raised
if (l) BG.building = cs;
plat_mutex_unlock(&P.lock);
if (!l) {
if (held >= 0) gate_leave(held);
if (there) BG.there++;
else BG.failed++, rt_log("vk_pipeline: background compute build: %s", why ? why : "no layout");
continue;
}
VkPipelineCache own = VK_NULL_HANDLE;
VkPipelineCacheCreateInfo oci = {.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO};
if (P.cache && vkCreatePipelineCache(P.device, &oci, NULL, &own) != VK_SUCCESS) own = VK_NULL_HANDLE;
double t0 = now_ms();
own_cache = own; // without one the build goes into P.cache as before
VkPipeline pipe = compute_create(cs, l, 0);
own_cache = VK_NULL_HANDLE;
double ms = now_ms() - t0;
if (held >= 0) gate_leave(held);
// The pipeline stays: only a shader a run dispatched is queued here (lb_compute), and it is entered before
// the merge below, so a dispatch that comes meanwhile finds it.
plat_mutex_lock(&P.lock);
if (flight) cs_flight_remove(cs);
BG.building = NULL;
Pipe *e = NULL;
if (pipe && !pipe_find(&k, sizeof k, h) &&
(e = pipe_add(&k, sizeof k, h, pipe, l, &B, VK_PIPELINE_BIND_POINT_COMPUTE, 0))) {
e->pre = 1;
P.stats.compute++;
} else if (pipe) { // a dispatch built its own meanwhile (no flight slot was free), or out of memory
vkDestroyPipeline(P.device, pipe, NULL);
}
plat_mutex_unlock(&P.lock);
if (own) bg_merge(own, pipe != VK_NULL_HANDLE); // after the entry: a dispatch waiting for it does not wait for this
if (plat_xchg32(&BG.raised, 0)) plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
if (pipe) BG.built++, BG.ms += ms;
else BG.failed++;
double c_proc, c_used, c_limit;
plat_commit_info(&c_proc, &c_used, &c_limit);
rt_log("vk_pipeline: background compute build: %02x%02x%02x%02x%02x%02x%02x%02x (%llu SPIR-V words) %s in %.0f ms; "
"commit: process %.0f MB, system %.0f of %.0f MB", iid[0], iid[1], iid[2], iid[3], iid[4], iid[5], iid[6],
iid[7], (unsigned long long)words, pipe ? "built" : "failed", ms, c_proc, c_used, c_limit);
}
rt_log("vk_pipeline: background compute builds %s: %llu built (%.0f ms in all), %llu failed, %llu dispatched before "
"their turn; %.0f ms after the prebuild", LB.stop ? "stopped" : "done", (unsigned long long)BG.built, BG.ms,
(unsigned long long)BG.failed, (unsigned long long)BG.there, now_ms() - t_all);
}
// The last boot prebuild worker starts the builder when it queued anything.
static void bg_start(void) {
if (!BG.n || LB.stop || BG.th) return;
if (!(BG.th = plat_thread_new(bg_main, NULL, "big compute")))
rt_log("vk_pipeline: cannot start the background compute builder; %zu large compute shaders build at their first "
"dispatch", BG.n);
}
// keep 0: the pipeline is built for the pipeline cache only and destroyed at once, unless a dispatch came to wait for
// it meanwhile; a shader over LB.big_words is then not built at all (comment at compute-used.bin). cs may be a copy
// the caller releases afterwards when keep is 0: nothing here holds on to it then.
static void lb_compute(const GfxShader *cs, const uint8_t iid[32], int keep) {
struct { const GfxShader *cs; uint64_t compute; } k = {cs, 1}; // no padding: the key is hashed and compared as bytes
uint64_t h = key_hash(&k, sizeof k);
Bindings B = {0};
const char *why = NULL;
uint64_t words = 0;
gfx_shader_spirv(cs, &words);
int big = LB.big_words && words > LB.big_words; // only from the caches (LB_BIG_WORDS)
if (big && !keep) { // no run dispatched it: a pipeline of it would hold about 100 MiB of device memory unused
plat_mutex_lock(&LB.stat);
LB.big_unused++;
plat_mutex_unlock(&LB.stat);
return;
}
if (big && !P.cache_control) {
if (bg_on()) bg_queue(cs, iid, words);
else lb_defer(iid, words);
return;
}
lb_lock();
int busy = cs_flight_has(cs); // the replay compiles it now (vk_pipeline_compute)
Layout *l = busy || pipe_find(&k, sizeof k, h) ? NULL : compute_layout(cs, &B, &why);
PlatThread *me = LB.idle && lb_me >= 0 ? LB.th[lb_me] : NULL;
int have = !l && !why, flight = l ? cs_flight_add(cs, me) : 0;
lb_unlock();
if (!l) {
lb_count(2, have ? 1 : 2, 0);
return;
}
double t0 = now_ms();
// The compile runs below normal priority: after an update or a driver update every one of these pipelines is
// compiled again (about 140 ms each on an RTX 5090's driver, minutes of two threads for a long play history), and
// at normal priority those threads took cores from the game's own on a CPU with few of them. While the shader is in
// flight a dispatch of it waits for this build (vk_pipeline_compute) and raises the thread to normal: a compile left
// at idle priority under load held that frame for seconds. The dispatch may have come between the unlock and the
// call below, which would undo its raise, hence the look at the slot after it. Without a slot or a handle nobody
// could raise the thread, so it compiles at normal priority.
if (LB.idle && flight && me) {
plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
if (cs_flight_is_wanted(cs)) plat_thread_priority(PLAT_PRIO_NORMAL);
} else if (LB.idle) {
plat_thread_priority(PLAT_PRIO_NORMAL);
}
VkPipeline pipe = compute_create(cs, l, big);
double ms = now_ms() - t0;
int compiled = pipe && cc_hit != 1, kept = 0;
lb_lock();
if (flight && cs_flight_remove(cs)) keep = 1; // a dispatch waits for it
Pipe *e = NULL;
if (pipe && pipe_find(&k, sizeof k, h)) {
vkDestroyPipeline(P.device, pipe, NULL);
have = 1;
} else if (pipe && keep && (e = pipe_add(&k, sizeof k, h, pipe, l, &B, VK_PIPELINE_BIND_POINT_COMPUTE, 0))) {
e->pre = 1, kept = 1;
P.stats.compute++;
} else if (pipe) {
vkDestroyPipeline(P.device, pipe, NULL);
// P.stats.compute does not count it, so the autosave would not see what its compile added to P.cache
if (compiled) __atomic_fetch_add(&cache_merges, 1, __ATOMIC_RELAXED);
}
lb_unlock();
if (!pipe && big && bg_on()) bg_queue(cs, iid, words);
else if (!pipe && big) lb_defer(iid, words);
else lb_count(2, !pipe ? 2 : have ? 1 : kept ? 0 : 3, ms);
}
// The pre-rasterization (vertex) or fragment-shader library of one shader with the LB state.
// keep 0: build into the driver's pipeline cache only (sh is a private copy; the library is destroyed).
static void lb_library(const GfxShader *sh, int vertex, int keep) {
int kind = vertex ? LIB_PR : LIB_FS, stat = vertex ? 0 : 1;
VkPipeState s;
vk_pipe_state_defaults(&s);
GKey k;
memset(&k, 0, sizeof k);
k.clamp = LB.clamp, k.poly_mode = LB.poly, k.minus_w = LB.minus_w, k.samples = LB.samples;
PrKey pk;
FsKey fk;
memset(&pk, 0, sizeof pk), memset(&fk, 0, sizeof fk);
const void *key;
uint32_t size;
if (vertex) {
GfxShaderIo fin[32];
s.vs = sh;
pk.kind = LIB_PR, pk.vs = sh, pk.clamp = k.clamp, pk.poly_mode = k.poly_mode, pk.minus_w = k.minus_w;
pk.missing = vs_union_peek(sh, fin);
for (int i = 0; i < 32; i++)
if (pk.missing >> i & 1) pk.fin[i] = fin[i];
key = &pk, size = sizeof pk;
} else {
s.fs = sh;
fk.kind = LIB_FS, fk.fs = sh, fk.samples = k.samples;
key = &fk, size = sizeof fk;
}
uint64_t h = key_hash(key, size);
Bindings B = {0}, F = {0};
VkPipelineLayout lay = VK_NULL_HANDLE;
lb_lock();
int have = keep && lib_find(h, key, size) != NULL;
if (!have && add_stage(vertex ? &B : &F, sh, 0)) {
Layout *l = layout_pair(&B, &F);
if (l) lay = half_layout(l, vertex ? 0 : 1);
}
lb_unlock();
if (have || !lay) {
lb_count(stat, have ? 1 : 2, 0);
return;
}
double t0 = now_ms();
VkPipeline pipe = lib_create(kind, &s, &k, lay, key);
double ms = now_ms() - t0;
if (!keep) {
if (pipe) vkDestroyPipeline(P.device, pipe, NULL);
lb_count(stat, pipe ? 3 : 2, ms);
return;
}
lb_lock();
have = lib_find(h, key, size) != NULL;
VkPipeline got = lib_put(h, key, size, pipe);
if (!have && pipe) {
Lib *b = lib_find(h, key, size);
if (b) b->pre = 1;
}
lb_unlock();
lb_count(stat, !pipe ? 2 : have || got != pipe ? 1 : 0, ms);
}
// A vertex or fragment variant with the graphics pipeline library off: no stage library to build ahead.
static void lb_skip_lib(void) {
plat_mutex_lock(&LB.stat);
LB.nolib++;
plat_mutex_unlock(&LB.stat);
}
static void lb_report(const char *how) {
float mem_used, mem_total, mem_proc;
plat_mem_info(&mem_used, &mem_total, &mem_proc);
plat_mutex_lock_shared(&LB.stat);
rt_log("vk_pipeline: stage library prebuild %s: %llu of %zu variants in %.0f ms; kept: vertex %llu built (avg %.1f ms), "
"%llu there; fragment %llu built (avg %.1f ms), %llu there; compute %llu built (avg %.1f ms), %llu there; driver "
"cache only: vertex %llu (avg %.1f ms), fragment %llu (avg %.1f ms), compute %llu (avg %.1f ms); %llu failed, %llu not in the shader cache, "
"%llu vertex/fragment skipped (graphics pipeline library off), %llu skipped (the driver crashed on them before, "
"shader-prepare-skip.txt), %llu large compute shaders left to their first dispatch (NX2NAVI_LIB_PREBUILD_BIG), %zu "
"queued for the background builder (NX2NAVI_LIB_PREBUILD_BIG_BG), %llu large compute shaders not built because "
"no run has dispatched them; process working set %.0f MB", how, (unsigned long long)LB.done, LB.n,
now_ms() - LB.t0, (unsigned long long)LB.built[0], LB.built[0] ? LB.ms[0] / LB.built[0] : 0.0,
(unsigned long long)LB.have[0], (unsigned long long)LB.built[1], LB.built[1] ? LB.ms[1] / LB.built[1] : 0.0,
(unsigned long long)LB.have[1], (unsigned long long)LB.built[2], LB.built[2] ? LB.ms[2] / LB.built[2] : 0.0,
(unsigned long long)LB.have[2], (unsigned long long)LB.warmed[0], LB.warmed[0] ? LB.warm_ms[0] / LB.warmed[0] : 0.0,
(unsigned long long)LB.warmed[1], LB.warmed[1] ? LB.warm_ms[1] / LB.warmed[1] : 0.0,
(unsigned long long)LB.warmed[2], LB.warmed[2] ? LB.warm_ms[2] / LB.warmed[2] : 0.0,
(unsigned long long)(LB.fail[0] + LB.fail[1] + LB.fail[2]),
(unsigned long long)LB.noshader, (unsigned long long)LB.nolib, (unsigned long long)LB.skipped,
(unsigned long long)LB.deferred, BG.n, (unsigned long long)LB.big_unused, (double)mem_proc);
plat_mutex_unlock_shared(&LB.stat);
}
// Boot: compute pipelines (the first LB.n_keep entered for the process, the others only built into the pipeline
// cache) and kept libraries.
static void lb_worker(void *arg) {
lb_me = (int)(intptr_t)arg; // its handle is LB.th[lb_me]
plat_thread_priority(PLAT_PRIO_IDLE);
for (;;) {
size_t i = lb_next(lb_me);
if (i >= LB.n) break;
if (skip_listed(LB.e[i].iid)) {
skip_count();
plat_mutex_lock(&LB.stat);
LB.done++;
plat_mutex_unlock(&LB.stat);
continue;
}
// The memory gate (gate_wait): without room for another build the worker waits, and after GATE_WAIT_MS it and
// the other workers end. What they leave is built at its first dispatch or draw, as what a run never recorded.
double held;
if (LB.short_mem || !gate_wait("stage library prebuild", &LB.stop, &held)) {
if (!LB.stop && !plat_xchg32(&LB.short_mem, 1)) {
char g[256];
rt_log("vk_pipeline: stage library prebuild: ends here, at %zu of %zu variants: no commit headroom for "
"another build (%s). The others are built when the game first uses them", i, LB.n,
gate_text(g, sizeof g));
}
break;
}
// A compute shader whose pipeline does not stay is read as a copy of this thread's own and released after its
// build: kept in the shader cache's memory, the 3,900 of an installed copy's list were 240 MB of the process's
// commit for shaders most runs never dispatch. A dispatch that does come reads its shader then (0.05 to
// 0.15 ms from the files this pass has just read). The shaders of kept pipelines and libraries stay loaded:
// their pipeline and library entries point at them.
int keep = i < LB.n_keep, owned = 0;
plat_thread_priority(PLAT_PRIO_NORMAL); // gfx_shader's cache lock, disk reads
const GfxShader *sh = keep || LB.e[i].stage != GFX_STAGE_CS ? gfx_shader_load_input(LB.e[i].iid, 0)
: gfx_shader_load_private(LB.e[i].iid, &owned);
plat_thread_priority(PLAT_PRIO_IDLE);
if (!sh) {
plat_mutex_lock(&LB.stat);
LB.noshader++;
plat_mutex_unlock(&LB.stat);
} else if (gfx_shader_stage(sh) == GFX_STAGE_CS) {
lb_compute(sh, LB.e[i].iid, keep);
} else if (!owned && (gfx_shader_stage(sh) == GFX_STAGE_VS || gfx_shader_stage(sh) == GFX_STAGE_FS)) {
if (P.gpl) lb_library(sh, gfx_shader_stage(sh) == GFX_STAGE_VS, 1); // a kept library points at sh: never a copy
else lb_skip_lib();
}
if (owned) gfx_shader_release_private(sh);
gate_leave(held);
plat_mutex_lock(&LB.stat);
LB.done++;
plat_mutex_unlock(&LB.stat);
}
if (!plat_dec32(&LB.running)) {
lb_report(LB.stop ? "stopped" : LB.short_mem ? "ended for want of commit headroom" : "done");
fb_log("after the stage library prebuild");
vk_mem_stage_log("the compute pipeline prebuild");
bg_start();
}
return;
}
// The library state from the key file's records (prewarm); defaults without one.
static void lb_state(const uint32_t pr_hist[16], const uint32_t fs_hist[4]) {
// state index: clamp | poly (0 fill, 1 line, 2 point -> VkPolygonMode) << 1 | minus_w << 3 (pr); samples log2 (fs)
uint32_t best = 0, bf = 0;
for (uint32_t i = 1; i < 16; i++) best = pr_hist[i] > pr_hist[best] ? i : best;
for (uint32_t i = 1; i < 4; i++) bf = fs_hist[i] > fs_hist[bf] ? i : bf;
if (!pr_hist[best]) best = 8; // no key file: NX2navi clips -w..w (all 2706 records of a recorded key file)
LB.clamp = (uint8_t)(best & 1), LB.poly = (uint8_t)(best >> 1 & 3), LB.minus_w = (uint8_t)(best >> 3 & 1);
LB.samples = (uint8_t)(1u << bf);
}
// ---- the driver's cache of stage libraries, warmed again during play
//
// The preparation (lb_warm_prepared) builds every prepared variant's stage library once, so the driver's disk cache
// holds it and the library a draw needs later takes a few milliseconds instead of a compile: 3 ms against 100 ms for
// a vertex and 340 ms for a fragment library on an RX 7900 XT (driver 26.10.1). The driver's cache does not outlive
// everything, though: a driver update empties it, the AMD driver keeps a limited amount and drops old entries, and a
// change to what a library is built with (the dynamic states it lists, as when dynamic vertex input was turned off for
// this driver) gives every library another entry. After any of them each pipeline a draw first needs compiles cold:
// the draw is skipped for the frames that takes, or waits for it. Measured on that card with the cache cold, a walk
// through a village not seen before skipped 11,180 draws while their pipelines built and 94 draws waited 5.5 s in
// all, one of them 1.9 s.
//
// So every start checks: a sample of the prepared fragment libraries is built (lw_probe), and when their median time
// says the cache is cold, the whole prepared list is built again into the driver's cache on a few idle-priority
// threads while the game runs (lw_worker), as the preparation does but without stopping play. shader-warm.txt beside
// the caches says where that stands: "warming" while it runs, so a start after a run that ended meanwhile goes on,
// and "done" with the driver and a count of the times the cache was cold again right after a finished warm. At two in
// a row the driver does not keep stage libraries at all and warming stops being tried (NX2NAVI_LIB_WARM=0 turns it off;
// =force warms at every start).
#define LW_SAMPLE 12
#define LW_COLD_MS 15.0
#define LW_MAX_THREADS 6
static struct {
GfxWarmEnt *e;
size_t n;
volatile int64_t next, done;
volatile int32_t running, stop, started;
volatile int32_t short_mem; // the memory gate refused a build for GATE_WAIT_MS: the warm ended for this run
PlatThread *th[LW_MAX_THREADS + 1];
int threads, strikes;
uint32_t vendor; // the GPU's vendor id, for what a cold cache's log line adds (vk_warm_hint.h)
double t0;
char stamp[PLAT_PATH_MAX * 2 + 32], id[64];
} LW;
static void lw_stamp(const char *state, int strikes) {
char line[128];
int n = snprintf(line, sizeof line, "%s %s %d\n", state, LW.id, strikes);
if (!plat_file_write_atomic(LW.stamp, line, (size_t)n)) rt_log("vk_pipeline: cannot write %s", LW.stamp);
}
// One prepared vertex or fragment variant's library into the driver's cache; its time in ms, negative when the
// variant is on the skip list, not in the shader cache or of another stage.
static double lw_build(const GfxWarmEnt *e) {
if (e->stage != GFX_STAGE_VS && e->stage != GFX_STAGE_FS) return -1;
if (skip_listed(e->iid)) return -1;
int owned = 0;
const GfxShader *sh = gfx_shader_load_private(e->iid, &owned);
if (!sh) return -1;
double t0 = now_ms();
int st = gfx_shader_stage(sh);
if (st == GFX_STAGE_VS || st == GFX_STAGE_FS) lb_library(sh, st == GFX_STAGE_VS, 0);
double ms = now_ms() - t0;
if (owned) gfx_shader_release_private(sh);
return ms;
}
static void lw_worker(void *arg) {
(void)arg;
plat_thread_priority(PLAT_PRIO_IDLE);
no_cache = 1; // into the driver's own cache only (see no_cache)
for (;;) {
size_t i = (size_t)plat_inc64(&LW.next) - 1;
if (i >= LW.n || LW.stop) break;
// The memory gate (gate_wait): without room the warm waits, and after GATE_WAIT_MS it ends for this run.
// shader-warm.txt keeps saying "warming", so the next start goes on with it.
double held;
if (LW.short_mem || !gate_wait("library warm", &LW.stop, &held)) {
if (!LW.stop && !plat_xchg32(&LW.short_mem, 1)) {
char g[256];
rt_log("vk_pipeline: library warm: ends for this run at %lld of %zu prepared variants: no commit headroom "
"for another build (%s). The next start goes on with it", (long long)LW.done, LW.n,
gate_text(g, sizeof g));
}
break;
}
lw_build(&LW.e[i]);
gate_leave(held);
int64_t d = plat_inc64(&LW.done);
if (d % 8000 == 0)
rt_log("vk_pipeline: library warm: %lld of %zu prepared variants in %.0f s", (long long)d, LW.n,
(now_ms() - LW.t0) / 1000);
}
if (!plat_dec32(&LW.running) && !LW.stop && !LW.short_mem) {
lw_stamp("done", LW.strikes);
rt_log("vk_pipeline: library warm: done, %zu prepared variants in %.0f s on %d idle threads; new pipelines "
"link from the driver's cache from here on", LW.n, (now_ms() - LW.t0) / 1000, LW.threads);
}
}
// The median time of LW_SAMPLE prepared fragment libraries spread over the list; negative without that many.
static double lw_probe(void) {
double ms[LW_SAMPLE];
int got = 0;
size_t step = LW.n / LW_SAMPLE ? LW.n / LW_SAMPLE : 1, at = (size_t)(plat_now_ns() / 1000) % step;
for (size_t i = at; i < LW.n && got < LW_SAMPLE && !LW.stop; i += step)
for (size_t j = i; j < i + step && j < LW.n; j++) { // the next fragment variant from here
if (LW.e[j].stage != GFX_STAGE_FS) continue;
double t = lw_build(&LW.e[j]);
if (t >= 0) ms[got++] = t;
break;
}
if (got < LW_SAMPLE / 2) return -1;
for (int i = 1; i < got; i++)
for (int j = i; j > 0 && ms[j] < ms[j - 1]; j--) {
double t = ms[j];
ms[j] = ms[j - 1], ms[j - 1] = t;
}
return ms[got / 2];
}
static void lw_main(void *arg) {
(void)arg;
plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
char was[16] = "", id[64] = "";
int strikes = 0;
uint64_t size = 0;
char *f = (char *)plat_file_read(LW.stamp, &size);
if (f && size < 200) {
char line[200];
memcpy(line, f, size), line[size] = 0;
if (sscanf(line, "%15s %63s %d", was, id, &strikes) < 2) was[0] = 0;
}
free(f);
int same = !strcmp(id, LW.id), resume = same && !strcmp(was, "warming");
const char *mode = cfg_value("NX2NAVI_LIB_WARM");
double median = resume ? 0 : lw_probe();
if (LW.stop) return;
int cold = median > LW_COLD_MS || !strcmp(mode, "force");
if (!resume && !cold) {
if (same && strikes) lw_stamp("done", 0); // the driver kept them this time
rt_log("vk_pipeline: library warm: the driver's cache has the prepared libraries (a sample's median %.1f ms)",
median);
return;
}
if (!resume && same && strikes >= 2 && strcmp(mode, "force")) {
rt_log("vk_pipeline: library warm: the prepared libraries are cold again (a sample's median %.0f ms) after two "
"finished warms on this driver: it does not keep them, so pipelines compile when a draw first needs "
"them (NX2NAVI_LIB_WARM=force warms anyway).%s", median,
lw_cold_hint(LW.vendor, LW_COLD_GAVE_UP, LW_ON_WINDOWS));
return;
}
LW.strikes = resume ? strikes : same && !strcmp(was, "done") ? strikes + 1 : 0;
lw_stamp("warming", LW.strikes);
int cpus = (int)plat_cpu_usable(), t = cpus / 4;
t = t < 2 ? 2 : t > LW_MAX_THREADS ? LW_MAX_THREADS : t;
if (resume)
rt_log("vk_pipeline: library warm: going on with the warm an earlier run began: %zu prepared variants on %d "
"idle threads", LW.n, t);
else // cold right after a finished warm on this driver says more than a first cold (vk_warm_hint.h)
rt_log("vk_pipeline: library warm: the driver's cache does not have the prepared libraries (a sample's median "
"%.0f ms; a driver update or a change of what they are built with): building %zu prepared variants "
"into it on %d idle threads while the game runs. Until it is done, a pipeline a draw first needs may "
"compile, which skips that draw for some frames or makes it wait.%s", median, LW.n, t,
lw_cold_hint(LW.vendor,
median <= LW_COLD_MS ? LW_WARM_FORCED // warm, but lib_warm = force
: same && !strcmp(was, "done") ? LW_COLD_AFTER_WARM : LW_COLD_FIRST,
LW_ON_WINDOWS));
LW.t0 = now_ms();
LW.running = t;
int started = 0;
for (int i = 0; i < t; i++)
if ((LW.th[1 + started] = plat_thread_new(lw_worker, NULL, "lib warm"))) started++;
LW.threads = started;
if (plat_add32(&LW.running, started - t) == 0 && !started) rt_log("vk_pipeline: library warm: no thread started");
}
// Boot, once the library state is known (lb_state): the prepared list is read and the check starts on its own thread.
static void lw_start(void) {
const char *mode = cfg_value("NX2NAVI_LIB_WARM");
if (!P.gpl || !strcmp(mode, "0") || plat_xchg32(&LW.started, 1)) return;
char dir[PLAT_PATH_MAX * 2], path[PLAT_PATH_MAX * 2 + 32];
if (!cache_dir(dir, sizeof dir)) return;
snprintf(path, sizeof path, "%s/shader-prepared.bin", dir);
snprintf(LW.stamp, sizeof LW.stamp, "%s/shader-warm.txt", dir);
size_t n = 0;
uint8_t *f = gfx_shader_prepared_read(path, &n, NULL, NULL); // the records alone, whichever version the file is
LW.e = n ? calloc(n, sizeof *LW.e) : NULL;
for (size_t i = 0; LW.e && i < n; i++)
memcpy(LW.e[i].iid, f + PRP_REC * i + 4, 32), LW.e[i].stage = f[PRP_REC * i], LW.e[i].prio = f[PRP_REC * i + 1];
free(f);
if (!LW.e) return; // nothing prepared (a build without the preparation step)
LW.n = n;
VkPhysicalDeviceProperties pp;
vkGetPhysicalDeviceProperties(vk_ctx()->phys, &pp);
snprintf(LW.id, sizeof LW.id, "%04x:%04x:%08x", pp.vendorID, pp.deviceID, pp.driverVersion);
LW.vendor = pp.vendorID;
LW.th[0] = plat_thread_new(lw_main, NULL, "lib warm check");
}
static void lw_stop(void) {
plat_xchg32(&LW.stop, 1);
for (int i = 0; i <= LW_MAX_THREADS; i++)
if (LW.th[i]) plat_thread_join(LW.th[i]), LW.th[i] = NULL;
}
// Boot: compute shaders of the warm list (recorded and prepared), those whose pipeline stays first, then the first
// KEEP recorded graphics variants.
static void lb_start(const uint32_t pr_hist[16], const uint32_t fs_hist[4]) {
const char *e = cfg_value("NX2NAVI_LIB_PREBUILD"), *ke = cfg_get("NX2NAVI_LIB_PREBUILD_KEEP");
const char *te = cfg_get("NX2NAVI_LIB_PREBUILD_THREADS"); // no fixed default: 2 when unset (1 under 4 CPUs)
const char *be = cfg_get("NX2NAVI_LIB_PREBUILD_BIG"); // SPIR-V words; LB_BIG_WORDS when unset, 0 no limit
if (!A.on || !cfg_bool("NX2NAVI_LIB_PREBUILD")) return;
lb_state(pr_hist, fs_hist); // the libraries' state, also for lw_start
lw_start();
LB.big_words = be && *be ? strtoull(be, NULL, 10) : LB_BIG_WORDS;
char dir[PLAT_PATH_MAX * 2];
if (cache_dir(dir, sizeof dir)) skip_init(dir);
GfxWarmEnt *all = NULL;
size_t total = gfx_shader_warm_list(&all), n = 0, n_cs = 0;
LB.keep_n = ke && *ke ? atoi(ke) : 0;
LB.e = malloc((total + 1) * sizeof *LB.e);
// the compute shaders whose pipeline stays come first: they are the ones the game is about to dispatch
uint8_t *kf = total ? malloc(total) : NULL;
for (size_t i = 0; LB.e && kf && i < total; i++)
if (all[i].stage == GFX_STAGE_CS && (kf[i] = (uint8_t)cu_keep(all[i].iid))) LB.e[n++] = all[i];
LB.n_keep = n;
for (size_t i = 0; LB.e && i < total; i++)
if (all[i].stage == GFX_STAGE_CS && !(kf && kf[i])) LB.e[n++] = all[i];
free(kf);
n_cs = n;
for (size_t i = 0; LB.e && i < total && (int32_t)(n - n_cs) < LB.keep_n; i++)
if (all[i].stage != GFX_STAGE_CS && all[i].prio == 0) LB.e[n++] = all[i];
free(all);
if (atoi(e) > 1 && (size_t)atoi(e) < n) n = (size_t)atoi(e); // a count above 1 caps the list; 1 is plain on
if (LB.n_keep > n) LB.n_keep = n;
if (!n) {
free(LB.e), LB.e = NULL;
return;
}
LB.n = n;
LB.idle = 1;
int cpus = (int)plat_cpu_usable();
// Two threads by default, not half the CPUs. On a 32-thread CPU with NVIDIA's driver, 16 builders kept the driver's
// lock busy: 77% of the process's running time was a spin loop in nvoglv64.dll (about 8 cores), the prebuild had not
// finished after 34,000 frames, and lib_prebuild = 0 took it to 5% and the game to its frame limit (RTX 4080 SUPER,
// driver 617.14, 2026-11-08). With the driver's disk cache warm (--prepare-shaders, --warm-shaders) a build takes
// well under a millisecond, so two threads still finish the list quickly.
// The two take the shaders dispatched most recently (e[0..n_keep)); one goes on with the rest (lb_next), which is
// only built into the caches. Where the rest still runs during play, two builders there cost frames: one against
// two for the whole list, one 20 s route each on an RTX 5090, Castle Town 76.1 against 73.7 fps with 1 frame over
// 25 ms against 6, Hyrule Field 85.7 against 80.7 fps. NX2NAVI_LIB_PREBUILD_THREADS keeps its count for the whole
// list.
int t = te && *te ? atoi(te) : (cpus < 4 ? 1 : 2); // idle priority; a cold driver takes ~2 s per large compute shader
LB.tail_one = !(te && *te);
t = t < 1 ? 1 : t > LB_MAX_THREADS ? LB_MAX_THREADS : t;
if ((size_t)t > n) t = (int)n;
LB.t0 = now_ms();
LB.running = t;
int started = 0;
for (int i = 0; i < t; i++)
if ((LB.th[started] = plat_thread_new(lb_worker, (void *)(intptr_t)started, "lib prebuild"))) started++;
plat_add32(&LB.running, started - t);
LB.threads = started;
char big[128];
if (!LB.big_words) snprintf(big, sizeof big, "compute shaders of every size");
else snprintf(big, sizeof big, "compute shaders over %llu SPIR-V words %s", (unsigned long long)LB.big_words,
P.cache_control ? "only from the pipeline caches" : "left to their first dispatch");
rt_log("vk_pipeline: stage library prebuild: %zu compute pipelines, of which %zu stay (the shaders dispatched most "
"recently, NX2NAVI_COMPUTE_KEEP) and the others are built into the pipeline cache and destroyed, to be created "
"at their first dispatch; %zu recorded variants' libraries (NX2NAVI_LIB_PREBUILD_KEEP %d); on %d idle-priority "
"threads; library state clamp %u, polygon mode %u, depth -w..w %u, samples %u; %s (NX2NAVI_LIB_PREBUILD_BIG)",
n_cs < n ? n_cs : n, LB.n_keep, n > n_cs ? n - n_cs : 0, LB.keep_n, started, LB.clamp, LB.poly, LB.minus_w,
LB.samples, big);
if (LB.tail_one && started > 1)
rt_log("vk_pipeline: stage library prebuild: after the %zu recent compute pipelines, one worker continues the "
"remaining list; NX2NAVI_LIB_PREBUILD_THREADS overrides this background limit", LB.n_keep);
}
static void lb_stop(void) {
lw_stop();
plat_xchg32(&LB.stop, 1);
for (int i = 0; i < LB.threads; i++)
if (LB.th[i]) plat_thread_join(LB.th[i]), LB.th[i] = NULL;
LB.threads = 0;
if (BG.th) plat_thread_join(BG.th), BG.th = NULL; // after its current build, if one runs
}
// Preparation: one prepared variant at a time into the driver's disk cache (private shader copy, nothing kept). arg is
// the worker's journal slot.
static void lb_warm_worker(void *arg) {
int slot = (int)(intptr_t)arg;
for (;;) {
size_t i = (size_t)plat_inc64(&LB.next) - 1;
if (i >= LB.n) return;
int owned = 0, skip = skip_listed(LB.e[i].iid);
const GfxShader *sh = skip ? NULL : gfx_shader_load_private(LB.e[i].iid, &owned);
int st = sh ? gfx_shader_stage(sh) : -1;
if (sh) journal_mark(slot, &LB.e[i]); // from here to its clearing below, a crash names this variant
if (skip) {
skip_count();
} else if (!sh) {
plat_mutex_lock(&LB.stat);
LB.noshader++;
plat_mutex_unlock(&LB.stat);
} else if (st == GFX_STAGE_CS) {
Bindings B = {0};
const char *why = NULL;
lb_lock();
Layout *l = compute_layout(sh, &B, &why);
lb_unlock();
double t0 = now_ms();
VkPipeline pipe = l ? compute_create(sh, l, 0) : VK_NULL_HANDLE;
if (pipe) vkDestroyPipeline(P.device, pipe, NULL);
lb_count(2, pipe ? 3 : 2, now_ms() - t0);
} else if (st == GFX_STAGE_VS || st == GFX_STAGE_FS) {
if (P.gpl) lb_library(sh, st == GFX_STAGE_VS, 0);
else lb_skip_lib();
}
if (sh) journal_mark(slot, NULL);
if (owned) gfx_shader_release_private(sh);
plat_mutex_lock(&LB.stat);
uint64_t d = ++LB.done;
plat_mutex_unlock(&LB.stat);
if (d % 4000 == 0) lb_report("preparation progress");
}
}
// Preparation: every variant of <dir>/shader-prepared.bin on every core at normal priority.
static void lb_warm_prepared(const char *dir) {
char path[PLAT_PATH_MAX * 2 + 32];
snprintf(path, sizeof path, "%s/shader-prepared.bin", dir);
size_t n = 0;
uint8_t *f = gfx_shader_prepared_read(path, &n, NULL, NULL); // the records alone, whichever version the file is
LB.e = n ? calloc(n, sizeof *LB.e) : NULL;
for (size_t i = 0; LB.e && i < n; i++)
memcpy(LB.e[i].iid, f + PRP_REC * i + 4, 32), LB.e[i].stage = f[PRP_REC * i], LB.e[i].prio = f[PRP_REC * i + 1];
free(f);
if (!LB.e) return;
skip_init(dir);
char jp[PLAT_PATH_MAX * 2 + 48];
snprintf(jp, sizeof jp, "%s/shader-prepare-journal.bin", dir);
SK.journal = plat_fs_create(jp, NULL); // skip_init removed a leftover one
if (!SK.journal) rt_log("vk_pipeline: cannot create %s: a driver crash in this run will not be recorded", jp);
LB.n = n, LB.idle = 0, LB.t0 = now_ms();
static const uint32_t no_pr[16], no_fs[4];
lb_state(no_pr, no_fs);
int cpus = (int)plat_cpu_usable(), k = 0;
PlatThread *t[JR_SLOTS];
for (int i = 0; i < cpus && i < JR_SLOTS; i++)
if ((t[k] = plat_thread_new(lb_warm_worker, (void *)(intptr_t)k, "lib warm"))) k++;
if (!k) lb_warm_worker((void *)(intptr_t)0);
// every 2 s a count on stdout for tools/make_game.py (step_shaders), as gfx_shader.c prepare_files prints for the
// translation: a driver building for a long time still shows the step moving
for (int i = 0; i < k; i++)
while (!plat_thread_join_ns(t[i], 2000000000u)) {
plat_mutex_lock(&LB.stat);
uint64_t d = LB.done;
plat_mutex_unlock(&LB.stat);
printf("prepare-shaders: progress build %llu %zu\n", (unsigned long long)d, n);
fflush(stdout);
}
printf("prepare-shaders: progress build %zu %zu\n", n, n);
fflush(stdout);
if (SK.journal) plat_file_close(SK.journal), SK.journal = NULL, plat_file_delete(jp); // every build returned
lb_report("preparation done");
// with gpl off only compute variants are built: say so, rather than count the skipped ones as built
unsigned long long skipped = LB.skipped;
size_t tried = n - (size_t)skipped;
if (LB.nolib)
printf("prepare-shaders: %zu variants on %d threads in %.0f ms, %llu vertex/fragment variants skipped (graphics "
"pipeline library off, they build at their first draw)\n", tried, k, now_ms() - LB.t0,
(unsigned long long)LB.nolib);
else
printf("prepare-shaders: %zu variants built into the driver cache on %d threads in %.0f ms\n", tried, k,
now_ms() - LB.t0);
if (skipped) // installer/steps.py Prepare reads this line
printf("prepare-shaders: %llu variants skipped: the graphics driver crashed while building them in an earlier "
"run (shader-prepare-skip.txt); they build at their first draw\n", skipped);
free(LB.e), LB.e = NULL, LB.n = 0, LB.next = 0, LB.done = 0, LB.skipped = 0;
}
// ---- the shared pipeline list (tools/pipeline_list.py; seed-pipeline-keys.bin next to the game)
// tools/prepare_shaders.py writes the list's key records there, in the layout of pipeline-keys.bin, after putting the
// shader variants they name into the seed. The preparation builds them once, after the seed is translated: each
// pipeline goes through the pipeline cache and so into pipeline-cache.bin and the driver's own disk cache, and is
// dropped with the process. Nothing is added to this folder's pipeline-keys.bin: the boot keeps building only what
// this install drew, and a listed pipeline costs no start-up time or video memory until a draw meets it. That draw
// then finds its pipeline in the caches: a few milliseconds for a mesh pipeline, which has no stage libraries and
// otherwise leaves its draws out of the picture for its whole compile (0.2 to 1.1 s each on a first visit of a forest,
// 18 of them).
// NX2NAVI_PREPARE_PIPELINES: mesh (default) builds the list's mesh pipelines only; all, every record; 0, none. A
// vertex pipeline is linked from its prepared stage libraries in 2 to 6 ms when a draw misses it, and its module
// depends on the vertex output union of the pairs this install has recorded (vs_outs), so one built here is not
// certain to be the one a draw asks for. Without the graphics pipeline library (vk_caps_gpl: AMD's Windows driver for
// RDNA 2 and older) neither holds: a vertex pipeline a draw misses is compiled whole while that draw is left out, and
// its vertex outputs come from its own pair (no independent layouts), so a listed one is the one the draw asks for.
// There mesh builds every record too.
// A driver whose compiler crashes on one of these would crash at every preparation: seed-pipelines-running.txt marks
// the step, and a run that finds the marker of a run that died skips the step for good on this folder (the pipelines
// are then built at their first draw, as without a list) until the marker is deleted.
static int cache_save(uint32_t wait_ms, int exiting);
static int seed_pipelines(const char *dir) {
const char *mode = cfg_value("NX2NAVI_PREPARE_PIPELINES");
int all = !strcmp(mode, "all") || (!strcmp(mode, "mesh") && !P.gpl);
char path[PLAT_PATH_MAX * 2 + 48], mark[PLAT_PATH_MAX * 2 + 48];
snprintf(path, sizeof path, "%s/seed-pipeline-keys.bin", dir);
snprintf(mark, sizeof mark, "%s/seed-pipelines-running.txt", dir);
FILE *f = fopen(path, "rb");
if (!f) return 0; // no list: nothing to say
if (!strcmp(mode, "0")) {
fclose(f);
printf("prepare-pipelines: off (NX2NAVI_PREPARE_PIPELINES=0)\n");
return 0;
}
if (plat_file_exists(mark)) {
fclose(f);
printf("prepare-pipelines: skipped: an earlier preparation ended while it built the listed pipelines (%s); they "
"are built at their first draw\n", mark);
return 0;
}
KeysHeader h;
fseek(f, 0, SEEK_END);
long len = ftell(f);
fseek(f, 0, SEEK_SET);
size_t n = 0;
KeyRec *r = NULL;
if (len >= (long)sizeof h && fread(&h, sizeof h, 1, f) == 1 && !memcmp(h.magic, "NX2NPKEY", 8) &&
h.version == KEYS_VERSION && h.rec_size == sizeof(KeyRec)) {
n = ((size_t)len - sizeof h) / sizeof(KeyRec);
r = malloc(n * sizeof *r + 1);
if (r && fread(r, sizeof *r, n, f) != n) n = 0;
} else {
printf("prepare-pipelines: %s is a list for another version of the game's runtime (its record layout differs); "
"no pipeline is built from it\n", path);
}
fclose(f);
uint8_t (*ids)[32] = n ? malloc(3 * n * 32) : NULL;
const GfxShader **sh = n ? calloc(3 * n, sizeof *sh) : NULL;
if (!r || !ids || !sh) {
free(r), free(ids), free(sh);
return 0;
}
double t0 = now_ms();
size_t m = 0, u = 0;
for (size_t i = 0; i < n; i++) {
memcpy(ids[m++], r[i].vs, 32);
if (r[i].has_fs) memcpy(ids[m++], r[i].fs, 32);
if (r[i].has_ts) memcpy(ids[m++], r[i].ts, 32);
}
qsort(ids, m, 32, id_cmp);
for (size_t i = 0; i < m; i++)
if (!u || memcmp(ids[u - 1], ids[i], 32)) memcpy(ids[u++], ids[i], 32);
int cpus = (int)plat_cpu_usable();
const char *pe = cfg_get("NX2NAVI_PREWARM_XLATE");
int procs = pe && *pe ? atoi(pe) : cpus / 2 > 16 ? 16 : cpus / 2;
gfx_shader_prewarm((const uint8_t(*)[32])ids, u, sh, cpus, procs < 1 ? 1 : procs);
static const char note[] = "the listed pipelines are being built; a run that finds this file skips them\n";
plat_file_write_atomic(mark, note, sizeof note - 1);
uint32_t queued = 0, dropped = 0, known = 0, other = 0, mesh = 0, no_mesh = 0;
plat_mutex_lock(&P.lock);
uint32_t built0 = K.built, failed0 = K.failed;
K.loading = 1;
for (size_t i = 0; i < n; i++) {
const uint8_t (*v)[32] = bsearch(r[i].vs, ids, u, 32, id_cmp);
const uint8_t (*fi)[32] = r[i].has_fs ? bsearch(r[i].fs, ids, u, 32, id_cmp) : NULL;
const uint8_t (*t)[32] = r[i].has_ts ? bsearch(r[i].ts, ids, u, 32, id_cmp) : NULL;
const GfxShader *vs = v ? sh[v - ids] : NULL, *fs = fi ? sh[fi - ids] : NULL, *ts = t ? sh[t - ids] : NULL;
if (!vs || (r[i].has_fs && !fs) || (r[i].has_ts && !ts)) {
dropped++;
continue;
}
VkPipeState s = r[i].s;
s.vs = vs, s.fs = fs, s.ts = ts;
if (!is_mesh(&s) && !all) {
other++;
continue;
}
if (is_mesh(&s) && !vk_ctx()->has_mesh_shader) { // this GPU draws no mesh pipeline
no_mesh++;
continue;
}
if (!is_mesh(&s) && P.indep && fs && gfx_shader_stage(vs) == GFX_STAGE_VS) { // its vertex output union, as the boot
GfxShaderIo io[32];
vs_outs(vs, fs, io);
}
GKey k;
gkey_fill(&s, &k);
uint64_t hk = key_hash(&k, sizeof k);
int q = 0;
if (pipe_find(&k, sizeof k, hk)) known++;
else pipe_miss(&s, &k, hk, 2, &q), queued += (uint32_t)q, mesh += q && is_mesh(&s);
}
K.loading = 0;
K.until = 0; // every one of them: no boot waits for this
GATE.off = 1; // and no game runs beside it: the memory gate (gate_enter) refuses nothing
plat_mutex_unlock(&P.lock);
free(ids), free(sh), free(r);
PlatThread *th[64];
int nt = 0;
if (queued) warm_builder((void *)1); // the first alone (warm_builder)
for (int i = 0; i < cpus && i < 64 && queued; i++)
if ((th[nt] = plat_thread_new(warm_builder, NULL, "pipe list"))) nt++;
if (!nt && queued) warm_builder(NULL);
for (int i = 0; i < nt; i++)
while (!plat_thread_join_ns(th[i], 2000000000u)) {
printf("prepare-pipelines: progress %u %u\n",
__atomic_load_n(&K.built, __ATOMIC_RELAXED) + __atomic_load_n(&K.failed, __ATOMIC_RELAXED) - built0 - failed0,
queued);
fflush(stdout);
}
plat_mutex_lock(&P.lock);
while (K.left) plat_cond_wait(&done_cv, &P.lock); // those a pool worker took
uint32_t built = K.built - built0, failed = K.failed - failed0;
plat_mutex_unlock(&P.lock);
int saved = cache_save(5000, 0) > 0; // explicit preparation waits for a moment with no build running
plat_file_delete(mark);
char msg[640];
snprintf(msg, sizeof msg, "prepare-pipelines: %zu listed pipelines: %u built (%u mesh), %u failed, %u the same as one "
"built before, %u without a shader this game version or its shader cache has, %u vertex pipelines left to "
"their first draw (NX2NAVI_PREPARE_PIPELINES=%s), %u mesh pipelines this GPU cannot draw; on %d threads in "
"%.0f ms; pipeline cache %s", n, built, mesh, failed, known, dropped, other,
!strcmp(mode, "all") ? "all" : all ? "mesh, every record without the graphics pipeline library" : "mesh", no_mesh,
nt ? nt : 1, now_ms() - t0,
saved ? "saved" : "not saved (builds kept running, or its file could not be written)");
printf("%s\n", msg);
fflush(stdout);
rt_log("vk_pipeline: %s", msg);
fb_log("after the listed pipelines");
return 0;
}
// nx2navi.exe --prepare-pipelines: the list's pipelines alone, for a folder whose shaders are prepared already.
int vk_pipeline_prepare_pipelines(void) {
prep_mode = 1; // builds into pipeline-cache.bin alone (cache_load)
if (!ensure_init()) {
fprintf(stderr, "prepare-pipelines: no Vulkan device\n");
return 1;
}
char dir[PLAT_PATH_MAX * 2];
if (!cache_dir(dir, sizeof dir)) return 1;
char path[PLAT_PATH_MAX * 2 + 48];
snprintf(path, sizeof path, "%s/seed-pipeline-keys.bin", dir);
if (!plat_file_exists(path)) {
fprintf(stderr, "prepare-pipelines: no %s (tools/prepare_shaders.py writes it from the pipeline list)\n", path);
return 1;
}
return seed_pipelines(dir);
}
// nx2navi.exe --prepare-shaders (tools/prepare_shaders.py): the device sets the translator's host answers (they are in every
// cache key), the seed next to nx2navi.exe is translated on every core, then every prepared variant's stage library or
// compute pipeline is built once into the driver's disk cache (lb_warm_prepared). No game runs.
int vk_pipeline_prepare_shaders(void) {
prep_mode = 1; // builds into pipeline-cache.bin alone (cache_load)
if (!ensure_init()) {
fprintf(stderr, "prepare-shaders: no Vulkan device\n");
return 1;
}
char dir[PLAT_PATH_MAX * 2];
if (!cache_dir(dir, sizeof dir)) return 1;
int cpus = (int)plat_cpu_usable();
const char *pe = cfg_get("NX2NAVI_PREWARM_XLATE"); // no fixed default: half the CPUs, at most 16, when unset
int procs = pe && *pe ? atoi(pe) : cpus / 2 > 16 ? 16 : cpus / 2;
int rc = gfx_shader_prepare_seed(dir, cpus, procs < 1 ? 1 : procs);
// The listed pipelines before the stage libraries: the pipeline cache is saved with them in it and without the
// tens of thousands of libraries the next step builds, which would take the file past its size limit.
if (!rc) seed_pipelines(dir);
if (!rc) lb_warm_prepared(dir);
return rc;
}
// nx2navi.exe --warm-shaders: every variant of shader-prepared.bin built into the driver's disk cache again from the shader
// cache, without translating the seed. The AMD cache is keyed to the exe path (a relink keeps it: 29,587 variants in
// 1.8 s) and holds a limited amount: preparing another build dir evicted this one's libraries, and a library a draw
// needed then took 40-220 ms in play instead of 0.2 ms. Seconds when the libraries are still there.
int vk_pipeline_warm_shaders(void) {
prep_mode = 1; // builds into pipeline-cache.bin alone (cache_load)
if (!ensure_init()) {
fprintf(stderr, "warm-shaders: no Vulkan device\n");
return 1;
}
char dir[PLAT_PATH_MAX * 2];
if (!cache_dir(dir, sizeof dir)) return 1;
char prp[PLAT_PATH_MAX * 2 + 32];
snprintf(prp, sizeof prp, "%s/shader-prepared.bin", dir);
if (!plat_file_exists(prp)) {
fprintf(stderr, "warm-shaders: no %s (tools/prepare_shaders.py writes it)\n", prp);
return 1;
}
seed_pipelines(dir); // before the libraries, as in vk_pipeline_prepare_shaders
lb_warm_prepared(dir);
return 0;
}
// nx2navi.exe --probe-prewarm: the boot's shader and pipeline stages without the game, from the caches next to
// nx2navi.exe (pipeline-cache.bin, pipeline-keys.bin, shader-answers.bin, the shader cache): the pipeline cache load,
// the shader prewarm, the recorded pipelines and the compute pipeline prebuild, each followed by vk_mem_stage_log's
// line, so the log tells what every stage leaves in device memory and in the process's commit. Then up to
// PROBE_DISPATCHES compute shaders whose pipeline the boot did not keep are asked for as a dispatch would
// (gfx_shader_load_input for the shader, then vk_pipeline_compute), which gives what a first dispatch costs on this
// machine and how many of them the pipeline cache held. Fails when one of those pipelines is not created, when the
// prebuild kept a pipeline it should not have, when a large compute shader went to the background builder although no
// run dispatched it, or when the record of dispatched shaders, written to a file of the probe's own and read back,
// does not hold the probe's dispatches. compute-used.bin itself stays as it is; the pipeline cache is saved as after
// a boot.
#define PROBE_DISPATCHES 256
int vk_pipeline_probe_prewarm(void) {
vk_pipeline_async_enable();
CU.quiet = 1;
vk_pipeline_prewarm(); // brings the device up itself: it has to own the key file before ensure_init runs
if (!P.ready) {
printf("probe-prewarm: SKIP (the boot prewarm brought no Vulkan device up: none on this machine, NX2NAVI_GPU=null "
"or NX2NAVI_PIPE_SYNC=1)\n");
return 0;
}
while (__atomic_load_n(&LB.running, __ATOMIC_ACQUIRE) > 0) plat_sleep_ms(50);
if (!LB.n) {
printf("probe-prewarm: SKIP (no recorded compute shaders next to the executable: %s)\n", P.cache_path);
return 0;
}
VkPipeStats st;
vk_pipeline_stats(&st);
plat_mutex_lock_shared(&LB.stat);
uint64_t kept = LB.built[2], cache_only = LB.warmed[2], failed = LB.fail[2], there = LB.have[2];
uint64_t big_unused = LB.big_unused;
size_t queued = BG.n;
plat_mutex_unlock_shared(&LB.stat);
printf("probe-prewarm: %zu compute shaders, %zu to keep: %llu kept, %llu built into the pipeline cache only, %llu "
"there already, %llu failed, %llu large ones not built (no run dispatched them), %zu large ones for the "
"background builder; %llu graphics pipelines\n", LB.n, LB.n_keep, (unsigned long long)kept,
(unsigned long long)cache_only, (unsigned long long)there, (unsigned long long)failed,
(unsigned long long)big_unused, queued, (unsigned long long)st.graphics);
int fails = kept > LB.n_keep; // a dispatch never waits in this mode, so nothing else may have been kept
if (fails) printf("probe-prewarm: FAIL: %llu pipelines kept, %zu expected at most\n", (unsigned long long)kept, LB.n_keep);
if (queued > LB.n_keep) // only a shader a run dispatched may be built ahead when it is large
fails++, printf("probe-prewarm: FAIL: %zu large compute shaders queued for the background builder, %zu kept "
"shaders at most could be among them\n", queued, LB.n_keep);
uint32_t asked = 0, got = 0;
double t_all = 0, t_max = 0, t_load = 0;
for (size_t i = LB.n_keep; i < LB.n && asked < PROBE_DISPATCHES; i++) {
if (LB.e[i].stage != GFX_STAGE_CS) continue;
double l0 = now_ms();
const GfxShader *sh = gfx_shader_load_input(LB.e[i].iid, 0); // from the disk: the boot released its copy
double load = now_ms() - l0;
uint64_t words = 0;
if (!sh || gfx_shader_stage(sh) != GFX_STAGE_CS) continue;
gfx_shader_spirv(sh, &words);
if (LB.big_words && words > LB.big_words) continue; // a first dispatch of one compiles it: not what is timed here
const VkPipeInfo *info = NULL;
double t0 = now_ms();
VkPipeline p = vk_pipeline_compute(sh, &info);
double ms = now_ms() - t0;
asked++, got += p != VK_NULL_HANDLE;
t_all += ms, t_load += load;
if (ms > t_max) t_max = ms;
}
vk_mem_stage_log("the probe's first dispatches");
plat_mutex_lock_shared(&P.lock);
uint64_t hit = CF.hit;
plat_mutex_unlock_shared(&P.lock);
printf("probe-prewarm: %u first dispatches of pipelines the boot did not keep: %u created, avg %.2f ms, longest %.2f "
"ms, the pipeline cache held %llu of them; reading their shaders took avg %.2f ms\n", asked, got,
asked ? t_all / asked : 0.0, t_max, (unsigned long long)hit, asked ? t_load / asked : 0.0);
if (got != asked) fails++, printf("probe-prewarm: FAIL: %u pipelines were not created\n", asked - got);
if (CU.opened) {
char rp[sizeof CU.path + 8];
snprintf(rp, sizeof rp, "%s.probe", CU.path);
uint64_t rsize = 0;
CuHeader rh = {0};
uint32_t mine = 0;
int wrote = cu_write(rp);
uint8_t *rf = wrote ? plat_file_read(rp, &rsize) : NULL;
long rn = cu_parse(rf, rsize, &rh);
for (long i = 0; i < rn; i++) {
CuRec r;
memcpy(&r, rf + sizeof rh + (size_t)i * sizeof r, sizeof r);
mine += r.run == CU.run;
}
free(rf);
plat_file_delete(rp);
printf("probe-prewarm: the record of dispatched compute shaders: %ld entries, %u of this run (run %u)\n", rn, mine,
CU.run);
if (!wrote || rn < 0 || rh.run != CU.run || mine != got)
fails++, printf("probe-prewarm: FAIL: the record does not hold the %u shaders dispatched here\n", got);
}
lb_stop();
vk_pipeline_save();
printf("probe-prewarm: %s\n", fails ? "FAILED" : "passed");
return fails ? 1 : 0;
}
// ---- dynamic state
void vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s) {
VkDynShadow sh;
uint64_t skipped = 0;
sh.have = 0; // nothing known: every call is recorded
vk_pipeline_set_dynamic_delta(cb, s, &sh, 0, &skipped);
}
// Every graphics pipeline the replay binds lists the same dynamic states (P.dyn; mesh pipelines leave out topology,
// primitive restart and vertex input), so a value recorded into a command buffer holds across pipeline binds until it
// is set again. Each call is recorded only when its value differs from the one *sh says the command buffer has: a new
// pipeline usually changes the shaders and a field or two, not the twenty states this used to record with every bind.
int vk_pipeline_set_dynamic_delta(VkCommandBuffer cb, const VkPipeState *s, VkDynShadow *sh, int mesh, uint64_t *skipped) {
if (!ensure_init()) return 0;
const poly_st *pl = &s->poly;
const ds_st *d = &s->ds;
int core = (sh->have & VK_DYN_CORE) != 0, vtx = !mesh && (sh->have & VK_DYN_VTX), blend = (sh->have & VK_DYN_BLEND) != 0;
int n = 0, left = 0;
#define DYN(known, field, val, call) \
do { \
if ((known) && sh->field == (val)) left++; \
else sh->field = (val), call, n++; \
} while (0)
// The front face maps unchanged: NX2NAVI sets the upper-left window origin, and the y-flipped viewport (y_flip) puts
// NDC +y at row 0 as NVN does, so the framebuffer winding Vulkan tests is the one NVN tests
// (a lower-left origin would need it mirrored).
int front_cw = pl->front == NVN_FRONT_FACE_CW;
VkCullModeFlags cull = (VkCullModeFlags)(pl->cull & 3); // NVN_FACE_* has VkCullModeFlagBits' values
VkFrontFace front = front_cw ? VK_FRONT_FACE_CLOCKWISE : VK_FRONT_FACE_COUNTER_CLOCKWISE;
DYN(core, cull, cull, vkCmdSetCullMode(cb, cull));
DYN(core, front, front, vkCmdSetFrontFace(cb, front));
if (!mesh) {
VkPrimitiveTopology topo = vk_pipeline_topology(s->prim);
DYN(vtx, topo, topo, vkCmdSetPrimitiveTopology(cb, topo));
}
DYN(core, dt, (VkBool32)d->dt, vkCmdSetDepthTestEnable(cb, d->dt));
DYN(core, dw, (VkBool32)d->dw, vkCmdSetDepthWriteEnable(cb, d->dw));
DYN(core, dop, compare_op(d->df), vkCmdSetDepthCompareOp(cb, compare_op(d->df)));
DYN(core, st, (VkBool32)d->st, vkCmdSetStencilTestEnable(cb, d->st));
for (int f = 0; f < 2; f++) {
VkStencilOp o[3] = {stencil_op(f ? d->fail_b : d->fail_f), stencil_op(f ? d->zpass_b : d->zpass_f),
stencil_op(f ? d->zfail_b : d->zfail_f)};
VkCompareOp c = compare_op(f ? d->sf_b : d->sf_f);
if (core && sh->scmp[f] == c && !memcmp(sh->sop[f], o, sizeof o)) {
left++;
continue;
}
vkCmdSetStencilOp(cb, f ? VK_STENCIL_FACE_BACK_BIT : VK_STENCIL_FACE_FRONT_BIT, o[0], o[1], o[2], c);
sh->scmp[f] = c, memcpy(sh->sop[f], o, sizeof o), n++;
}
// NVN_POLYGON_OFFSET_ENABLE_* bit of the polygon mode in use: point 1, line 2, fill 4
unsigned mode = pl->mode <= NVN_POLYGON_MODE_FILL ? pl->mode : NVN_POLYGON_MODE_FILL;
VkBool32 bias_en = (pl->offset_en >> mode & 1) != 0, discard = s->raster_discard != 0, restart = s->prim_restart != 0;
DYN(core, bias_en, bias_en, vkCmdSetDepthBiasEnable(cb, bias_en));
DYN(core, discard, discard, vkCmdSetRasterizerDiscardEnable(cb, discard));
if (!mesh) DYN(vtx, restart, restart, vkCmdSetPrimitiveRestartEnable(cb, restart));
if (!mesh && P.dyn_vi && s->vs) {
VtxInput v;
vertex_input(s, &v);
if (vtx && sh->vi.n_attr == v.n_attr && sh->vi.n_bind == v.n_bind &&
!memcmp(sh->vi.a, v.a, sizeof v.a[0] * v.n_attr) && !memcmp(sh->vi.b, v.b, sizeof v.b[0] * v.n_bind)) {
left++;
} else {
rec_vertex_input(P.set_vi, cb, v.n_bind, v.b, v.n_attr, v.a);
sh->vi = v, n++;
}
} else if (!mesh) {
sh->vi.n_attr = UINT32_MAX; // not recorded: unknown to the next draw
}
if (P.dyn_clamp) {
VkBool32 clamp = s->depth_clamp != 0;
DYN(core, clamp, clamp, rec_bool(P.set_clamp, cb, clamp));
}
if (P.dyn_poly) {
VkPolygonMode pm = polygon_mode(pl->mode);
DYN(core, poly, pm, rec_polygon_mode(P.set_poly, cb, pm));
}
if (P.dyn_a2c) { // the same signature as the depth clamp call
VkBool32 a2c = s->ms.a2c != 0;
DYN(core, a2c, a2c, rec_bool((PFN_vkCmdSetDepthClampEnableEXT)P.set_a2c, cb, a2c));
}
#undef DYN
sh->have |= VK_DYN_CORE;
if (mesh) sh->have &= ~(uint32_t)VK_DYN_VTX;
else sh->have |= VK_DYN_VTX;
int nt = target_count(s);
if (nt) {
VkBool32 en[8];
VkColorBlendEquationEXT eq[8];
VkColorComponentFlags wm[8];
for (int i = 0; i < nt; i++) {
const blend_st *b = &s->blend[i];
en[i] = (s->color.blend_en >> i & 1) && blendable(target_format(s->color_fmt[i], NULL, NULL));
eq[i] = (VkColorBlendEquationEXT){blend_factor(b->src_rgb), blend_factor(b->dst_rgb), blend_op(b->eq_rgb),
blend_factor(b->src_a), blend_factor(b->dst_a), blend_op(b->eq_a)};
wm[i] = target_wmask(s, i);
}
// The calls write targets [0, nt); targets past nt keep what the command buffer had.
int known = blend && (uint32_t)nt <= sh->nt;
if (P.dyn_blend_en) {
if (known && !memcmp(sh->en, en, sizeof en[0] * nt)) left++;
else rec_blend_enable(P.set_blend_en, cb, 0, (uint32_t)nt, en), n++;
}
if (P.dyn_blend_eq) {
if (known && !memcmp(sh->eq, eq, sizeof eq[0] * nt)) left++;
else rec_blend_equation(P.set_blend_eq, cb, 0, (uint32_t)nt, eq), n++;
}
if (P.dyn_wmask) {
if (known && !memcmp(sh->wm, wm, sizeof wm[0] * nt)) left++;
else rec_write_mask(P.set_wmask, cb, 0, (uint32_t)nt, wm), n++;
}
memcpy(sh->en, en, sizeof en[0] * nt), memcpy(sh->eq, eq, sizeof eq[0] * nt), memcpy(sh->wm, wm, sizeof wm[0] * nt);
sh->nt = blend && sh->nt > (uint32_t)nt ? sh->nt : (uint32_t)nt;
sh->have |= VK_DYN_BLEND;
}
*skipped += (uint64_t)left;
return n;
}
// ---- statistics, persistence, teardown
void vk_pipeline_stats(VkPipeStats *s) {
plat_mutex_lock_shared(&P.lock);
*s = P.stats;
plat_mutex_unlock_shared(&P.lock);
}
static PlatMutex save_lock = PLAT_MUTEX_INIT; // one cache save at a time; shutdown takes it to destroy the cache
// Copies a pipeline cache's data into its file, with cache_rw held exclusively for the copy only (not the file write):
// P.play into pipeline-cache-play.bin when there is a play cache, else P.cache into pipeline-cache.bin; and P.cache
// once, with or without a play cache, after the play file was merged into it at the start (P.fold), which then
// removes the play file.
// Only when the data changed: the size of a cache's data is asked first, and a cache whose data has the size its file
// holds is neither copied nor written. A pipeline cache only gains entries, so the same size is the same data. Before,
// every save copied and wrote the whole cache whenever a pipeline had been created since the last one, also when the
// cache had held it: three saves of 358.1 MB in one run that added 0.3 MB (2026-11-11).
// The lock is only ever tried, never waited for: a waiting exclusive request would hold back every new build
// (a shared request queues behind it), so a pending autosave could stall the replay. The save at exit (exiting) holds
// new builds back instead (cache_saving_at_exit) and so only waits for the builds already running. wait_ms: how long to
// keep trying while builds run (0: one try). Returns 0 when no build-free moment came, -1 when the data could not be
// copied or its file not written (the log says which), 1 otherwise (saved, or nothing to save).
static int cache_save_one(int which, uint32_t wait_ms, int exiting) {
VkPipelineCache cache = which ? P.play : P.cache;
const char *path = which ? P.play_path : P.cache_path;
if (!cache) return 1;
uint64_t until = plat_now_ms() + wait_ms;
if (exiting) __atomic_store_n(&cache_saving_at_exit, 1, __ATOMIC_RELEASE);
while (!plat_mutex_trylock(&cache_rw)) {
if (plat_now_ms() >= until) {
if (exiting) __atomic_store_n(&cache_saving_at_exit, 0, __ATOMIC_RELEASE);
return 0;
}
plat_sleep_ms(2);
}
size_t size = 0;
void *data = NULL;
int sized = vkGetPipelineCacheData(P.device, cache, &size, NULL) == VK_SUCCESS && size;
int same = sized && size == P.saved[which];
int got = sized && !same && (data = malloc(size)) && vkGetPipelineCacheData(P.device, cache, &size, data) == VK_SUCCESS;
plat_mutex_unlock(&cache_rw);
// The held-back builds go on before the log line and the file write: one of them may hold a lock those wait for.
if (exiting) __atomic_store_n(&cache_saving_at_exit, 0, __ATOMIC_RELEASE);
static uint64_t skipped[2];
int failed = !sized || (!same && !got);
if (same) skipped[which]++;
if (failed) rt_log("vk_pipeline: %s not saved: no copy of its %.1f MB", path, size / 1048576.0);
if (got) {
uint64_t t0 = plat_now_ms();
if (!plat_file_write_atomic(path, data, size)) {
rt_log("vk_pipeline: cannot write %s (%.1f MB)", path, size / 1048576.0);
failed = 1;
} else {
uint64_t loaded = which ? P.play_loaded : P.cache_loaded;
rt_log("vk_pipeline: %s saved: %.1f MB (%+.1f MB since the boot) written in %llu ms; %llu saves before it were "
"skipped with the data unchanged", path, size / 1048576.0, ((double)size - (double)loaded) / 1048576.0,
(unsigned long long)(plat_now_ms() - t0), (unsigned long long)skipped[which]);
P.saved[which] = size, skipped[which] = 0;
if (!which && P.fold) { // the play file's content is in pipeline-cache.bin now
P.fold = 0;
plat_file_delete(P.play_path);
rt_log("vk_pipeline: %s removed: %s holds its pipelines", P.play_path, P.cache_path);
}
if (P.feedback) {
char when[96];
snprintf(when, sizeof when, "at a save of %.1f MB (%+.1f MB since the boot)", size / 1048576.0,
((double)size - (double)loaded) / 1048576.0);
fb_log(when);
}
}
}
free(data);
return failed ? -1 : 1;
}
// Both caches' saves (cache_save_one): 1 when each is done, 0 when one found no build-free moment, -1 when one failed.
static int cache_save(uint32_t wait_ms, int exiting) {
if (!P.ready || !P.cache || !P.cache_path[0]) return 1;
// Not under P.lock: holding P.lock through the copy and the file write (about 60 ms on the 1.4.3 fight) stopped
// every pipeline lookup of the replay, a freeze at each 20 s autosave.
plat_mutex_lock(&save_lock);
if (!P.cache) { // vk_pipeline_shutdown destroyed it
plat_mutex_unlock(&save_lock);
return 1;
}
int ok = 1, r;
if (!P.play || P.fold) ok = cache_save_one(0, wait_ms, exiting);
if (P.play && (r = cache_save_one(1, wait_ms, exiting)) < ok) ok = r; // the worse of the two
plat_mutex_unlock(&save_lock);
return ok;
}
// Builds still running after this long at a save that waits (exit, the shader preparation) leave the file as it was.
#define SAVE_WAIT_MS 5000
// Returns cache_save's answer.
static int save_with(uint32_t wait_ms, int exiting) {
if (P.ready) keys_flush(), pb_flush(), cu_flush(), cf_log();
if (P.ready && P.gpl) {
rt_log("vk_pipeline: gpl: %llu fast links (%llu by workers, %llu of those by the library workers for skipped draws; "
"%llu of pipelines with a feedback loop; avg %.2f ms, "
"max %.2f ms, %llu failed), %llu lookups by a draw found a stage library not made in this process yet and "
"left it to a worker (whether the driver's cache has it shows in the library times below), %llu optimised "
"pipelines swapped in (%llu queued "
"low after their fast link), %llu drawing-thread links over 5 ms; %llu waiting draws built their libraries "
"(avg %.2f ms, max %.2f ms)",
(unsigned long long)P.g.links, (unsigned long long)P.g.wlinks, (unsigned long long)P.g.flinks,
(unsigned long long)P.g.fb_links,
P.g.links ? P.g.link_ms / P.g.links : 0.0,
P.g.link_max, (unsigned long long)P.g.link_fail, (unsigned long long)P.g.defer,
(unsigned long long)P.g.swaps, (unsigned long long)P.g.requeued, (unsigned long long)P.g.slow,
(unsigned long long)P.g.dlinks, P.g.dlinks ? P.g.dlink_ms / P.g.dlinks : 0.0, P.g.dlink_max);
uint64_t rn = P.g.ready_n[0] + P.g.ready_n[1] + P.g.ready_n[2] + P.g.ready_n[3] + P.g.ready_n[4] + P.g.ready_n[5];
rt_log("vk_pipeline: gpl: pipelines a draw missed were usable after under 4 ms %llu, 4-8 ms %llu, 8-17 ms %llu, "
"17-50 ms %llu, 50-150 ms %llu, longer %llu (avg %.1f ms, longest %.0f ms); %llu of them had no fast link; "
"%llu were mesh pipelines (no stage libraries), %llu were named in pipeline-keys.bin and not prebuilt yet "
"(the others were new to this install), %llu had a shader translated in this session",
(unsigned long long)P.g.ready_n[0], (unsigned long long)P.g.ready_n[1], (unsigned long long)P.g.ready_n[2],
(unsigned long long)P.g.ready_n[3], (unsigned long long)P.g.ready_n[4], (unsigned long long)P.g.ready_n[5],
rn ? P.g.ready_ms / (double)rn : 0.0, P.g.ready_max, (unsigned long long)P.g.ready_nofast,
(unsigned long long)P.g.ready_mesh, (unsigned long long)P.g.ready_recorded,
(unsigned long long)P.g.ready_fresh);
if (P.g.hints)
rt_log("vk_pipeline: gpl: looking ahead named %llu pipeline states not known yet and queued %llu stage libraries "
"for them", (unsigned long long)P.g.hints, (unsigned long long)P.g.hint_libs);
if (A.first || A.taken)
rt_log("vk_pipeline: whole builds a draw waited for while they were still queued: %llu moved to the head of "
"the queue, %llu built by the waiting thread (a wait outside the frame's budget, no worker idle)",
(unsigned long long)A.first, (unsigned long long)A.taken);
if (U.waits || U.queued)
rt_log("vk_pipeline: gpl: library jobs for waiting draws: %llu queued, %llu built by a worker, %llu by the "
"thread that needed them (no worker free), %llu waits ended at their limit; %llu fast links on the %d "
"library workers",
(unsigned long long)U.queued, (unsigned long long)U.by_worker, (unsigned long long)U.stolen,
(unsigned long long)U.timeouts, (unsigned long long)U.links, U.lib_workers);
static const char *const kn[4] = {"vertex input", "pre-rasterization", "fragment shader", "fragment output"};
for (int i = 0; i < 4; i++)
rt_log("vk_pipeline: gpl: %s libraries %llu (avg %.2f ms, max %.2f ms; %llu of 25 ms or more, %.0f ms in all)",
kn[i], (unsigned long long)P.g.lib_n[i], P.g.lib_n[i] ? P.g.lib_ms[i] / P.g.lib_n[i] : 0.0, P.g.lib_max[i],
(unsigned long long)P.g.lib_slow[i], P.g.lib_slow_ms[i]);
if (P.g.pr_added)
rt_log("vk_pipeline: gpl: %llu of the pre-rasterization libraries had outputs added for their fragment shaders "
"(avg %.1f ms): another module than the one the preparation built, so the driver's cache has none of them",
(unsigned long long)P.g.pr_added, P.g.pr_added_ms / (double)P.g.pr_added);
if (LB.n)
rt_log("vk_pipeline: gpl: prebuilt libraries a draw used: pre-rasterization %llu, fragment shader %llu; "
"compute pipelines built on the replay thread %llu", (unsigned long long)P.g.pre_used[LIB_PR],
(unsigned long long)P.g.pre_used[LIB_FS], (unsigned long long)LB.cs_draw);
}
gfx_shader_record_flush(1);
if (P.ready && !P.cache) pb_log("at a save"); // with P.cache the line comes with its own save below
int saved = cache_save(wait_ms, exiting);
if (!saved && wait_ms)
rt_log("vk_pipeline: %s not saved: pipeline builds kept running for %u ms", P.play ? P.play_path : P.cache_path,
(unsigned)wait_ms);
return saved;
}
// The save after the shader preparation and at shutdown: it waits up to SAVE_WAIT_MS for a moment with no build running.
void vk_pipeline_save(void) {
save_with(SAVE_WAIT_MS, 0);
}
// The save as the process ends (window closed, the game's exit, a timed run's end): new builds wait while it waits up to
// SAVE_WAIT_MS for the running ones to finish (cache_saving_at_exit).
void vk_pipeline_save_exit(void) {
save_with(SAVE_WAIT_MS, 1);
}
// The game never shuts the device down (boot.c and the window end the process), so the cache is saved while it
// runs: at most every 20 s, and only after new pipelines were built or while a save that did not happen is still owed
// (vk_save_retry.h). New pipeline keys are appended every 2 s.
static void autosave_once(void) {
static uint64_t last_keys;
static SaveRetry sr;
if (!P.ready) return;
uint64_t now = plat_now_ms();
if (now - last_keys >= 2000) {
last_keys = now;
if (*(volatile uint32_t *)&K.pend_n) keys_flush();
pb_flush();
cu_flush();
}
gfx_shader_record_flush(0);
if (now - sr.at_ms < SAVE_EVERY_MS) return;
VkPipeStats s;
vk_pipeline_stats(&s);
uint64_t n = s.graphics + s.compute + cache_merges; // a merge comes after its pipeline was counted (bg_main)
if (!save_due(&sr, n, now, P.fold)) return; // P.fold: the play file merged at the start is to be written
// Only at a moment with no build running (cache_save): while one runs the save waits for the next tick, 500 ms
// on, before it logs or flushes anything, so a busy stretch does not repeat the statistics lines every tick.
if (P.cache && P.cache_path[0]) {
if (!plat_mutex_trylock(&cache_rw)) return;
plat_mutex_unlock(&cache_rw);
}
if (save_tried(&sr, n, now, save_with(0, 0) > 0))
rt_log("vk_pipeline: the pipeline cache was not saved in %d tries in a row; the next try is made when a new "
"pipeline is built, or at exit", SAVE_RETRIES + 1);
}
// On its own thread: the save (vkGetPipelineCacheData and the file write, about 60 ms) and the key appends (a flush to
// the disk each) ran on the present thread, and the render thread waited for it to take the next frame.
static void autosave_main(void *arg) {
(void)arg;
plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
for (;;) {
plat_sleep_ms(500);
autosave_once();
}
}
void vk_pipeline_autosave(void) {
static int32_t started;
if (!P.ready || plat_xchg32(&started, 1)) return;
if (!plat_thread_spawn(autosave_main, NULL, "pipe autosave", 0))
rt_log("vk_pipeline: cannot start the autosave thread; the cache is saved at exit only");
}
void vk_pipeline_shutdown(void) {
if (!P.ready) return;
lb_stop();
workers_drain(); // jobs point at layouts and Pipe entries freed below
vk_pipeline_save();
plat_mutex_lock(&P.lock);
vkDeviceWaitIdle(P.device);
for (int i = 0; i < PIPE_BUCKETS; i++) {
for (Pipe *p = pipes[i], *n; p; p = n) {
n = p->next;
if (p->info.pipeline) vkDestroyPipeline(P.device, p->info.pipeline, NULL);
if (p->fast && p->fast != p->info.pipeline) vkDestroyPipeline(P.device, p->fast, NULL);
free(p->bindings), free(p);
}
pipes[i] = NULL;
}
libs_destroy();
for (int i = 0; i < LAYOUT_BUCKETS; i++) {
for (Layout *l = layouts[i], *n; l; l = n) {
n = l->next;
for (int k = 0; k < 2; k++)
if (l->tpl[k]) P.tpl_destroy(P.device, l->tpl[k], NULL);
vkDestroyPipelineLayout(P.device, l->layout, NULL);
for (int h = 0; h < 2; h++)
if (l->half[h]) vkDestroyPipelineLayout(P.device, l->half[h], NULL);
vkDestroyDescriptorSetLayout(P.device, l->set, NULL);
if (l->set1) vkDestroyDescriptorSetLayout(P.device, l->set1, NULL);
free(l);
}
layouts[i] = NULL;
}
plat_mutex_lock(&save_lock); // not while the autosave thread copies it
if (P.cache) vkDestroyPipelineCache(P.device, P.cache, NULL);
if (P.play) vkDestroyPipelineCache(P.device, P.play, NULL);
P.cache = P.play = VK_NULL_HANDLE;
plat_mutex_unlock(&save_lock);
memset(&P.stats, 0, sizeof P.stats);
plat_xchg32(&P.ready, 0);
plat_mutex_unlock(&P.lock);
}
// ---- self-test: census shaders (probe_census_dir, tools/shader_census.py) through gfx_shader_get into pipelines.
// Graphics pairs are a fragment shader and a vertex shader of the same source file, spread over the census; compute
// shaders are spread the same way. Every pipeline is built twice (the second call must hit the cache) and the
// dynamic state of the first graphics pipeline is recorded into a throwaway command buffer.
typedef struct { const char *key, *c1, *src; size_t c1_len, src_len; int stage; uint32_t local[3]; } CensusEntry;
static const char *st_field(const char *from, const char *to, const char *name, size_t *len) {
char pat[32];
snprintf(pat, sizeof pat, "\"%s\": \"", name);
const char *p = strstr(from, pat);
if (!p || p >= to) return NULL;
p += strlen(pat);
const char *e = memchr(p, '"', (size_t)(to - p));
if (!e) return NULL;
*len = (size_t)(e - p);
return p;
}
static int census_cmp(const void *x, const void *y) {
const CensusEntry *a = x, *b = y;
size_t n = a->src_len < b->src_len ? a->src_len : b->src_len;
int c = memcmp(a->src, b->src, n);
if (c) return c;
if (a->src_len != b->src_len) return a->src_len < b->src_len ? -1 : 1;
return a->stage - b->stage;
}
static char *census_read(const char *root, size_t *size) {
char path[PLAT_PATH_MAX * 2 + 64];
snprintf(path, sizeof path, "%s/jobs.json", root);
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
char *b = malloc((size_t)n + 1);
if (b && fread(b, 1, (size_t)n, f) != (size_t)n) free(b), b = NULL;
fclose(f);
if (b) b[n] = 0, *size = (size_t)n;
return b;
}
static int census_parse(const char *jobs, CensusEntry *out, int max) {
int n = 0;
for (const char *p = jobs; n < max && (p = strstr(p, "\"job\": {")); p += 8) {
const char *end = strstr(p, "\"origin\": ");
if (!end) break;
size_t kl, sl, cl, rl;
const char *k = st_field(p, end, "key", &kl), *s = st_field(p, end, "stage", &sl), *c = st_field(p, end, "c1", &cl),
*r = st_field(p, end, "source", &rl);
if (!k || !s || !c || !r || kl != 64) continue;
CensusEntry *e = &out[n++];
e->key = k, e->c1 = c, e->c1_len = cl / 2, e->src = r, e->src_len = rl;
e->stage = !strncmp(s, "vs", sl) ? GFX_STAGE_VS : !strncmp(s, "ps", sl) ? GFX_STAGE_FS : GFX_STAGE_CS;
e->local[0] = e->local[1] = e->local[2] = 1;
const char *l = strstr(p, "\"local\": [");
if (l && l < end) {
l += 10;
for (int i = 0; i < 3; i++) {
while (*l == ' ' || *l == '\n' || *l == '\r' || *l == ',') l++;
e->local[i] = (uint32_t)strtoul(l, (char **)&l, 10);
}
}
}
return n;
}
static int hexv(char c) { return c <= '9' ? c - '0' : (c | 32) - 'a' + 10; }
// The program the game would hand NVN for a census entry, translated with default answers (as gfx_shader_probe.c).
static const GfxShader *census_shader(const char *root, const CensusEntry *e) {
char path[PLAT_PATH_MAX * 2 + 128];
static uint8_t c1[8192];
uint8_t control[0x880] = {0};
if (e->c1_len > sizeof c1) return NULL;
for (size_t b = 0; b < e->c1_len; b++) c1[b] = (uint8_t)(hexv(e->c1[b * 2]) << 4 | hexv(e->c1[b * 2 + 1]));
snprintf(path, sizeof path, "%s/code/%.64s.code", root, e->key);
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long body = ftell(f);
fseek(f, 0, SEEK_SET);
size_t skip = e->stage == GFX_STAGE_CS ? 0 : 0x30, total = skip + (size_t)body;
uint8_t *code = calloc(1, total);
int ok = code && fread(code + skip, 1, (size_t)body, f) == (size_t)body;
fclose(f);
if (!ok) return free(code), NULL;
uint32_t c1_off = (uint32_t)(total - e->c1_len), length = (uint32_t)total;
memcpy(control + 0x700, &c1_off, 4), memcpy(control + 0x704, &length, 4);
GfxShaderAnswers a = {0};
if (e->stage == GFX_STAGE_CS) for (int k = 0; k < 3; k++) a.local[k] = (uint16_t)e->local[k];
const GfxShader *s = gfx_shader_get(e->stage, control, code, total, c1, &a);
free(code);
return s;
}
// A state for a census pair: one target per fragment output (RGBA8 of the output's type), D32S8 depth with the test on,
// alpha blending on target 0, one RGBA32 attribute per vertex input, interleaved in stream 0.
static void selftest_state(VkPipeState *s, const GfxShader *vs, const GfxShader *fs) {
vk_pipe_state_defaults(s);
s->vs = vs, s->fs = fs;
GfxShaderIo io[32];
uint32_t outs = gfx_shader_outputs(fs, io);
for (int i = 0; i < 8; i++)
if (outs >> i & 1)
s->color_fmt[i] = io[i].type == GFX_TYPE_UINT ? NVN_FORMAT_RGBA8UI : io[i].type == GFX_TYPE_SINT ? NVN_FORMAT_RGBA8I
: NVN_FORMAT_RGBA8;
s->depth_fmt = NVN_FORMAT_DEPTH32F_STENCIL8;
s->ds.dt = 1;
s->color.blend_en = 1;
s->blend[0].src_rgb = NVN_BLEND_FUNC_SRC_ALPHA, s->blend[0].dst_rgb = NVN_BLEND_FUNC_ONE_MINUS_SRC_ALPHA;
s->poly.cull = NVN_FACE_BACK;
uint32_t ins = gfx_shader_inputs(vs, io), off = 0;
for (int l = 0; l < 16; l++)
if (ins >> l & 1) {
va_st a = {io[l].type == GFX_TYPE_UINT ? NVN_FORMAT_RGBA32UI : io[l].type == GFX_TYPE_SINT ? NVN_FORMAT_RGBA32I
: NVN_FORMAT_RGBA32F,
off, 0};
memcpy(&s->vattr[l], &a, sizeof a);
off += 16;
}
s->vstream[0].stride = off ? off : 16;
}
// Records the dynamic state of a pipeline into a command buffer that is never submitted (validation checks the calls).
static int selftest_record(const VkPipeState *s, VkPipeline pipe) {
VkCtx *c = vk_ctx();
VkCommandPoolCreateInfo pi = {.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .queueFamilyIndex = c->gfx_family};
VkCommandPool pool;
if (vkCreateCommandPool(c->device, &pi, NULL, &pool) != VK_SUCCESS) return 0;
VkCommandBufferAllocateInfo ai = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .commandPool = pool,
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = 1};
VkCommandBuffer cb;
int ok = vkAllocateCommandBuffers(c->device, &ai, &cb) == VK_SUCCESS;
if (ok) {
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
vkBeginCommandBuffer(cb, &bi);
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe);
vk_pipeline_set_dynamic(cb, s);
ok = vkEndCommandBuffer(cb) == VK_SUCCESS;
}
vkDestroyCommandPool(c->device, pool, NULL);
return ok;
}
int vk_pipeline_selftest(void) {
enum { MAXN = 20000, WANT_PAIRS = 64, WANT_CS = 16 };
char root[PLAT_PATH_MAX * 2];
if (!vk_init() || !ensure_init()) { printf("vk_pipeline selftest: FAILED: no Vulkan device\n"); return 1; }
if (!probe_census_dir(root, sizeof root)) { // the census folder, build/<version>/shaders
printf("vk_pipeline selftest: FAILED: no build/v%d/shaders/jobs.json above the exe\n", rt_game_version);
return 1;
}
size_t size;
char *jobs = census_read(root, &size);
CensusEntry *e = malloc(sizeof *e * MAXN);
int n = jobs && e ? census_parse(jobs, e, MAXN) : 0;
if (!n) { printf("vk_pipeline selftest: FAILED: no census entries\n"); free(jobs), free(e); return 1; }
qsort(e, (size_t)n, sizeof *e, census_cmp);
long err0 = vk_validation_errors_total();
VkPipeStats st0;
vk_pipeline_stats(&st0);
// candidate pairs: each fragment shader of a source file with one of the file's vertex shaders (round robin)
int (*pairs)[2] = malloc(sizeof *pairs * (size_t)n), np = 0, *cs = malloc(sizeof *cs * (size_t)n), nc = 0;
for (int i = 0; i < n;) {
int j = i, v0 = -1, nv = 0, nf = 0;
for (; j < n && e[j].src_len == e[i].src_len && !memcmp(e[j].src, e[i].src, e[i].src_len); j++) {
if (e[j].stage == GFX_STAGE_VS && v0 < 0) v0 = j; // sorted by stage within a file: vertex shaders first
nv += e[j].stage == GFX_STAGE_VS;
if (e[j].stage == GFX_STAGE_CS) cs[nc++] = j;
}
for (int k = i; k < j && nv; k++)
if (e[k].stage == GFX_STAGE_FS) pairs[np][0] = v0 + nf++ % nv, pairs[np][1] = k, np++;
i = j;
}
int tried = 0, built = 0, cached = 1, shader_fail = 0, ctried = 0, cbuilt = 0, recorded = 0, varying_fix = 0;
for (int t = 0; t < WANT_PAIRS && t < np; t++) {
const int *pr = pairs[(long long)t * np / (np < WANT_PAIRS ? np : WANT_PAIRS)];
tried++;
const GfxShader *vs = census_shader(root, &e[pr[0]]), *fs = census_shader(root, &e[pr[1]]);
if (!vs || !fs) { shader_fail++; continue; }
if (gfx_shader_inputs(fs, NULL) & ~gfx_shader_outputs(vs, NULL)) varying_fix++;
VkPipeState s;
selftest_state(&s, vs, fs);
GfxShaderAnswers fa = {0};
vk_pipeline_answers(&s, NULL, &fa);
const VkPipeInfo *info;
VkPipeline p = vk_pipeline_graphics(&s, &info);
if (!p) continue;
built++;
const VkPipeInfo *again;
if (vk_pipeline_graphics(&s, &again) != p || again != info) cached = 0;
if (!recorded) recorded = selftest_record(&s, p);
}
for (int t = 0; t < WANT_CS && t < nc; t++) {
const CensusEntry *c = &e[cs[(long long)t * nc / (nc < WANT_CS ? nc : WANT_CS)]];
ctried++;
const GfxShader *s = census_shader(root, c);
if (!s) { shader_fail++; continue; }
const VkPipeInfo *info;
VkPipeline p = vk_pipeline_compute(s, &info);
if (!p) continue;
cbuilt++;
const VkPipeInfo *again;
if (vk_pipeline_compute(s, &again) != p || again != info) cached = 0;
}
vk_pipeline_save();
VkPipeStats st;
vk_pipeline_stats(&st);
long errors = vk_validation_errors_total() - err0;
printf("vk_pipeline selftest: graphics %d/%d census pairs (%d needed vertex outputs added), compute %d/%d, "
"%d shaders failed to translate; layouts %llu (%llu push); cache hits %llu; dynamic state recorded %d; "
"validation errors %ld\n",
built, tried, varying_fix, cbuilt, ctried, shader_fail, (unsigned long long)(st.layouts - st0.layouts),
(unsigned long long)(st.push_layouts - st0.push_layouts), (unsigned long long)(st.hits - st0.hits), recorded,
errors);
free(pairs), free(cs), free(e), free(jobs);
int ok = built > 0 && cbuilt > 0 && cached && recorded && errors == 0;
printf("vk_pipeline selftest: %s\n", ok ? "passed" : "FAILED");
return ok ? 0 : 1;
}
// NX2NAVI_TRACE=perf "mem: sizes" (perf.c): the pipeline table's entries with their pipelines and fast links (never
// evicted), layouts, stage libraries, the job queues and the pipeline-keys.bin set. A walk of the hash chains under P.lock
// held shared: the workers' inserts and a draw's memo miss wait for it (well under a millisecond).
void vk_pipeline_sizes(PerfSizes *s) {
uint64_t ent = 0, built = 0, fast = 0, libn = 0, libb = 0, lq = 0;
plat_mutex_lock_shared(&P.lock);
for (int i = 0; i < PIPE_BUCKETS; i++)
for (const Pipe *p = pipes[i]; p; p = p->next) ent++, built += p->info.pipeline != VK_NULL_HANDLE, fast += p->fast != VK_NULL_HANDLE;
for (int i = 0; i < LIB_BUCKETS; i++)
for (const Lib *b = libs[i]; b; b = b->next) libn++, libb += b->pipe != VK_NULL_HANDLE;
for (const LibJob *j = U.head; j; j = j->next) lq++;
uint64_t lay = P.stats.layouts, fails = P.stats.failures, jobs = A.inflight, seen = K.seen_n, seen_b = (uint64_t)K.seen_cap * 8;
plat_mutex_unlock_shared(&P.lock);
perf_size(s, "pipeline entries", ent, 0, PERF_SIZE_NONE);
perf_size(s, "with a pipeline", built, 0, PERF_SIZE_NONE);
perf_size(s, "fast links", fast, 0, PERF_SIZE_NONE);
perf_size(s, "pipeline failures", fails, 0, PERF_SIZE_NONE);
perf_size(s, "pipeline layouts", lay, 0, PERF_SIZE_NONE);
perf_size(s, "stage library entries", libn, 0, PERF_SIZE_NONE);
perf_size(s, "built stage libraries", libb, 0, PERF_SIZE_NONE);
perf_size(s, "library jobs queued", lq, 0, PERF_SIZE_NONE);
perf_size(s, "pipeline jobs queued or running", jobs, 0, PERF_SIZE_NONE);
perf_size(s, "pipeline keys seen", seen, 0, seen_b);
}