Files
Valiantsin DzerakhandClaude Opus 5.5 9dd50131ca Start function-local variables at 0 on Mesa drivers only
SPIR-V leaves a register read before any write undefined and Mesa folds undefined values freely. The unconditional patch 0014 guarded against that and was taken out in #701 because the whole picture shook with DLSS on NVIDIA. Patch 0016 brings the zero start back behind a zero_locals job flag, which vk_caps_driver turns on for RADV, ANV and NVK. TOTK_ZERO_LOCALS overrides it, and gfx_shader_set_host mixes it into the translator identity so the variant has its own cache keys.

FXAA_FS gains the flag-on hash of the game's FXAA shader, so vk_dlaa_skip_draw knows it on Mesa too. A game run with TOTK_ZERO_LOCALS=1 on an RX 9070 produced that hash; a TOTK_FXAA=0 run logged no skip with either hash, so the switch itself is not confirmed.

All 29736 stored 1.4.3 inputs translate with the flag off and on, 0 spirv-val failures. FXAA hashes to 5c43ae18948d52cb with the flag off and aad42238801d31d8 with it on. Flag-on output matches the old translator byte for byte on a sample of 312 shaders. Not run on a Mesa driver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:05:21 +03:00

2144 lines
94 KiB
C

// Shader service client: turns an NVN program (control section, code, c1 constants and the
// translator's answers) into SPIR-V plus reflection through `totk_xlate.exe --serve` children, started lazily with
// pipes and kept for the life of the process (a pool: TOTK_XLATE_PROCS at run time, more for the boot prewarm).
// Results are cached in memory and on disk (shader-cache/<key>.spv and <key>.res.json next to totk.exe), so a shader
// is translated once per machine and translator.
//
// Keys. The code hash is sha256 over stage (u32), the 0x880-byte control section, code_size (u64), the code, the c1
// length (u32) and bytes. Per code hash and translator the cache holds a use mask (<sha256("TKSU", xlate id, code
// hash)>.use): the texture/image slots the translator queried (.res.json tex_queried, img_queried), the vertex
// attributes a VS declares and the colour targets an FS declares (SPIR-V Locations). The input id is sha256 over "TKSI",
// IN_VERSION, the code hash and the answers with everything outside the mask zeroed (fixed little-endian layout), so
// stale bindings in slots the shader never reads do not make new ids; it does not depend on the translator. The cache
// key is sha256 over "TKSK", KEY_VERSION, the translator identity and the input id. A code hash without a known mask is
// translated under a provisional key ("TKSP", full answers); its files are renamed to the cache key and the mask stored.
//
// Translator identity: build/xlate/totk_xlate.id, sha256 hex that tools/build_xlate.py writes over the sources it
// builds from (Program.cs, the csproj, the patches, the vendor tree); without it the sha256 of the exe. TOTK_XLATE_ID
// (64 hex digits) overrides it. A translator change therefore changes every cache key without a hand-made KEY_VERSION
// bump; KEY_VERSION is for changes on this side (key layout, how the output is used).
//
// Pack: tools/pack_shader_cache.py folds the .spv, .res.json, .use and in/*.in files into <cache>/shader-cache.pack
// (one file instead of four per shader: ~117,000 files after an install) and deletes them; the installer runs it after
// preparing the shaders. The pack is read-only here. Every read looks in the pack first and then for the loose file,
// and new translations are written as loose files as before, until the tool runs again (section "cache pack").
//
// Stored inputs: every translation also writes <cache>/in/<input id>.in (magic "TKSI"; stage, the full answers, the
// control section, the code up to its length, c1), so a pipeline key record (vk_pipeline.c), which names its shaders by
// input id, can be translated again under a new translator without guest memory (gfx_shader_load_input,
// gfx_shader_prewarm).
//
// Reflection: every shader that enters the cache has its .res.json parsed into GfxShaderRes arrays and its SPIR-V
// scanned for interface Locations and the workgroup size (gfx.h accessors at the end of the file).
//
// Provenance: the c2 handle ranges of handle_slot are those of MacNativeGames' totk_xlate Service.cs (MIT).
// The code layout the translator sees (graphics: code[0x30..], compute: code[0..]; c1 = code[c1..length]
// with c1 and length at control+0x700) follows tools/shader_census.py, itself MacNativeGames' totk shader tooling (MIT).
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "gfx.h"
#include "host.h"
#include "platform/platform.h"
#define CONTROL_SIZE 0x880
#define CODE_SKIP 0x30 // graphics code starts after the first 0x30 bytes of the program
#define BUCKETS 4096
#define LINE_MAX_LEN 4096
#define KEY_VERSION 4 // in every cache key; bump when the key layout or the use of the
// output changes here (4: translator identity, input ids);
// translator changes are covered by its identity
#define IN_VERSION 1 // in every input id and .in file: bump when the answers layout
// or the program hash changes (drops every stored input)
#define USE_MAGIC 0x4d554b54u // "TKUM", .use files
#define USE_VERSION 1
#define IN_MAGIC 0x49534b54u // "TKSI", .in files
#define MAX_SVC 32 // translator processes
typedef struct { // which answers a shader's translation depends on
uint32_t slots; // tex_type, img_type, img_format of these c2 slots
uint32_t attrs; // attr_type (VS input Locations)
uint32_t fouts; // frag_out_type (FS output Locations)
} UseMask;
typedef struct UseEnt { // code hash -> use mask; known 0 caches a disk miss
struct UseEnt *next;
uint8_t code_hash[32];
int known;
UseMask m;
} UseEnt;
struct GfxShader {
struct GfxShader *next;
uint8_t key[32];
uint8_t iid[32]; // input id (see the keys at the top)
uint32_t *spirv;
uint64_t words;
char *reflection; // the service's .res.json text, parsed into the fields below
uint64_t reflection_size;
// Reflection (reflect(), once per shader before it enters the cache)
int stage; // GFX_STAGE_*
GfxShaderRes *res[GFX_RES_KINDS];
int nres[GFX_RES_KINDS];
uint32_t in_mask, out_mask; // user Locations of Input / Output variables
GfxShaderIo in[32], out[32];
uint32_t local[3];
uint32_t mvprev_banks; // .res.json "mvprev_banks" (patch 0015), 0 none
};
static PlatMutex cache_lock = PLAT_MUTEX_INIT; // memory cache
static GfxShader *buckets[BUCKETS];
static UseEnt *use_buckets[BUCKETS]; // under cache_lock
static volatile int64_t n_requests, n_hits, n_failures, n_keys, n_xlated; // n_keys: memory cache entries
typedef struct { // one translator child
PlatChild *child; // NULL: not started
int busy; // a request owns it (under S.lock)
size_t in_len;
char inbuf[LINE_MAX_LEN * 2];
} Svc;
static struct {
PlatMutex lock;
PlatCond freed;
int limit; // children in use at most (0: svc_runtime_limit)
Svc s[MAX_SVC];
} S = {PLAT_MUTEX_INIT, PLAT_COND_INIT};
static struct { // translations in flight: cache key, or code hash while the
PlatMutex lock; // use mask is unknown; one translation per id at a time
PlatCond done;
uint8_t id[2 * MAX_SVC][32];
int n;
} F = {PLAT_MUTEX_INIT, PLAT_COND_INIT};
#define PATH_CAP 1024 // bytes of a UTF-8 path
static char cache_dir[PATH_CAP];
static char xlate_exe[PATH_CAP];
static uint8_t xlate_id[32]; // translator identity (see the keys at the top)
static int host_f64 = 1, host_zero_locals, host_set; // gfx_shader_set_host
static uint32_t host_sbuf, host_sg[3] = {32, 32, 32}, host_gather;
static PlatOnce paths_once = PLAT_ONCE_INIT;
// ---- small helpers
#define file_exists plat_file_exists
#define mkdir_p plat_file_mkdir_p
#define read_file plat_file_read
#define write_file plat_file_write
// ---- cache pack (<cache>/shader-cache.pack, written by tools/pack_shader_cache.py)
// Little-endian: "TKSPACK1", u32 count, u32 0, then count entries sorted by (kind, name), then the data. name is the
// 32 bytes the loose file's 64 hex digits spell. The index is read once (48 bytes a file: ~5.6 MB after an install);
// the data is read per lookup with positioned reads on one handle, safe from every thread. A pack that fails a check is
// ignored as a whole and the loose files are used.
enum { PACK_SPV = 1, PACK_RES = 2, PACK_USE = 3, PACK_IN = 4 };
typedef struct { uint8_t name[32]; uint32_t kind, size; uint64_t offset; } PackEnt;
_Static_assert(sizeof(PackEnt) == 48, "shader-cache.pack entry layout");
static PlatFile *pack_file;
static PackEnt *pack_ix;
static uint32_t pack_n;
static int pack_cmp(const PackEnt *a, uint32_t kind, const uint8_t name[32]) {
if (a->kind != kind) return a->kind < kind ? -1 : 1;
return memcmp(a->name, name, 32);
}
static int pack_read_at(uint64_t offset, void *out, uint32_t n) {
return plat_file_pread(pack_file, out, n, offset) == (int64_t)n;
}
// Opens <cache>/shader-cache.pack and reads its index (init_paths). Nothing happens when there is none.
static void pack_open(void) {
char p[PATH_CAP + 32];
snprintf(p, sizeof p, "%s/shader-cache.pack", cache_dir);
PlatFile *h = plat_file_open(p);
if (!h) return;
PlatFileId id;
uint8_t head[16];
uint32_t n = 0;
const char *why = NULL;
PackEnt *ix = NULL;
pack_file = h;
if (!plat_file_id(h, &id) || !pack_read_at(0, head, sizeof head) || memcmp(head, "TKSPACK1", 8)) why = "header";
if (!why) {
memcpy(&n, head + 8, 4);
if (n > (1u << 24) || (uint64_t)n * sizeof *ix + sizeof head > id.size ||
!(ix = malloc((size_t)n * sizeof *ix + 1)))
why = "index size";
else if (!pack_read_at(sizeof head, ix, n * (uint32_t)sizeof *ix)) why = "index read";
}
for (uint32_t i = 0; !why && i < n; i++) {
if (ix[i].offset + ix[i].size > id.size) why = "entry out of bounds";
else if (i && pack_cmp(&ix[i - 1], ix[i].kind, ix[i].name) >= 0) why = "index order";
}
if (why) {
rt_log("gfx_shader: %s is not a usable cache pack (%s); reading the loose files", p, why);
free(ix);
plat_file_close(h);
pack_file = NULL;
return;
}
pack_ix = ix, pack_n = n;
rt_log("gfx_shader: cache pack %s: %u files", p, n);
}
static const PackEnt *pack_find(uint32_t kind, const uint8_t name[32]) {
size_t lo = 0, hi = pack_n;
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
int c = pack_cmp(&pack_ix[mid], kind, name);
if (!c) return &pack_ix[mid];
if (c < 0) lo = mid + 1;
else hi = mid;
}
return NULL;
}
// A packed file as plat_file_read returns a loose one: malloc'd, NUL after the last byte; NULL when absent or empty.
static uint8_t *pack_read(uint32_t kind, const uint8_t name[32], uint64_t *size) {
const PackEnt *e = pack_find(kind, name);
uint8_t *buf = e && e->size ? malloc((size_t)e->size + 1) : NULL;
if (!buf) return NULL;
if (!pack_read_at(e->offset, buf, e->size)) {
free(buf);
return NULL;
}
buf[e->size] = 0;
*size = e->size;
return buf;
}
// Appends a UTF-8 string as a JSON string body: ASCII as is, quotes and backslashes escaped, the rest as \uXXXX
// (UTF-16 units), so the service's stdin encoding does not matter.
static void json_esc(char **o, const char *s) {
const unsigned char *p = (const unsigned char *)s;
while (*p) {
unsigned c = *p++;
if (c >= 0x80) {
int more = c >= 0xf0 ? 3 : c >= 0xe0 ? 2 : 1;
c &= 0x3f >> more;
while (more-- && (*p & 0xc0) == 0x80) c = c << 6 | (*p++ & 0x3f);
}
if (c == '"' || c == '\\') *(*o)++ = '\\', *(*o)++ = (char)c;
else if (c >= 0x20 && c < 0x7f) *(*o)++ = (char)c;
else if (c >= 0x10000) {
c -= 0x10000;
*o += sprintf(*o, "\\u%04x\\u%04x", 0xd800 + (c >> 10), 0xdc00 + (c & 0x3ff));
} else *o += sprintf(*o, "\\u%04x", c);
}
}
// ---- paths and the translator identity
static int hex32(const char *h, uint8_t out[32]) { // 64 hex digits, then nothing but white space
for (int i = 0; i < 64; i++) {
char c = h[i] >= 'A' && h[i] <= 'F' ? (char)(h[i] | 32) : h[i];
int v = c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1;
if (v < 0) return 0;
out[i / 2] = (uint8_t)(i & 1 ? out[i / 2] | v : v << 4);
}
for (const char *p = h + 64; *p; p++)
if (*p != '\n' && *p != '\r' && *p != ' ' && *p != '\t') return 0;
return 1;
}
// TOTK_XLATE_ID (64 hex digits, or any other text, which is hashed), else <translator>.id next to the exe
// (tools/build_xlate.py), else the sha256 of the exe.
static void xlate_identity(void) {
char text[256] = "";
const char *how = "TOTK_XLATE_ID";
const char *id_env = cfg_get("TOTK_XLATE_ID");
if (id_env && *id_env && strlen(id_env) < sizeof text) {
strcpy(text, id_env);
if (!hex32(text, xlate_id)) rt_sha256(text, strlen(text), xlate_id);
} else {
char p[PATH_CAP + 8];
strcpy(p, xlate_exe);
char *dot = strrchr(p, '.'), *sep = plat_last_sep(p);
if (dot && dot > sep) *dot = 0;
strcat(p, ".id");
uint64_t size = 0;
char *f = (char *)read_file(p, &size);
how = "totk_xlate.id";
if (!f || !hex32(f, xlate_id)) {
uint8_t *exe = read_file(xlate_exe, &size);
how = exe ? "sha256 of the exe; tools/build_xlate.py writes totk_xlate.id" : "none: no translator";
if (exe) rt_sha256(exe, (size_t)size, xlate_id);
free(exe);
}
free(f);
}
rt_log("gfx_shader: translator identity %02x%02x%02x%02x%02x%02x%02x%02x (%s)", xlate_id[0], xlate_id[1], xlate_id[2],
xlate_id[3], xlate_id[4], xlate_id[5], xlate_id[6], xlate_id[7], how);
}
// The repository root is the nearest parent of totk.exe that holds build/xlate/totk_xlate.exe. The shader cache lives
// next to totk.exe, so every build dir has its own (as the pipeline cache, vk_pipeline.c cache_path_init).
static void use_preload_start(void);
static void init_paths(void *param) {
(void)param;
char exe[PATH_CAP], root[PATH_CAP] = "", exe_dir[PATH_CAP] = "";
if (plat_exe_path(exe, sizeof exe)) {
char *last = plat_last_sep(exe);
if (last) snprintf(exe_dir, sizeof exe_dir, "%.*s", (int)(last - exe), exe);
for (char *cut = plat_last_sep(exe); cut; cut = plat_last_sep(exe)) {
*cut = 0;
char probe[PATH_CAP + 32];
snprintf(probe, sizeof probe, "%s/build/xlate/totk_xlate" PLAT_EXE_SUFFIX, exe);
if (file_exists(probe)) {
strcpy(root, exe);
break;
}
}
}
const char *xlate_cfg = cfg_get("TOTK_XLATE"), *cache_cfg = cfg_get("TOTK_SHADER_CACHE");
char beside[PATH_CAP + 32]; // a packaged folder (tools/package_build.py) keeps the translator next to totk.exe
if (exe_dir[0]) snprintf(beside, sizeof beside, "%s/totk_xlate" PLAT_EXE_SUFFIX, exe_dir);
if (xlate_cfg && *xlate_cfg && strlen(xlate_cfg) < sizeof xlate_exe) strcpy(xlate_exe, xlate_cfg);
else if (exe_dir[0] && file_exists(beside)) strcpy(xlate_exe, beside);
else if (root[0]) snprintf(xlate_exe, sizeof xlate_exe, "%s/build/xlate/totk_xlate" PLAT_EXE_SUFFIX, root);
if (cache_cfg && *cache_cfg && strlen(cache_cfg) < sizeof cache_dir) strcpy(cache_dir, cache_cfg);
else if (exe_dir[0]) snprintf(cache_dir, sizeof cache_dir, "%s/shader-cache", exe_dir);
xlate_identity();
if (cache_dir[0]) pack_open();
use_preload_start();
return;
}
static void paths(void) { plat_once(&paths_once, init_paths, NULL); }
// Host answers (vk_caps.c, at device creation, before any translation). Answers other than the defaults (Float64,
// exact storage offsets, 32 lanes, no gather bias, undefined locals) are mixed into the translator identity, so they
// get their own cache keys.
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3], uint32_t gather_bits, int zero_locals) {
paths();
if (host_set++) return;
host_f64 = float64, host_sbuf = sbuf_align, host_gather = gather_bits, host_zero_locals = zero_locals;
memcpy(host_sg, subgroup, sizeof host_sg);
if (float64 && !sbuf_align && subgroup[0] == 32 && subgroup[1] == 32 && subgroup[2] == 32 && !gather_bits && !zero_locals) return;
uint8_t buf[32 + 96];
int n = snprintf((char *)buf + 32, 96, "TKHO f64=%d sbuf=%u sg=%u,%u,%u", float64, sbuf_align, subgroup[0], subgroup[1],
subgroup[2]);
if (gather_bits) n += snprintf((char *)buf + 32 + n, (size_t)(96 - n), " gather=%u", gather_bits);
if (zero_locals) n += snprintf((char *)buf + 32 + n, (size_t)(96 - n), " zero_locals");
memcpy(buf, xlate_id, 32);
rt_sha256(buf, 32 + (size_t)n, xlate_id);
rt_log("gfx_shader: host options %s: own shader cache keys", (char *)buf + 37);
}
// ---- the translator children
static void child_stop(Svc *v) {
plat_child_stop(v->child);
v->child = NULL;
v->in_len = 0;
}
static int child_start(Svc *v) {
paths();
if (!xlate_exe[0] || !file_exists(xlate_exe)) {
rt_log("gfx_shader: translator not found (%s); set TOTK_XLATE", xlate_exe[0] ? xlate_exe : "<none>");
return 0;
}
uint32_t err = 0;
v->child = plat_child_start(xlate_exe, "--serve", &err);
if (!v->child) {
rt_log("gfx_shader: cannot start %s (error %lu)", xlate_exe, (unsigned long)err);
return 0;
}
v->in_len = 0;
return 1;
}
// One line out, one line in (no newline in the reply). 0 when the pipe broke.
static int child_exchange_raw(Svc *v, const char *line, char *reply, size_t reply_size) {
if (!v->child && !child_start(v)) return 0;
plat_inc64(&n_requests);
size_t n = strlen(line);
if (!plat_child_write(v->child, line, n)) return 0;
if (!plat_child_write(v->child, "\n", 1)) return 0;
for (;;) {
char *nl = memchr(v->inbuf, '\n', v->in_len);
if (nl) {
size_t len = (size_t)(nl - v->inbuf), keep = len < reply_size - 1 ? len : reply_size - 1;
memcpy(reply, v->inbuf, keep);
while (keep && reply[keep - 1] == '\r') keep--;
reply[keep] = 0;
memmove(v->inbuf, nl + 1, v->in_len - len - 1);
v->in_len -= len + 1;
return 1;
}
if (v->in_len == sizeof v->inbuf) v->in_len = 0; // an overlong line: drop it, wait for its end
long got = plat_child_read(v->child, v->inbuf + v->in_len, sizeof v->inbuf - v->in_len);
if (got <= 0) return 0;
v->in_len += (size_t)got;
}
}
static int child_exchange(Svc *v, const char *line, char *reply, size_t reply_size) {
uint64_t p0 = perf_begin(); // TOTK_TRACE=perf: render-thread time spent translating
int ok = child_exchange_raw(v, line, reply, reply_size);
perf_end(PERF_XLATE, p0);
return ok;
}
// Children allowed at run time: TOTK_XLATE_PROCS, default 2.
static int svc_runtime_limit(void) {
static volatile int32_t n;
if (!n) {
const char *e = cfg_get("TOTK_XLATE_PROCS");
int v = e && *e ? atoi(e) : 2;
plat_xchg32(&n, v < 1 ? 1 : v > MAX_SVC ? MAX_SVC : v);
}
return (int)n;
}
static int svc_used(void) { // caller holds S.lock
int used = 0;
for (int i = 0; i < MAX_SVC; i++) used += S.s[i].busy || S.s[i].child;
return used;
}
// A child for one request: an idle running one, else a new slot while fewer than S.limit are in use, else waits.
static Svc *svc_get(void) {
plat_mutex_lock(&S.lock);
if (!S.limit) S.limit = svc_runtime_limit();
Svc *v;
for (;;) {
Svc *empty = NULL;
v = NULL;
for (int i = 0; i < MAX_SVC && !v; i++) {
Svc *c = &S.s[i];
if (!c->busy && c->child) v = c;
else if (!c->busy && !empty) empty = c;
}
if (!v && empty && svc_used() < S.limit) v = empty;
if (v) break;
plat_cond_wait(&S.freed, &S.lock);
}
v->busy = 1;
plat_mutex_unlock(&S.lock);
return v;
}
// Returns a child; one over the limit (svc_set_limit lowered it) is stopped.
static void svc_put(Svc *v) {
plat_mutex_lock(&S.lock);
int stop = v->child && svc_used() > S.limit;
if (!stop) v->busy = 0;
plat_mutex_unlock(&S.lock);
if (stop) {
child_stop(v);
plat_mutex_lock(&S.lock);
v->busy = 0;
plat_mutex_unlock(&S.lock);
}
plat_cond_broadcast(&S.freed);
}
// Sets the number of children; idle ones over it are stopped now, busy ones when they are returned.
static void svc_set_limit(int n) {
Svc *stop[MAX_SVC];
int k = 0;
plat_mutex_lock(&S.lock);
S.limit = n < 1 ? 1 : n > MAX_SVC ? MAX_SVC : n;
for (int i = 0, used = svc_used(); i < MAX_SVC && used > S.limit; i++)
if (!S.s[i].busy && S.s[i].child) S.s[i].busy = 1, stop[k++] = &S.s[i], used--;
plat_mutex_unlock(&S.lock);
for (int i = 0; i < k; i++) child_stop(stop[i]);
plat_mutex_lock(&S.lock);
for (int i = 0; i < k; i++) stop[i]->busy = 0;
plat_mutex_unlock(&S.lock);
plat_cond_broadcast(&S.freed);
}
// One request on a child; a dead child is restarted once. 0 when the translator is unavailable. The caller has given
// up its virtual core (kern_core_away): the wait for a child and the translation are host time.
static int svc_request(const char *line, char *reply, size_t reply_size) {
Svc *v = svc_get();
int ok = 0;
for (int attempt = 0; attempt < 2 && !ok; attempt++)
if (!(ok = child_exchange(v, line, reply, reply_size))) child_stop(v);
svc_put(v);
return ok;
}
// One translation per id at a time (F); F.id full also waits.
static void flight_enter(const uint8_t id[32]) {
plat_mutex_lock(&F.lock);
for (;;) {
int busy = F.n == 2 * MAX_SVC;
for (int i = 0; i < F.n && !busy; i++) busy = !memcmp(F.id[i], id, 32);
if (!busy) break;
plat_cond_wait(&F.done, &F.lock);
}
memcpy(F.id[F.n++], id, 32);
plat_mutex_unlock(&F.lock);
}
static void flight_leave(const uint8_t id[32]) {
plat_mutex_lock(&F.lock);
for (int i = 0; i < F.n; i++)
if (!memcmp(F.id[i], id, 32)) {
memcpy(F.id[i], F.id[--F.n], 32);
break;
}
plat_mutex_unlock(&F.lock);
plat_cond_broadcast(&F.done);
}
// ---- cache
static GfxShader *mem_find(const uint8_t key[32]) {
plat_mutex_lock_shared(&cache_lock);
GfxShader *s = buckets[(key[0] | key[1] << 8) % BUCKETS];
while (s && memcmp(s->key, key, 32)) s = s->next;
plat_mutex_unlock_shared(&cache_lock);
return s;
}
static void shader_free(GfxShader *s) {
for (int k = 0; k < GFX_RES_KINDS; k++) free(s->res[k]);
free(s->spirv), free(s->reflection), free(s);
}
static void reflect(GfxShader *s);
// Inserts s, or frees it and returns the entry another thread inserted first.
static GfxShader *mem_insert(GfxShader *s) {
plat_mutex_lock(&cache_lock);
GfxShader **head = &buckets[(s->key[0] | s->key[1] << 8) % BUCKETS], *r = *head;
while (r && memcmp(r->key, s->key, 32)) r = r->next;
if (r) {
shader_free(s);
s = r;
} else {
s->next = *head;
*head = s;
n_keys++;
}
plat_mutex_unlock(&cache_lock);
return s;
}
static void key_hex(const uint8_t key[32], char out[65]) {
static const char hex[] = "0123456789abcdef";
for (int i = 0; i < 32; i++) out[i * 2] = hex[key[i] >> 4], out[i * 2 + 1] = hex[key[i] & 15];
out[64] = 0;
}
// A .res.json the translator wrote: one JSON object that names its stage, no NUL inside. A file an interrupted write
// left zero-filled (3647 of 21924 in one cache were) fails this; reflect() would take it for a vertex shader with
// no resources, and its pipeline layout would lack the shader's buffers and textures.
static int reflection_valid(const char *res, uint64_t size) {
if (strlen(res) != size) return 0;
const char *b = res, *e = res + size;
while (b < e && isspace((unsigned char)*b)) b++;
while (e > b && isspace((unsigned char)e[-1])) e--;
if (e - b < 2 || *b != '{' || e[-1] != '}') return 0;
const char *st = strstr(b, "\"stage\"");
if (!st) return 0;
st = strchr(st + 7, '"');
return st && (!strncmp(st, "\"vs\"", 4) || !strncmp(st, "\"ps\"", 4) || !strncmp(st, "\"cs\"", 4));
}
// Every instruction after the header has a word count and the last one ends at the module's end: a module with
// zero-filled clusters fails.
static int spirv_complete(const uint32_t *w, uint64_t n) {
uint64_t i = 5;
while (i < n) {
uint32_t len = w[i] >> 16;
if (!len) return 0;
i += len;
}
return i == n;
}
// Reads <key>.spv and <key>.res.json from the pack (packed 1) or the loose files; NULL if either is missing or
// malformed. iid: the input id, NULL when the caller sets it.
static GfxShader *disk_load_from(const uint8_t key[32], const uint8_t *iid, int packed) {
static volatile int32_t n_bad;
char hex[65], p[PATH_CAP + 80];
key_hex(key, hex);
uint64_t spv_size = 0, res_size = 0;
snprintf(p, PATH_CAP + 80, "%s/%s.res.json", cache_dir, hex);
uint8_t *res = packed ? pack_read(PACK_RES, key, &res_size) : read_file(p, &res_size);
if (!res) return NULL;
if (!reflection_valid((const char *)res, res_size)) {
int32_t n = plat_inc32(&n_bad);
if (n <= 3 || !(n & (n - 1))) rt_log("gfx_shader: %s.res.json is malformed, translating again (%d so far)", hex, n);
free(res);
return NULL;
}
snprintf(p, PATH_CAP + 80, "%s/%s.spv", cache_dir, hex);
uint8_t *spv = packed ? pack_read(PACK_SPV, key, &spv_size) : read_file(p, &spv_size);
uint32_t magic = 0;
if (spv && spv_size >= 20) memcpy(&magic, spv, 4);
if (!spv || spv_size % 4 || magic != 0x07230203 || !spirv_complete((const uint32_t *)spv, spv_size / 4)) {
free(res), free(spv);
return NULL;
}
GfxShader *s = calloc(1, sizeof *s);
memcpy(s->key, key, 32);
if (iid) memcpy(s->iid, iid, 32);
s->spirv = (uint32_t *)spv;
s->words = spv_size / 4;
s->reflection = (char *)res;
s->reflection_size = res_size;
reflect(s);
if (s->stage == GFX_STAGE_FS) vk_hdr_shader_swap(&s->spirv, &s->words); // the in-memory copy; the cache keeps its own
return s;
}
const GfxShader *gfx_shader_own(const uint32_t *spirv, uint64_t words, const char *res_json) {
if (words < 5 || spirv[0] != 0x07230203 || !spirv_complete(spirv, words) ||
!reflection_valid(res_json, strlen(res_json)))
return NULL;
GfxShader *s = calloc(1, sizeof *s);
if (!s) return NULL;
size_t rn = strlen(res_json);
uint8_t *buf = malloc(words * 4 + rn);
s->spirv = malloc(words * 4), s->reflection = malloc(rn + 1);
if (!buf || !s->spirv || !s->reflection) rt_fatal("gfx_shader: out of memory");
memcpy(buf, spirv, words * 4), memcpy(buf + words * 4, res_json, rn);
rt_sha256(buf, words * 4 + rn, s->key);
free(buf);
memcpy(s->spirv, spirv, words * 4), memcpy(s->reflection, res_json, rn + 1);
s->words = words, s->reflection_size = rn;
reflect(s);
return s;
}
// The pack, then the loose files (a newer translation of a key the pack holds a bad copy of); NULL when neither has a
// valid pair (the caller translates again and writes loose files).
static GfxShader *disk_load(const uint8_t key[32], const uint8_t *iid) {
GfxShader *s = pack_n ? disk_load_from(key, iid, 1) : NULL;
return s ? s : disk_load_from(key, iid, 0);
}
// ---- request building
static const char *const tex_names[] = {NULL, "1d", "1darray", "buffer", "2d", "2darray", "2dms", "2dmsarray", "3d",
"cube", "cubearray"}; // GfxShaderAnswers tex_type / img_type codes
static const char *const fmt_names[] = { // img_format codes: Ryujinx TextureFormat enum order
NULL, "R8Unorm", "R8Snorm", "R8Uint", "R8Sint", "R16Float", "R16Unorm", "R16Snorm", "R16Uint", "R16Sint", "R32Float",
"R32Uint", "R32Sint", "R8G8Unorm", "R8G8Snorm", "R8G8Uint", "R8G8Sint", "R16G16Float", "R16G16Unorm", "R16G16Snorm",
"R16G16Uint", "R16G16Sint", "R32G32Float", "R32G32Uint", "R32G32Sint", "R8G8B8A8Unorm", "R8G8B8A8Snorm",
"R8G8B8A8Uint", "R8G8B8A8Sint", "R16G16B16A16Float", "R16G16B16A16Unorm", "R16G16B16A16Snorm", "R16G16B16A16Uint",
"R16G16B16A16Sint", "R32G32B32A32Float", "R32G32B32A32Uint", "R32G32B32A32Sint", "R10G10B10A2Unorm",
"R10G10B10A2Uint", "R11G11B10Float"};
#define COUNT(a) ((int)(sizeof a / sizeof *a))
static const char *attr_name(uint8_t t) { return t == 1 ? "sint" : t == 2 ? "uint" : "float"; }
// ---- reflection: the .res.json the translator writes (tools/xlate/Program.cs Translate) and the SPIR-V itself
// `"name":` inside [o, e): the first byte after the colon and its spaces, NULL when absent.
static const char *json_key(const char *o, const char *e, const char *name) {
char pat[40];
int n = snprintf(pat, sizeof pat, "\"%s\":", name);
for (const char *p = o; p + n <= e; p++)
if (*p == '"' && !memcmp(p, pat, (size_t)n)) {
p += n;
while (p < e && (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')) p++;
return p;
}
return NULL;
}
static long json_int(const char *o, const char *e, const char *name, long def) {
const char *p = json_key(o, e, name);
return p ? strtol(p, NULL, 10) : def;
}
// Copies a string value into buf (empty when absent or not a string).
static void json_str(const char *o, const char *e, const char *name, char *buf, size_t n) {
const char *p = json_key(o, e, name);
size_t k = 0;
if (p && *p == '"')
for (p++; p < e && *p != '"' && k + 1 < n; p++) buf[k++] = *p;
buf[k] = 0;
}
// The objects of the flat array `"name": [ {...}, ... ]`: calls fn for each object's [start, end). Returns the count.
static int json_objects(const char *js, const char *name, void (*fn)(GfxShader *, int, const char *, const char *),
GfxShader *s, int kind) {
const char *end = js + strlen(js), *p = json_key(js, end, name);
if (!p || *p != '[') return 0;
int n = 0;
for (p++; p < end && *p != ']'; p++) {
if (*p != '{') continue;
const char *q = memchr(p, '}', (size_t)(end - p));
if (!q) break;
if (fn) fn(s, kind, p, q);
n++;
p = q;
}
return n;
}
static int fmt_code(const char *name) {
for (int i = 1; i < COUNT(fmt_names); i++)
if (!strcmp(fmt_names[i], name)) return i;
return 0;
}
// c2 slot of a texture or image handle (Service.cs of MNG's totk_xlate: combined handles at 0x20, 32 slots; images at
// 0x120; separate textures at 0x168), -1 outside those ranges or for a handle in another constant buffer.
static int handle_slot(int kind, int cbuf_slot, int handle, int separate) {
if (cbuf_slot >= 0 && cbuf_slot != 2) return -1;
int bytes = (handle & 0x3fff) * 4;
if (kind == GFX_RES_IMG) return bytes >= 0x120 && bytes < 0x168 ? (bytes - 0x120) / 8 : -1;
if (separate) return bytes >= 0x168 && bytes < 0x568 ? (bytes - 0x168) / 8 : -1;
return bytes >= 0x20 && bytes < 0x120 ? (bytes - 0x20) / 8 : -1;
}
static void add_res(GfxShader *s, int kind, const char *o, const char *e) {
GfxShaderRes *r = &s->res[kind][s->nres[kind]++];
r->set = 0;
r->binding = (uint16_t)json_int(o, e, "binding", 0);
if (kind == GFX_RES_UBO || kind == GFX_RES_SSBO) {
r->slot = (int16_t)json_int(o, e, "slot", -1);
r->count = 1;
if (kind == GFX_RES_SSBO) {
r->cbuf_slot = (int8_t)json_int(o, e, "cb_slot", -1);
r->handle = (uint16_t)json_int(o, e, "cb_offset", 0);
r->flags = (uint8_t)json_int(o, e, "flags", 0);
}
return;
}
char buf[32];
json_str(o, e, "format", buf, sizeof buf);
const char *sep = json_key(o, e, "separate");
r->type = (uint16_t)json_int(o, e, "type", 0);
r->format = (uint8_t)fmt_code(buf);
r->flags = (uint8_t)json_int(o, e, "flags", 0);
r->cbuf_slot = (int8_t)json_int(o, e, "cbuf_slot", -1);
int handle = (int)json_int(o, e, "handle", 0);
r->handle = (uint16_t)handle;
r->count = (uint16_t)json_int(o, e, "array", 1);
r->separate = sep && *sep == 't';
r->slot = (int16_t)handle_slot(kind, r->cbuf_slot, handle, r->separate);
}
// SPIR-V opcodes and enumerants used below (SPIR-V 1.6 specification, section 3).
enum { OP_ENTRY_POINT = 15, OP_EXECUTION_MODE = 16, OP_TYPE_INT = 21, OP_TYPE_FLOAT = 22, OP_TYPE_VECTOR = 23,
OP_TYPE_ARRAY = 28, OP_TYPE_POINTER = 32, OP_VARIABLE = 59, OP_DECORATE = 71,
DEC_BUILTIN = 11, DEC_LOCATION = 30, DEC_COMPONENT = 31, SC_INPUT = 1, SC_OUTPUT = 3, EM_LOCAL_SIZE = 17 };
// Interface Locations and the compute workgroup size from the module. Ids beyond 2^20 are ignored (Ryujinx modules stay
// far below).
static void reflect_spirv(GfxShader *s) {
const uint32_t *w = s->spirv;
uint64_t n = s->words;
s->local[0] = s->local[1] = s->local[2] = 1;
if (n < 5) return;
uint32_t bound = w[3] < (1u << 20) ? w[3] : (1u << 20);
// per id: location + 1 (0 = none), component flag, scalar/vector description of types, pointee of pointers
int32_t *loc = calloc(bound, sizeof *loc);
uint8_t *packed = calloc(bound, 1), *ttype = calloc(bound, 1), *tcomps = calloc(bound, 1);
uint32_t *pointee = calloc(bound, sizeof *pointee);
if (!loc || !packed || !ttype || !tcomps || !pointee) goto out;
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;
uint32_t id = len > 1 ? a[0] : 0;
if (op == OP_EXECUTION_MODE && len >= 6 && a[1] == EM_LOCAL_SIZE) {
s->local[0] = a[2], s->local[1] = a[3], s->local[2] = a[4];
} else if (op == OP_DECORATE && len >= 4 && id < bound) {
if (a[1] == DEC_LOCATION) loc[id] = (int32_t)a[2] + 1;
else if (a[1] == DEC_COMPONENT) packed[id] = 1;
} else if (op == OP_TYPE_INT && len >= 4 && id < bound) {
ttype[id] = a[2] ? GFX_TYPE_SINT : GFX_TYPE_UINT, tcomps[id] = 1;
} else if (op == OP_TYPE_FLOAT && len >= 3 && id < bound) {
ttype[id] = GFX_TYPE_FLOAT, tcomps[id] = 1;
} else if (op == OP_TYPE_VECTOR && len >= 4 && id < bound && a[1] < bound) {
ttype[id] = ttype[a[1]], tcomps[id] = (uint8_t)a[2];
} else if (op == OP_TYPE_ARRAY && len >= 4 && id < bound && a[1] < bound) {
ttype[id] = ttype[a[1]], tcomps[id] = tcomps[a[1]]; // element type (arrayed interface blocks)
} else if (op == OP_TYPE_POINTER && len >= 4 && id < bound) {
pointee[id] = a[2];
} else if (op == OP_VARIABLE && len >= 4) {
uint32_t type = a[0], var = a[1], sc = a[2];
if ((sc == SC_INPUT || sc == SC_OUTPUT) && var < bound && type < bound && loc[var] > 0 && loc[var] <= 32) {
int l = loc[var] - 1;
uint32_t t = pointee[type] < bound ? pointee[type] : 0;
GfxShaderIo io = {ttype[t], tcomps[t] ? tcomps[t] : 4, packed[var]};
if (sc == SC_INPUT) s->in_mask |= 1u << l, s->in[l] = io;
else s->out_mask |= 1u << l, s->out[l] = io;
}
}
i += len;
}
out:
free(loc), free(packed), free(ttype), free(tcomps), free(pointee);
}
// Every gradient sample (OpImageSampleExplicitLod with Grad) whose two gradients are each built from two components gets
// them transposed, dx = (a, c) and dy = (b, d) from dx = (a, b) and dy = (c, d), by two new OpCompositeConstruct before
// it. TotK's shaders fill a TXD's gradient registers per coordinate (du/dx, du/dy, dv/dx, dv/dy), and the translator
// (Ryujinx, InstEmitTexture EmitTxd) builds dx = (first two registers), dy = (last two) = (du/dx, du/dy) and
// (dv/dx, dv/dy): a mip level chosen from the wrong derivatives, too sharp on terrain faces turned across the screen,
// which crawled in patches that followed the faces (1.4.3, the Depths). TOTK_TXD_TRANSPOSE=0 turns it off.
// Applied to the SPIR-V after it is loaded, so the shader cache is untouched.
enum { OP_COMPOSITE_CONSTRUCT = 80, OP_SAMPLE_EXPLICIT = 88, IO_GRAD = 4 };
static void patch_txd_transpose(GfxShader *s) {
static int on = -1;
static volatile int32_t n_logged;
if (on < 0) {
const char *e = cfg_get("TOTK_TXD_TRANSPOSE");
on = !(e && *e == '0');
}
uint32_t *w = s->spirv;
uint64_t n = s->words;
if (!on || n < 5) return;
uint32_t bound = w[3] < (1u << 20) ? w[3] : (1u << 20);
uint64_t *cc = calloc(bound, sizeof *cc); // word index of a 2-constituent OpCompositeConstruct defining the id
int found = 0;
if (!cc) return;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len || i + len > n) goto out;
if (op == OP_COMPOSITE_CONSTRUCT && len == 5 && w[i + 2] < bound) cc[w[i + 2]] = i;
if (op == OP_SAMPLE_EXPLICIT && len >= 8 && w[i + 5] == IO_GRAD && w[i + 6] < bound && w[i + 7] < bound &&
cc[w[i + 6]] && cc[w[i + 7]] && w[cc[w[i + 6]] + 1] == w[cc[w[i + 7]] + 1])
found++;
i += len;
}
if (!found) goto out;
uint32_t *o = malloc((n + (uint64_t)found * 10) * sizeof *o);
if (!o) goto out;
uint64_t k = 0;
uint32_t next = w[3];
for (uint64_t i = 0; i < 5; i++) o[k++] = w[i];
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (op == OP_SAMPLE_EXPLICIT && len >= 8 && w[i + 5] == IO_GRAD && w[i + 6] < bound && w[i + 7] < bound &&
cc[w[i + 6]] && cc[w[i + 7]] && w[cc[w[i + 6]] + 1] == w[cc[w[i + 7]] + 1]) {
const uint32_t *dx = w + cc[w[i + 6]], *dy = w + cc[w[i + 7]]; // [hdr, type, id, a, b], [hdr, type, id, c, d]
uint32_t ndx = next++, ndy = next++;
o[k++] = 5u << 16 | OP_COMPOSITE_CONSTRUCT, o[k++] = dx[1], o[k++] = ndx, o[k++] = dx[3], o[k++] = dy[3];
o[k++] = 5u << 16 | OP_COMPOSITE_CONSTRUCT, o[k++] = dx[1], o[k++] = ndy, o[k++] = dx[4], o[k++] = dy[4];
for (uint32_t j = 0; j < len; j++) o[k++] = w[i + j];
o[k - len + 6] = ndx, o[k - len + 7] = ndy;
} else {
for (uint32_t j = 0; j < len; j++) o[k++] = w[i + j];
}
i += len;
}
o[3] = next;
free(s->spirv);
s->spirv = o, s->words = k;
if (plat_inc32(&n_logged) <= 4) rt_log("gfx_shader: %d gradient samples now read their derivatives per coordinate", found);
out:
free(cc);
}
static void reflect(GfxShader *s) {
static const char *const kinds[GFX_RES_KINDS] = {"cbufs", "sbufs", "textures", "images"};
const char *js = s->reflection;
char stage[8];
json_str(js, js + strlen(js), "stage", stage, sizeof stage);
s->stage = !strcmp(stage, "cs") ? GFX_STAGE_CS : !strcmp(stage, "ps") ? GFX_STAGE_FS : GFX_STAGE_VS;
s->mvprev_banks = (uint32_t)json_int(js, js + strlen(js), "mvprev_banks", 0);
for (int k = 0; k < GFX_RES_KINDS; k++) {
int count = json_objects(js, kinds[k], NULL, s, k);
s->res[k] = calloc(count ? count : 1, sizeof *s->res[k]);
json_objects(js, kinds[k], add_res, s, k);
}
reflect_spirv(s);
patch_txd_transpose(s);
}
// ---- use masks (see the cache key at the top)
// The slots of the int array `"name": [...]` below 32 as a mask.
static uint32_t json_slot_mask(const char *js, const char *name) {
const char *end = js + strlen(js), *p = json_key(js, end, name);
uint32_t m = 0;
if (!p || *p != '[') return 0;
for (p++; p < end && *p != ']';) {
char *q;
long v = strtol(p, &q, 10);
if (q == p) {
p++;
continue;
}
if (v >= 0 && v < 32) m |= 1u << v;
p = q;
}
return m;
}
static UseMask use_of(const GfxShader *s) {
UseMask m = {json_slot_mask(s->reflection, "tex_queried") | json_slot_mask(s->reflection, "img_queried"), 0, 0};
if (s->stage == GFX_STAGE_VS) m.attrs = s->in_mask;
else if (s->stage == GFX_STAGE_FS) m.fouts = s->out_mask & 0xff;
return m;
}
// <sha256("TKSU", translator identity, code hash)>.use: masks are per translator.
static void use_name(const uint8_t h[32], uint8_t name[32]) {
uint8_t buf[4 + 32 + 32];
memcpy(buf, "TKSU", 4), memcpy(buf + 4, xlate_id, 32), memcpy(buf + 36, h, 32);
rt_sha256(buf, sizeof buf, name);
}
static void use_path_of(const uint8_t name[32], char *p) {
char hex[65];
key_hex(name, hex);
snprintf(p, PATH_CAP + 80, "%s/%s.use", cache_dir, hex);
}
static void use_path(const uint8_t h[32], char *p) {
uint8_t name[32];
use_name(h, name);
use_path_of(name, p);
}
// Every .use file of the cache, read by a background thread when the paths are set (use_preload), by file name: a
// lookup on the render thread then opens no file. Opening one took 0.1 ms alone but 11-40 ms in play, behind the
// translator's and the pipeline cache's writes and flushes on the same volume, and each new shader of a draw opened one.
// The table is complete and read-only once use_files_ready is 1; masks found later are in use_buckets.
#define USE_FILE_BUCKETS 65536
typedef struct UseFile { struct UseFile *next; uint8_t name[32]; UseMask m; } UseFile;
static UseFile *use_files[USE_FILE_BUCKETS];
static volatile int32_t use_files_ready;
static UseFile **use_file_slot(const uint8_t name[32]) {
UseFile **e = &use_files[(name[0] | name[1] << 8) % USE_FILE_BUCKETS];
while (*e && memcmp((*e)->name, name, 32)) e = &(*e)->next;
return e;
}
static int hex_byte(const char *c) {
int v = 0;
for (int i = 0; i < 2; i++) {
int d = isdigit((unsigned char)c[i]) ? c[i] - '0' : c[i] >= 'a' && c[i] <= 'f' ? c[i] - 'a' + 10 : -1;
if (d < 0) return -1;
v = v << 4 | d;
}
return v;
}
typedef struct { int n, bad; } UsePreload;
// One <64 hex digits>.use file of the cache directory into the table.
static int use_preload_file(const char *name, void *ctx) {
UsePreload *st = ctx;
char p[PATH_CAP + 80];
UseFile e = {0};
int ok = strlen(name) == 68;
for (int i = 0; ok && i < 32; i++) {
int v = hex_byte(name + 2 * i);
ok = v >= 0, e.name[i] = (uint8_t)v;
}
uint64_t len = 0;
uint32_t *u = NULL;
if (ok) {
snprintf(p, sizeof p, "%s/%s", cache_dir, name);
u = (uint32_t *)read_file(p, &len);
ok = u && len == 20 && u[0] == USE_MAGIC && u[1] == USE_VERSION;
}
UseFile **slot = ok ? use_file_slot(e.name) : NULL, *x;
if (slot && !*slot && (x = malloc(sizeof *x))) {
e.m.slots = u[2], e.m.attrs = u[3], e.m.fouts = u[4];
*x = e, *slot = x, st->n++;
} else if (!ok) {
st->bad++;
}
free(u);
return 1;
}
static void use_preload(void *arg) {
(void)arg;
plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
uint64_t t0 = plat_now_ns();
int n = 0, bad = 0;
for (uint32_t i = 0; i < pack_n; i++) { // the packed masks; the loose files below add the masks found since
uint32_t u[5];
UseFile **slot, *x;
if (pack_ix[i].kind != PACK_USE) continue;
if (pack_ix[i].size != sizeof u || !pack_read_at(pack_ix[i].offset, u, sizeof u) || u[0] != USE_MAGIC ||
u[1] != USE_VERSION) {
bad++;
continue;
}
if (!*(slot = use_file_slot(pack_ix[i].name)) && (x = malloc(sizeof *x))) {
memcpy(x->name, pack_ix[i].name, 32);
x->next = NULL, x->m.slots = u[2], x->m.attrs = u[3], x->m.fouts = u[4];
*slot = x, n++;
}
}
UsePreload st = {0};
plat_file_list(cache_dir, ".use", use_preload_file, &st);
n += st.n, bad += st.bad;
plat_xchg32(&use_files_ready, 1);
rt_log("gfx_shader: %d use masks read in %.0f ms on a background thread (%d unreadable)", n,
(plat_now_ns() - t0) / 1e6, bad);
return;
}
static void use_preload_start(void) {
if (!cache_dir[0]) return;
plat_thread_spawn(use_preload, NULL, "use preload", 0);
}
static UseEnt **use_slot(const uint8_t h[32]) { // caller holds cache_lock
UseEnt **e = &use_buckets[(h[0] | h[1] << 8) % BUCKETS];
while (*e && memcmp((*e)->code_hash, h, 32)) e = &(*e)->next;
return e;
}
// Sets or adds the entry for h. Caller holds cache_lock exclusively.
static void use_put(const uint8_t h[32], int known, const UseMask *m) {
UseEnt **e = use_slot(h);
if (!*e && !(*e = calloc(1, sizeof **e))) return;
memcpy((*e)->code_hash, h, 32);
if (known) (*e)->known = 1, (*e)->m = *m;
}
// 1 with *m when the use mask of h is known (memory, then <hash>.use; a disk miss is remembered).
static int use_find(const uint8_t h[32], UseMask *m) {
plat_mutex_lock_shared(&cache_lock);
UseEnt *e = *use_slot(h);
int seen = e != NULL, known = e && e->known;
if (known) *m = e->m;
plat_mutex_unlock_shared(&cache_lock);
if (seen) return known;
uint8_t name[32];
use_name(h, name);
if (plat_cas32(&use_files_ready, 0, 0)) { // complete: a name not in it has no file
UseFile *u = *use_file_slot(name);
if ((known = u != NULL)) *m = u->m;
} else {
char p[PATH_CAP + 80];
use_path_of(name, p);
uint64_t n = 0;
uint32_t *f = (uint32_t *)pack_read(PACK_USE, name, &n);
if (!f) f = (uint32_t *)read_file(p, &n);
known = f && n == 20 && f[0] == USE_MAGIC && f[1] == USE_VERSION;
if (known) m->slots = f[2], m->attrs = f[3], m->fouts = f[4];
free(f);
}
plat_mutex_lock(&cache_lock);
use_put(h, known, m);
plat_mutex_unlock(&cache_lock);
return known;
}
static void use_store(const uint8_t h[32], const UseMask *m) {
char p[PATH_CAP + 80];
use_path(h, p);
uint32_t f[5] = {USE_MAGIC, USE_VERSION, m->slots, m->attrs, m->fouts};
plat_file_write_atomic(p, f, sizeof f);
plat_mutex_lock(&cache_lock);
use_put(h, 1, m);
plat_mutex_unlock(&cache_lock);
}
static void put_u32(uint8_t **p, uint32_t v) { memcpy(*p, &v, 4), *p += 4; }
// Fixed-layout answers for hashing; returns the byte count (at most 256).
static size_t pack_answers(uint8_t *o, const GfxShaderAnswers *a) {
uint8_t *p = o;
GfxShaderAnswers zero = {0};
if (!a) a = &zero;
memcpy(p, a->tex_type, 32), p += 32;
memcpy(p, a->img_type, 32), p += 32;
memcpy(p, a->img_format, 32), p += 32;
memcpy(p, a->attr_type, 32), p += 32;
memcpy(p, a->frag_out_type, 8), p += 8;
for (int i = 0; i < 3; i++) *p++ = (uint8_t)a->local[i], *p++ = (uint8_t)(a->local[i] >> 8);
put_u32(&p, a->shared_bytes);
*p++ = (uint8_t)(a->depth_minus_w | (a->mvprev ? 0x80 : 0));
return (size_t)(p - o);
}
static int build_code_hash(int stage, const uint8_t *control, const uint8_t *code, uint64_t code_size,
const uint8_t *c1, uint32_t c1_len, uint8_t hash[32]) {
size_t total = 4 + CONTROL_SIZE + 8 + (size_t)code_size + 4 + c1_len;
uint8_t *buf = malloc(total), *p = buf;
if (!buf) return 0;
put_u32(&p, (uint32_t)stage);
memcpy(p, control, CONTROL_SIZE), p += CONTROL_SIZE;
memcpy(p, &code_size, 8), p += 8;
memcpy(p, code, (size_t)code_size), p += code_size;
put_u32(&p, c1_len);
if (c1_len) memcpy(p, c1, c1_len);
rt_sha256(buf, total, hash);
free(buf);
return 1;
}
// The answers with everything outside the mask zeroed; the translator ignores depth_minus_w (Program.cs).
static void mask_answers(const GfxShaderAnswers *a, const UseMask *m, GfxShaderAnswers *o) {
memset(o, 0, sizeof *o);
if (!a) return;
for (int i = 0; i < 32; i++) {
if (m->slots >> i & 1) o->tex_type[i] = a->tex_type[i], o->img_type[i] = a->img_type[i], o->img_format[i] = a->img_format[i];
if (m->attrs >> i & 1) o->attr_type[i] = a->attr_type[i];
}
for (int i = 0; i < 8; i++)
if (m->fouts >> i & 1) o->frag_out_type[i] = a->frag_out_type[i];
memcpy(o->local, a->local, sizeof o->local);
o->shared_bytes = a->shared_bytes;
o->mvprev = a->mvprev; // another translation, not an answer the use mask covers
}
// The cache key of an input id under the current translator.
static void key_of_input(const uint8_t iid[32], uint8_t key[32]) {
uint8_t buf[8 + 32 + 32], *p = buf;
memcpy(p, "TKSK", 4), p += 4;
put_u32(&p, KEY_VERSION);
memcpy(p, xlate_id, 32), p += 32;
memcpy(p, iid, 32), p += 32;
rt_sha256(buf, (size_t)(p - buf), key);
}
// The input id and cache key (m given), or the provisional key (m NULL: full answers; iid untouched).
static void build_key(const uint8_t code_hash[32], const GfxShaderAnswers *a, const UseMask *m, uint8_t key[32],
uint8_t iid[32]) {
GfxShaderAnswers masked;
uint8_t buf[8 + 32 + 32 + 256], *p = buf;
if (m) {
memcpy(p, "TKSI", 4), p += 4;
put_u32(&p, IN_VERSION);
memcpy(p, code_hash, 32), p += 32;
mask_answers(a, m, &masked);
p += pack_answers(p, &masked);
rt_sha256(buf, (size_t)(p - buf), iid);
key_of_input(iid, key);
return;
}
memcpy(p, "TKSP", 4), p += 4;
put_u32(&p, KEY_VERSION);
memcpy(p, xlate_id, 32), p += 32;
memcpy(p, code_hash, 32), p += 32;
p += pack_answers(p, a);
rt_sha256(buf, (size_t)(p - buf), key);
}
static size_t build_request(char *o0, const char *hex, const char *stage, const char *code_path, const uint8_t *c1,
uint32_t c1_len, const GfxShaderAnswers *a, const uint8_t *control) {
char *o = o0;
o += sprintf(o, "{\"job\":{\"key\":\"");
json_esc(&o, hex);
o += sprintf(o, "\",\"stage\":\"%s\",\"debug\":false,\"code\":\"", stage);
json_esc(&o, code_path);
o += sprintf(o, "\",\"c1\":\"");
for (uint32_t i = 0; i < c1_len; i++) o += sprintf(o, "%02x", c1[i]);
o += sprintf(o, "\"");
if (a) {
const char *sep = "";
o += sprintf(o, ",\"tex\":{");
for (int i = 0; i < 32; i++)
if (a->tex_type[i] && a->tex_type[i] < COUNT(tex_names)) o += sprintf(o, "%s\"%d\":\"%s\"", sep, i, tex_names[a->tex_type[i]]), sep = ",";
sep = "";
o += sprintf(o, "},\"img\":{");
for (int i = 0; i < 32; i++)
if (a->img_type[i] && a->img_type[i] < COUNT(tex_names)) {
const char *f = a->img_format[i] && a->img_format[i] < COUNT(fmt_names) ? fmt_names[a->img_format[i]] : "R8G8B8A8Unorm";
o += sprintf(o, "%s\"%d\":{\"type\":\"%s\",\"fmt\":\"%s\"}", sep, i, tex_names[a->img_type[i]], f), sep = ",";
}
sep = "";
o += sprintf(o, "},\"attr\":{");
for (int i = 0; i < 32; i++)
if (a->attr_type[i]) o += sprintf(o, "%s\"%d\":\"%s\"", sep, i, attr_name(a->attr_type[i])), sep = ",";
sep = "";
o += sprintf(o, "},\"fout\":{");
for (int i = 0; i < 8; i++)
if (a->frag_out_type[i]) o += sprintf(o, "%s\"%d\":\"%s\"", sep, i, attr_name(a->frag_out_type[i])), sep = ",";
o += sprintf(o, "},\"shared\":%u", a->shared_bytes);
}
if (!strcmp(stage, "cs")) {
uint32_t l[3] = {1, 1, 1};
if (a && (a->local[0] | a->local[1] | a->local[2])) l[0] = a->local[0], l[1] = a->local[1], l[2] = a->local[2];
else memcpy(l, control + 0x748, 12);
o += sprintf(o, ",\"local\":[%u,%u,%u]", l[0], l[1], l[2]);
}
/* Control byte +0x718 of a fragment shader: early fragment tests (the NVN driver's early-Z-force, MacNativeGames
ae8f4f4). Without it a shader that stores to memory is depth-tested late, and the G-buffer draws' EQUAL test
after the depth prepass fails along contour lines. Already part of the code hash (the whole control section). */
if (!strcmp(stage, "ps") && (control[0x718] & 1)) o += sprintf(o, ",\"early_fragment_tests\":true");
int si = !strcmp(stage, "vs") ? 0 : !strcmp(stage, "cs") ? 2 : 1; // gfx_shader_set_host answers
if (!host_f64) o += sprintf(o, ",\"softfloat64\":true");
if (a && a->mvprev && !strcmp(stage, "vs")) o += sprintf(o, ",\"mvprev\":true");
if (host_sbuf) o += sprintf(o, ",\"sbuf_align\":%u", host_sbuf);
if (host_sg[si] != 32) o += sprintf(o, ",\"subgroup\":%u", host_sg[si]);
if (host_gather) o += sprintf(o, ",\"gather_bias\":%u", host_gather);
if (host_zero_locals) o += sprintf(o, ",\"zero_locals\":true");
o += sprintf(o, "},\"out\":\"");
json_esc(&o, cache_dir);
o += sprintf(o, "\"}");
return (size_t)(o - o0);
}
// ---- public
// Raw line exchange for the probe: one line to the service, its reply in `reply`. Starts the service if needed.
int gfx_shader_raw_request(const char *line, char *reply, size_t reply_size) {
int away = kern_core_away();
Svc *v = svc_get();
int ok = child_exchange(v, line, reply, reply_size);
if (!ok) child_stop(v);
svc_put(v);
kern_core_back(&away);
return ok;
}
uint64_t gfx_shader_failures(void) { return (uint64_t)n_failures; }
uint64_t gfx_shader_requests(void) { return (uint64_t)n_requests; } // lines sent to the service
uint64_t gfx_shader_cache_hits(void) { return (uint64_t)n_hits; } // gets answered from memory or disk
// Drops the memory cache (the probe uses it to prove the disk cache). Pointers handed out before are invalid.
void gfx_shader_drop_memory(void) {
plat_mutex_lock(&cache_lock);
for (int i = 0; i < BUCKETS; i++) {
for (GfxShader *s = buckets[i], *n; s; s = n) {
n = s->next;
shader_free(s);
}
buckets[i] = NULL;
n_keys = 0;
for (UseEnt *e = use_buckets[i], *n; e; e = n) n = e->next, free(e);
use_buckets[i] = NULL;
}
// the preloaded .use table misses the masks this run wrote, which only use_buckets held: back to the disk
plat_xchg32(&use_files_ready, 0);
for (int i = 0; i < USE_FILE_BUCKETS; i++) {
for (UseFile *e = use_files[i], *n; e; e = n) n = e->next, free(e);
use_files[i] = NULL;
}
plat_mutex_unlock(&cache_lock);
}
static const GfxShader *fail(const char *why, const char *hex) {
plat_inc64(&n_failures);
rt_log("gfx_shader: translation failed (%s): %s", hex ? hex : "-", why);
return NULL;
}
// A validated request: the program's code length, its c1 constants and the cache key. 0 (failure counted) when invalid.
typedef struct {
const char *stage_name;
uint32_t length, c1_len;
const uint8_t *c1;
uint8_t code_hash[32];
uint8_t key[32]; // cache key, or the provisional one with final 0
uint8_t iid[32]; // input id with final 1
int final;
} Req;
static int req_init(Req *r, int stage, const uint8_t *control, const uint8_t *code, uint64_t code_size, const uint8_t *c1,
const GfxShaderAnswers *a) {
r->stage_name = stage == GFX_STAGE_VS ? "vs" : stage == GFX_STAGE_FS ? "ps" : stage == GFX_STAGE_CS ? "cs" : NULL;
if (!r->stage_name) return fail("stage is not translated", NULL), 0;
uint32_t c1_off;
memcpy(&c1_off, control + 0x700, 4), memcpy(&r->length, control + 0x704, 4);
uint64_t skip = stage == GFX_STAGE_CS ? 0 : CODE_SKIP;
if (!code || r->length > code_size || c1_off > r->length || skip >= r->length) return fail("bad control section", NULL), 0;
r->c1_len = r->length - c1_off;
r->c1 = c1 ? c1 : code + c1_off;
if (!build_code_hash(stage, control, code, r->length, r->c1, r->c1_len, r->code_hash))
return fail("out of memory", NULL), 0;
paths();
UseMask m;
r->final = use_find(r->code_hash, &m);
build_key(r->code_hash, a, r->final ? &m : NULL, r->key, r->iid);
return 1;
}
static const GfxShader *translate(const Req *r, int stage, const uint8_t *control, const uint8_t *code,
const GfxShaderAnswers *a);
static void ans_note(const Req *r, int stage, const uint8_t *control, const uint8_t *code, const GfxShaderAnswers *a,
const GfxShader *s);
static const GfxShader *get_async_one(Req r, int stage, const uint8_t *control, const uint8_t *code,
const GfxShaderAnswers *a, int *pending);
static const GfxShader *get_one(const Req *r, int stage, const uint8_t *control, const uint8_t *code,
const GfxShaderAnswers *a) {
GfxShader *s = mem_find(r->key);
if (s) return plat_inc64(&n_hits), s;
paths();
if (!cache_dir[0]) return fail("no cache directory", NULL);
if (r->final && (s = disk_load(r->key, r->iid))) return plat_inc64(&n_hits), mem_insert(s);
return translate(r, stage, control, code, a);
}
const GfxShader *gfx_shader_get(int stage, const uint8_t *control, const uint8_t *code, uint64_t code_size,
const uint8_t *c1, const GfxShaderAnswers *a) {
Req r;
if (!req_init(&r, stage, control, code, code_size, c1, a)) return NULL;
const GfxShader *s = get_one(&r, stage, control, code, a);
if (s) ans_note(&r, stage, control, code, a, s);
return s;
}
// ---- stored inputs (<cache>/in/<input id>.in, see the top)
typedef struct {
uint32_t magic, version, stage, length, c1_len, answers_size;
} InHeader; // then GfxShaderAnswers, control, code[length], c1[c1_len]
typedef struct {
uint8_t *buf; // the file; the pointers below point into it
int stage;
uint32_t length, c1_len;
const uint8_t *control, *code, *c1;
GfxShaderAnswers a;
} Inputs;
static void in_path(const uint8_t iid[32], char *p) {
char hex[65];
key_hex(iid, hex);
snprintf(p, PATH_CAP + 80, "%s/in/%s.in", cache_dir, hex);
}
// Stores the inputs of a translation unless they are stored already; the file appears whole (written, then renamed).
static void in_store(const uint8_t iid[32], int stage, const uint8_t *control, const uint8_t *code, const Req *r,
const GfxShaderAnswers *a) {
char p[PATH_CAP + 80], tmp[PATH_CAP + 120];
in_path(iid, p);
if (pack_find(PACK_IN, iid) || file_exists(p)) return;
GfxShaderAnswers zero = {0};
InHeader h = {IN_MAGIC, IN_VERSION, (uint32_t)stage, r->length, r->c1_len, sizeof(GfxShaderAnswers)};
size_t n = sizeof h + sizeof zero + CONTROL_SIZE + r->length + r->c1_len;
uint8_t *buf = malloc(n), *o = buf;
if (!buf) return;
memcpy(o, &h, sizeof h), o += sizeof h;
memcpy(o, a ? a : &zero, sizeof zero), o += sizeof zero;
memcpy(o, control, CONTROL_SIZE), o += CONTROL_SIZE;
memcpy(o, code, r->length), o += r->length;
if (r->c1_len) memcpy(o, r->c1, r->c1_len);
snprintf(tmp, PATH_CAP + 120, "%s/in", cache_dir);
mkdir_p(tmp);
plat_file_write_atomic(p, buf, n);
free(buf);
}
// Reads stored inputs into *in (free in->buf); 0 when absent or malformed.
static int in_load(const uint8_t iid[32], Inputs *in) {
char p[PATH_CAP + 80];
in_path(iid, p);
uint64_t n = 0;
uint8_t *f = pack_read(PACK_IN, iid, &n);
if (!f) f = read_file(p, &n);
InHeader h;
if (!f || n < sizeof h) return free(f), 0;
memcpy(&h, f, sizeof h);
if (h.magic != IN_MAGIC || h.version != IN_VERSION || h.answers_size != sizeof(GfxShaderAnswers) ||
n != sizeof h + sizeof(GfxShaderAnswers) + CONTROL_SIZE + (uint64_t)h.length + h.c1_len)
return free(f), 0;
const uint8_t *o = f + sizeof h;
memcpy(&in->a, o, sizeof in->a), o += sizeof in->a;
in->control = o, o += CONTROL_SIZE;
in->code = o, o += h.length;
in->c1 = o;
in->buf = f, in->stage = (int)h.stage, in->length = h.length, in->c1_len = h.c1_len;
return 1;
}
// ---- translation
static const GfxShader *translate_one(const Req *r, int stage, const uint8_t *control, const uint8_t *code,
const GfxShaderAnswers *a) {
const uint8_t *key = r->key;
GfxShader *s;
UseMask m;
uint8_t mk[32], miid[32];
// Another thread may have translated it while this one waited in flight_enter.
const uint8_t *look = r->final ? key : NULL, *look_iid = r->iid;
if (!r->final && use_find(r->code_hash, &m)) build_key(r->code_hash, a, &m, mk, miid), look = mk, look_iid = miid;
if (look && (s = mem_find(look))) return plat_inc64(&n_hits), s;
if (look && (s = disk_load(look, look_iid))) return plat_inc64(&n_hits), mem_insert(s);
uint64_t skip = stage == GFX_STAGE_CS ? 0 : CODE_SKIP;
char hex[65], code_path[PATH_CAP + 80], spv_path[PATH_CAP + 80], res_path[PATH_CAP + 80];
key_hex(key, hex);
mkdir_p(cache_dir);
snprintf(code_path, PATH_CAP + 80, "%s/%s.code", cache_dir, hex);
snprintf(spv_path, PATH_CAP + 80, "%s/%s.spv", cache_dir, hex);
snprintf(res_path, PATH_CAP + 80, "%s/%s.res.json", cache_dir, hex);
if (!write_file(code_path, code + skip, (size_t)(r->length - skip))) return fail("cannot write the code file", hex);
size_t cap = 2 * PATH_CAP * 6 + 2 * r->c1_len + 8192;
char *req = malloc(cap), reply[LINE_MAX_LEN];
if (!req) return plat_file_delete(code_path), fail("out of memory", hex);
build_request(req, hex, r->stage_name, code_path, r->c1, r->c1_len, a, control);
int ok = svc_request(req, reply, sizeof reply);
// TOTK_XLATE_KEEP=1: a last-frame variant's job stays as <key>.job (one request line) with its code, to run the
// translator on by hand (totk_xlate --serve < <key>.job)
static int keep = -1;
if (keep < 0) keep = rt_knob("XLATE_KEEP", "0");
int kept = keep && a && a->mvprev;
if (kept) {
char job_path[PATH_CAP + 80];
snprintf(job_path, sizeof job_path, "%s/%s.job", cache_dir, hex);
write_file(job_path, req, strlen(req));
}
free(req);
if (!kept) plat_file_delete(code_path);
if (!ok) return fail("translator unavailable", hex);
if (strncmp(reply, "ok ", 3)) {
plat_file_delete(spv_path), plat_file_delete(res_path);
return fail(reply, hex);
}
plat_file_flush(spv_path), plat_file_flush(res_path); // the translator wrote them; on disk before use or rename
if (!(s = disk_load(key, r->iid))) return fail("translator output missing or malformed", hex);
if (!r->final) { // name the files after the cache key, remember the mask
m = use_of(s);
build_key(r->code_hash, a, &m, mk, miid);
char mhex[65], to[PATH_CAP + 80];
key_hex(mk, mhex);
snprintf(to, PATH_CAP + 80, "%s/%s.spv", cache_dir, mhex);
int moved = plat_file_replace(spv_path, to);
snprintf(to, PATH_CAP + 80, "%s/%s.res.json", cache_dir, mhex);
moved = moved && plat_file_replace(res_path, to);
if (moved) {
use_store(r->code_hash, &m);
} else { // keep the mask for this run only
plat_file_delete(spv_path), plat_file_delete(res_path), rt_log("gfx_shader: cannot rename %s", hex);
plat_mutex_lock(&cache_lock);
use_put(r->code_hash, 1, &m);
plat_mutex_unlock(&cache_lock);
}
memcpy(s->key, mk, 32), memcpy(s->iid, miid, 32);
}
in_store(s->iid, stage, control, code, r, a);
plat_inc64(&n_xlated);
return mem_insert(s);
}
// The service round trip for a request that is in neither cache. One translation per cache key (per code hash while
// the use mask is unknown) runs at a time; different shaders run in parallel on the children.
static const GfxShader *translate(const Req *r, int stage, const uint8_t *control, const uint8_t *code,
const GfxShaderAnswers *a) {
int away = kern_core_away();
const uint8_t *id = r->final ? r->key : r->code_hash;
flight_enter(id);
const GfxShader *s = translate_one(r, stage, control, code, a);
flight_leave(id);
kern_core_back(&away);
return s;
}
// ---- background translation (gfx_shader_get_async)
// svc_runtime_limit() workers, one per translator child. A job owns copies of the control section and the code, so the
// guest may free or rewrite its program while the job waits. A key stays in X.waits while its job is queued or running,
// and for good when the translation failed, so a failing shader is not sent again on every draw.
typedef struct XJob {
struct XJob *next;
int stage;
Req r; // r.c1 points into code
uint8_t *control, *code;
GfxShaderAnswers a;
} XJob;
typedef struct XWait {
struct XWait *next;
uint8_t key[32];
int failed;
} XWait;
static struct {
PlatMutex lock;
PlatCond work, done; // done: a job finished (gfx_shader_await)
XJob *head, *tail;
XWait *waits[256];
int started; // 1 running, -1 no thread could be created
} X = {PLAT_MUTEX_INIT, PLAT_COND_INIT, PLAT_COND_INIT};
static volatile int64_t n_async;
// Caller holds X.lock.
static XWait **wait_slot(const uint8_t key[32]) {
XWait **w = &X.waits[key[0]];
while (*w && memcmp((*w)->key, key, 32)) w = &(*w)->next;
return w;
}
static void xlate_worker(void *arg) {
(void)arg;
for (;;) {
plat_mutex_lock(&X.lock);
while (!X.head) plat_cond_wait(&X.work, &X.lock);
XJob *j = X.head;
if (!(X.head = j->next)) X.tail = NULL;
plat_mutex_unlock(&X.lock);
const GfxShader *s = translate(&j->r, j->stage, j->control, j->code, &j->a);
plat_mutex_lock(&X.lock);
XWait **w = wait_slot(j->r.key), *e = *w;
if (e && s) *w = e->next, free(e);
else if (e) e->failed = 1;
plat_mutex_unlock(&X.lock);
plat_cond_broadcast(&X.done);
if (s) plat_inc64(&n_async);
free(j->control), free(j->code), free(j);
}
return;
}
const GfxShader *gfx_shader_get_async(int stage, const uint8_t *control, const uint8_t *code, uint64_t code_size,
const GfxShaderAnswers *a, int *pending) {
*pending = 0;
Req r;
if (!req_init(&r, stage, control, code, code_size, NULL, a)) return NULL;
const GfxShader *s = get_async_one(r, stage, control, code, a, pending);
if (s) ans_note(&r, stage, control, code, a, s);
return s;
}
static const GfxShader *get_async_one(Req r, int stage, const uint8_t *control, const uint8_t *code,
const GfxShaderAnswers *a, int *pending) {
GfxShader *s = mem_find(r.key);
if (s) return plat_inc64(&n_hits), s;
plat_mutex_lock_shared(&X.lock);
XWait *w = *wait_slot(r.key);
int failed = w && w->failed;
plat_mutex_unlock_shared(&X.lock);
if (w) return *pending = !failed, NULL;
paths();
if (!cache_dir[0]) return fail("no cache directory", NULL);
if (r.final && (s = disk_load(r.key, r.iid))) return plat_inc64(&n_hits), mem_insert(s);
XJob *j = calloc(1, sizeof *j);
XWait *e = calloc(1, sizeof *e);
uint8_t *ctl = malloc(CONTROL_SIZE), *cd = malloc(r.length);
plat_mutex_lock(&X.lock);
if (!X.started) {
int n = 0;
for (int i = 0; i < svc_runtime_limit(); i++) {
if (plat_thread_spawn(xlate_worker, NULL, "xlate", 0)) n++;
}
X.started = n ? 1 : -1;
if (n) rt_log("gfx_shader: graphics shaders are translated by %d background workers (TOTK_XLATE_PROCS)", n);
else rt_log("gfx_shader: cannot start the translation workers; translating synchronously");
}
XWait **slot = wait_slot(r.key);
int raced = *slot != NULL; // another thread queued it between the checks
int queued = !raced && X.started == 1 && j && e && ctl && cd;
if (queued) {
memcpy(ctl, control, CONTROL_SIZE), memcpy(cd, code, r.length);
*j = (XJob){.stage = stage, .r = r, .control = ctl, .code = cd, .a = *a};
j->r.c1 = cd + (r.length - r.c1_len);
memcpy(e->key, r.key, 32);
*slot = e;
if (X.tail) X.tail->next = j;
else X.head = j;
X.tail = j;
plat_cond_signal(&X.work);
}
plat_mutex_unlock(&X.lock);
if (queued) return *pending = 1, NULL;
free(j), free(e), free(ctl), free(cd);
if (raced) return *pending = 1, NULL;
return translate(&r, stage, control, code, a); // no worker or out of memory
}
const GfxShader *gfx_shader_await(int stage, const uint8_t *control, const uint8_t *code, uint64_t code_size,
const GfxShaderAnswers *a) {
int pending;
const GfxShader *s = gfx_shader_get_async(stage, control, code, code_size, a, &pending);
Req r;
if (!pending || !req_init(&r, stage, control, code, code_size, NULL, a)) return s;
uint64_t p0 = perf_begin(); // the wait counts as the render thread's translation time
plat_mutex_lock(&X.lock);
for (XJob *j = X.head, *prev = NULL; j; prev = j, j = j->next)
if (!memcmp(j->r.key, r.key, 32)) { // queued: to the front, the worker takes it next
if (prev) {
prev->next = j->next;
if (X.tail == j) X.tail = prev;
j->next = X.head, X.head = j;
}
break;
}
XWait *w;
while ((w = *wait_slot(r.key)) && !w->failed) plat_cond_wait(&X.done, &X.lock);
plat_mutex_unlock(&X.lock);
perf_end(PERF_XLATE, p0);
if (w) return NULL; // failed: logged by the worker
s = gfx_shader_get_async(stage, control, code, code_size, a, &pending);
return pending ? NULL : s;
}
uint64_t gfx_shader_async_done(void) { return (uint64_t)n_async; }
void gfx_shader_counts(uint64_t *keys, uint64_t *translated) { *keys = (uint64_t)n_keys, *translated = (uint64_t)n_xlated; }
const uint8_t *gfx_shader_key(const GfxShader *s) { return s->key; }
const uint8_t *gfx_shader_input_id(const GfxShader *s) { return s->iid; }
// By input id: memory, then the disk cache under the current translator, then (xlate 1) a translation from the stored
// inputs. NULL when none has it; a failed translation is counted as a failure.
const GfxShader *gfx_shader_load_input(const uint8_t iid[32], int xlate) {
paths();
if (!cache_dir[0]) return NULL;
uint8_t key[32];
key_of_input(iid, key);
GfxShader *s = mem_find(key);
if (s) return s;
if ((s = disk_load(key, iid))) return mem_insert(s);
Inputs in;
if (!xlate || !in_load(iid, &in)) return NULL;
const GfxShader *t = gfx_shader_get(in.stage, in.control, in.code, in.length, in.c1, &in.a);
free(in.buf);
return t;
}
// From the disk cache only, as a shader the caller owns and releases with gfx_shader_release_private (the boot library
// prebuild warms the driver's pipeline cache with it without keeping 40 KB per shader in memory). When the memory
// cache has it, that entry is returned with *owned 0 and must not be released. NULL when not cached.
const GfxShader *gfx_shader_load_private(const uint8_t iid[32], int *owned) {
*owned = 0;
paths();
if (!cache_dir[0]) return NULL;
uint8_t key[32];
key_of_input(iid, key);
GfxShader *s = mem_find(key);
if (s) return s;
if ((s = disk_load(key, iid))) *owned = 1;
return s;
}
void gfx_shader_release_private(const GfxShader *s) {
if (s) shader_free((GfxShader *)s);
}
// ---- boot prewarm (gfx_shader_prewarm)
typedef struct {
const uint8_t (*named)[32]; // loaded into memory, results in out
size_t n_named;
const GfxShader **out;
uint8_t (*other)[32]; // other stored inputs: translated when not cached, not loaded
size_t n_other;
volatile int64_t next, done;
volatile int32_t missing, other_cached;
} Warm;
static void warm_worker(void *arg) {
Warm *w = arg;
for (;;) {
size_t i = (size_t)plat_inc64(&w->next) - 1;
if (i >= w->n_named + w->n_other) return;
if (i < w->n_named) {
if (!(w->out[i] = gfx_shader_load_input(w->named[i], 1))) plat_inc32(&w->missing);
} else {
const uint8_t *iid = w->other[i - w->n_named];
uint8_t key[32];
char hex[65], p[PATH_CAP + 80];
key_of_input(iid, key);
key_hex(key, hex);
snprintf(p, PATH_CAP + 80, "%s/%s.spv", cache_dir, hex);
if (pack_find(PACK_SPV, key) || file_exists(p)) plat_inc32(&w->other_cached);
else gfx_shader_load_input(iid, 1);
}
plat_inc64(&w->done);
}
}
static int iid_cmp(const void *x, const void *y) { return memcmp(x, y, 32); }
typedef struct {
const uint8_t (*named)[32];
size_t n_named, n, cap;
uint8_t (*v)[32];
} InList;
static int in_collect(const char *file, void *ctx) {
InList *l = ctx;
char name[80];
uint8_t iid[32];
if (strlen(file) != 67) return 1; // 64 hex digits + ".in"
memcpy(name, file, 64);
name[64] = 0;
if (!hex32(name, iid) || (l->n_named && bsearch(iid, l->named, l->n_named, 32, iid_cmp))) return 1;
if (l->n == l->cap) {
size_t c = l->cap ? l->cap * 2 : 1024;
uint8_t (*t)[32] = realloc(l->v, c * 32);
if (!t) return 0;
l->v = t, l->cap = c;
}
memcpy(l->v[l->n++], iid, 32);
return 1;
}
// The stored inputs not in named (sorted): ids parsed from the in/ file names.
static size_t other_inputs(const uint8_t (*named)[32], size_t n_named, uint8_t (**out)[32]) {
char dir[PATH_CAP + 80];
snprintf(dir, sizeof dir, "%s/in", cache_dir);
InList l = {.named = named, .n_named = n_named};
char name[68];
for (uint32_t i = 0; i < pack_n; i++) { // the packed inputs, under the name their loose file would have
if (pack_ix[i].kind != PACK_IN) continue;
key_hex(pack_ix[i].name, name);
memcpy(name + 64, ".in", 4);
if (!in_collect(name, &l)) break;
}
plat_file_list(dir, ".in", in_collect, &l); // in_store writes no loose file for a packed input: no duplicates
return *out = l.v, l.n;
}
void gfx_shader_prewarm(const uint8_t (*iids)[32], size_t n, const GfxShader **out, int threads, int procs) {
paths();
double t0 = (double)plat_now_ms();
memset(out, 0, n * sizeof *out);
if (!cache_dir[0]) return;
uint64_t xl0 = (uint64_t)n_xlated, fail0 = (uint64_t)n_failures;
uint8_t (*sorted)[32] = n ? malloc(n * 32) : NULL;
if (sorted) memcpy(sorted, iids, n * 32), qsort(sorted, n, 32, iid_cmp);
Warm w = {.named = iids, .n_named = n, .out = out};
w.n_other = other_inputs(sorted, sorted ? n : 0, &w.other);
free(sorted);
size_t total = w.n_named + w.n_other;
threads = threads < 1 ? 1 : threads > 64 ? 64 : threads;
if ((size_t)threads > total) threads = (int)(total ? total : 1);
svc_set_limit(procs);
PlatThread *t[64];
int k = 0;
for (int i = 0; i < threads; i++)
if ((t[k] = plat_thread_new(warm_worker, &w, "shader prewarm"))) k++;
if (!k) warm_worker(&w);
for (int m = 0; m < k; m++)
while (!plat_thread_join_ns(t[m], 2000000000u))
rt_log("gfx_shader: prewarm: %lld of %zu shaders, %llu translated, %.0f ms", (long long)w.done, total,
(unsigned long long)((uint64_t)n_xlated - xl0), (double)plat_now_ms() - t0);
svc_set_limit(svc_runtime_limit());
rt_log("gfx_shader: prewarm: %zu shaders named by pipeline keys (%ld not available), %zu other stored inputs (%ld "
"cached); %llu translated, %llu failed, with up to %d translators and %d threads, in %.0f ms", w.n_named,
(long)w.missing, w.n_other, (long)w.other_cached, (unsigned long long)((uint64_t)n_xlated - xl0),
(unsigned long long)((uint64_t)n_failures - fail0), procs, k, (double)plat_now_ms() - t0);
free(w.other);
}
const uint32_t *gfx_shader_spirv(const GfxShader *s, uint64_t *words) {
if (words) *words = s->words;
return s->spirv;
}
int gfx_shader_stage(const GfxShader *s) { return s->stage; }
int gfx_shader_resources(const GfxShader *s, int kind, const GfxShaderRes **res) {
if (kind < 0 || kind >= GFX_RES_KINDS) return 0;
if (res) *res = s->res[kind];
return s->nres[kind];
}
// The slots of the use mask: answers outside it select the same translation (mask_answers), so a caller may leave
// their texture and image types zero. Parses the reflection: callers keep the result.
uint32_t gfx_shader_slot_mask(const GfxShader *s) { return use_of(s).slots; }
uint32_t gfx_shader_inputs(const GfxShader *s, GfxShaderIo io[32]) {
if (io) memcpy(io, s->in, sizeof s->in);
return s->in_mask;
}
uint32_t gfx_shader_mvprev_banks(const GfxShader *s) { return s->mvprev_banks; }
uint32_t gfx_shader_outputs(const GfxShader *s, GfxShaderIo io[32]) {
if (io) memcpy(io, s->out, sizeof s->out);
return s->out_mask;
}
void gfx_shader_local_size(const GfxShader *s, uint32_t local[3]) { memcpy(local, s->local, sizeof s->local); }
// ---- answers met in play
// The translated-shader key includes the answers a draw's state implies, so a translation made without play (the
// census) does not match the keys play asks for. Every (program, answers) that a gfx_shader_get* call resolves is
// recorded next to pipeline-keys.bin, so the first-start preparation (Task 2.6, here the TOTK_TEST_PREPARE_SHADERS hook)
// translates exactly the variants play needs, from these files alone:
// shader-answers.bin "TOTKSANS", version, record size; records: stage (u32), code hash, input id, pack_answers bytes
// of the full answers, zero padding. One record per input id: answers outside the shader's use
// mask do not change its SPIR-V, are out of its input id and cache key (mask_answers) and so do
// not make records of their own (a fight run meets 4126 full pairs, 1238 input ids). The full
// answers are kept so a translator with a wider mask still gets them. Under a new translator
// the masks, hence the input ids, are new, and play appends records for them.
// shader-programs.bin "TOTKSPRG", version, 0; records: stage, length, c1 length (u32 each), code hash, control section,
// code[length], c1. One per code hash; written before the answers that name it. Game content:
// it stays in the ignored pipeline cache directory.
// A file with another header is rewritten; a torn tail (killed process) is cut off at open.
#define ANS_VERSION 1
#define ANS_PACKED 147 // pack_answers' size
#define ANS_REC (4 + 32 + 32 + ANS_PACKED + 1)
#define PRG_VERSION 1
#define PRG_HEAD (12 + 32 + CONTROL_SIZE)
typedef struct { char magic[8]; uint32_t version, rec_size; } RecHeader;
typedef struct { uint64_t *v; uint32_t cap, n; } Set64; // open addressing, 0 empty
typedef struct { uint8_t *p; size_t n, cap; } Bytes;
static struct {
PlatMutex lock;
volatile int32_t on;
char ans_path[PATH_CAP], prg_path[PATH_CAP];
Set64 pairs, progs; // input ids, code hashes seen (in the files or this run)
Bytes ans, prg; // pending
uint64_t pairs_new; // added this run
} RC = {PLAT_MUTEX_INIT};
static int set_add(Set64 *s, uint64_t h) { // 1 if new
h = h ? h : 1;
if ((s->n + 1) * 2 > s->cap) {
uint32_t cap = s->cap ? s->cap * 2 : 4096;
uint64_t *t = calloc(cap, sizeof *t);
if (!t) return 0;
for (uint32_t i = 0; i < s->cap; i++)
if (s->v[i]) {
uint32_t j = (uint32_t)s->v[i] & (cap - 1);
while (t[j]) j = (j + 1) & (cap - 1);
t[j] = s->v[i];
}
free(s->v), s->v = t, s->cap = cap;
}
uint32_t j = (uint32_t)h & (s->cap - 1);
for (; s->v[j]; j = (j + 1) & (s->cap - 1))
if (s->v[j] == h) return 0;
s->v[j] = h, s->n++;
return 1;
}
static int bytes_put(Bytes *b, const void *p, size_t n) {
if (!n) return 1;
if (b->n + n > b->cap) {
size_t cap = b->cap ? b->cap * 2 : 65536;
while (cap < b->n + n) cap *= 2;
uint8_t *t = realloc(b->p, cap);
if (!t) return 0;
b->p = t, b->cap = cap;
}
memcpy(b->p + b->n, p, n), b->n += n;
return 1;
}
static uint64_t hash8(const uint8_t *sha) {
uint64_t v;
memcpy(&v, sha, 8);
return v;
}
static void unpack_answers(const uint8_t *p, GfxShaderAnswers *a) {
memset(a, 0, sizeof *a);
memcpy(a->tex_type, p, 32), p += 32;
memcpy(a->img_type, p, 32), p += 32;
memcpy(a->img_format, p, 32), p += 32;
memcpy(a->attr_type, p, 32), p += 32;
memcpy(a->frag_out_type, p, 8), p += 8;
for (int i = 0; i < 3; i++) a->local[i] = (uint16_t)(p[0] | p[1] << 8), p += 2;
memcpy(&a->shared_bytes, p, 4), p += 4;
a->depth_minus_w = *p & 0x7f, a->mvprev = *p >> 7;
}
// Reads a record file's body (after its header) into *body; 0 when absent, and a file with another header is removed.
static int rec_read(const char *path, const char *magic, uint32_t version, uint32_t rec_size, uint8_t **body, size_t *n) {
*body = NULL, *n = 0;
FILE *f = fopen(path, "rb");
if (!f) return 0;
RecHeader h;
fseek(f, 0, SEEK_END);
long len = ftell(f);
fseek(f, 0, SEEK_SET);
int ok = len >= (long)sizeof h && fread(&h, sizeof h, 1, f) == 1 && !memcmp(h.magic, magic, 8) &&
h.version == version && h.rec_size == rec_size;
if (ok && len > (long)sizeof h) {
*n = (size_t)len - sizeof h;
ok = (*body = malloc(*n)) && fread(*body, 1, *n, f) == *n;
}
fclose(f);
if (!ok) {
free(*body), *body = NULL, *n = 0;
if (len > 0) rt_log("gfx_shader: %s has another format or version; it is rewritten", path), remove(path);
}
return ok;
}
// Walks shader-programs.bin records; returns the byte length of the whole records. fn may be NULL.
static size_t prg_walk(const uint8_t *b, size_t n, void (*fn)(const uint8_t *rec, void *ctx), void *ctx) {
size_t o = 0;
while (n - o >= PRG_HEAD) {
uint32_t len, c1;
memcpy(&len, b + o + 4, 4), memcpy(&c1, b + o + 8, 4);
size_t size = PRG_HEAD + (size_t)len + c1;
if (len > (64u << 20) || c1 > len || size > n - o) break;
if (fn) fn(b + o, ctx);
o += size;
}
return o;
}
static void prg_seen(const uint8_t *rec, void *ctx) { set_add(ctx, hash8(rec + 12)); }
static void truncate_to(const char *path, size_t size) {
if (!plat_file_truncate(path, size)) return;
rt_log("gfx_shader: %s had a torn tail; cut to %zu bytes", path, size);
}
void gfx_shader_record_at(const char *keys_path) {
if (!keys_path || !keys_path[0] || RC.on) return;
plat_mutex_lock(&RC.lock);
const char *slash = strrchr(keys_path, '\\'), *fs = strrchr(keys_path, '/');
if (fs > slash) slash = fs;
int dir = slash ? (int)(slash + 1 - keys_path) : 0;
snprintf(RC.ans_path, sizeof RC.ans_path, "%.*sshader-answers.bin", dir, keys_path);
snprintf(RC.prg_path, sizeof RC.prg_path, "%.*sshader-programs.bin", dir, keys_path);
uint8_t *b;
size_t n;
if (rec_read(RC.ans_path, "TOTKSANS", ANS_VERSION, ANS_REC, &b, &n)) {
for (size_t o = 0; o + ANS_REC <= n; o += ANS_REC) set_add(&RC.pairs, hash8(b + o + 36));
if (n % ANS_REC) truncate_to(RC.ans_path, sizeof(RecHeader) + n / ANS_REC * ANS_REC);
}
free(b);
if (rec_read(RC.prg_path, "TOTKSPRG", PRG_VERSION, 0, &b, &n)) {
size_t whole = prg_walk(b, n, prg_seen, &RC.progs);
if (whole != n) truncate_to(RC.prg_path, sizeof(RecHeader) + whole);
}
free(b);
rt_log("gfx_shader: recording answers met in play to %s (%u records, %u programs recorded before)", RC.ans_path,
RC.pairs.n, RC.progs.n);
plat_xchg32(&RC.on, 1);
plat_mutex_unlock(&RC.lock);
}
// A get resolved (r, a) to s: queue the record when its input id is new, and its program when new. Cost: a set
// lookup under a lock per call; gets happen per (program, answers) change in vk_exec_draw's shader table, not per draw.
static void ans_note(const Req *r, int stage, const uint8_t *control, const uint8_t *code, const GfxShaderAnswers *a,
const GfxShader *s) {
if (!RC.on) return;
uint32_t st = (uint32_t)stage;
plat_mutex_lock(&RC.lock);
if (set_add(&RC.pairs, hash8(s->iid))) {
uint8_t rec[ANS_REC] = {0};
memcpy(rec, &st, 4), memcpy(rec + 4, r->code_hash, 32), memcpy(rec + 36, s->iid, 32);
pack_answers(rec + 68, a);
if (set_add(&RC.progs, hash8(r->code_hash))) {
uint32_t head[3] = {st, r->length, r->c1_len};
bytes_put(&RC.prg, head, 12), bytes_put(&RC.prg, r->code_hash, 32);
bytes_put(&RC.prg, control, CONTROL_SIZE), bytes_put(&RC.prg, code, r->length);
bytes_put(&RC.prg, r->c1, r->c1_len);
}
bytes_put(&RC.ans, rec, sizeof rec);
RC.pairs_new++;
}
plat_mutex_unlock(&RC.lock);
}
static int rec_append(const char *path, const char *magic, uint32_t version, uint32_t rec_size, const Bytes *b) {
FILE *f = fopen(path, "ab");
if (!f) return 0;
fseek(f, 0, SEEK_END);
int ok = 1;
if (ftell(f) == 0) {
RecHeader h = {{0}, version, rec_size};
memcpy(h.magic, magic, 8);
ok = fwrite(&h, sizeof h, 1, f) == 1;
}
ok = ok && fwrite(b->p, 1, b->n, f) == b->n;
return !fclose(f) && ok;
}
static PlatMutex rec_io = PLAT_MUTEX_INIT; // the record files: appends in order, no read during an append
void gfx_shader_record_flush(int force) {
static volatile int64_t last;
int64_t now = (int64_t)plat_now_ms();
if (!RC.on || (!force && now - last < 2000)) return;
last = now;
// The files are written outside RC.lock, which the render thread takes to record a new answer.
plat_mutex_lock(&rec_io);
plat_mutex_lock(&RC.lock);
Bytes ans = RC.ans, prg = RC.prg;
RC.ans = (Bytes){0}, RC.prg = (Bytes){0};
uint64_t total = RC.pairs.n, added = RC.pairs_new;
plat_mutex_unlock(&RC.lock);
if (prg.n && !rec_append(RC.prg_path, "TOTKSPRG", PRG_VERSION, 0, &prg)) rt_log("gfx_shader: cannot write %s", RC.prg_path);
if (ans.n && !rec_append(RC.ans_path, "TOTKSANS", ANS_VERSION, ANS_REC, &ans)) rt_log("gfx_shader: cannot write %s", RC.ans_path);
plat_mutex_unlock(&rec_io);
if (ans.n) rt_log("gfx_shader: answers: %zu new records appended (%llu new this run, %llu recorded)", ans.n / ANS_REC,
(unsigned long long)added, (unsigned long long)total);
free(ans.p), free(prg.p);
}
// ---- preparation from record files: the answers met in play (TOTK_TEST_PREPARE_SHADERS=1) or the seed that
// tools/prepare_shaders.py writes from the MacNativeGames shader list (totk.exe --prepare-shaders). Both use the record
// layouts above; a seed record has a zero input id (the use mask, hence the id, is the translator's to find) and its
// pad byte holds the list priority (0 title, 1 warm-up, 2 rest).
typedef struct { const uint8_t *rec; } PrgIdx;
typedef struct { PrgIdx *idx; size_t n; } PrgList;
typedef struct {
PrgList progs;
const uint8_t *ans;
size_t n_ans;
uint8_t (*iid)[32]; // per answers record: the resolved input id, zero on failure
volatile int64_t next, done, missing, failed;
} Prep;
static int prg_cmp(const void *x, const void *y) { return memcmp(((const PrgIdx *)x)->rec + 12, ((const PrgIdx *)y)->rec + 12, 32); }
static void prg_collect(const uint8_t *rec, void *ctx) {
PrgList *l = ctx;
l->idx[l->n++].rec = rec;
}
static void prep_worker(void *arg) {
Prep *p = arg;
for (;;) {
size_t i = (size_t)plat_inc64(&p->next) - 1;
if (i >= p->n_ans) return;
const uint8_t *r = p->ans + i * ANS_REC;
uint8_t key_rec[12 + 32];
memcpy(key_rec + 12, r + 4, 32);
PrgIdx k = {key_rec}, *e = bsearch(&k, p->progs.idx, p->progs.n, sizeof k, prg_cmp);
if (!e) {
plat_inc64(&p->missing), plat_inc64(&p->done);
continue;
}
uint32_t st, len;
memcpy(&st, e->rec, 4), memcpy(&len, e->rec + 4, 4);
const uint8_t *control = e->rec + 44, *code = control + CONTROL_SIZE, *c1 = code + len;
GfxShaderAnswers a;
unpack_answers(r + 68, &a);
const GfxShader *s = gfx_shader_get((int)st, control, code, len, c1, &a);
if (!s) plat_inc64(&p->failed);
else if (p->iid) memcpy(p->iid[i], s->iid, 32);
plat_inc64(&p->done);
}
}
// Translates every record of ans_path whose program is in prg_path on `threads` threads with up to `procs` translators.
// *ans_out, *iid_out (optional; free them): the answers records and the input id each resolved to (zero: failed).
// Returns the record count, -1 without the files.
static long long prepare_files(const char *ans_path, const char *prg_path, int threads, int procs, const char *what,
uint8_t **ans_out, uint8_t (**iid_out)[32]) {
paths();
double t0 = (double)plat_now_ms();
uint8_t *ab = NULL, *pb = NULL;
size_t an = 0, pn = 0;
if (!rec_read(ans_path, "TOTKSANS", ANS_VERSION, ANS_REC, &ab, &an) ||
!rec_read(prg_path, "TOTKSPRG", PRG_VERSION, 0, &pb, &pn)) {
rt_log("gfx_shader: prepare: no %s (%s)", what, ans_path);
free(ab), free(pb);
return -1;
}
Prep p = {.progs = {calloc(pn / PRG_HEAD + 1, sizeof(PrgIdx)), 0}, .ans = ab, .n_ans = an / ANS_REC};
if (iid_out) p.iid = calloc(p.n_ans + 1, 32);
if (!p.progs.idx || (iid_out && !p.iid)) return free(p.progs.idx), free(p.iid), free(ab), free(pb), -1;
prg_walk(pb, pn, prg_collect, &p.progs);
qsort(p.progs.idx, p.progs.n, sizeof *p.progs.idx, prg_cmp);
uint64_t xl0 = (uint64_t)n_xlated;
threads = threads < 1 ? 1 : threads > 64 ? 64 : threads;
svc_set_limit(procs);
PlatThread *t[64];
int k = 0;
for (int i = 0; i < threads; i++)
if ((t[k] = plat_thread_new(prep_worker, &p, "shader prepare"))) k++;
if (!k) prep_worker(&p);
for (int m = 0; m < k; m++)
while (!plat_thread_join_ns(t[m], 5000000000u))
rt_log("gfx_shader: prepare: %lld of %zu pairs, %llu translated, %.0f ms", (long long)p.done, p.n_ans,
(unsigned long long)((uint64_t)n_xlated - xl0), (double)plat_now_ms() - t0);
svc_set_limit(svc_runtime_limit());
rt_log("gfx_shader: prepare: %zu %s of %zu programs: %llu translated, %lld failed, %lld without a program, the rest "
"cached; %.0f ms", p.n_ans, what, p.progs.n, (unsigned long long)((uint64_t)n_xlated - xl0), (long long)p.failed,
(long long)p.missing, (double)plat_now_ms() - t0);
free(p.progs.idx), free(pb);
if (ans_out) *ans_out = ab;
else free(ab);
if (iid_out) *iid_out = p.iid;
return (long long)p.n_ans;
}
void gfx_shader_prepare_recorded(int threads, int procs) {
if (!RC.on) {
rt_log("gfx_shader: prepare: no recorded answers (not recording)");
return;
}
prepare_files(RC.ans_path, RC.prg_path, threads, procs, "recorded pairs", NULL, NULL);
}
// ---- prepared variants (shader-prepared.bin next to pipeline-keys.bin)
// "TOTKSPRP", version, record size; records: stage, priority, 2 zero bytes, input id. gfx_shader_prepare_seed writes
// them in priority order, one per input id; gfx_shader_warm_list reads them at boot.
#define PRP_VERSION 1
#define PRP_REC 36
int gfx_shader_prepare_seed(const char *dir, int threads, int procs) {
char ans[PATH_CAP + 32], prg[PATH_CAP + 32], out[PATH_CAP + 32];
snprintf(ans, sizeof ans, "%s/seed-answers.bin", dir);
snprintf(prg, sizeof prg, "%s/seed-programs.bin", dir);
snprintf(out, sizeof out, "%s/shader-prepared.bin", dir);
double t0 = (double)plat_now_ms();
uint64_t fail0 = (uint64_t)n_failures;
uint8_t *ab = NULL, (*iid)[32] = NULL;
long long n = prepare_files(ans, prg, threads, procs, "seed variants", &ab, &iid);
if (n < 0) return 1;
uint8_t *recs = calloc((size_t)n + 1, PRP_REC);
static const uint8_t zero[32];
size_t u = 0, by[3] = {0}, stages[6] = {0};
Set64 seen = {0};
for (long long i = 0; recs && i < n; i++) { // seed records come in priority order: the first id wins
if (!memcmp(iid[i], zero, 32) || !set_add(&seen, hash8(iid[i]))) continue;
uint8_t *r = recs + u * PRP_REC;
const uint8_t *a = ab + (size_t)i * ANS_REC;
r[0] = a[0] < 6 ? a[0] : 0, r[1] = a[ANS_REC - 1] > 2 ? 2 : a[ANS_REC - 1];
memcpy(r + 4, iid[i], 32);
by[r[1]]++, stages[r[0]]++, u++;
}
free(seen.v);
RecHeader h = {{'T', 'O', 'T', 'K', 'S', 'P', 'R', 'P'}, PRP_VERSION, PRP_REC};
uint8_t *file = recs ? malloc(sizeof h + u * PRP_REC + 1) : NULL;
int ok = file != NULL;
if (ok) {
memcpy(file, &h, sizeof h), memcpy(file + sizeof h, recs, u * PRP_REC);
ok = plat_file_write_atomic(out, file, sizeof h + u * PRP_REC);
}
char msg[640];
snprintf(msg, sizeof msg, "gfx_shader: prepared %zu shader variants (vertex %zu, fragment %zu, compute %zu; title %zu, "
"warm-up %zu, other %zu) from %lld seed records, %llu translations failed, in %.0f ms; %s %s", u,
stages[GFX_STAGE_VS], stages[GFX_STAGE_FS], stages[GFX_STAGE_CS], by[0], by[1], by[2], n,
(unsigned long long)((uint64_t)n_failures - fail0), (double)plat_now_ms() - t0,
ok ? "wrote" : "cannot write", out);
rt_log("%s", msg);
printf("%s\n", msg);
free(file), free(recs), free(iid), free(ab);
return ok ? 0 : 1;
}
// The variants the boot prebuild warms, most likely first: every input id play recorded (shader-answers.bin, priority
// 0), then the prepared ones (shader-prepared.bin, priority 1 + their list priority). One entry per input id.
size_t gfx_shader_warm_list(GfxWarmEnt **out) {
*out = NULL;
if (!RC.on) return 0;
char prp[PATH_CAP + 32];
snprintf(prp, sizeof prp, "%.*sshader-prepared.bin", (int)(strlen(RC.ans_path) - strlen("shader-answers.bin")),
RC.ans_path);
uint8_t *ab = NULL, *pb = NULL;
size_t an = 0, pn = 0;
plat_mutex_lock(&rec_io); // no append meanwhile
rec_read(RC.ans_path, "TOTKSANS", ANS_VERSION, ANS_REC, &ab, &an);
plat_mutex_unlock(&rec_io);
rec_read(prp, "TOTKSPRP", PRP_VERSION, PRP_REC, &pb, &pn);
size_t na = an / ANS_REC, np = pn / PRP_REC, n = 0;
GfxWarmEnt *e = calloc(na + np + 1, sizeof *e);
Set64 seen = {0};
for (size_t i = 0; e && i < na; i++) {
const uint8_t *r = ab + i * ANS_REC;
if (!set_add(&seen, hash8(r + 36))) continue;
memcpy(e[n].iid, r + 36, 32), e[n].stage = r[0], e[n].prio = 0, n++;
}
for (size_t i = 0; e && i < np; i++) {
const uint8_t *r = pb + i * PRP_REC;
if (!set_add(&seen, hash8(r + 4))) continue;
memcpy(e[n].iid, r + 4, 32), e[n].stage = r[0], e[n].prio = (uint8_t)(1 + r[1]), n++;
}
free(seen.v), free(ab), free(pb);
rt_log("gfx_shader: warm list: %zu variants (%zu met in play, %zu prepared)", n, na, np);
*out = e;
return n;
}