Files
Valiantsin DzerakhandClaude Opus 5.5 6a04fe9222 Merge pull request #89: Shadow resolution, cascade range, soft and contact shadows, anisotropic filtering (itaskaev2)
Merged with applezra's follow-ups on the same branch: the sun shadow filter narrowed by the shadow map scale
(translator patch 0011), anisotropic filtering forced only on tiling samplers, and TOTK_DUMP_UBO with a vec4 count.

Conflicts with dev: cfg.c (names, display section, defaults next to aspect and device_profile), menu.cpp (the new rows
after the aspect row), vk_texture.c (the pool alias check takes scale == 1 and dev's bind_pool_memory size argument),
build_xlate.py (patches 0005-0011 are ours; the Linux note kept), totk.ini.example (both sections). The contact pass
release went into gfx_present.c, the shared half of the old gfx_window.c.

aniso defaults to game: the picture stays as before until a player raises it in the menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:44:48 +03:00

925 lines
45 KiB
C

// TOTK_DUMP=<frame>: a frame dump for the Vulkan back end, which RenderDoc cannot capture (it hides
// VK_EXT_external_memory_host). Between presents frame-1 and frame, the replay's command buffers also copy, after each
// render pass, its colour and depth targets and, after each dispatch, the storage images it writes into host buffers;
// the completion thread turns them into 8-bit PNGs once the submission is done. 3D textures (LUTs, volumes) sampled or
// written in the frame are copied once each, slices side by side. Files in TOTK_DUMP_DIR (default "dump"):
// pass_<seq>_t<i>_<w>x<h>_<vkformat>_<name>.png colour target i (td: depth; img<slot>: storage image; lut<id>: 3D;
// a BC target drawn through a block view: its blocks as that view)
// ..._tm.png float formats: x/(1+x) of the same image (the plain one clamps)
// passes.txt per pass or dispatch: targets, draws, each distinct pipeline + texture set with its shaders' SPIR-V
// hashes (sh_<hash>.spv written once) and the bound textures with their samplers
// images.txt per image: channel min/max, distinct texel values over the image and over its top quarter
// Sampled 8-bit 2D textures up to TEX2D_CAP texels are copied once each as pass_<seq>_tex<id>_... at the end of the pass
// that samples them; texture lines carry the storage address, mirror state, upload count and the image's and slot's
// swizzle. Copies and texture clears are logged as "copy/clear after pass N". TOTK_DUMP_LOG=<from> also logs the
// frames from..frame-1 to passes.txt, with PNGs only of targets up to 64K texels. TOTK_DUMP_BUF=<addr>:<size>[:...]
// snapshots one GPU buffer range after every logged pass (buf_<pass>.bin and bufs.txt, see buf_snap).
// PNGs are opaque (alpha dropped). Integer formats are scaled by the image maximum, depth by its min..max range, SNORM
// as x/2+1/2, UNORM and sRGB bytes written as stored. Caps: MAX_PASSES passes copied, PNG_CAP bytes of PNGs.
// TOTK_DUMP_TEX=<id>|<w>x<h>[,...]: those texture slots, or 2D textures of that size, are copied where first sampled,
// whatever their format.
// TOTK_DUMP_UBO=<n>: each listed pipeline also prints the first n vec4 of its user uniform blocks (c3 and up; 1 means 16,
// the cap is 4096), read from guest memory when the entry is listed. A block the submission rewrites with UpdateUniformBuffer shows its value from
// before the submission (vk_exec_draw.c upd_note).
// TOTK_DUMP_SKIP_RUNS=1: a render pass with the same targets as the render pass before it is logged but not copied
// (the terrain decals draw one pass each into the G-buffer and would use up both caps).
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "nvn_api.h"
#include "vk.h"
#include "vk_exec.h"
#include "platform/platform.h"
#define MAX_PASSES 400
#define PNG_CAP (2ull << 30)
#define INFLIGHT_CAP (3ull << 30) // host buffers waiting for their submission
#define MAX_ENTS 48 // distinct pipeline + texture sets listed per pass
#define MAX_LUTS 256 // 3D textures and small sampled 8-bit 2D textures dumped per frame
#define TEX2D_CAP (1u << 20) // texels of a sampled 2D texture that is dumped
int vk_dump_armed;
static uint64_t dump_frame, log_from; // log_from: TOTK_DUMP_LOG, first frame logged to passes.txt without images
static char dir[400];
static FILE *passes_f, *images_f;
static int seq, finished, copied; // copied: passes and dispatches of the dump frame whose images were copied
static int skip_runs, dump_ubo; // TOTK_DUMP_SKIP_RUNS, TOTK_DUMP_UBO (vec4 per block, 0 for none)
static uint32_t want_tex[16][2]; // TOTK_DUMP_TEX: slot id and 0, or width and height
static int nwant_tex;
static uint64_t last_targets; // targets_sig of the last render pass; 0 after a dispatch
static const char *pass_skip; // why the current pass copies nothing, or NULL
static volatile int64_t png_bytes, inflight;
static PlatMutex rec_lock = PLAT_MUTEX_INIT, img_lock = PLAT_MUTEX_INIT;
// ---- formats
enum { K_UNORM, K_SNORM, K_UINT, K_SINT, K_FLOAT, K_RGB10A2, K_BGR10A2, K_RG11B10, K_E5, K_R5G6B5, K_D16, K_D24, K_D32F };
enum { M_UNORM, M_SNORM, M_INT, M_FLOAT, M_DEPTH };
typedef struct { VkFormat vk; const char *name; uint8_t bytes, nc, bits, kind, bgr; } Fmt;
#define F(v, n, by, nc, bi, k) {VK_FORMAT_##v, n, by, nc, bi, k, 0}
static const Fmt fmts[] = {
F(R8_UNORM, "r8", 1, 1, 8, K_UNORM), F(R8_SNORM, "r8sn", 1, 1, 8, K_SNORM), F(R8_UINT, "r8ui", 1, 1, 8, K_UINT),
F(R8_SINT, "r8i", 1, 1, 8, K_SINT), F(R8_SRGB, "r8srgb", 1, 1, 8, K_UNORM),
F(R8G8_UNORM, "rg8", 2, 2, 8, K_UNORM), F(R8G8_SNORM, "rg8sn", 2, 2, 8, K_SNORM),
F(R8G8_UINT, "rg8ui", 2, 2, 8, K_UINT), F(R8G8_SINT, "rg8i", 2, 2, 8, K_SINT),
F(R8G8B8A8_UNORM, "rgba8", 4, 4, 8, K_UNORM), F(R8G8B8A8_SNORM, "rgba8sn", 4, 4, 8, K_SNORM),
F(R8G8B8A8_UINT, "rgba8ui", 4, 4, 8, K_UINT), F(R8G8B8A8_SINT, "rgba8i", 4, 4, 8, K_SINT),
F(R8G8B8A8_SRGB, "rgba8srgb", 4, 4, 8, K_UNORM),
{VK_FORMAT_B8G8R8A8_UNORM, "bgra8", 4, 4, 8, K_UNORM, 1}, {VK_FORMAT_B8G8R8A8_SRGB, "bgra8srgb", 4, 4, 8, K_UNORM, 1},
F(A2B10G10R10_UNORM_PACK32, "rgb10a2", 4, 4, 10, K_RGB10A2), F(A2R10G10B10_UNORM_PACK32, "bgr10a2", 4, 4, 10, K_BGR10A2),
F(R5G6B5_UNORM_PACK16, "r5g6b5", 2, 3, 5, K_R5G6B5),
F(R16_UNORM, "r16", 2, 1, 16, K_UNORM), F(R16_SNORM, "r16sn", 2, 1, 16, K_SNORM), F(R16_UINT, "r16ui", 2, 1, 16, K_UINT),
F(R16_SINT, "r16i", 2, 1, 16, K_SINT), F(R16_SFLOAT, "r16f", 2, 1, 16, K_FLOAT),
F(R16G16_UNORM, "rg16", 4, 2, 16, K_UNORM), F(R16G16_SNORM, "rg16sn", 4, 2, 16, K_SNORM),
F(R16G16_UINT, "rg16ui", 4, 2, 16, K_UINT), F(R16G16_SINT, "rg16i", 4, 2, 16, K_SINT),
F(R16G16_SFLOAT, "rg16f", 4, 2, 16, K_FLOAT),
F(R16G16B16A16_UNORM, "rgba16", 8, 4, 16, K_UNORM), F(R16G16B16A16_SNORM, "rgba16sn", 8, 4, 16, K_SNORM),
F(R16G16B16A16_UINT, "rgba16ui", 8, 4, 16, K_UINT), F(R16G16B16A16_SINT, "rgba16i", 8, 4, 16, K_SINT),
F(R16G16B16A16_SFLOAT, "rgba16f", 8, 4, 16, K_FLOAT),
F(R32_UINT, "r32ui", 4, 1, 32, K_UINT), F(R32_SINT, "r32i", 4, 1, 32, K_SINT), F(R32_SFLOAT, "r32f", 4, 1, 32, K_FLOAT),
F(R32G32_UINT, "rg32ui", 8, 2, 32, K_UINT), F(R32G32_SINT, "rg32i", 8, 2, 32, K_SINT),
F(R32G32_SFLOAT, "rg32f", 8, 2, 32, K_FLOAT),
F(R32G32B32A32_UINT, "rgba32ui", 16, 4, 32, K_UINT), F(R32G32B32A32_SINT, "rgba32i", 16, 4, 32, K_SINT),
F(R32G32B32A32_SFLOAT, "rgba32f", 16, 4, 32, K_FLOAT),
F(B10G11R11_UFLOAT_PACK32, "r11g11b10f", 4, 3, 11, K_RG11B10), F(E5B9G9R9_UFLOAT_PACK32, "rgb9e5", 4, 3, 9, K_E5),
// depth aspect of depth formats, as vkCmdCopyImageToBuffer writes it
F(D16_UNORM, "d16", 2, 1, 16, K_D16), F(D16_UNORM_S8_UINT, "d16s8", 2, 1, 16, K_D16),
F(X8_D24_UNORM_PACK32, "d24", 4, 1, 24, K_D24), F(D24_UNORM_S8_UINT, "d24s8", 4, 1, 24, K_D24),
F(D32_SFLOAT, "d32f", 4, 1, 32, K_D32F), F(D32_SFLOAT_S8_UINT, "d32fs8", 4, 1, 32, K_D32F),
};
#undef F
static const Fmt *fmt_of(VkFormat f) {
for (size_t i = 0; i < sizeof fmts / sizeof fmts[0]; i++)
if (fmts[i].vk == f) return &fmts[i];
return NULL;
}
static const char *fmt_name(VkFormat f, char tmp[16]) {
const Fmt *d = fmt_of(f);
if (d) return d->name;
snprintf(tmp, 16, "vk%d", (int)f);
return tmp;
}
static int mode_of(const Fmt *f) {
switch (f->kind) {
case K_SNORM: return M_SNORM;
case K_UINT: case K_SINT: return M_INT;
case K_FLOAT: case K_RG11B10: case K_E5: return M_FLOAT;
case K_D16: case K_D24: case K_D32F: return M_DEPTH;
default: return M_UNORM;
}
}
// Unsigned small float with a 5-bit exponent and mbits of mantissa (half: 10, R11G11B10: 6 and 5).
static float ufloat(uint32_t v, int mbits) {
uint32_t e = v >> mbits, m = v & ((1u << mbits) - 1);
if (e == 0) return ldexpf((float)m, -14 - mbits);
if (e == 31) return m ? NAN : INFINITY;
return ldexpf((float)(m | 1u << mbits), (int)e - 15 - mbits);
}
static int32_t sext(uint32_t v, int bits) { return bits >= 32 ? (int32_t)v : (int32_t)(v << (32 - bits)) >> (32 - bits); }
static void decode(const Fmt *f, const uint8_t *p, float o[4]) {
uint32_t u = 0;
o[0] = o[1] = o[2] = 0, o[3] = 1;
switch (f->kind) {
case K_RGB10A2: case K_BGR10A2:
memcpy(&u, p, 4);
o[0] = (u & 1023) / 1023.f, o[1] = (u >> 10 & 1023) / 1023.f, o[2] = (u >> 20 & 1023) / 1023.f, o[3] = (u >> 30) / 3.f;
if (f->kind == K_BGR10A2) { float t = o[0]; o[0] = o[2], o[2] = t; }
return;
case K_RG11B10:
memcpy(&u, p, 4);
o[0] = ufloat(u & 2047, 6), o[1] = ufloat(u >> 11 & 2047, 6), o[2] = ufloat(u >> 22 & 1023, 5);
return;
case K_E5: {
memcpy(&u, p, 4);
float s = ldexpf(1.f, (int)(u >> 27) - 24);
o[0] = (u & 511) * s, o[1] = (u >> 9 & 511) * s, o[2] = (u >> 18 & 511) * s;
return;
}
case K_R5G6B5:
memcpy(&u, p, 2);
o[0] = (u >> 11 & 31) / 31.f, o[1] = (u >> 5 & 63) / 63.f, o[2] = (u & 31) / 31.f;
return;
case K_D16: memcpy(&u, p, 2); o[0] = u / 65535.f; return;
case K_D24: memcpy(&u, p, 4); o[0] = (u & 0xffffff) / 16777215.f; return;
case K_D32F: memcpy(&o[0], p, 4); return;
}
int cb = f->bits / 8;
for (int c = 0; c < f->nc; c++) {
u = 0;
memcpy(&u, p + c * cb, (size_t)cb);
float x;
switch (f->kind) {
case K_UNORM: x = (float)u / (float)((1ull << f->bits) - 1); break;
case K_SNORM: x = fmaxf((float)sext(u, f->bits) / (float)((1u << (f->bits - 1)) - 1), -1.f); break;
case K_UINT: x = (float)u; break;
case K_SINT: x = (float)sext(u, f->bits); break;
default:
if (f->bits == 16) x = (u & 0x8000 ? -1.f : 1.f) * ufloat(u & 0x7fff, 10);
else memcpy(&x, &u, 4);
}
o[c] = x;
}
if (f->bgr) { float t = o[0]; o[0] = o[2], o[2] = t; }
}
// ---- conversion on the completion thread
typedef struct {
VkBuffer buf;
VmaAllocation alloc;
const uint8_t *map;
uint64_t bytes;
const Fmt *f;
uint32_t w, h, d; // d: 3D slices, laid side by side in the PNG
char name[160];
} Job;
// Distinct raw texel values among rows [y0, y1) of every slice.
static uint64_t distinct(const Job *j, uint32_t y0, uint32_t y1) {
uint64_t n = (uint64_t)j->w * (y1 - y0) * j->d, cap = 1024;
while (cap < 2 * n) cap *= 2;
uint64_t *set = calloc(cap, sizeof *set), count = 0;
if (!set) return 0;
for (uint32_t z = 0; z < j->d; z++)
for (uint32_t y = y0; y < y1; y++)
for (uint32_t x = 0; x < j->w; x++) {
const uint8_t *p = j->map + (((uint64_t)z * j->h + y) * j->w + x) * j->f->bytes;
uint64_t a = 0, b = 0;
memcpy(&a, p, j->f->bytes < 8 ? j->f->bytes : 8);
if (j->f->bytes > 8) memcpy(&b, p + 8, j->f->bytes - 8u);
uint64_t k = (a ^ (b * 0x9E3779B97F4A7C15ull)) + 1;
if (!k) k = 1;
uint64_t i = (k * 0xff51afd7ed558ccdull) >> 17;
for (;; i++) {
uint64_t *s = &set[i & (cap - 1)];
if (*s == k) break;
if (!*s) { *s = k, count++; break; }
}
}
free(set);
return count;
}
static uint8_t u8(float x) { return x > 0 ? x < 1 ? (uint8_t)(x * 255.f + .5f) : 255 : 0; }
static void write_images(const Job *j) {
uint32_t pw = j->w * j->d, ph = j->h;
uint64_t n = (uint64_t)pw * ph;
float *px = malloc(n * 16);
uint8_t *out = malloc(n * 4);
if (!px || !out) rt_fatal("vk_dump: out of memory for %s", j->name);
float mn[4] = {INFINITY, INFINITY, INFINITY, INFINITY}, mx[4] = {-INFINITY, -INFINITY, -INFINITY, -INFINITY};
uint64_t nans = 0;
for (uint32_t z = 0; z < j->d; z++)
for (uint32_t y = 0; y < j->h; y++)
for (uint32_t x = 0; x < j->w; x++) {
float *o = px + 4 * ((uint64_t)y * pw + (uint64_t)z * j->w + x);
decode(j->f, j->map + (((uint64_t)z * j->h + y) * j->w + x) * j->f->bytes, o);
for (int c = 0; c < 4; c++) {
if (isnan(o[c])) { nans++, o[c] = 0; continue; }
if (o[c] < mn[c]) mn[c] = o[c];
if (o[c] > mx[c]) mx[c] = o[c];
}
}
int mode = mode_of(j->f), grey = j->f->nc == 1;
float scale = 1, lo = mn[0], range = mx[0] - mn[0];
if (mode == M_INT) {
float m = 1;
for (int c = 0; c < j->f->nc && c < 3; c++) m = fmaxf(m, fmaxf(fabsf(mx[c]), fabsf(mn[c])));
scale = 1.f / m;
}
for (int pass = 0; pass < (mode == M_FLOAT ? 2 : 1); pass++) {
for (uint64_t i = 0; i < n; i++) {
const float *o = px + 4 * i;
for (int c = 0; c < 3; c++) {
float x = o[grey ? 0 : c];
if (mode == M_SNORM) x = x * .5f + .5f;
else if (mode == M_INT) x *= scale;
else if (mode == M_DEPTH) x = range > 0 ? (x - lo) / range : 0;
else if (mode == M_FLOAT && pass) x = x > 0 ? x / (1 + x) : 0;
out[4 * i + c] = u8(x);
}
out[4 * i + 3] = 255;
}
char path[600];
snprintf(path, sizeof path, "%s/%s%s.png", dir, j->name, pass ? "_tm" : "");
rt_write_png(path, out, pw, ph);
}
uint64_t all = distinct(j, 0, j->h), top = distinct(j, 0, j->h / 4 ? j->h / 4 : 1);
plat_mutex_lock(&img_lock);
if (images_f) {
fprintf(images_f, "%s %ux%ux%u min %g %g %g %g max %g %g %g %g distinct %llu top-quarter %llu nan %llu\n", j->name,
j->w, j->h, j->d, mn[0], mn[1], mn[2], mn[3], mx[0], mx[1], mx[2], mx[3], (unsigned long long)all,
(unsigned long long)top, (unsigned long long)nans);
fflush(images_f);
}
plat_mutex_unlock(&img_lock);
free(out);
free(px);
}
static void job_done(void *arg) {
Job *j = arg;
VmaAllocator vma = vk_ctx()->vma;
vmaInvalidateAllocation(vma, j->alloc, 0, VK_WHOLE_SIZE);
write_images(j);
vmaDestroyBuffer(vma, j->buf, j->alloc);
plat_add64(&inflight, -(int64_t)j->bytes);
free(j);
}
// ---- recording
// Records the copy of one subresource into a host buffer and queues its PNGs. Returns NULL, or why it was skipped.
// The buffer is w x h x d texels of vf; the image region is `ext` image texels at depth plane z (differs for the
// blocks of a BC image read as an uncompressed format of the block size, and for a 3D slice).
static const char *copy_region(VkExec *ex, VkImage image, VkFormat vf, VkImageAspectFlags aspect, uint32_t level,
uint32_t layer, uint32_t z, VkExtent3D ext, uint32_t w, uint32_t h, uint32_t d,
const char *name) {
const Fmt *f = fmt_of(vf);
if (!f) return "format not decoded";
if (__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) + 1 != dump_frame && (uint64_t)w * h * d > 65536)
return "logged frame, large image";
if (aspect & VK_IMAGE_ASPECT_DEPTH_BIT) aspect = VK_IMAGE_ASPECT_DEPTH_BIT;
else if (aspect != VK_IMAGE_ASPECT_COLOR_BIT) return "stencil only";
uint64_t bytes = (uint64_t)w * h * d * f->bytes;
uint64_t est = ((uint64_t)h * (1 + 4ull * w * d) + 128) * (mode_of(f) == M_FLOAT ? 2 : 1);
if ((uint64_t)png_bytes + est > PNG_CAP) return "PNG cap reached";
if ((uint64_t)inflight + bytes > INFLIGHT_CAP) return "too many copies in flight";
Job *j = calloc(1, sizeof *j);
if (!j) rt_fatal("vk_dump: out of memory");
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = bytes,
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO,
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT};
VmaAllocationInfo info;
if (vmaCreateBuffer(vk_ctx()->vma, &bi, &ai, &j->buf, &j->alloc, &info) != VK_SUCCESS || !info.pMappedData) {
free(j);
return "host buffer allocation failed";
}
j->map = info.pMappedData, j->bytes = bytes, j->f = f, j->w = w, j->h = h, j->d = d;
snprintf(j->name, sizeof j->name, "%s", name);
VkMemoryBarrier2 pre = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT};
VkMemoryBarrier2 post = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT,
.dstAccessMask = VK_ACCESS_2_HOST_READ_BIT};
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &pre};
vkCmdPipelineBarrier2(ex->cb, &di);
VkBufferImageCopy r = {.imageSubresource = {aspect, level, layer, 1}, .imageOffset = {0, 0, (int32_t)z},
.imageExtent = ext}; // packed rows: a BC row of ext.width texels is w blocks
vkCmdCopyImageToBuffer(ex->cb, image, VK_IMAGE_LAYOUT_GENERAL, j->buf, 1, &r);
di.pMemoryBarriers = &post;
vkCmdPipelineBarrier2(ex->cb, &di);
ex->recorded += 3;
plat_add64(&png_bytes, (int64_t)est);
plat_add64(&inflight, (int64_t)bytes);
vk_completion_add(ex->fence, job_done, j);
return NULL;
}
static const char *copy_out(VkExec *ex, VkImage image, VkFormat vf, VkImageAspectFlags aspect, uint32_t level,
uint32_t layer, uint32_t w, uint32_t h, uint32_t d, const char *name) {
return copy_region(ex, image, vf, aspect, level, layer, 0, (VkExtent3D){w, h, d}, w, h, d, name);
}
// ---- TOTK_DUMP_BUF=<addr>:<size>[:<off>:<stride>:<w>:<h>]: after every pass and dispatch that is logged, the GPU
// buffer range is copied to a host buffer; when it differs from the previous copy it goes to buf_<pass>.bin, and
// bufs.txt gets the changed bytes' extent plus, with the grid arguments, one map per dword of the w x h records of
// `stride` bytes at `off` (each record a letter: its value's index in the table under the map).
static uint64_t bd_addr, bd_size, bd_off, bd_stride;
static uint32_t bd_w, bd_h;
static uint8_t *bd_prev;
static FILE *bufs_f;
typedef struct { VkBuffer buf; VmaAllocation alloc; const uint8_t *map; int pass; } BufJob;
static void buf_grid(const uint8_t *m) {
for (uint32_t dw = 0; dw < bd_stride / 4; dw++) {
uint32_t vals[52];
int nv = 0, over = 0;
fprintf(bufs_f, " dword %u:\n", dw);
for (uint32_t y = 0; y < bd_h; y++) {
char line[260];
uint32_t x;
for (x = 0; x < bd_w && x < 256; x++) {
uint64_t o = bd_off + ((uint64_t)y * bd_w + x) * bd_stride + dw * 4;
if (o + 4 > bd_size) { line[x] = ' '; continue; }
uint32_t v;
memcpy(&v, m + o, 4);
int k = 0;
while (k < nv && vals[k] != v) k++;
if (k == nv) {
if (nv < 52) vals[nv++] = v;
else { over = 1; line[x] = '?'; continue; }
}
line[x] = (char)(k < 26 ? 'a' + k : 'A' + k - 26);
}
line[x] = 0;
fprintf(bufs_f, " %2u %s\n", y, line);
}
for (int k = 0; k < nv; k++) fprintf(bufs_f, "%s%c=%08x", k % 8 ? " " : " ", k < 26 ? 'a' + k : 'A' + k - 26, vals[k]);
fprintf(bufs_f, "%s\n", over ? " (more: ?)" : "");
}
}
static void buf_done(void *arg) {
BufJob *j = arg;
vmaInvalidateAllocation(vk_ctx()->vma, j->alloc, 0, VK_WHOLE_SIZE);
plat_mutex_lock(&img_lock);
if (bufs_f) {
uint64_t lo = bd_size, hi = 0;
for (uint64_t i = 0; i < bd_size; i++)
if (!bd_prev || j->map[i] != bd_prev[i]) { if (i < lo) lo = i; hi = i + 1; }
if (hi) {
char path[600];
snprintf(path, sizeof path, "%s/buf_%04d.bin", dir, j->pass);
FILE *f = fopen(path, "wb");
if (f) fwrite(j->map, 1, bd_size, f), fclose(f);
fprintf(bufs_f, "after pass %04d: bytes %#llx..%#llx changed\n", j->pass, (unsigned long long)lo,
(unsigned long long)hi);
if (bd_stride) buf_grid(j->map);
fflush(bufs_f);
if (!bd_prev) bd_prev = malloc(bd_size);
if (bd_prev) memcpy(bd_prev, j->map, bd_size);
}
}
plat_mutex_unlock(&img_lock);
vmaDestroyBuffer(vk_ctx()->vma, j->buf, j->alloc);
free(j);
}
static void buf_snap(VkExec *ex, int pass) {
if (!bd_size) return;
VkMemSpan s;
if (!vk_mem_resolve(bd_addr, bd_size, &s) || s.avail < bd_size) return;
VK_MEM_PICK(s); // the GPU's view of the buffer
BufJob *j = calloc(1, sizeof *j);
if (!j) rt_fatal("vk_dump: out of memory");
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = bd_size,
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO,
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT};
VmaAllocationInfo info;
if (vmaCreateBuffer(vk_ctx()->vma, &bi, &ai, &j->buf, &j->alloc, &info) != VK_SUCCESS || !info.pMappedData) {
free(j);
return;
}
j->map = info.pMappedData, j->pass = pass;
VkMemoryBarrier2 mb[2] = {
{.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT},
{.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT | VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.dstAccessMask = VK_ACCESS_2_HOST_READ_BIT}};
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb[0]};
vkCmdPipelineBarrier2(ex->cb, &di);
VkBufferCopy c = {s.offset, 0, bd_size};
vkCmdCopyBuffer(ex->cb, s.buffer, j->buf, 1, &c);
di.pMemoryBarriers = &mb[1];
vkCmdPipelineBarrier2(ex->cb, &di);
ex->recorded += 3;
vk_completion_add(ex->fence, buf_done, j);
}
static int active(void) {
uint64_t f = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) + 1;
return !finished && (f == dump_frame || (log_from && f >= log_from && f < dump_frame));
}
static uint64_t frame_now(void) { return __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) + 1; }
typedef struct { char *s; size_t n, cap; } Str;
__attribute__((format(printf, 2, 3))) static void sp(Str *b, const char *fmt, ...) {
va_list a;
va_start(a, fmt);
int k = vsnprintf(NULL, 0, fmt, a);
va_end(a);
if (k < 0) return;
if (b->n + (size_t)k + 1 > b->cap) {
b->cap = (b->n + (size_t)k + 1) * 2;
b->s = realloc(b->s, b->cap);
if (!b->s) rt_fatal("vk_dump: out of memory");
}
va_start(a, fmt);
vsnprintf(b->s + b->n, (size_t)k + 1, fmt, a);
va_end(a);
b->n += (size_t)k;
}
// SPIR-V hash of a shader; the first time, its SPIR-V goes to sh_<hash>.spv.
static uint64_t shader_hash(const GfxShader *s) {
static struct { const GfxShader *s; uint64_t h; } tab[1024];
static int n;
if (!s) return 0;
for (int i = 0; i < n; i++)
if (tab[i].s == s) return tab[i].h;
uint64_t words = 0, h = 0xcbf29ce484222325ull;
const uint32_t *w = gfx_shader_spirv(s, &words);
const uint8_t *b = (const uint8_t *)w;
for (uint64_t i = 0; i < words * 4; i++) h = (h ^ b[i]) * 0x100000001b3ull;
char path[600];
snprintf(path, sizeof path, "%s/sh_%016llx.spv", dir, (unsigned long long)h);
FILE *f = fopen(path, "wb");
if (f) fwrite(w, 4, (size_t)words, f), fclose(f);
if (n < 1024) tab[n].s = s, tab[n].h = h, n++;
return h;
}
// Per pass: distinct pipeline + texture sets, draws, 3D textures to copy at its end.
typedef struct { VkPipeline p; uint64_t th; int draws; char *text; } Ent;
static Ent ents[MAX_ENTS];
static int nents, ents_over, pass_draws;
typedef struct { VkImage image; VkFormat format; uint32_t w, h, d; int id, is2d; } Lut;
static Lut luts[MAX_LUTS]; // queued for the next pass end or dispatch
static int nluts;
static VkImage luts_done[MAX_LUTS];
static int nluts_done;
static void queue_lut(const VkTexImage *t, int id) {
if (frame_now() != dump_frame) return;
for (int i = 0; i < nluts_done; i++)
if (luts_done[i] == t->image) return;
if (nluts_done == MAX_LUTS) return;
luts_done[nluts_done++] = t->image;
luts[nluts++] = (Lut){t->image, t->format, t->width, t->height, t->type == VK_IMAGE_TYPE_3D ? t->depth : 1, id,
t->type != VK_IMAGE_TYPE_3D};
}
// The uncompressed format of a BC format's block size, UNDEFINED for other formats: a BC texture is dumped as its raw
// blocks (one texel per block), which shows at least whether it was ever written.
static VkFormat block_format(VkFormat f) {
if (f == VK_FORMAT_BC4_UNORM_BLOCK || f == VK_FORMAT_BC4_SNORM_BLOCK ||
(f >= VK_FORMAT_BC1_RGB_UNORM_BLOCK && f <= VK_FORMAT_BC1_RGBA_SRGB_BLOCK))
return VK_FORMAT_R32G32_UINT;
if (f >= VK_FORMAT_BC2_UNORM_BLOCK && f <= VK_FORMAT_BC7_SRGB_BLOCK) return VK_FORMAT_R32G32B32A32_UINT;
return VK_FORMAT_UNDEFINED;
}
static void flush_luts(VkExec *ex, int n) {
char fn[16];
for (int i = 0; i < nluts; i++) {
const Lut *l = &luts[i];
char name[160];
VkFormat bf = block_format(l->format);
snprintf(name, sizeof name, "pass_%04d_%s%d_%ux%ux%u_%d_%s%s", n, l->is2d ? "tex" : "lut", l->id, l->w, l->h, l->d,
(int)l->format, fmt_name(l->format, fn), bf ? "_blocks" : "");
const char *why = bf ? copy_region(ex, l->image, bf, VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 0,
(VkExtent3D){l->w, l->h, l->d}, (l->w + 3) / 4, (l->h + 3) / 4, l->d, name)
: copy_out(ex, l->image, l->format, VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, l->w, l->h, l->d, name);
fprintf(passes_f, " %s tex %d -> %s.png%s%s\n", l->is2d ? "2D" : "3D", l->id, name, why ? " skipped: " : "",
why ? why : "");
}
nluts = 0;
}
static int nvn_stage(int gfx_stage) { return gfx_stage == GFX_STAGE_VS ? 0 : gfx_stage == GFX_STAGE_FS ? 1 : 5; }
static const char *decl_type(uint16_t t, char tmp[24]) {
static const char *base[] = {"?", "1D", "buffer", "2D", "3D", "cube"};
snprintf(tmp, 24, "%s%s%s%s", base[(t & 0xff) < 6 ? t & 0xff : 0], t & 0x100 ? "Array" : "", t & 0x200 ? "MS" : "",
t & 0x400 ? "Shadow" : "");
return tmp;
}
static const char *wrap_name(int w) {
static const char *n[] = {"clamp", "repeat", "mirror_clamp", "mirror_clamp_edge", "mirror_clamp_border",
"clamp_border", "mirrored_repeat", "clamp_edge"};
return w >= 0 && w < 8 ? n[w] : "?";
}
static uint64_t handle_of(const VkExec *ex, int st, int slot, int img) {
if (img) return slot >= 0 && slot < 32 ? ex->img[st][slot] : 0;
return slot >= 0 && slot < 128 ? ex->tex[st][slot] : 0;
}
// The textures, images and samplers of info's bindings; 3D textures are queued for a copy.
static void describe_bindings(Str *b, const VkExec *ex, const VkPipeInfo *info) {
static const char *stn[6] = {"vs", "fs", "?", "?", "?", "cs"};
for (uint32_t i = 0; i < info->n_bindings; i++) {
const VkPipeBinding *bd = &info->bindings[i];
const GfxShaderRes *r = bd->res;
if (r && bd->kind == GFX_RES_SSBO) { // where the address lives (c0: the stage's NVN slot table) and the stage's slots
int st = nvn_stage(bd->stage);
sp(b, " %s ssbo at c%d+%#x%s, bound:", stn[st], r->cbuf_slot, r->handle * 4, r->flags & 1 ? " written" : "");
for (int k = 0; k < 16; k++)
if (ex->ssbo[st][k].addr)
sp(b, " %d=%#llx+%#llx", k, (unsigned long long)ex->ssbo[st][k].addr, (unsigned long long)ex->ssbo[st][k].size);
sp(b, "\n");
continue;
}
if (r && bd->kind == GFX_RES_UBO && dump_ubo && r->slot >= 3 && r->slot - 3 < 18) {
int st = nvn_stage(bd->stage);
const p_bind_buf *u = &ex->ubo[st][r->slot - 3];
VkMemSpan ms;
sp(b, " %s ubo c%d addr %#llx size %#llx", stn[st], r->slot, (unsigned long long)u->addr, (unsigned long long)u->size);
if (u->addr && vk_mem_resolve(u->addr, 1, &ms)) {
uint64_t n = u->size && u->size < ms.avail ? u->size : ms.avail;
const float *f = (const float *)(uintptr_t)u->addr;
for (uint64_t k = 0; k < (uint64_t)dump_ubo && (k + 1) * 16 <= n; k++)
sp(b, "%s[%llu] %g %g %g %g", k % 4 ? " " : "\n ", (unsigned long long)k, f[4 * k], f[4 * k + 1],
f[4 * k + 2], f[4 * k + 3]);
} else {
sp(b, " (not mapped)");
}
sp(b, "\n");
continue;
}
if (!r || (bd->kind != GFX_RES_TEX && bd->kind != GFX_RES_IMG)) continue;
int st = nvn_stage(bd->stage), img = bd->kind == GFX_RES_IMG;
char dt[24], fn[16];
if (bd->type == VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER || bd->type == VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER) {
sp(b, " %s %s slot %d: buffer texture (zero view bound)\n", stn[st], img ? "img" : "tex", r->slot);
continue;
}
for (uint32_t k = 0; k < (bd->count ? bd->count : 1u); k++) {
int slot = r->slot + (int)k;
uint64_t h = handle_of(ex, st, slot, img);
sp(b, " %s %s slot %d decl %s%s: ", stn[st], img ? "img" : "tex", slot, decl_type(r->type, dt),
img && (r->flags & 4) ? " store" : "");
if (!h) { sp(b, "no handle (dummy)\n"); continue; }
view_t v;
int hv = 0;
const VkTexImage *t = vk_texture_by_id(NVN_HANDLE_TEX(h), &v, &hv);
if (!t) { sp(b, "tex %d unresolved (dummy)\n", NVN_HANDLE_TEX(h)); continue; }
static const char *vt[] = {"1D", "2D", "3D", "cube", "1DArray", "2DArray", "cubeArray"};
sp(b, "tex %d %s(%d) %ux%ux%u levels %u layers %u image %s", NVN_HANDLE_TEX(h), fmt_name(t->format, fn),
(int)t->format, t->width, t->height, t->depth, t->levels, t->layers,
t->view_type <= 6 ? vt[t->view_type] : "?");
if (hv && v.set_format) sp(b, " view-nvn-fmt %#x", v.format);
if (hv && v.set_levels) sp(b, " view-levels %u+%u", v.base_level, v.nlevels);
if (hv && v.set_layers) sp(b, " view-layers %u+%u", v.min_layer, v.nlayers);
samp_par s;
if (img) {
// storage image: no sampler
} else if (r->separate) {
sp(b, " | separate sampler (not replayed: default sampler)");
} else if (vk_sampler_params(NVN_HANDLE_SAMP(h), &s)) {
static const char *minn[] = {"N", "L", "N_mipN", "L_mipN", "N_mipL", "L_mipL"};
int mf = s.minf;
sp(b, " | samp %d: mag %s min %s (vk min %s mip %s) wrap %s/%s/%s aniso %g lod %g..%g bias %g%s",
NVN_HANDLE_SAMP(h), s.magf == 1 ? "L" : "N", mf < 6 ? minn[mf] : "?",
mf == 1 || mf == 3 || mf == 5 ? "L" : "N", mf <= 1 ? "none" : mf >= 4 ? "L" : "N",
wrap_name(s.ws), wrap_name(s.wt), wrap_name(s.wr), s.aniso, s.lod_min, s.lod_max, s.lod_bias,
s.cmp_mode ? " compare" : "");
} else {
sp(b, " | samp %d empty (default sampler)", NVN_HANDLE_SAMP(h));
}
uint32_t ups;
int last, wr;
vk_texture_debug(t, &ups, &last, &wr);
sp(b, " | addr %#llx mirrored %d gpu_owned %d bound %d uploads %u last-check %d written-since %d",
(unsigned long long)t->addr, t->mirrored, t->gpu_owned, t->bound, ups, last, wr);
uint8_t ssw[4] = {0};
vk_texture_slot_swz(NVN_HANDLE_TEX(h), ssw);
sp(b, " | swz image %u%u%u%u slot %u%u%u%u", t->swz[0], t->swz[1], t->swz[2], t->swz[3], ssw[0], ssw[1], ssw[2],
ssw[3]);
if (hv && v.set_swizzle) sp(b, " view %u%u%u%u", v.swz[0], v.swz[1], v.swz[2], v.swz[3]);
sp(b, "\n");
int wanted = 0;
for (int k = 0; k < nwant_tex; k++)
wanted |= want_tex[k][1] ? want_tex[k][0] == t->width && want_tex[k][1] == t->height
: want_tex[k][0] == (uint32_t)NVN_HANDLE_TEX(h);
if (t->type == VK_IMAGE_TYPE_3D) queue_lut(t, NVN_HANDLE_TEX(h));
else if (wanted && t->type == VK_IMAGE_TYPE_2D && t->samples == 1) queue_lut(t, NVN_HANDLE_TEX(h));
else if (!img && t->type == VK_IMAGE_TYPE_2D && t->samples == 1 && (uint64_t)t->width * t->height <= TEX2D_CAP &&
(t->format == VK_FORMAT_R8_UNORM || t->format == VK_FORMAT_R8G8_UNORM ||
t->format == VK_FORMAT_R8G8B8A8_UNORM || t->format == VK_FORMAT_R8G8B8A8_SRGB ||
t->format == VK_FORMAT_B8G8R8A8_UNORM || t->format == VK_FORMAT_B8G8R8A8_SRGB))
queue_lut(t, NVN_HANDLE_TEX(h));
}
}
}
static uint64_t bindings_hash(const VkExec *ex, const VkPipeInfo *info) {
uint64_t h = 0xcbf29ce484222325ull;
for (uint32_t i = 0; i < info->n_bindings; i++) {
const VkPipeBinding *bd = &info->bindings[i];
if (bd->res && bd->kind == GFX_RES_SSBO) {
int st = nvn_stage(bd->stage), slot = bd->res->slot;
if (slot >= 0 && slot < 16) h = (h ^ ex->ssbo[st][slot].addr) * 0x100000001b3ull;
continue;
}
if (!bd->res || (bd->kind != GFX_RES_TEX && bd->kind != GFX_RES_IMG)) continue;
for (uint32_t k = 0; k < (bd->count ? bd->count : 1u); k++)
h = (h ^ handle_of(ex, nvn_stage(bd->stage), bd->res->slot + (int)k, bd->kind == GFX_RES_IMG)) * 0x100000001b3ull;
}
return h;
}
void vk_dump_draw(VkExec *ex, const VkPipeState *s, VkPipeline p, const VkPipeInfo *info) {
plat_mutex_lock(&rec_lock);
if (!active()) goto out;
pass_draws++;
uint64_t th = bindings_hash(ex, info);
for (int i = 0; i < nents; i++)
if (ents[i].p == p && ents[i].th == th) {
ents[i].draws++;
goto out;
}
if (nents == MAX_ENTS) {
ents_over++;
goto out;
}
Str b = {0};
sp(&b, "vs %016llx fs %016llx prog %llu prim %d\n", (unsigned long long)shader_hash(s->vs),
(unsigned long long)shader_hash(s->fs), (unsigned long long)(ex->prog ? ex->prog->serial : 0), s->prim);
describe_bindings(&b, ex, info);
ents[nents++] = (Ent){p, th, 1, b.s};
out:
plat_mutex_unlock(&rec_lock);
}
static void target_line(VkExec *ex, const VkExecTarget *t, const char *tag, int n) {
char fn[16], name[160];
if (!t->image) {
fprintf(passes_f, " %s: nvn %#x target %d %ux%u: not renderable, left out\n", tag, t->tex->d.format,
t->tex->d.target, t->tex->d.width, t->tex->d.height);
return;
}
snprintf(name, sizeof name, "pass_%04d_%s_%ux%u_%d_%s", n, tag, t->width, t->height, (int)t->format,
fmt_name(t->format, fn));
// a block view: the plane's blocks, read as the view format (one texel per block)
uint32_t tw = t->tex->d.width >> t->level ? t->tex->d.width >> t->level : 1;
uint32_t th = t->tex->d.height >> t->level ? t->tex->d.height >> t->level : 1;
(void)n;
const char *why = pass_skip ? pass_skip : t->samples > 1 ? "multisampled" :
t->blocks ? copy_region(ex, t->image, t->format, t->aspect, t->level, t->is3d ? 0 : t->base_layer,
t->is3d ? t->base_layer : 0, (VkExtent3D){tw, th, 1}, t->width,
t->height, 1, name) :
t->is3d ? "3D slice (the volume is dumped as lut<id> where it is sampled)" :
copy_out(ex, t->image, t->format, t->aspect, t->level, t->base_layer, t->width, t->height, 1, name);
fprintf(passes_f, " %s: nvn %#x %s(%d) %ux%u level %u layers %u+%u samples %u addr %#llx -> %s.png%s%s\n", tag,
t->nvn_format, fmt_name(t->format, fn), (int)t->format, t->width, t->height, t->level, t->base_layer,
t->layers, t->samples, (unsigned long long)(uintptr_t)t->tex->mem, name, why ? " skipped: " : "",
why ? why : "");
}
static uint64_t targets_sig(const VkExec *ex) {
uint64_t h = 1469598103934665603ull;
for (int i = 0; i < 9; i++) {
const VkExecTarget *t = i < 8 ? &ex->color[i] : &ex->depth;
uint64_t v[3] = {(uint64_t)(uintptr_t)t->image, t->level, t->base_layer};
for (int k = 0; k < 3; k++) h = (h ^ v[k]) * 1099511628211ull;
}
return h;
}
// Sets pass_skip for the pass or dispatch being logged and counts it when it copies.
static void pass_decide(int same_targets) {
if (frame_now() != dump_frame) copied = 0;
pass_skip = copied >= MAX_PASSES ? "pass cap" : same_targets ? "same targets as the previous pass" : NULL;
if (!pass_skip) copied++;
}
void vk_dump_pass_end(VkExec *ex) {
plat_mutex_lock(&rec_lock);
if (!active()) goto out;
int n = seq++;
uint64_t sig = targets_sig(ex);
pass_decide(skip_runs && sig == last_targets);
last_targets = sig;
fprintf(passes_f, "pass %04d: render pass, draws %d, area %ux%u layers %u, fence %llu, frame %llu\n", n, pass_draws,
ex->pass_w, ex->pass_h, ex->pass_layers, (unsigned long long)ex->fence, (unsigned long long)frame_now());
for (int i = 0; i < 8; i++) {
if (!ex->color[i].tex) continue;
char tag[8];
snprintf(tag, sizeof tag, "t%d", i);
target_line(ex, &ex->color[i], tag, n);
}
if (ex->depth.tex) target_line(ex, &ex->depth, "td", n);
for (int i = 0; i < nents; i++) {
fprintf(passes_f, " pipe %d (draws %d): %s", i, ents[i].draws, ents[i].text);
free(ents[i].text);
}
if (ents_over) fprintf(passes_f, " ... %d more draws with other pipeline/texture sets\n", ents_over);
if (!pass_skip) flush_luts(ex, n);
else nluts = 0;
buf_snap(ex, n);
fflush(passes_f);
nents = ents_over = pass_draws = 0;
out:
plat_mutex_unlock(&rec_lock);
}
void vk_dump_dispatch(VkExec *ex, const GfxShader *cs, VkPipeline p, const VkPipeInfo *info, const p_dispatch *d) {
(void)p;
plat_mutex_lock(&rec_lock);
if (!active()) goto out;
int n = seq++;
pass_decide(0);
last_targets = 0;
Str b = {0};
describe_bindings(&b, ex, info);
fprintf(passes_f, "pass %04d: dispatch %dx%dx%d cs %016llx prog %llu caller %#llx, fence %llu\n%s", n, d->x, d->y,
d->z, (unsigned long long)shader_hash(cs), (unsigned long long)(ex->prog ? ex->prog->serial : 0),
(unsigned long long)d->caller, (unsigned long long)ex->fence, b.s ? b.s : "");
free(b.s);
for (uint32_t i = 0; i < info->n_bindings && !pass_skip; i++) { // storage images the dispatch writes
const VkPipeBinding *bd = &info->bindings[i];
if (!bd->res || bd->kind != GFX_RES_IMG || !(bd->res->flags & 4) || bd->type != VK_DESCRIPTOR_TYPE_STORAGE_IMAGE)
continue;
uint64_t h = handle_of(ex, 5, bd->res->slot, 1);
view_t v;
int hv = 0;
const VkTexImage *t = h ? vk_texture_by_id(NVN_HANDLE_TEX(h), &v, &hv) : NULL;
if (!t) continue;
uint32_t lv = hv && v.set_levels ? v.base_level : 0, layer = hv && v.set_layers ? v.min_layer : 0;
if (lv >= t->levels || layer >= t->layers) continue;
uint32_t w = t->width >> lv ? t->width >> lv : 1, hh = t->height >> lv ? t->height >> lv : 1;
uint32_t dd = t->type == VK_IMAGE_TYPE_3D ? (t->depth >> lv ? t->depth >> lv : 1) : 1;
char fn[16], name[160];
snprintf(name, sizeof name, "pass_%04d_img%d_%ux%u%s_%d_%s", n, bd->res->slot, w, hh, dd > 1 ? "x3d" : "",
(int)t->format, fmt_name(t->format, fn));
if (t->type == VK_IMAGE_TYPE_3D) { // a written volume: this copy replaces the sampled-3D one
for (int k = 0; k < nluts_done; k++)
if (luts_done[k] == t->image) luts_done[k] = VK_NULL_HANDLE;
}
const char *why = t->samples > 1 ? "multisampled" :
copy_out(ex, t->image, t->format, t->aspect, lv, layer, w, hh, dd, name);
fprintf(passes_f, " writes img slot %d level %u layer %u depth %u -> %s.png%s%s\n", bd->res->slot, lv, layer,
dd, name, why ? " skipped: " : "", why ? why : "");
}
if (!pass_skip) flush_luts(ex, n);
else nluts = 0;
buf_snap(ex, n);
fflush(passes_f);
out:
plat_mutex_unlock(&rec_lock);
}
static const char *tex_desc(const tex_t *t, char *buf, size_t n) {
if (!t) return "null";
snprintf(buf, n, "addr %#llx nvn-fmt %#x %ux%ux%u", (unsigned long long)(uintptr_t)t->mem, t->d.format, t->d.width,
t->d.height, t->d.depth);
return buf;
}
static void region_desc(const NVNcopyRegion *r, char *buf, size_t n) {
snprintf(buf, n, "(%d,%d,%d %dx%dx%d)", r->xoffset, r->yoffset, r->zoffset, r->width, r->height, r->depth);
}
void vk_dump_copy(VkExec *ex, uint32_t op, const void *payload) {
(void)ex;
plat_mutex_lock(&rec_lock);
if (!active()) goto out;
char a[96], b[96], ra[64], rb[64];
switch (op) {
case OP_COPY_B2B: {
const p_copy_b2b *p = payload;
fprintf(passes_f, "copy after pass %d: buffer %#llx -> buffer %#llx, %llu bytes\n", seq - 1,
(unsigned long long)p->src, (unsigned long long)p->dst, (unsigned long long)p->size);
break;
}
case OP_COPY_B2T: case OP_COPY_T2B: {
const p_copy_b2t *p = payload;
region_desc(&p->r, ra, sizeof ra);
fprintf(passes_f, "copy after pass %d: %s buffer %#llx %s texture %s region %s flags %#x\n", seq - 1,
op == OP_COPY_B2T ? "buffer->texture" : "texture->buffer", (unsigned long long)p->src,
op == OP_COPY_B2T ? "->" : "<-", tex_desc(p->tex, a, sizeof a), ra, p->flags);
break;
}
case OP_COPY_T2T: {
const p_copy_t2t *p = payload;
region_desc(&p->sr, ra, sizeof ra);
region_desc(&p->dr, rb, sizeof rb);
fprintf(passes_f, "copy after pass %d: texture %s %s -> texture %s %s flags %#x\n", seq - 1,
tex_desc(p->src, a, sizeof a), ra, tex_desc(p->dst, b, sizeof b), rb, p->flags);
break;
}
case OP_CLEAR_TEX: {
const p_clear_tex *p = payload;
region_desc(&p->r, ra, sizeof ra);
fprintf(passes_f, "clear after pass %d: texture %s region %s mask %#x\n", seq - 1, tex_desc(p->tex, a, sizeof a),
ra, p->mask);
break;
}
default: break;
}
out:
plat_mutex_unlock(&rec_lock);
}
static void make_dirs(char *path) { plat_file_mkdir_p(path); }
void vk_dump_init(void) {
const char *e = cfg_get("TOTK_DUMP");
if (!e || !*e || !(dump_frame = strtoull(e, NULL, 10))) return;
const char *lf = cfg_get("TOTK_DUMP_LOG");
if (lf && *lf) log_from = strtoull(lf, NULL, 10);
const char *sr = cfg_get("TOTK_DUMP_SKIP_RUNS");
skip_runs = sr && *sr && *sr != '0';
const char *du = cfg_get("TOTK_DUMP_UBO");
dump_ubo = du && *du ? atoi(du) : 0;
if (dump_ubo == 1) dump_ubo = 16;
if (dump_ubo > 4096) dump_ubo = 4096;
if (dump_ubo < 0) dump_ubo = 0;
const char *wt = cfg_get("TOTK_DUMP_TEX");
for (char *q = (char *)wt; q && *q && nwant_tex < 16; q += *q == ',') {
uint32_t *w = want_tex[nwant_tex++];
w[0] = (uint32_t)strtoul(q, &q, 10);
w[1] = *q == 'x' ? (uint32_t)strtoul(q + 1, &q, 10) : 0;
}
const char *d = cfg_get("TOTK_DUMP_DIR");
snprintf(dir, sizeof dir, "%s", d && *d ? d : "dump");
make_dirs(dir);
char path[600];
snprintf(path, sizeof path, "%s/passes.txt", dir);
passes_f = fopen(path, "w");
snprintf(path, sizeof path, "%s/images.txt", dir);
images_f = fopen(path, "w");
if (!passes_f || !images_f) rt_fatal("vk_dump: cannot write to %s", dir);
const char *bf = cfg_get("TOTK_DUMP_BUF");
if (bf && *bf) {
char *q;
bd_addr = strtoull(bf, &q, 0);
if (*q == ':') bd_size = strtoull(q + 1, &q, 0);
if (*q == ':') bd_off = strtoull(q + 1, &q, 0);
if (*q == ':') bd_stride = strtoull(q + 1, &q, 0) & ~3ull;
if (*q == ':') bd_w = (uint32_t)strtoul(q + 1, &q, 0);
if (*q == ':') bd_h = (uint32_t)strtoul(q + 1, &q, 0);
if (!bd_w || !bd_h) bd_stride = 0;
snprintf(path, sizeof path, "%s/bufs.txt", dir);
if (bd_size && !(bufs_f = fopen(path, "w"))) bd_size = 0;
}
vk_dump_armed = 1;
rt_log("vk_dump: frame %llu goes to %s", (unsigned long long)dump_frame, dir);
}
void vk_dump_present(uint64_t frame) {
if (frame != dump_frame) return;
plat_mutex_lock(&rec_lock);
finished = 1;
plat_mutex_unlock(&rec_lock);
vk_sync_finish(); // the frame's copies are converted and written
plat_mutex_lock(&img_lock);
fclose(images_f);
images_f = NULL;
if (bufs_f) fclose(bufs_f), bufs_f = NULL;
plat_mutex_unlock(&img_lock);
fclose(passes_f);
passes_f = NULL;
rt_log("vk_dump: frame %llu: %d passes and dispatches, about %llu MB of PNGs in %s", (unsigned long long)frame, seq,
(unsigned long long)(png_bytes >> 20), dir);
}