mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
A texture whose pages show a write is uploaded again unless a hash of its bytes says they are the same. The hash covered textures up to 64 KiB; for larger ones it was a switch left off until measured in play (tex_same_big). A run through Hyrule Field measured it (RTX 5090, about three minutes of running): without it 44,000 to 52,000 second uploads of 33 to 47 GiB of storage in four runs of six, one 1536x1536 texture 29 times within 300 frames with the same bytes each time, and 66 thousand million texels decoded on the GPU in a run; with it 214 to 592 second uploads, 24 GiB found unchanged, and 20 to 24 thousand million texels decoded, which is the first uploads. The GPU's upload work fell from 0.41 to 0.14 ms a frame, and the second uploads' 2.9 s of the submitting thread are gone for about as much hashing. tex_same_big = 0 hashes only the small ones, as before. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
1461 lines
87 KiB
C
1461 lines
87 KiB
C
// 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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#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;
|
|
}
|