// TOTK_DUMP=: 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__t_x__.png colour target i (td: depth; img: storage image; lut: 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_.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__tex_... 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= also logs the // frames from..frame-1 to passes.txt, with PNGs only of targets up to 64K texels. TOTK_DUMP_BUF=:[:...] // snapshots one GPU buffer range after every logged pass (buf_.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=|x[,...]: those texture slots, or 2D textures of that size, are copied where first sampled, // whatever their format. // TOTK_DUMP_UBO=: 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 #include #include #include #include #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=:[::::]: 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_.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_.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 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); }