// nx2navi.exe --probe-vkmem: texture memory checks. The CPU part: block-linear copies against an // independent reference, ASTC void-extent blocks, and the NVN -> VkFormat table against GPU0. The GPU part: // guest heap imported as a VkBuffer, write watch against CPU, GPU and ReadFile writes, the virtual-pool page table, // and textures uploaded from block-linear storage and read back, among them ASTC textures at the texture detail in // effect (NX2NAVI_TEXTURE_DETAIL=reduced or low to check the levels left out). // The reference swizzler walks the GOBs in storage order and places each byte by bit operations, so it shares no // addressing code with nvn_bl_offset. #include #include #include #include "cfg.h" #include "host.h" #include "nvn_api.h" #include "tex.h" #include "vk.h" #include "vk_exec.h" #include "vk_gpa.h" #include "platform/platform.h" static uint64_t g_rng = 0x9E3779B97F4A7C15ull; static uint8_t rnd8(void) { g_rng ^= g_rng << 13; g_rng ^= g_rng >> 7; g_rng ^= g_rng << 17; return (uint8_t)(g_rng >> 32); } // Byte offset inside a 64 x 8 GOB from the bit layout: x[3:0] -> 0-3, y[0] -> 4, x[4] -> 5, y[2:1] -> 6-7, x[5] -> 8. // Bit by bit, independent of tex_deswizzle.c's arithmetic: Tegra X1, or Switch 2 for NVN2 titles (the order // tex_deswizzle.c describes). static unsigned gob_bits(unsigned x, unsigned y) { if (rt_nvn_api_version[0] >= 200) return (x & 15) | (y & 3) << 4 | (x >> 4 & 1) << 6 | (y >> 2 & 1) << 7 | (x >> 5 & 1) << 8; return (x & 15) | (y & 1) << 4 | (x >> 4 & 1) << 5 | (y >> 1 & 3) << 6 | (x >> 5 & 1) << 8; } // lin: slices * rows * row_bytes (tight). bl: block-linear storage, pre-filled with a marker. static void ref_swizzle(const uint8_t *lin, uint8_t *bl, uint32_t row_bytes, uint32_t rows, uint32_t slices, unsigned th, unsigned td) { uint32_t bh = 1u << th, bd = 1u << td; uint32_t gx = (row_bytes + 63) / 64, by = (rows + 8 * bh - 1) / (8 * bh), bz = (slices + bd - 1) / bd; uint8_t *p = bl; for (uint32_t kz = 0; kz < bz; kz++) for (uint32_t ky = 0; ky < by; ky++) for (uint32_t kx = 0; kx < gx; kx++) for (uint32_t z = 0; z < bd; z++) // GOBs of a block: y fastest, then z for (uint32_t g = 0; g < bh; g++) { for (unsigned y = 0; y < 8; y++) for (unsigned x = 0; x < 64; x++) { uint32_t ax = kx * 64 + x, ay = (ky * bh + g) * 8 + y, az = kz * bd + z; if (ax < row_bytes && ay < rows && az < slices) p[gob_bits(x, y)] = lin[((size_t)az * rows + ay) * row_bytes + ax]; } p += 512; } } static size_t bl_size(uint32_t row_bytes, uint32_t rows, uint32_t slices, unsigned th, unsigned td) { uint32_t bh = 1u << th, bd = 1u << td; return (size_t)((row_bytes + 63) / 64) * ((rows + 8 * bh - 1) / (8 * bh)) * ((slices + bd - 1) / bd) * 512 * bh * bd; } static unsigned g_cases; static int check_swizzle(uint32_t row_bytes, uint32_t rows, uint32_t slices, unsigned th, unsigned td, uint32_t bpb) { size_t n = (size_t)row_bytes * rows * slices, bn = bl_size(row_bytes, rows, slices, th, td); uint8_t *lin = malloc(n), *back = malloc(n), *ref = malloc(bn), *got = malloc(bn); for (size_t i = 0; i < n; i++) lin[i] = rnd8(); memset(ref, 0xEE, bn); memset(got, 0xEE, bn); ref_swizzle(lin, ref, row_bytes, rows, slices, th, td); tex_bl_copy(got, lin, row_bytes, row_bytes, rows, slices, th, td, 0); int bad = 0; if (memcmp(ref, got, bn)) { printf("probe-vkmem: FAIL swizzle bpb=%u th=%u td=%u %ux%ux%u differs from the reference\n", bpb, th, td, row_bytes, rows, slices); bad = 1; } memset(back, 0, n); tex_bl_copy(ref, back, row_bytes, row_bytes, rows, slices, th, td, 1); if (!bad && memcmp(lin, back, n)) { printf("probe-vkmem: FAIL deswizzle bpb=%u th=%u td=%u %ux%ux%u differs from the source\n", bpb, th, td, row_bytes, rows, slices); bad = 1; } g_cases++; free(lin); free(back); free(ref); free(got); return bad; } static int test_deswizzle(void) { static const uint32_t bpbs[] = {1, 2, 4, 8, 16}; for (unsigned b = 0; b < 5; b++) for (unsigned th = 0; th <= 5; th++) { uint32_t w = 37 * bpbs[b]; if (check_swizzle(w, 61, 1, th, 0, bpbs[b])) return 1; // spans several blocks in x and y if (check_swizzle(bpbs[b], 1, 1, th, 0, bpbs[b])) return 1; // one block if (check_swizzle(70 * bpbs[b], 9, 1, th, 0, bpbs[b])) return 1; } for (unsigned td = 1; td <= 3; td++) // 3D: depth 5 against block depths 2, 4, 8 for (unsigned th = 0; th <= 2; th++) if (check_swizzle(23 * 4, 19, 5, th, td, 4)) return 1; // levels of 256 KiB and more if (check_swizzle(1000 * 4, 300, 1, 4, 0, 4)) return 1; if (check_swizzle(97 * 16, 70, 9, 2, 2, 16)) return 1; // Through a descriptor: RGBA8 2D with mip levels, level 1 and 2, layer 0, against the reference on the same geometry. nvn_texdesc d = {.target = 1, .format = 0x25, .width = 200, .height = 150, .depth = 1, .levels = 4}; nvn_texlayout L; nvn_tex_layout(&d, &L); uint8_t *store = malloc(L.size), *lin; for (uint64_t i = 0; i < L.size; i++) store[i] = rnd8(); for (unsigned lv = 0; lv < 4; lv++) { uint32_t rb, rows, sl, bpb; uint64_t base; unsigned th, td; if (nvn_deswizzle_geom(&d, lv, 0, &rb, &rows, &sl, &base, &th, &td, &bpb)) { printf("probe-vkmem: FAIL geometry of level %u\n", lv); return 1; } size_t pitch = rb + 16, n = (size_t)pitch * rows; lin = calloc(1, n); if (nvn_deswizzle(&d, lv, 0, store, lin, pitch)) { printf("probe-vkmem: FAIL nvn_deswizzle level %u\n", lv); return 1; } size_t bn = bl_size(rb, rows, sl, th, td); uint8_t *tight = malloc((size_t)rb * rows), *ref0 = malloc(bn), *ref1 = malloc(bn); for (uint32_t y = 0; y < rows; y++) memcpy(tight + (size_t)y * rb, lin + (size_t)y * pitch, rb); memset(ref0, 0x00, bn); memset(ref1, 0xFF, bn); ref_swizzle(tight, ref0, rb, rows, sl, th, td); ref_swizzle(tight, ref1, rb, rows, sl, th, td); // bytes where both fills agree were written by the reference; the untouched storage must hold the same for (size_t i = 0; i < bn; i++) if (ref0[i] == ref1[i] && store[base + i] != ref0[i]) { printf("probe-vkmem: FAIL level %u differs from the reference at byte %zu\n", lv, i); return 1; } // swizzling the linear copy back leaves the storage unchanged uint8_t *keep = malloc(bn); memcpy(keep, store + base, bn); if (nvn_swizzle(&d, lv, 0, lin, pitch, store)) { printf("probe-vkmem: FAIL nvn_swizzle level %u\n", lv); return 1; } if (memcmp(keep, store + base, bn)) { printf("probe-vkmem: FAIL swizzle round trip level %u\n", lv); return 1; } free(keep); free(ref0); free(ref1); g_cases++; free(lin); free(tight); } free(store); return 0; } // nvn_texels_box against the whole-level path on random boxes: a box read equals the box cut from the deswizzled level, // and a box write leaves the storage as deswizzle, overwrite the box, swizzle does (nothing outside the box changes). static int check_box(const nvn_texdesc *d, unsigned lv, unsigned layer) { uint32_t rb, rows, sl, bpb; uint64_t base; unsigned th, td; if (nvn_deswizzle_geom(d, lv, layer, &rb, &rows, &sl, &base, &th, &td, &bpb)) { printf("probe-vkmem: FAIL box geometry\n"); return 1; } nvn_texlayout L; nvn_tex_layout(d, &L); uint8_t *store = malloc(L.size), *want = malloc(L.size), *lin = malloc((size_t)rb * rows * sl); for (uint64_t i = 0; i < L.size; i++) store[i] = rnd8(); uint32_t cols = rb / bpb; for (int k = 0; k < 8; k++) { uint32_t bx = rnd8() % cols, bn = 1 + rnd8() % (cols - bx), y = rnd8() % rows, h = 1 + rnd8() % (rows - y); uint32_t z = sl > 1 ? rnd8() % sl : 0, x = bx * bpb, wb = bn * bpb; size_t pitch = wb + 8; uint8_t *box = malloc(pitch * h), *ref = malloc(pitch * h); // read if (nvn_deswizzle(d, lv, layer, store, lin, rb)) { printf("probe-vkmem: FAIL box deswizzle\n"); return 1; } memset(box, 0xAB, pitch * h), memset(ref, 0xAB, pitch * h); for (uint32_t r = 0; r < h; r++) memcpy(ref + r * pitch, lin + ((size_t)z * rows + y + r) * rb + x, wb); if (nvn_texels_box(d, lv, layer, x, y, z, wb, h, store, box, pitch, 1) || memcmp(box, ref, pitch * h)) { printf("probe-vkmem: FAIL box read format %#x level %u layer %u box %u,%u,%u %ux%u\n", d->format, lv, layer, x, y, z, wb, h); return 1; } // write for (size_t i = 0; i < pitch * h; i++) box[i] = rnd8(); memcpy(want, store, L.size); for (uint32_t r = 0; r < h; r++) memcpy(lin + ((size_t)z * rows + y + r) * rb + x, box + r * pitch, wb); if (nvn_swizzle(d, lv, layer, lin, rb, want)) { printf("probe-vkmem: FAIL box swizzle\n"); return 1; } if (nvn_texels_box(d, lv, layer, x, y, z, wb, h, store, box, pitch, 0) || memcmp(store, want, L.size)) { printf("probe-vkmem: FAIL box write format %#x level %u layer %u box %u,%u,%u %ux%u\n", d->format, lv, layer, x, y, z, wb, h); return 1; } g_cases++; free(box), free(ref); } free(store), free(want), free(lin); return 0; } static int test_box(void) { // one format per block size: 1, 8 and 16 bytes per texel, 8 and 16 bytes per 4x4 block int fmt[5] = {0}, n = 0; static const int want_bpb[5] = {1, 8, 16, 8, 16}, want_bw[5] = {1, 1, 1, 4, 4}; for (int f = 1; f < 0x200 && n < 5; f++) { int bw, bh, bpb = nvn_fmt_bpb(f, &bw, &bh); for (int i = 0; i < 5; i++) if (!fmt[i] && bpb == want_bpb[i] && bw == want_bw[i] && bh == bw) fmt[i] = f, n++; } for (int i = 0; i < 5; i++) { if (!fmt[i]) continue; nvn_texdesc d2 = {.target = 1, .format = fmt[i], .width = 300, .height = 170, .depth = 1, .levels = 3}; nvn_texdesc da = {.target = 4, .format = fmt[i], .width = 96, .height = 80, .depth = 3, .levels = 2}; nvn_texdesc d3 = {.target = 2, .format = fmt[i], .width = 72, .height = 40, .depth = 5, .levels = 2}; for (unsigned lv = 0; lv < 2; lv++) if (check_box(&d2, lv, 0) || check_box(&da, lv, 2) || check_box(&d3, lv, 0)) return 1; } return 0; } // ---- ASTC ---- static void void_block(uint8_t b[16], uint16_t r, uint16_t g, uint16_t bl, uint16_t a, int hdr) { uint64_t lo = 0xFFFFFFFFFFFFFDFCull; // 0x1FC mode, LDR, reserved bits 11, extent all ones if (hdr) lo |= 0x200; memcpy(b, &lo, 8); uint16_t c[4] = {r, g, bl, a}; memcpy(b + 8, c, 8); } static unsigned g_astc_blocks; static int test_astc(void) { static const uint8_t dims[14][2] = {{4, 4}, {5, 4}, {5, 5}, {6, 5}, {6, 6}, {8, 5}, {8, 6}, {8, 8}, {10, 5}, {10, 6}, {10, 8}, {10, 10}, {12, 10}, {12, 12}}; for (int i = 0; i < 14; i++) { unsigned bw = dims[i][0], bh = dims[i][1]; // a 3x2 grid of void-extent blocks cropped to a size that cuts the last column and row unsigned w = bw * 3 - 1, h = bh * 2 - 1; uint8_t blocks[6 * 16]; uint8_t want[6][4]; for (int k = 0; k < 6; k++) { uint8_t c[4]; for (int j = 0; j < 4; j++) c[j] = rnd8(); // UNORM16 = c * 257 gives back c in the top 8 bits, plus noise in the low byte that must be dropped uint16_t v[4]; for (int j = 0; j < 4; j++) v[j] = (uint16_t)(c[j] << 8 | rnd8()); void_block(blocks + 16 * k, v[0], v[1], v[2], v[3], 0); memcpy(want[k], c, 4); } uint8_t *out = calloc(1, (size_t)w * h * 4); for (int s = 0; s < 2; s++) { astc_decode_ldr(blocks, w, h, bw, bh, s, out); for (unsigned y = 0; y < h; y++) for (unsigned x = 0; x < w; x++) if (memcmp(out + ((size_t)y * w + x) * 4, want[y / bh * 3 + x / bw], 4)) { printf("probe-vkmem: FAIL astc %ux%u void extent at (%u,%u)\n", bw, bh, x, y); return 1; } } free(out); // HDR void extent is an error block in LDR: magenta, and astc_decode_block reports it uint8_t hb[16], tmp[12 * 12 * 4]; void_block(hb, 0x1234, 0x5678, 0x9ABC, 0xDEF0, 1); if (astc_decode_block(hb, bw, bh, false, tmp) || memcmp(tmp, "\xff\x00\xff\xff", 4)) { printf("probe-vkmem: FAIL astc %ux%u HDR void extent is not an error block\n", bw, bh); return 1; } g_astc_blocks += 7; } // sRGB void extent: the colour is the stored top byte as well (no transfer curve for constant blocks) uint8_t b[16], px[16 * 4]; void_block(b, 0xFF00, 0x8000, 0x0100, 0xFFFF, 0); astc_decode_block(b, 4, 4, true, px); if (memcmp(px, "\xff\x80\x01\xff", 4)) { printf("probe-vkmem: FAIL astc sRGB void extent\n"); return 1; } g_astc_blocks++; // the worker-pool decode gives the same bytes: random blocks (every mode, error blocks included), cropped edges for (int i = 0; i < 14; i++) { unsigned bw = dims[i][0], bh = dims[i][1], w = 1021, h = 517, n = (w + bw - 1) / bw * ((h + bh - 1) / bh); uint8_t *blk = malloc((size_t)n * 16), *a = malloc((size_t)w * h * 4), *m = malloc((size_t)w * h * 4); for (size_t k = 0; k < (size_t)n * 16; k++) blk[k] = rnd8(); for (int s = 0; s < 2; s++) { memset(a, 0x11, (size_t)w * h * 4), memset(m, 0x22, (size_t)w * h * 4); astc_decode_ldr(blk, w, h, bw, bh, s, a); astc_decode_ldr_mt(blk, w, h, bw, bh, s, m); if (memcmp(a, m, (size_t)w * h * 4)) { printf("probe-vkmem: FAIL astc %ux%u: the worker-pool decode differs from astc_decode_ldr\n", bw, bh); return 1; } } free(blk), free(a), free(m); g_astc_blocks += n; } return 0; } // ---- formats ---- static unsigned n_mapped, n_supported, n_unmapped, n_unsupported, n_fallback; static int test_formats(void) { if (!vk_init()) { printf("probe-vkmem: FAIL vk_init\n"); return 1; } printf("probe-vkmem: GPU %s\n", vk_ctx()->props.deviceName); for (int f = 0; f <= 0x9C; f++) { TexVkFormat t; int ok = tex_vk_format(f, &t); if (!ok) { n_unmapped++; printf("probe-vkmem: unmapped %#x: %s\n", f, t.reason ? t.reason : "(no reason)"); if (!t.reason) return 1; continue; } n_mapped++; if (t.vk != t.ideal) n_fallback++; int bw, bh, bpb = nvn_fmt_bpb(f, &bw, &bh); if (t.astc_bw && (bpb != 16 || bw != t.astc_bw || bh != t.astc_bh)) { printf("probe-vkmem: FAIL ASTC format %#x: block %dx%d/%d bytes, table says %ux%u\n", f, bw, bh, bpb, t.astc_bw, t.astc_bh); return 1; } if (tex_vk_format_supported(&t)) n_supported++; else { n_unsupported++; printf("probe-vkmem: unsupported %#x: VkFormat %d lacks features %#x on this GPU\n", f, (int)t.vk, (unsigned)t.need); } } // The GPU-side promises of the fallbacks: RGBA8 and D32S8 must be usable, otherwise the fallback is empty. TexVkFormat t; tex_vk_format(0x79, &t); int ok_astc = tex_vk_format_supported(&t); tex_vk_format(0x35, &t); int ok_d = tex_vk_format_supported(&t) && t.vk == VK_FORMAT_D32_SFLOAT_S8_UINT; if (!ok_astc || !ok_d) { printf("probe-vkmem: FAIL fallback formats (RGBA8 %d, D32S8 %d)\n", ok_astc, ok_d); return 1; } return 0; } // ---- GPU part ---- #define GCHECK(cond, ...) \ do { \ if (!(cond)) { \ printf("probe-vkmem: FAIL "); \ printf(__VA_ARGS__); \ printf("\n"); \ return 1; \ } \ } while (0) static unsigned g_gpu_checks; static uint64_t g_gpu_seen, g_rf_seen[2], g_rf_us[2]; static void copy_wait(VkBuffer src, VkDeviceSize so, VkBuffer dst, VkDeviceSize doff, VkDeviceSize n) { VkCommandBuffer cb = vk_upload_begin(); VkBufferCopy rg = {so, doff, n}; vkCmdCopyBuffer(cb, src, dst, 1, &rg); VkMemoryBarrier2 mb = {.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 = &mb}; vkCmdPipelineBarrier2(cb, &di); vk_upload_end(); vk_upload_flush(1); } // A CPU-visible pool over the guest heap: GPU writes land in guest memory, CPU writes are read by the GPU, write watch // reports the CPU writes and the explicit ones. GPU writes are noted and reported as the replay does // (vk_mem_gpu_pending, vk_gpu_written at the fence), and a GPU read follows vk_mem_shadow_sync as a submit does: with // shadow storage that is what moves the bytes; with an import they are already in place. static int test_import(pool_t *pool) { VkCtx *c = vk_ctx(); uint8_t *m = pool->mem; uint64_t a = (uintptr_t)m, pages[64], n; // What the CPU wrote before the pool existed is still reported once its pages are imported (or copied into a // shadow): making the pool must not take the write watch's record of those writes, which the first sync of a // mirror and the texture checks read afterwards. Every page is touched first, so that the count printed tells // what making the pool marks by itself. memset(m, 0, pool->size); mem_written_pages(a, pool->size, pages, 64); m[0x3c000] = 0x5a, m[0x3e000] = 0xa5; GCHECK(vk_pool_init(pool), "vk_pool_init of a CPU-visible pool at %#llx", (unsigned long long)a); n = mem_written_peek(a, pool->size, pages, 64); int before = 0; for (uint64_t i = 0; n != UINT64_MAX && i < n && i < 64; i++) before += pages[i] == a + 0x3c000 || pages[i] == a + 0x3e000; GCHECK(before == 2, "of 2 pages the CPU wrote before the pool was made, %d are reported after it (%llu pages in all)", before, (unsigned long long)n); printf("probe-vkmem: CPU writes made before the pool was made: both pages reported after it (%llu of %llu pages " "reported in all)\n", (unsigned long long)n, (unsigned long long)(pool->size >> 12)); g_gpu_checks++; VkMemSpan sp; GCHECK(vk_mem_resolve(a + 0x1000, 0x20000, &sp) && sp.buffer && sp.offset == 0x1000 && sp.avail == pool->size - 0x1000, "resolve inside the imported pool (offset %#llx, avail %#llx)", (unsigned long long)sp.offset, (unsigned long long)sp.avail); GCHECK(!vk_mem_resolve(a + pool->size - 0x10, 0x20, &sp), "a range past the pool end resolved"); g_gpu_checks += 2; VkBuffer sb; VmaAllocation sa; uint8_t *s = vk_staging(0x20000, &sb, &sa); for (int i = 0; i < 0x20000; i++) s[i] = rnd8(); vmaFlushAllocation(c->vma, sa, 0, VK_WHOLE_SIZE); vk_mem_resolve(a + 0x1000, 0x20000, &sp); mem_written_pages(a, pool->size, pages, 64); vk_mem_gpu_pending(a + 0x1000, 0x20000); // As a submit does: a new write-watch epoch (the copy-in reads each region's snapshot once per submission, so a // second sync in the same epoch would not see the CPU writes made since the first), then the part's CPU inputs // go in before its GPU work. vk_texture_new_submit(); vk_mem_shadow_sync(0); copy_wait(sb, 0, sp.buffer, sp.offset, 0x20000); vk_gpu_written(a + 0x1000, 0x20000); GCHECK(!memcmp(m + 0x1000, s, 0x20000), "GPU copy into the imported heap range differs from the source"); n = mem_written_pages(a, pool->size, pages, 64); g_gpu_seen = n; printf("probe-vkmem: GPU copy of 128 KiB into the imported heap: equal; write watch reported %llu of 32 pages\n", (unsigned long long)n); // CPU writes: reported exactly, and the GPU reads them through the same buffer for (int i = 0; i < 0x4000; i++) m[0x30000 + i] = rnd8(); n = mem_written_peek(a, pool->size, pages, 64); GCHECK(n == 4 && pages[0] == a + 0x30000 && pages[3] == a + 0x33000, "CPU writes to 4 pages: %llu reported", (unsigned long long)n); vk_mem_resolve(a + 0x30000, 0x4000, &sp); // A GPU write noted for the part being submitted must not keep the part's CPU inputs out: a buffer the shaders // add to and the CPU resets (the game's lighting accumulators) depends on the reset reaching the GPU first. vk_mem_gpu_pending(a + 0x30000, 0x4000); vk_texture_new_submit(); vk_mem_shadow_sync(0); copy_wait(sp.buffer, sp.offset, sb, 0, 0x4000); vmaInvalidateAllocation(c->vma, sa, 0, VK_WHOLE_SIZE); GCHECK(!memcmp(s, m + 0x30000, 0x4000), "GPU read of CPU-written heap differs"); vk_gpu_written(a + 0x30000, 0x4000); vmaDestroyBuffer(c->vma, sb, sa); // generations: written after `since` -> 1, then 0, explicit host write -> 1 uint64_t g0, g1, g2, g3; vk_mem_changed(a, pool->size, 0, &g0); m[0x8000] = 1; GCHECK(vk_mem_changed(a + 0x8000, 0x1000, g0, &g1) == 1, "a CPU write after a generation is not seen"); GCHECK(vk_mem_changed(a, pool->size, g1, &g2) == 0, "no write since the last generation is reported as written"); vk_guest_written(a + 0x9000, 4); GCHECK(vk_mem_changed(a + 0x9000, 0x1000, g2, &g3) == 1, "an explicit guest_written range is not seen"); GCHECK(vk_mem_changed(RT_KERNEL_BASE, 0x1000, 0, &g3) == -1, "the kernel region is reported as tracked"); printf("probe-vkmem: write watch: CPU writes reported page-exact; generations see CPU and explicit writes\n"); g_gpu_checks += 7; return 0; } // The write-watch snapshot of many ranges at once (vk_memory.c vk_mem_snap_note_locked, vk_mem_snap_noted_locked), as // the texture pool's freshness check asks for it: ranges in three adjacent 2 MiB regions of the heap and in one apart, // noted in no address order, take one query per run of adjacent regions; afterwards every region has the epoch's // snapshot, the regions with a CPU write since `since` say so, the one without does not, and the pages are exact. // The bytes written are the ones already there, so the tests after this one find their memory as it was. static int test_snap_runs(uint8_t *h) { static const uint32_t off[4] = {0xe10000, 0xa80000, 0x603000, 0x9f0000}; // regions at 14, 10, 6 and 8 MiB uint64_t base = (uintptr_t)h; GCHECK(!(base & 0x1fffff), "the heap at %#llx does not start on a 2 MiB region", (unsigned long long)base); int left = -1; uint64_t g0, now; vk_texture_new_submit(); uint32_t ep = vk_texture_epoch(); vk_mem_gen_lock(); for (int i = 0; i < 4; i++) vk_mem_snap_note_locked(base + off[i], 0x2000, ep); vk_mem_snap_noted_locked(ep, &left); // what was written before this test vk_mem_gen_unlock(); GCHECK(!left, "a run inside the heap was left to the queries by range"); vk_mem_changed(base + 0x200000, 0x1000, 0, &g0); volatile uint8_t *w[3] = {h + 0xe10010, h + 0xa80800, h + 0x604000}; for (int i = 0; i < 3; i++) *w[i] = *w[i]; vk_texture_new_submit(); ep = vk_texture_epoch(); vk_mem_gen_lock(); for (int i = 0; i < 4; i++) vk_mem_snap_note_locked(base + off[i], 0x2000, ep); for (int i = 0; i < 4; i++) vk_mem_snap_note_locked(base + off[i], 0x2000, ep); // a region noted twice is asked once uint64_t q = vk_mem_snap_noted_locked(ep, &left), again = 0; int why[4]; for (int i = 0; i < 4; i++) { why[i] = vk_mem_unchanged_why_locked(base + off[i], 0x2000, g0, ep); again += vk_mem_snap_range_locked(base + off[i], 0x2000, ep); } vk_mem_gen_unlock(); GCHECK(q == 2 && !left, "four regions in two runs took %llu queries (left %d)", (unsigned long long)q, left); GCHECK(!again, "%llu regions had no snapshot after the queries by run", (unsigned long long)again); GCHECK(why[0] == 3 && why[1] == 3 && why[2] == 3 && why[3] == 0, "regions written, written, written, not written " "answer %d %d %d %d", why[0], why[1], why[2], why[3]); GCHECK(vk_mem_changed_snap(base + 0x604000, 0x1000, g0, ep, &now) == 1 && vk_mem_changed_snap(base + 0x603000, 0x1000, g0, ep, &now) == 0 && vk_mem_changed_snap(base + 0x9f0000, 0x2000, g0, ep, &now) == 0, "the pages written are not reported exactly"); printf("probe-vkmem: write-watch snapshot by runs: 4 regions in 2 runs took 2 queries; written regions and pages exact\n"); g_gpu_checks += 5; return 0; } // A GPU write is noted on a pool, and the pool is finalized and made again at its address before the write is // reported: the report must leave alone what the CPU has written to the new pool since. With shadow storage the new // pool's shadow is a copy of guest memory from before those writes and holds no byte of the old write // (vk_memory.c shadow_write_back); with an import the report copies nothing. static int test_remade(uint8_t *guest) { pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x24, .mem = guest + 0xc00000, .size = 0x10000, .game_mem = guest + 0xc00000}; uint8_t *m = pool.mem; uint64_t a = (uintptr_t)m; memset(m, 0x11, pool.size); GCHECK(vk_pool_init(&pool), "vk_pool_init of the pool to be made again"); VkMemSpan sp; GCHECK(vk_mem_resolve(a, 0x2000, &sp) && sp.buffer, "resolve inside the pool to be made again"); vk_mem_gpu_pending(a, 0x2000); // the submission being recorded writes its first two pages vk_pool_fini(&pool); GCHECK(vk_pool_init(&pool), "vk_pool_init of the pool made again"); memset(m, 0x22, 0x1000); // the CPU writes the first page of the new pool vk_gpu_written(a, 0x2000); // the old submission's fence GCHECK(m[0] == 0x22 && m[0xfff] == 0x22, "the report of a write noted on a finalized pool put %#x over the CPU's " "bytes in the pool made at its address", m[0]); GCHECK(m[0x1000] == 0x11 && m[0x1fff] == 0x11, "the page the CPU left alone changed (%#x)", m[0x1000]); vk_pool_fini(&pool); printf("probe-vkmem: a write reported for a pool that was made again leaves the CPU's newer bytes alone\n"); g_gpu_checks += 2; return 0; } // A pool in the device address table gets its VRAM mirror while a guest submission is recorded, as on a draw's first // use of it as a buffer (vk_memory.c mirror_create). The table takes the mirror up by a store the GPU runs after the // mirror's first copy (vk_gpa.c vk_gpa_flip_cmd), so nothing is changed when the mirror is made; read from the table's // own memory once the submission is done, the pool's page reads the mirror and writes guest memory as well, and the // mirror holds the pool's bytes. static void mirror_use_record(VkCommandBuffer cb, void *arg) { vk_texture_new_submit(); vk_mem_mirror_begin(); vk_mem_use(arg); vk_mem_mirror_end(cb, vk_sync_issued() + 1); } static int test_table_mirror(uint8_t *guest) { VkCtx *c = vk_ctx(); if (vk_mem_shadow_storage() || !vk_gpa_wanted()) { printf("probe-vkmem: a new mirror in the device address table: skipped (%s)\n", vk_mem_shadow_storage() ? "shadow storage, no mirrors" : "no device address table"); return 0; } pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x24, .mem = guest + 0xD00000, .size = 0x10000, .game_mem = guest + 0xD00000}; volatile uint32_t *w = (volatile uint32_t *)(pool.mem + 0x3000); uint64_t a = (uintptr_t)w, f0 = 0, t0 = 0, f1 = 0, t1 = 0, fm = 0, tm = 0; *w = 0x600DF00Du; GCHECK(vk_gpa_on() && vk_pool_init(&pool), "a CPU-visible pool in the device address table"); VkBuffer sb; VmaAllocation sa; volatile uint32_t *s = vk_staging(0x1000, &sb, &sa); VkMemSpan sp; int bad = 1; #define TM_CHECK(cond, ...) do { if (!(cond)) { printf("probe-vkmem: FAIL table mirror: "); printf(__VA_ARGS__); printf("\n"); goto done; } } while (0) TM_CHECK(vk_mem_lookup(a, 4, &sp) && sp.buffer && sp.pool, "the pool has no buffer"); VkBuffer imported = sp.buffer; TM_CHECK(vk_gpa_probe(a, &f0, &t0) && f0 == t0 && f0 == vk_gpa_device_address(imported) + sp.offset, "before a mirror the table gives %#llx / %#llx for the pool's page, expected its imported buffer twice", (unsigned long long)f0, (unsigned long long)t0); uint64_t fence = vk_sync_submit_own(mirror_use_record, &sp); // read while the submission may still be in flight: the table as the CPU left it when the mirror was made int mid = vk_gpa_probe(a, &fm, &tm); TM_CHECK(vk_sync_fence_wait(fence, 5000000000ull), "fence timed out"); if (sp.buffer == imported) { printf("probe-vkmem: a new mirror in the device address table: skipped (the pool got no mirror)\n"); bad = 0; goto done; } TM_CHECK(vk_gpa_probe(a, &f1, &t1), "the pool's page left the table"); if (vk_mem_gpa_late()) // else the table changes when the mirror is made, as up to 0.9.2 TM_CHECK(mid && (fm == f0 || fm == f1) && tm == f0, "while the submission ran the table gave %#llx / %#llx: neither " "the state before the store (%#llx twice) nor after it", (unsigned long long)fm, (unsigned long long)tm, (unsigned long long)f0); TM_CHECK(f1 == vk_gpa_device_address(sp.buffer) + sp.offset, "after the submission the page reads %#llx, expected the " "mirror's %#llx", (unsigned long long)f1, (unsigned long long)(vk_gpa_device_address(sp.buffer) + sp.offset)); TM_CHECK(t1 == f0, "after the submission the page's twin is %#llx, expected the imported buffer's %#llx", (unsigned long long)t1, (unsigned long long)f0); copy_wait(sp.buffer, sp.offset, sb, 0, 4); vmaInvalidateAllocation(c->vma, sa, 0, VK_WHOLE_SIZE); TM_CHECK(s[0] == 0x600DF00Du, "the mirror holds %#010x, expected the pool's 0x600df00d", s[0]); g_gpu_checks += 4; printf("probe-vkmem: a pool that gets its mirror in a recorded submission: the device address table names the mirror " "after it, with guest memory as the twin (%s)\n", vk_mem_gpa_late() ? "by a store in queue order, after the mirror's first copy" : "changed at once, NX2NAVI_GPA_LATE=0"); bad = 0; done: vk_sync_finish(); vmaDestroyBuffer(c->vma, sb, sa); vk_pool_fini(&pool); #undef TM_CHECK return bad; } // test_table_sync, test_table_store: a guest submission of one part that records nothing. bound: its record binds the // device address table, as the replay of a draw whose shader reads through the table does (vk_exec_draw.c ssbo_info), // so its prologue brings every shadow of the table up to date (vk_memory.c vk_mem_shadow_sync). static void table_part_record(VkCommandBuffer cb, void *arg) { vk_texture_new_submit(); vk_mem_mirror_begin(); if (arg) vk_mem_shadow_table_read(); vk_mem_mirror_end(cb, vk_sync_issued() + 1); } static int table_part_of(int bound) { static int binds; uint64_t fence = vk_sync_submit_own(table_part_record, bound ? &binds : NULL); return vk_sync_fence_wait(fence, 5000000000ull); // its completion callbacks have run when the wait returns } static int table_part(void) { return table_part_of(1); } // Shadow storage: a pool that no part ever binds or resolves, as the pools a shader reaches only through a pointer it // read from another buffer. Its shadow, which the device address table names, starts with the bytes guest memory held // when the pool was made; a CPU write reaches it at the next guest submission whose record binds the table, and not // at one that does not (nothing else brings it there, which is what left the foliage's instances stale). The shadow // is read back with a GPU copy out of the buffer a lookup names, which does not touch the pool. static int test_table_sync(uint8_t *guest) { VkCtx *c = vk_ctx(); if (!vk_mem_shadow_storage() || !vk_gpa_wanted()) { printf("probe-vkmem: the sync of the device address table's shadows: skipped (%s)\n", !vk_mem_shadow_storage() ? "no shadow storage" : "no device address table"); return 0; } pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x24, .mem = guest + 0xA00000, .size = 0x10000, .game_mem = guest + 0xA00000}; volatile uint32_t *w = (volatile uint32_t *)(pool.mem + 0x3000); *w = 0x13579BDFu; // before vk_pool_init: a shadow starts with the bytes an import would show GCHECK(vk_gpa_on() && vk_pool_init(&pool), "a CPU-visible pool in the device address table"); VkBuffer sb; VmaAllocation sa; volatile uint32_t *s = vk_staging(0x1000, &sb, &sa); VkMemSpan sp; int bad = 1; uint32_t got; #define TY_CHECK(cond, ...) do { if (!(cond)) { printf("probe-vkmem: FAIL table sync: "); printf(__VA_ARGS__); printf("\n"); goto done; } } while (0) #define TY_SHADOW(into) \ do { \ TY_CHECK(vk_mem_lookup((uintptr_t)w, 4, &sp) && sp.buffer, "the pool has no buffer"); \ copy_wait(sp.buffer, sp.offset, sb, 0, 4); \ vmaInvalidateAllocation(c->vma, sa, 0, VK_WHOLE_SIZE); \ (into) = s[0]; \ } while (0) TY_SHADOW(got); TY_CHECK(got == 0x13579BDFu, "the new shadow holds %#010x, expected the pool's 0x13579bdf", got); *w = 0x2468ACE0u; // a plain CPU write: nothing resolves, uses or flushes the pool TY_CHECK(table_part_of(0), "fence timed out"); TY_SHADOW(got); TY_CHECK(got == 0x13579BDFu, "a guest submission that does not bind the table left %#010x in the shadow, expected " "the old 0x13579bdf (the check tests nothing if something else syncs the pool)", got); TY_CHECK(table_part_of(1), "fence timed out"); TY_SHADOW(got); TY_CHECK(got == 0x2468ACE0u, "after a guest submission that binds the table the shadow holds %#010x, expected the " "CPU's 0x2468ace0", got); *w = 0x0F1E2D3Cu; // and again, a guest submission later TY_CHECK(table_part_of(1), "fence timed out"); TY_SHADOW(got); TY_CHECK(got == 0x0F1E2D3Cu, "after the second such guest submission the shadow holds %#010x, expected 0x0f1e2d3c", got); g_gpu_checks += 4; printf("probe-vkmem: a pool that is never bound: its shadow in the device address table takes the CPU's writes at " "each guest submission that binds the table, and at no other\n"); bad = 0; done: vk_sync_finish(); vmaDestroyBuffer(c->vma, sb, sa); vk_pool_fini(&pool); #undef TY_CHECK #undef TY_SHADOW return bad; } // Shadow storage: what a shader stores through the device address table lands in the pool's shadow copy and nothing // notes it (vk_memory.c "Pages a shader stores into"). A GPU copy into the shadow buffer that is not noted stands in // for the store. Word X of a page takes the stores, the CPU rewrites word Y of the same page before every part: // - the store made after the page's first whole copy-in is overwritten by the second, which finds it (nothing can // keep it); // - every later store stays in the shadow through the copy-ins that bring Y, and is in guest memory after the fence; // - a CPU write of X after that reaches the shadow. static int test_table_store(uint8_t *guest) { VkCtx *c = vk_ctx(); if (!vk_mem_shadow_storage() || !vk_gpa_wanted() || !vk_mem_shadow_table_stores()) { printf("probe-vkmem: stores through the device address table into a shadow copy: skipped (%s)\n", !vk_mem_shadow_storage() ? "no shadow storage" : !vk_gpa_wanted() ? "no device address table" : "NX2NAVI_SHADOW_TABLE_STORES is not 1"); return 0; } pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x24, .mem = guest + 0xB00000, .size = 0x10000, .game_mem = guest + 0xB00000}; volatile uint32_t *x = (volatile uint32_t *)(pool.mem + 0x200), *y = (volatile uint32_t *)(pool.mem + 0x100); *x = 0, *y = 1; GCHECK(vk_gpa_on() && vk_pool_init(&pool), "a CPU-visible pool in the device address table"); VkBuffer sb; VmaAllocation sa; volatile uint32_t *s = vk_staging(0x1000, &sb, &sa); VkMemSpan sp; int bad = 1; #define TS_CHECK(cond, ...) do { if (!(cond)) { printf("probe-vkmem: FAIL table store: "); printf(__VA_ARGS__); printf("\n"); goto done; } } while (0) #define TS_STORE(v) \ do { \ s[0] = (v); \ vmaFlushAllocation(c->vma, sa, 0, VK_WHOLE_SIZE); \ TS_CHECK(vk_mem_lookup((uintptr_t)x, 4, &sp) && sp.buffer, "the pool has no buffer"); \ copy_wait(sb, 0, sp.buffer, sp.offset, 4); \ } while (0) #define TS_SHADOW(off, into) \ do { \ TS_CHECK(vk_mem_lookup((uintptr_t)pool.mem + (off), 4, &sp) && sp.buffer, "the pool has no buffer"); \ copy_wait(sp.buffer, sp.offset, sb, 16, 4); \ vmaInvalidateAllocation(c->vma, sa, 0, VK_WHOLE_SIZE); \ (into) = s[4]; \ } while (0) uint32_t sx, sy; *y = 2; TS_CHECK(table_part(), "fence timed out"); // the page's first whole copy-in: its mark TS_STORE(0xAAAA0001u); *y = 3; TS_CHECK(table_part(), "fence timed out"); // finds the store, overwrites it, the page keeps a base copy TS_SHADOW(0x200, sx); TS_CHECK(sx == 0, "the copy-in that found the store left X %#010x in the shadow, expected guest memory's 0", sx); TS_STORE(0xAAAA0002u); *y = 4; TS_CHECK(table_part(), "fence timed out"); // brings Y, keeps X; its fence writes X back TS_SHADOW(0x200, sx); TS_SHADOW(0x100, sy); TS_CHECK(sx == 0xAAAA0002u && sy == 4, "after a CPU write of the page the shadow holds X %#010x Y %u, expected " "0xaaaa0002 and 4", sx, sy); TS_CHECK(*x == 0xAAAA0002u && *y == 4, "guest memory holds X %#010x Y %u after the fence, expected 0xaaaa0002 and 4", (uint32_t)*x, (uint32_t)*y); *x = 0; // the CPU resets the stored word TS_CHECK(table_part(), "fence timed out"); TS_SHADOW(0x200, sx); TS_CHECK(sx == 0 && *x == 0, "after the CPU reset X the shadow holds %#010x and guest memory %#010x", sx, (uint32_t)*x); TS_STORE(0xAAAA0003u); // and a store after the reset comes back as well TS_CHECK(table_part(), "fence timed out"); TS_CHECK(*x == 0xAAAA0003u && *y == 4, "guest memory holds X %#010x Y %u after the second fence", (uint32_t)*x, (uint32_t)*y); g_gpu_checks += 5; printf("probe-vkmem: a store into a shadow page that nothing notes survives the CPU's writes of that page, reaches " "guest memory at the fence, and gives way to a CPU write of the same word\n"); bad = 0; done: vk_sync_finish(); vmaDestroyBuffer(c->vma, sb, sa); vk_pool_fini(&pool); #undef TS_CHECK #undef TS_STORE #undef TS_SHADOW return bad; } // ReadFile into a watched page, buffered and unbuffered: timed, and whether write watch saw the kernel's write. static int test_readfile(uint8_t *dst) { char dir[PLAT_PATH_MAX], path[PLAT_PATH_MAX + 64]; GCHECK(plat_temp_dir(dir, sizeof dir), "temp directory"); snprintf(path, sizeof path, "%s/tvk-%u.tmp", dir, plat_process_id()); static uint8_t data[0x2000]; for (size_t i = 0; i < sizeof data; i++) data[i] = rnd8(); GCHECK(plat_file_write(path, data, sizeof data), "temp file write"); for (int k = 0; k < 2; k++) { uint8_t *p = dst + (size_t)k * 0x2000; uint64_t pages[8]; PlatFile *h = k ? plat_file_open_unbuffered(path) : plat_file_open(path); GCHECK(h, "temp file open"); mem_written_pages((uintptr_t)p, 0x2000, pages, 8); uint64_t t0 = plat_now_ns(); int64_t got = plat_file_pread(h, p, 0x2000, 0); uint64_t t1 = plat_now_ns(); plat_file_close(h); GCHECK(got == 0x2000 && !memcmp(p, data, sizeof data), "ReadFile (%s) result", k ? "unbuffered" : "buffered"); g_rf_seen[k] = mem_written_pages((uintptr_t)p, 0x2000, pages, 8); g_rf_us[k] = (t1 - t0) / 1000; printf("probe-vkmem: ReadFile %s of 8 KiB into a watched page: %llu us, write watch saw %llu of 2 pages\n", k ? "unbuffered" : "buffered", (unsigned long long)g_rf_us[k], (unsigned long long)g_rf_seen[k]); } plat_file_delete(path); g_gpu_checks += 2; return 0; } // A physical pool, a virtual pool mapping 64 KiB pages of it, resolution and unmapping; the GPU writes through a // virtual address and the bytes are read through the physical one. static int test_virtual(pool_t *phys, pool_t *virt) { VkCtx *c = vk_ctx(); GCHECK(vk_pool_init(phys) && vk_pool_init(virt), "vk_pool_init of physical / virtual pools"); uint64_t p = (uintptr_t)phys->mem, v = (uintptr_t)virt->mem; nvn_mapping_t rq[2] = {{(const NVNmemoryPool *)(const void *)phys, 0x20000, 0, 0x20000, 0, 0}, {(const NVNmemoryPool *)(const void *)phys, 0, 0x50000, 0x10000, 0, 0}}; GCHECK(vk_pool_map_virtual(virt, 2, rq), "MapVirtual of 2 requests"); VkMemSpan ps, sp; GCHECK(vk_mem_resolve(p, 0x40000, &ps) && ps.offset == 0 && ps.alloc, "resolve of the physical pool"); GCHECK(vk_mem_resolve(v + 8, 4, &sp) && sp.buffer == ps.buffer && sp.offset == 0x20008, "virtual page 0 -> %#llx", (unsigned long long)sp.offset); GCHECK(vk_mem_resolve(v + 0x10010, 0x10, &sp) && sp.offset == 0x30010, "virtual page 1 -> %#llx", (unsigned long long)sp.offset); GCHECK(vk_mem_resolve(v + 0x8000, 0x18000, &sp) && sp.avail == 0x18000, "contiguous pages 0-1 (avail %#llx)", (unsigned long long)sp.avail); GCHECK(!vk_mem_resolve(v + 0x18000, 0x10000, &sp), "a range into unmapped page 2 resolved"); GCHECK(!vk_mem_resolve(v + 0x20000, 4, &sp), "unmapped page 2 resolved"); GCHECK(vk_mem_resolve(v + 0x50000, 0x10000, &sp) && sp.offset == 0 && sp.alloc_offset == 0, "virtual page 5 -> %#llx", (unsigned long long)sp.offset); VkCommandBuffer cb = vk_upload_begin(); vkCmdFillBuffer(cb, sp.buffer, sp.offset + 0x100, 0x100, 0xA5C3A5C3u); VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_CLEAR_BIT, .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, .dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT}; VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb}; vkCmdPipelineBarrier2(cb, &di); vk_upload_end(); VkBuffer rb; VmaAllocation ra; uint32_t *r = vk_staging(0x100, &rb, &ra); copy_wait(ps.buffer, 0x100, rb, 0, 0x100); vmaInvalidateAllocation(c->vma, ra, 0, VK_WHOLE_SIZE); int same = 1; for (int i = 0; i < 0x40; i++) same &= r[i] == 0xA5C3A5C3u; vmaDestroyBuffer(c->vma, rb, ra); GCHECK(same, "a GPU fill through virtual page 5 is not in physical page 0"); nvn_mapping_t un = {NULL, 0, 0, 0x10000, 0, 0}; GCHECK(vk_pool_map_virtual(virt, 1, &un), "MapVirtual unmap"); GCHECK(!vk_mem_resolve(v + 8, 4, &sp), "unmapped page 0 still resolves"); GCHECK(vk_mem_resolve(v + 0x10000, 4, &sp) && sp.offset == 0x30000, "page 1 lost by unmapping page 0"); printf("probe-vkmem: virtual pool: 3 pages mapped, 1 unmapped, resolution and a GPU write through the alias correct\n"); g_gpu_checks += 12; return 0; } static tex_t make_tex(pool_t *pool, uint64_t off, int format, uint32_t w, uint32_t h, uint32_t levels, uint64_t serial) { tex_t t; memset(&t, 0, sizeof t); t.magic = MAGIC('T', 'E', 'X', 0); t.d.target = 1, t.d.format = format, t.d.width = w, t.d.height = h, t.d.depth = 1, t.d.levels = levels; nvn_tex_layout(&t.d, &t.L); // pool_flags as nvnTextureInitialize records them: tex_mirrored (vk_texture.c) reads them, not the pool object t.pool = pool, t.pool_flags = (uint16_t)pool->flags, t.off = (int64_t)off, t.mem = pool->mem + off, t.serial = serial; return t; } // Fills every level of t's storage with random texels through the reference swizzler; lin[lv] keeps them tight. static int fill_storage(const tex_t *t, uint8_t **lin) { for (unsigned lv = 0; lv < t->d.levels; lv++) { uint32_t rb, rows, sl, bpb; uint64_t base; unsigned th, td; GCHECK(!nvn_deswizzle_geom(&t->d, lv, 0, &rb, &rows, &sl, &base, &th, &td, &bpb), "geometry of level %u", lv); size_t n = (size_t)rb * rows; if (!lin[lv]) lin[lv] = malloc(n); for (size_t i = 0; i < n; i++) lin[lv][i] = rnd8(); ref_swizzle(lin[lv], t->mem + base, rb, rows, 1, th, td); } return 0; } static int read_back(const tex_t *t, unsigned lv, uint8_t *const *lin, int gpu, const char *what) { uint32_t rb, rows, sl, bpb; uint64_t base; unsigned th, td; nvn_deswizzle_geom(&t->d, lv, 0, &rb, &rows, &sl, &base, &th, &td, &bpb); view_t v; memset(&v, 0, sizeof v); v.set_levels = 1, v.base_level = (uint8_t)lv, v.nlevels = 1; NVNcopyRegion r = {0, 0, 0, (int)(t->d.width >> lv ? t->d.width >> lv : 1), (int)(t->d.height >> lv ? t->d.height >> lv : 1), 1}; size_t n = (size_t)rb * rows; uint8_t *got = calloc(1, n); int ok = gpu ? vk_texture_read_image(t, &v, &r, got, rb, n) : (vk_read_texels(t, &v, &r, got, rb, n), 1); int same = ok && !memcmp(got, lin[lv], n); free(got); GCHECK(same, "%s level %u (%ux%u, format %#x) read back %s the CPU reference", what, lv, r.width, r.height, t->d.format, ok ? "differs from" : "failed against"); g_gpu_checks++; return 0; } // Level 0 of image (w x h RGBA8) read through a staging buffer after everything recorded so far has run: whether every // texel is `want`, the way a draw sampling the image would see it. static int image_is_color(VkImage image, uint32_t w, uint32_t h, const uint8_t want[4]) { VkCtx *c = vk_ctx(); VkBuffer rb; VmaAllocation ra; size_t n = (size_t)w * h * 4; uint8_t *r = vk_staging(n, &rb, &ra); VkCommandBuffer cb = vk_upload_begin(); VkMemoryBarrier2 mb = {.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}; VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb}; vkCmdPipelineBarrier2(cb, &di); VkBufferImageCopy rg = {.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .imageExtent = {w, h, 1}}; vkCmdCopyImageToBuffer(cb, image, VK_IMAGE_LAYOUT_GENERAL, rb, 1, &rg); mb = (VkMemoryBarrier2){.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}; vkCmdPipelineBarrier2(cb, &di); vk_upload_end(); vk_upload_flush(1); vmaInvalidateAllocation(c->vma, ra, 0, VK_WHOLE_SIZE); int same = 1; for (size_t i = 0; i < n; i += 4) same &= !memcmp(r + i, want, 4); vmaDestroyBuffer(c->vma, rb, ra); return same; } // ---- texture detail (vk_texture.c "texture detail"): an ASTC texture made without its top mip levels // Every level and layer of t's storage becomes ASTC void-extent blocks of one colour, each channel 0 or 255 so that // the RGBA8 decode and its BC3 encoding both hold it exactly. The colour says which level and layer it is. static void detail_color(unsigned lv, unsigned layer, unsigned round, uint8_t c[4]) { unsigned k = 1 + (lv * 3 + layer + round * 2) % 7; // never black: 1 to 7 c[0] = k & 1 ? 255 : 0, c[1] = k & 2 ? 255 : 0, c[2] = k & 4 ? 255 : 0, c[3] = 255; } static int detail_fill(const tex_t *t, unsigned layers, unsigned round) { for (unsigned lv = 0; lv < t->d.levels; lv++) for (unsigned ly = 0; ly < layers; ly++) { uint32_t rb, rows, sl, bpb; uint64_t base; unsigned th, td; GCHECK(!nvn_deswizzle_geom(&t->d, lv, ly, &rb, &rows, &sl, &base, &th, &td, &bpb), "ASTC geometry of level %u", lv); size_t n = (size_t)rb * rows; uint8_t *tight = malloc(n), c[4]; detail_color(lv, ly, round, c); for (size_t i = 0; i < n; i += 16) void_block(tight + i, (uint16_t)(c[0] * 257), (uint16_t)(c[1] * 257), (uint16_t)(c[2] * 257), 0xFFFF, 0); ref_swizzle(tight, t->mem + base, rb, rows, 1, th, td); free(tight); } return 0; } // The image formats of an ASTC texture whose levels detail_level_is can read: the RGBA8 decode, its BC3 encoding, and // the BC1 one an opaque texture gets (vk_texture.c astc_reformat; these cases' blocks are opaque). An image in the ASTC // format itself, where the device samples it, is not read here. static int detail_readable(VkFormat f) { return f == VK_FORMAT_R8G8B8A8_UNORM || f == VK_FORMAT_BC3_UNORM_BLOCK || f == VK_FORMAT_BC1_RGB_UNORM_BLOCK; } // Whether level lv, layer ly of image (RGBA8, BC3 or BC1, w x h at that level) is `want` in every texel. static int detail_level_is(const VkTexImage *img, uint32_t lv, uint32_t ly, uint32_t w, uint32_t h, const uint8_t want[4]) { VkCtx *c = vk_ctx(); int bc = img->format == VK_FORMAT_BC3_UNORM_BLOCK || img->format == VK_FORMAT_BC3_SRGB_BLOCK; int bc1 = img->format == VK_FORMAT_BC1_RGB_UNORM_BLOCK || img->format == VK_FORMAT_BC1_RGB_SRGB_BLOCK; size_t n = bc || bc1 ? (size_t)((w + 3) / 4) * ((h + 3) / 4) * (bc ? 16 : 8) : (size_t)w * h * 4; VkBuffer rb; VmaAllocation ra; uint8_t *r = vk_staging(n, &rb, &ra); VkCommandBuffer cb = vk_upload_begin(); VkMemoryBarrier2 mb = {.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}; VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb}; vkCmdPipelineBarrier2(cb, &di); VkBufferImageCopy rg = {.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, lv, ly, 1}, .imageExtent = {w, h, 1}}; vkCmdCopyImageToBuffer(cb, img->image, VK_IMAGE_LAYOUT_GENERAL, rb, 1, &rg); mb = (VkMemoryBarrier2){.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}; vkCmdPipelineBarrier2(cb, &di); vk_upload_end(); vk_upload_flush(1); vmaInvalidateAllocation(c->vma, ra, 0, VK_WHOLE_SIZE); int same = 1; if (bc) { // BC3: two 5:6:5 endpoints at bytes 8 to 11; both are the colour, so every texel is uint16_t e = (uint16_t)((want[0] >> 3) << 11 | (want[1] >> 2) << 5 | want[2] >> 3); for (size_t i = 0; i < n; i += 16) { uint16_t e0, e1; memcpy(&e0, r + i + 8, 2), memcpy(&e1, r + i + 10, 2); same &= e0 == e && e1 == e; } } else if (bc1) { // BC1: the endpoints at bytes 0 to 3, then 2 bits a texel. With equal endpoints the selectors 0 // to 2 all give the colour; 3 is black in this format, so none may be 3. uint16_t e = (uint16_t)((want[0] >> 3) << 11 | (want[1] >> 2) << 5 | want[2] >> 3); for (size_t i = 0; i < n; i += 8) { uint16_t e0, e1; uint32_t sel; memcpy(&e0, r + i, 2), memcpy(&e1, r + i + 2, 2), memcpy(&sel, r + i + 4, 4); same &= e0 == e && e1 == e && !(sel & sel >> 1 & 0x55555555u); } } else { for (size_t i = 0; i < n; i += 4) same &= !memcmp(r + i, want, 4); } vmaDestroyBuffer(c->vma, rb, ra); return same; } // The image of an ASTC texture has the game's levels from the skip the setting gives it: its size is that level's, and // each of its levels and layers holds that level's blocks, after the upload and after a rewrite of the storage. static int detail_case(pool_t *pool, uint64_t off, uint64_t room, uint32_t size, uint32_t levels, unsigned layers, uint64_t serial, uint32_t want_skip) { tex_t t = make_tex(pool, off, 0x80, size, size, levels, serial); // ASTC 8x8 if (layers > 1) { t.d.target = NVN_TEXTURE_TARGET_2D_ARRAY, t.d.depth = layers; nvn_tex_layout(&t.d, &t.L); } GCHECK(t.L.size <= room && off + room <= pool->size, "ASTC %ux%u storage of %llu bytes does not fit its %llu in the " "probe's pool", size, size, (unsigned long long)t.L.size, (unsigned long long)room); for (unsigned round = 0; round < 2; round++) { if (detail_fill(&t, layers, round)) return 1; const VkTexImage *img = vk_texture_get(&t); GCHECK(img && img->mirrored, "ASTC %ux%u texture image", size, size); if (!detail_readable(img->format)) return 0; // native ASTC GCHECK(img->skip == want_skip && img->levels == levels - want_skip && img->width == size >> want_skip && img->height == size >> want_skip && img->layers == layers, "ASTC %ux%u, %u levels, %u layers: image %ux%u with %u levels, %u layers, made without %u (want %u)", size, size, levels, layers, img->width, img->height, img->levels, img->layers, img->skip, want_skip); for (uint32_t lv = 0; lv < img->levels; lv++) for (uint32_t ly = 0; ly < layers; ly++) { uint8_t want[4]; uint32_t w = size >> (lv + img->skip) ? size >> (lv + img->skip) : 1; detail_color(lv + img->skip, ly, round, want); GCHECK(detail_level_is(img, lv, ly, w, w, want), "ASTC %ux%u made without %u levels: image level %u layer %u is not the game's level %u (%s)", size, size, img->skip, lv, ly, lv + img->skip, round ? "after a rewrite of the storage" : "first upload"); g_gpu_checks++; } } vk_texture_release(&t); return 0; } // An ASTC upload whose GPU decode gets no scratch memory (vk_astc.c scratch_get, as when the video memory is used up; // forced here through vk_astc_scratch_fail) does not count as done: the texture stays "not uploaded", is left alone for // the frames of the wait, and the first lookup after them uploads it, after which the image holds the storage's blocks. // Skipped (0) where ASTC is not decoded on the GPU: there is no scratch buffer then. static int detail_retry(pool_t *pool, uint64_t off, uint64_t room) { if (!vk_astc_gpu()) return 0; tex_t t = make_tex(pool, off, 0x80, 128, 128, 1, 24); // ASTC 8x8 GCHECK(t.L.size <= room && off + room <= pool->size, "the retry case's storage does not fit the probe's pool"); if (detail_fill(&t, 1, 0)) return 1; uint8_t want[4]; detail_color(0, 0, 0, want); uint64_t r0 = vk_texture_upload_retries(); uint32_t ups = 0; int ch, pw; vk_astc_scratch_fail(1); const VkTexImage *img = vk_texture_get(&t); vk_astc_scratch_fail(0); GCHECK(img && img->mirrored, "the retry case's texture image"); if (!detail_readable(img->format)) return 0; // native ASTC vk_texture_debug(img, &ups, &ch, &pw); GCHECK(vk_texture_upload_retries() == r0 + 1 && ups == 1, "an ASTC upload whose decode had no memory was not left for another try (%llu tries noted, %u uploads)", (unsigned long long)(vk_texture_upload_retries() - r0), ups); img = vk_texture_get(&t); // inside the wait: left alone vk_texture_debug(img, &ups, &ch, &pw); GCHECK(ups == 1, "the upload was tried again inside its wait (%u uploads)", ups); __atomic_fetch_add(&gfx_counters.frames, 30, __ATOMIC_RELAXED); // vk_texture.c UP_RETRY_FRAMES img = vk_texture_get(&t); vk_texture_debug(img, &ups, &ch, &pw); GCHECK(ups == 2 && vk_texture_upload_retries() == r0 + 1, "the upload was not tried again after its wait (%u uploads)", ups); GCHECK(detail_level_is(img, 0, 0, 128, 128, want), "after the second try the image does not hold the storage's blocks"); img = vk_texture_get(&t); // uploaded now: nothing more vk_texture_debug(img, &ups, &ch, &pw); GCHECK(ups == 2, "an uploaded texture was uploaded again (%u uploads)", ups); g_gpu_checks += 4; vk_texture_release(&t); printf("probe-vkmem: an ASTC upload whose decode had no scratch memory is tried again after its wait and then holds " "its blocks\n"); return 0; } static int test_detail(pool_t *pool) { const char *v = cfg_value("NX2NAVI_TEXTURE_DETAIL"); int most = !strcmp(v, "full") ? 0 : !strcmp(v, "reduced") ? 1 : !strcmp(v, "low") ? 2 : vk_texture_detail_auto(); // 1024x1024 with 6 levels (two levels less at low), a 256x256 array of 3 layers (one less below full), and the // ones the rule leaves whole: 128x128, and 512x512 without smaller levels if (detail_case(pool, 0, 0x80000, 1024, 6, 1, 20, most > 1 ? 2 : (uint32_t)most) || detail_case(pool, 0x80000, 0x40000, 256, 4, 3, 21, most ? 1 : 0) || detail_case(pool, 0xc0000, 0x10000, 128, 4, 1, 22, 0) || detail_case(pool, 0xd0000, 0x20000, 512, 1, 1, 23, 0) || detail_retry(pool, 0xf0000, 0x10000)) return 1; // the sampler of a texture made without levels: an open LOD clamp is the slot's sampler, a closed one moves down samp_par sp; memset(&sp, 0, sizeof sp); sp.minf = NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR, sp.magf = NVN_MAG_FILTER_LINEAR, sp.ws = sp.wt = sp.wr = NVN_WRAP_MODE_REPEAT; sp.lod_max = 1000; vk_sampler_register(2, &sp); sp.lod_min = 1, sp.lod_max = 4; vk_sampler_register(3, &sp); sp.lod_min = 0, sp.lod_max = 3; vk_texture_lock(); VkSampler open1 = vk_sampler_detail_by_id_locked(2, 1), shut1 = vk_sampler_detail_by_id_locked(3, 1); vk_texture_unlock(); GCHECK(open1 && open1 == vk_sampler_by_id(2), "the sampler of a reduced texture with an open LOD clamp is not the slot's"); GCHECK(shut1 && shut1 != vk_sampler_by_id(3) && shut1 == vk_sampler_for(&sp), "the sampler of a reduced texture with LOD 1..4 does not clamp to 0..3"); g_gpu_checks += 2; printf("probe-vkmem: texture detail %s: ASTC images hold the game's levels from the one they start at, after upload " "and re-upload (1024x1024, a 256x256 array, and 128x128 and unmipped left whole); sampler LOD clamps follow\n", most == 0 ? "full" : most == 1 ? "reduced" : "low"); return 0; } static int test_textures(pool_t *tpool, pool_t *phys) { uint8_t *lin[4] = {0}; // block-linear RGBA8 with 3 levels in a CPU-visible pool: uploaded through the GPU, read back from the image tex_t t = make_tex(tpool, 0, 0x25, 200, 120, 3, 1); if (fill_storage(&t, lin)) return 1; const VkTexImage *img = vk_texture_get(&t); GCHECK(img && img->mirrored && img->levels == 3 && img->format == VK_FORMAT_R8G8B8A8_UNORM, "RGBA8 texture image"); for (unsigned lv = 0; lv < 3; lv++) if (read_back(&t, lv, lin, 1, "RGBA8")) return 1; // CPU writes to the storage: write watch -> re-upload on the next use, and every mip the GPU sees has the new bytes uint64_t up0, re0, re1; vk_texture_stats(NULL, &up0, &re0); if (fill_storage(&t, lin)) return 1; for (unsigned lv = 0; lv < 3; lv++) if (read_back(&t, lv, lin, 1, "RGBA8 rewritten")) return 1; vk_texture_stats(NULL, NULL, &re1); #ifdef _WIN32 // elsewhere an import's pinning can mark pages written again without a CPU write: the count is not exact GCHECK(re1 == re0 + 1, "storage rewritten: %llu re-uploads (want 1)", (unsigned long long)(re1 - re0)); // The same bytes written again, so that every page of the storage shows a write. The texture is over 64 KiB: with // NX2NAVI_TEX_SAME_BIG (the default) it is compared by content and left as it is, with 0 it is uploaded again. Other // bytes upload either way, and the image holds them. After two comparisons in a row that found other bytes its // uploads take no hash any more, so the same bytes written again then upload with the switch too. { int big = rt_knob("TEX_SAME_BIG", "1") == 1; volatile uint8_t *sm = (volatile uint8_t *)t.mem; uint64_t re2, re3, re4, re5; GCHECK(t.L.size > (64u << 10), "the RGBA8 texture's storage is %llu bytes, not over 64 KiB", (unsigned long long)t.L.size); for (uint64_t o = 0; o < t.L.size; o += 0x1000) sm[o] = sm[o]; if (read_back(&t, 0, lin, 1, "RGBA8 written again with its own bytes")) return 1; vk_texture_stats(NULL, NULL, &re2); GCHECK(re2 == re1 + (big ? 0 : 1), "the same bytes written again: %llu re-uploads (want %d)", (unsigned long long)(re2 - re1), big ? 0 : 1); if (fill_storage(&t, lin)) return 1; for (unsigned lv = 0; lv < 3; lv++) if (read_back(&t, lv, lin, 1, "RGBA8 rewritten after the comparison")) return 1; vk_texture_stats(NULL, NULL, &re3); GCHECK(re3 == re2 + 1, "other bytes after the comparison: %llu re-uploads (want 1)", (unsigned long long)(re3 - re2)); if (fill_storage(&t, lin)) return 1; if (read_back(&t, 0, lin, 1, "RGBA8 rewritten a second time in a row")) return 1; vk_texture_stats(NULL, NULL, &re4); GCHECK(re4 == re3 + 1, "other bytes a second time: %llu re-uploads (want 1)", (unsigned long long)(re4 - re3)); for (uint64_t o = 0; o < t.L.size; o += 0x1000) sm[o] = sm[o]; if (read_back(&t, 2, lin, 1, "RGBA8 written again with its own bytes after two rewrites")) return 1; vk_texture_stats(NULL, NULL, &re5); GCHECK(re5 == re4 + 1, "the same bytes after two rewrites in a row: %llu re-uploads (want 1: no hash is taken any " "more)", (unsigned long long)(re5 - re4)); printf("probe-vkmem: a texture over 64 KiB written again with its own bytes: %s (NX2NAVI_TEX_SAME_BIG=%d); other " "bytes upload, and after two rewrites in a row the same bytes do too\n", big ? "left as it is" : "uploaded again", big); g_gpu_checks += 5; } #endif if (read_back(&t, 1, lin, 0, "RGBA8 ReadTexels from storage")) return 1; // BC1, 64x48: blocks uploaded as they are, read back from the image uint8_t *bl[4] = {0}; tex_t b = make_tex(tpool, 0x40000, 0x42, 64, 48, 1, 2); if (fill_storage(&b, bl)) return 1; img = vk_texture_get(&b); GCHECK(img && img->format == VK_FORMAT_BC1_RGBA_UNORM_BLOCK, "BC1 texture image"); if (read_back(&b, 0, bl, 1, "BC1")) return 1; // RGBA8 in a physical pool: aliases the pool memory; WriteTexels then ReadTexels through the image tex_t g = make_tex(phys, 0, 0x25, 64, 64, 1, 3); img = vk_texture_get(&g); GCHECK(img && !img->mirrored && img->bound, "texture in a physical pool (mirrored %d, bound %d)", img ? img->mirrored : -1, img ? img->bound : -1); uint8_t *gl[1] = {malloc(64 * 64 * 4)}; for (int i = 0; i < 64 * 64 * 4; i++) gl[0][i] = rnd8(); NVNcopyRegion all = {0, 0, 0, 64, 64, 1}; vk_write_texels(&g, NULL, &all, gl[0], 256, 256 * 64); if (read_back(&g, 0, gl, 0, "physical-pool RGBA8 WriteTexels")) return 1; // A finalized CPU-backed slot must not revive its stale bytes or follow a new, unrendered CPU texture. vk_texture_register(5, &b, NULL); vk_texture_release(&b); tex_t b2 = make_tex(tpool, 0x40000, 0x25, 16, 12, 1, 4); const VkTexImage *live = vk_texture_get(&b2); GCHECK(live && !vk_texture_by_id(5, NULL, NULL), "stale CPU slot 5 revived or followed unrendered storage"); // Registration is the lifetime boundary that makes the descriptor current again, even after its image was evicted. vk_texture_register(5, &b, NULL); const VkTexImage *renewed = vk_texture_by_id(5, NULL, NULL); GCHECK(renewed && renewed->format == VK_FORMAT_BC1_RGBA_UNORM_BLOCK && renewed->width == 64 && renewed->height == 48, "re-registered CPU slot 5 did not resolve its own descriptor"); // A release finalizes only the slots of its own texture object: another texture initialized at the same storage // with the same parameters before the finalize keeps its slot (vk_texture_release, terrain heightmaps). tex_t bn = b; bn.serial = 6; vk_texture_register(6, &bn, NULL); vk_texture_release(&b); const VkTexImage *kept = vk_texture_by_id(6, NULL, NULL); GCHECK(kept && kept->width == 64 && kept->height == 48, "a release finalized slot 6 of another live texture"); // Its own slot 5 is finalized. The texture's image still holds the texture (slot 6's texture keeps it current), so // with the kept-stale rule the slot shows that image (test_stale_kept, vk_texture.c stale_kept); without the rule // it resolves to nothing. const VkTexImage *own5 = vk_texture_by_id(5, NULL, NULL); if (rt_knob("TEX_STALE_KEPT", "1") == 1) GCHECK(own5 && own5->image == kept->image, "finalized slot 5 does not show its texture's image, which is still current"); else GCHECK(!own5, "a release left its own slot 5 current"); vk_texture_release(&bn); // Once the live texture at its storage holds GPU content the storage bytes lack, the finalized CPU slot follows it. // The live texture is cleared on the GPU and noted as rendered into, as a draw into it would be; the slot's image // must then show the clear, which neither the storage bytes nor a recreated image of the slot's own descriptor hold. GCHECK(live && live->image && live->width == 16 && live->height == 12, "RGBA8 image of the texture at the slot's storage"); vk_texture_gpu_written(&b2); // the clear below is its content, not the storage const float clear[4] = {0.0f, 1.0f, 1.0f, 1.0f}; const uint8_t cleared[4] = {0x00, 0xFF, 0xFF, 0xFF}; VkClearColorValue cv; memcpy(cv.float32, clear, sizeof cv.float32); VkImageSubresourceRange rng = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; VkCommandBuffer ccb = vk_upload_begin(); vkCmdClearColorImage(ccb, live->image, VK_IMAGE_LAYOUT_GENERAL, &cv, 1, &rng); vk_upload_end(); const VkTexImage *followed = vk_texture_by_id(5, NULL, NULL); GCHECK(followed && followed->image == live->image, "finalized CPU slot 5 does not follow the rendered texture at its storage"); GCHECK(image_is_color(followed->image, 16, 12, cleared), "the image of slot 5 does not hold the clear written into the live texture"); // GPU-owned slots do follow the live image at their storage, even when its descriptor has changed, but only one of // their own size (vk_texture.c follow_size_ok): the game's render targets share storage, and the live one at an // address is often a target of another size that a frame laid out there. The 64x64 slot gets the image of a 64x64 // texture of another format; once that one is released and a 32x32 one is live there, the slot gets an image of // its own descriptor. (In the other order the slot's own entry is live again after the second step, and a slot // whose own entry is live keeps it.) vk_texture_register(5, &g, NULL); vk_texture_release(&g); tex_t g2 = make_tex(phys, 0, NVN_FORMAT_R32UI, 64, 64, 1, 5); live = vk_texture_get(&g2); VkImage live_img = live ? live->image : VK_NULL_HANDLE; const VkTexImage *slot = vk_texture_by_id(5, NULL, NULL); GCHECK(live && slot && slot->image == live_img, "slot 5 does not follow the live texture of its size at its storage"); vk_texture_release(&g2); tex_t g3 = make_tex(phys, 0, NVN_FORMAT_R32UI, 32, 32, 1, 8); live = vk_texture_get(&g3); slot = vk_texture_by_id(5, NULL, NULL); GCHECK(live && slot && slot->image != live->image && slot->width == 64 && slot->height == 64 && slot->format == VK_FORMAT_R8G8B8A8_UNORM, "64x64 slot 5 follows the 32x32 texture at its storage, or has no " "image of its own descriptor"); samp_par sp; memset(&sp, 0, sizeof sp); sp.minf = NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR, sp.magf = NVN_MAG_FILTER_LINEAR, sp.ws = sp.wt = sp.wr = NVN_WRAP_MODE_REPEAT; sp.lod_max = 1000, sp.aniso = 16; vk_sampler_register(0, &sp); VkSampler s0 = vk_sampler_by_id(0), s1 = vk_sampler_for(&sp); GCHECK(s0 && s0 == s1 && !vk_sampler_by_id(1), "sampler slots"); g_gpu_checks += 14; vk_texture_release(&t), vk_texture_release(&b2), vk_texture_release(&g2), vk_texture_release(&g3); for (int i = 0; i < 4; i++) free(lin[i]), free(bl[i]); free(gl[0]); uint64_t images, ups, reups; vk_texture_stats(&images, &ups, &reups); printf("probe-vkmem: textures: RGBA8 3 levels and BC1 equal to the CPU reference after upload, re-upload after CPU " "writes, physical-pool alias WriteTexels/ReadTexels, stale CPU slot, CPU slot following rendered content, GPU slot " "redirect, samplers (%llu images, %llu uploads, " "%llu re-uploads)\n", (unsigned long long)images, (unsigned long long)ups, (unsigned long long)reups); return 0; } // Level 0 of image (w x h RGBA8) read into out through a staging buffer after everything recorded so far has run. static void image_bytes(VkImage image, uint32_t w, uint32_t h, uint8_t *out) { VkCtx *c = vk_ctx(); VkBuffer rb; VmaAllocation ra; size_t n = (size_t)w * h * 4; uint8_t *r = vk_staging(n, &rb, &ra); VkCommandBuffer cb = vk_upload_begin(); VkMemoryBarrier2 mb = {.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}; VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb}; vkCmdPipelineBarrier2(cb, &di); VkBufferImageCopy rg = {.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .imageExtent = {w, h, 1}}; vkCmdCopyImageToBuffer(cb, image, VK_IMAGE_LAYOUT_GENERAL, rb, 1, &rg); mb = (VkMemoryBarrier2){.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}; vkCmdPipelineBarrier2(cb, &di); vk_upload_end(); vk_upload_flush(1); vmaInvalidateAllocation(c->vma, ra, 0, VK_WHOLE_SIZE); memcpy(out, r, n); vmaDestroyBuffer(c->vma, rb, ra); } // The kept-stale rule (vk_texture.c stale_kept, slot_resolve; NX2NAVI_TEX_STALE_KEPT, default on). The slot of a // texture the CPU writes, finalized with its texture, goes on showing that texture's own image while the image still // holds the texture: the game finalizes ahead of the replay of the frames it recorded before. The image is sampled as // it is, so bytes the storage gets after the finalize do not reach it and nothing is uploaded. With the switch off the // slot resolves to nothing, which a draw shows as the stand-in. Either way a registration makes the descriptor current // again, with the storage's bytes as they are then. That another descriptor made at the storage ends the rule is the // "stale CPU slot" case of test_textures. static int test_stale_kept(pool_t *tpool) { int on = rt_knob("TEX_STALE_KEPT", "1") == 1; uint8_t *ref[1] = {0}, *ref2[1] = {0}, got[16 * 16 * 4]; tex_t k = make_tex(tpool, 0xc0000, 0x25, 16, 16, 1, 60); if (fill_storage(&k, ref)) return 1; vk_texture_register(9, &k, NULL); const VkTexImage *own = vk_texture_by_id(9, NULL, NULL); GCHECK(own && own->image && own->width == 16 && own->height == 16, "slot 9 does not resolve its registered texture"); VkImage own_img = own->image; image_bytes(own_img, 16, 16, got); GCHECK(!memcmp(got, ref[0], sizeof got), "the image of slot 9 does not hold the storage's bytes"); uint64_t up0, re0, up1, re1; vk_texture_stats(NULL, &up0, &re0); vk_texture_release(&k); if (fill_storage(&k, ref2)) return 1; // what the game puts into the storage next const VkTexImage *after = vk_texture_by_id(9, NULL, NULL); vk_texture_stats(NULL, &up1, &re1); if (on) { GCHECK(after && after->image == own_img, "a finalized CPU slot does not show its texture's own image"); image_bytes(own_img, 16, 16, got); GCHECK(!memcmp(got, ref[0], sizeof got) && up1 == up0 && re1 == re0, "the kept image of a finalized slot took the " "storage's later bytes (%llu uploads and %llu re-uploads since the finalize)", (unsigned long long)(up1 - up0), (unsigned long long)(re1 - re0)); } else { GCHECK(!after && up1 == up0 && re1 == re0, "a finalized CPU slot resolves with NX2NAVI_TEX_STALE_KEPT=0"); } // In a later submission: an entry's storage is checked for writes once per submission (vk_texture_by_id_locked), // and this one's was at the first lookup above. vk_texture_new_submit(); vk_texture_register(9, &k, NULL); const VkTexImage *again = vk_texture_by_id(9, NULL, NULL); GCHECK(again && again->image && again->width == 16 && again->height == 16, "slot 9 registered again does not resolve"); image_bytes(again->image, 16, 16, got); GCHECK(!memcmp(got, ref2[0], sizeof got), "slot 9 registered again does not hold the bytes the storage has now"); vk_texture_release(&k); free(ref[0]), free(ref2[0]); printf("probe-vkmem: a finalized slot of a CPU-written texture: %s (NX2NAVI_TEX_STALE_KEPT=%d); registered again it " "holds the storage's bytes of then\n", on ? "its own image as it was, whatever the storage gets afterwards" : "nothing", on); g_gpu_checks += on ? 6 : 5; return 0; } // ---- two descriptors on one CPU-written storage (vk_texture.c "split fill", start_at) // D1 is a texture the CPU writes. D2 names the same storage with a texture flag for which the cache does not take the // content from the CPU (COMPRESSIBLE here), so a slot of D2 shows D1's image for as long as D1 has a live entry. // What the slot shows once D1's entry is gone, in the two orders a game can let go of them: // - keep: D2's slot stays registered while D1 is released and registered again (a texture made again in place); // - both: D1 and D2 are released, the slot of D2 is looked up all the same (the texture pool refresh looks at every // registered slot), then the storage gets new bytes and both are registered again (the next texture in that memory). // recorded: the lookup that makes D2's entry runs while a submission is recorded, as a draw of a replay makes it, so a // copy from D1's image can be recorded there; otherwise outside any recording, where only the storage can fill it. // *what: 0 the slot's image is all zero, 1 it holds the storage's bytes as they are at the end, 2 the bytes the storage // held when D2's entry was made (the same as 1 in "keep", where the storage is not rewritten), 3 none of these. enum { SPLIT_W = 32, SPLIT_H = 32, SPLIT_N = SPLIT_W * SPLIT_H * 4 }; static int split_what(const uint8_t *got, const uint8_t *first, const uint8_t *last) { static const uint8_t zero[SPLIT_N]; return !memcmp(got, zero, SPLIT_N) ? 0 : !memcmp(got, last, SPLIT_N) ? 1 : !memcmp(got, first, SPLIT_N) ? 2 : 3; } typedef struct { int id; const VkTexImage *got; } SplitRec; static void split_record(VkCommandBuffer cb, void *arg) { // as gpa_read_record: the frame of a recorded submission SplitRec *r = arg; vk_texture_new_submit(); vk_mem_mirror_begin(); vk_exec_probe_inline(cb, vk_sync_issued() + 1); r->got = vk_texture_by_id(r->id, NULL, NULL); vk_exec_probe_inline(VK_NULL_HANDLE, 0); vk_mem_mirror_end(cb, vk_sync_issued() + 1); } static int split_case(pool_t *pool, uint64_t off, uint64_t serial, int both, int recorded, int *what) { uint8_t *lin[1] = {0}, first[SPLIT_N], got[SPLIT_N]; tex_t d1 = make_tex(pool, off, 0x25, SPLIT_W, SPLIT_H, 1, serial); tex_t d2 = d1; d2.d.flags |= NVN_TEXTURE_FLAGS_COMPRESSIBLE, d2.serial = serial + 1; GCHECK(off + d1.L.size <= pool->size, "the two-descriptor case's storage does not fit the probe's pool"); if (fill_storage(&d1, lin)) return 1; memcpy(first, lin[0], SPLIT_N); const VkTexImage *e1 = vk_texture_get(&d1); GCHECK(e1 && e1->mirrored, "the CPU-written texture of the two-descriptor case"); VkImage img1 = e1->image; vk_texture_register(6, &d2, NULL); const VkTexImage *s2 = vk_texture_by_id(6, NULL, NULL); GCHECK(s2 && s2->image == img1, "the slot of the second descriptor does not show the CPU-written texture's image"); vk_texture_release(&d1); if (both) vk_texture_release(&d2); if (recorded) { SplitRec r = {6, NULL}; uint64_t fence = vk_sync_submit_own(split_record, &r); GCHECK(vk_sync_fence_wait(fence, 5000000000ull), "the submission that looks the second descriptor's slot up timed out"); s2 = r.got; } else { vk_texture_new_submit(); s2 = vk_texture_by_id(6, NULL, NULL); // the texture pool refresh looks the slot up } GCHECK(s2 && s2->image != img1 && !s2->mirrored, "after the release of the CPU-written texture the slot of the second " "descriptor has no image of its own (%s)", s2 ? "it still shows the released one" : "none at all"); VkImage img2 = s2->image; if (both) { // the next texture in that memory if (fill_storage(&d1, lin)) return 1; vk_texture_register(6, &d2, NULL); } vk_texture_register(7, &d1, NULL); vk_texture_new_submit(); const VkTexImage *s1 = vk_texture_by_id(7, NULL, NULL); GCHECK(s1 && s1->mirrored && s1->image != img2, "the CPU-written texture registered again has no image of its own"); image_bytes(s1->image, SPLIT_W, SPLIT_H, got); GCHECK(!memcmp(got, lin[0], SPLIT_N), "the CPU-written texture registered again does not hold its storage's bytes"); s2 = vk_texture_by_id(6, NULL, NULL); GCHECK(s2 && s2->image == img2, "the slot of the second descriptor left its own image when the first came back"); image_bytes(s2->image, SPLIT_W, SPLIT_H, got); *what = split_what(got, first, lin[0]); // a shader sampled the slot: with NX2NAVI_TRACE=perf the log names an entry that never had content uint32_t marks[8] = {0}; marks[6] = 1; vk_pool_marks_read(marks, 8); g_gpu_checks += 6; vk_texture_release(&d1), vk_texture_release(&d2); free(lin[0]); return 0; } // The three cases with split fill off (tex_split_fill = 0), on (1) and filling once (2). Off, the slot shows an image // nothing filled, which is what the setting is for; on, the storage's bytes, copied from D1's image where a submission // was being recorded and uploaded from the storage elsewhere; filling once, the bytes of the texture that was in the // memory before when both descriptors were let go of. static int test_split(pool_t *pool) { static const char *const names[4] = {"zeros", "the storage's bytes", "the bytes the storage held before", "other bytes"}; int what[3][3]; uint64_t made[3][3][2]; for (int mode = 0; mode < 3; mode++) { vk_texture_split_fill_set(mode); for (int k = 0; k < 3; k++) { // keep, both, keep with the lookup recorded uint64_t c0, f0, c1, f1; vk_texture_split_counts(&c0, &f0); if (split_case(pool, 0x80000 + (uint64_t)(mode * 3 + k) * 0x4000, 30 + (uint64_t)(mode * 3 + k) * 2, k == 1, k == 2, &what[mode][k])) return 1; vk_texture_split_counts(&c1, &f1); made[mode][k][0] = c1 - c0, made[mode][k][1] = f1 - f0; } } vk_texture_split_fill_set(-1); // the setting decides again vk_sync_finish(); printf("probe-vkmem: a second descriptor on a CPU-written storage, not CPU-written itself, once the first one's entry " "is gone. Its slot shows, with tex_split_fill 0 / 1 / 2: when it stayed registered %s / %s / %s; when both were " "released and registered again over new bytes %s / %s / %s; when its entry was made in a recorded submission " "%s / %s / %s (copies from the first one's image %llu / %llu / %llu, fills from storage %llu / %llu / %llu)\n", names[what[0][0]], names[what[1][0]], names[what[2][0]], names[what[0][1]], names[what[1][1]], names[what[2][1]], names[what[0][2]], names[what[1][2]], names[what[2][2]], (unsigned long long)made[0][2][0], (unsigned long long)made[1][2][0], (unsigned long long)made[2][2][0], (unsigned long long)made[0][2][1], (unsigned long long)made[1][2][1], (unsigned long long)made[2][2][1]); for (int k = 0; k < 3; k++) { // off: an image nothing filled (zeros with NX2NAVI_FIX_ZERO, the default; else whatever the memory held) GCHECK(vk_fix_zero() ? what[0][k] == 0 : what[0][k] != 1, "with split fill off the slot shows %s in case %d: the " "sequence no longer leaves the second descriptor an empty image", names[what[0][k]], k); GCHECK(!made[0][k][0] && !made[0][k][1], "with split fill off an entry was filled in case %d", k); GCHECK(what[1][k] == 1, "with split fill on the slot shows %s in case %d, not the storage's bytes", names[what[1][k]], k); GCHECK(what[2][k] == (k == 1 ? 2 : 1), "filling once, the slot shows %s in case %d", names[what[2][k]], k); for (int mode = 1; mode < 3; mode++) GCHECK(made[mode][k][0] + made[mode][k][1] == 1, "split fill %d made %llu copies and %llu fills in case %d, not " "one of them", mode, (unsigned long long)made[mode][k][0], (unsigned long long)made[mode][k][1], k); } for (int mode = 1; mode < 3; mode++) { GCHECK(made[mode][0][1] == 1 && made[mode][1][1] == 1, "outside a recording split fill %d did not fill from storage", mode); #ifdef _WIN32 // elsewhere an import's pinning can mark pages written without a CPU write, and the storage is uploaded GCHECK(made[mode][2][0] == 1, "in a recorded submission split fill %d did not copy from the first one's image", mode); #endif } g_gpu_checks += 9; return 0; } static int test_gpu(void) { uint64_t heap = 0; GCHECK(mem_set_heap_size(16ull << 20, &heap) == 0 && heap, "SetHeapSize 16 MiB"); uint8_t *h = (uint8_t *)(uintptr_t)heap; pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x24, .mem = h + 0x100000, .size = 0x40000, .game_mem = h + 0x100000}; pool_t tpool = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = h + 0x400000, .size = 0x100000, .game_mem = h + 0x400000}; pool_t phys = {.magic = NVN_POOL_MAGIC, .flags = 0x189, .mem = (uint8_t *)(uintptr_t)(RT_GPU_VA_BASE + 0x100000000ull), .size = 0x40000, .game_mem = h + 0x800000, .span = 0x40000}; pool_t virt = {.magic = NVN_POOL_MAGIC, .flags = 0x2a1, .mem = (uint8_t *)(uintptr_t)(RT_GPU_VA_BASE + 0x200000000ull), .size = 0x80000, .span = 0x80000}; if (test_import(&pool) || test_readfile(pool.mem + 0x38000) || test_snap_runs(h) || test_virtual(&phys, &virt)) return 1; GCHECK(vk_pool_init(&tpool), "vk_pool_init of the texture pool"); if (test_textures(&tpool, &phys) || test_stale_kept(&tpool)) return 1; pool_t dpool = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = h + 0x500000, .size = 0x100000, .game_mem = h + 0x500000}; GCHECK(vk_pool_init(&dpool), "vk_pool_init of the texture detail pool"); int detail_bad = test_detail(&dpool); vk_pool_fini(&dpool); if (detail_bad) return 1; if (test_split(&tpool)) return 1; VkMemStats st; vk_mem_stats(&st); GCHECK(st.imported_pools == 2 && st.physical_pools == 1 && st.virtual_pools == 1 && !st.import_failures, "pool counts %llu/%llu/%llu, %llu import failures", (unsigned long long)st.imported_pools, (unsigned long long)st.physical_pools, (unsigned long long)st.virtual_pools, (unsigned long long)st.import_failures); vk_pool_fini(&virt), vk_pool_fini(&phys), vk_pool_fini(&tpool), vk_pool_fini(&pool); vk_mem_stats(&st); GCHECK(!st.imported_pools && !st.physical_pools && !st.virtual_pools, "pools left after fini"); g_gpu_checks += 2; return test_table_sync(h) || test_table_store(h) || test_remade(h) || test_table_mirror(h); } int probe_vkmem(void) { if (test_deswizzle() || test_box() || test_astc() || test_formats()) return 1; printf("probe-vkmem: cpu part passed (%u deswizzle cases, %u astc blocks, formats: %u mapped, %u supported by the GPU, " "%u unsupported by the GPU, %u unmapped, %u with a fallback)\n", g_cases, g_astc_blocks, n_mapped, n_supported, n_unsupported, n_unmapped, n_fallback); int bad = test_gpu(); vk_mem_shutdown(); vk_shutdown(); long errors = vk_validation_errors_total(); if (bad || errors) { printf("probe-vkmem: FAILED (%s, %ld validation errors)\n", bad ? "a GPU check failed" : "GPU checks passed", errors); return 1; } printf("probe-vkmem: passed (%u deswizzle cases, %u astc blocks, %u formats mapped, %u GPU checks, GPU writes seen by " "write watch %llu/32 pages, ReadFile seen %llu/2 buffered and %llu/2 unbuffered, 0 validation errors)\n", g_cases, g_astc_blocks, n_mapped, g_gpu_checks, (unsigned long long)g_gpu_seen, (unsigned long long)g_rf_seen[0], (unsigned long long)g_rf_seen[1]); return 0; }