mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
Crash logs ended in fatal errors when the system's commit ran out for a moment: a staging buffer (-2), a guest commit with the system at 24442 of 24447 MB, image creation (-2). Windows grows a system-managed page file and some drivers charge device memory to the process commit, so the same request often succeeds a moment later. oom.c holds the wait nvn_api_cmd.c realloc_wait already used: it requests a release, retries every 10 ms for up to 30 s, logs the first 8 with the commit numbers. rt_oom_request sets a flag that vk_texture_new_submit takes (one atomic load per submit): it drops up to 256 released textures of the two kinds sweep_pressure drops (overwritten, clean mirrored), never GPU-written content, and frees the headroom blocks. vk_oom_release does at once what needs no submit: headroom blocks, finished upload batches and deferred objects. - memory.c commit: waits and retries without frz_lock between tries; every caller waits, boot ones too. - vk_staging and the draw-data chunks (vk_exec_draw.c scratch): release, then wait; the open upload batch is not submitted, its caller still records into it. - create_tex: release and one retry; no wait under the texture lock, the texture is asked for again at its next use. - the texture write-back buffer: release, then wait. - the headroom thread keeps no spare block for 30 s after a failure. TOTK_TEST_OOM=<site>:<n> (commit, staging, image, chunk) fails the first n attempts; probe-kernel forces 3 refused commits, probe-vkexec 2 staging buffers, 1 image and 1 chunk, and both pass through them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1050 lines
57 KiB
C
1050 lines
57 KiB
C
// totk.exe --probe-vkexec: the Vulkan replay, through gfx_vulkan_backend. Sync part: empty
|
|
// submissions get consecutive fence values; fence_wait and fence_done agree; completion callbacks run in submit order on
|
|
// the completion thread; a guest-memory word a callback writes is visible once fence_wait returns; finish drains every
|
|
// submission; a present of a cleared vk_texture.c image (layout GENERAL) reads back exactly through the swapchain
|
|
// capture. Clear part: a hand-built op stream (SET_RT to an RGBA8 and a D32 texture, whole-target clears,
|
|
// then scissored clears) through submit reads back exactly with read_texels. Pass marks part (test_marks): the GPU's
|
|
// buffer marks and the ring records name the last pass of a submission, and the device-lost report text says so.
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "cfg.h"
|
|
#include "gfx.h"
|
|
#include "host.h"
|
|
#include "nvn_api.h"
|
|
#include "nvn_tex.h"
|
|
#include "vk.h"
|
|
#include "vk_exec.h"
|
|
#include "platform/platform.h"
|
|
|
|
#define SUBMITS 100
|
|
#define TAIL 10
|
|
#define TEX_W 256
|
|
#define TEX_H 144
|
|
#define CHECK(cond, ...) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
printf("probe-vkexec: FAILED: "); \
|
|
printf(__VA_ARGS__); \
|
|
printf("\n"); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
static uint32_t main_tid;
|
|
static uint64_t ran[SUBMITS + TAIL + 8];
|
|
static long nran, on_main;
|
|
|
|
static void note(void *arg) { // completion thread
|
|
if (nran < (long)(sizeof ran / sizeof ran[0])) ran[nran] = (uint64_t)(uintptr_t)arg;
|
|
nran++;
|
|
if (plat_thread_id() == main_tid) on_main++;
|
|
}
|
|
|
|
typedef struct { volatile uint32_t *word; uint32_t value; } WordWrite;
|
|
static void write_word(void *arg) {
|
|
const WordWrite *w = arg;
|
|
*w->word = w->value;
|
|
}
|
|
|
|
static long ran_now(void) { return __atomic_load_n(&nran, __ATOMIC_ACQUIRE); }
|
|
|
|
static int test_sync(const GfxBackend *b, uint8_t *guest, uint64_t *first, uint64_t *last) {
|
|
// An empty recording as the front end hands it over: {OP_BEGIN, OP_END}.
|
|
static op_hdr empty[2] = {{OP_BEGIN, sizeof(op_hdr)}, {OP_END, sizeof(op_hdr)}};
|
|
uint64_t handles[2] = {(uint64_t)(uintptr_t)empty, (uint64_t)(uintptr_t)empty};
|
|
uint64_t prev = b->fence_done();
|
|
*first = prev + 1;
|
|
for (int i = 0; i < SUBMITS; i++) {
|
|
vk_completion_add(prev + 1, note, (void *)(uintptr_t)(prev + 1)); // queued before the value exists, as vk_exec.c does
|
|
uint64_t f = b->submit(i % 3 ? handles : NULL, i % 3);
|
|
CHECK(f == prev + 1, "submission %d got fence %llu after %llu", i, (unsigned long long)f, (unsigned long long)prev);
|
|
CHECK(b->fence_done() <= f, "fence_done %llu ahead of the newest submission %llu",
|
|
(unsigned long long)b->fence_done(), (unsigned long long)f);
|
|
CHECK(!b->fence_wait(f + 1, 0), "fence_wait of a value never submitted returned 1");
|
|
if (i % 10 == 9) {
|
|
CHECK(b->fence_wait(f, 5000000000ull), "fence_wait(%llu) timed out", (unsigned long long)f);
|
|
CHECK(b->fence_done() >= f, "fence_done %llu after fence_wait(%llu)", (unsigned long long)b->fence_done(),
|
|
(unsigned long long)f);
|
|
CHECK(ran_now() == i + 1, "%ld callbacks ran after fence_wait of submission %d", ran_now(), i);
|
|
}
|
|
prev = f;
|
|
}
|
|
// A guest-memory word written by a completion callback is visible to the waiter.
|
|
volatile uint32_t *word = (volatile uint32_t *)(guest + 0x1000);
|
|
*word = 0;
|
|
static WordWrite ww;
|
|
ww = (WordWrite){word, 0x5EC0DE01u};
|
|
vk_completion_add(prev + 1, write_word, &ww);
|
|
uint64_t f = b->submit(NULL, 0);
|
|
CHECK(f == prev + 1, "word submission got fence %llu", (unsigned long long)f);
|
|
CHECK(b->fence_wait(f, 5000000000ull), "fence_wait of the word submission timed out");
|
|
CHECK(*word == 0x5EC0DE01u, "guest word is %#x after fence_wait", *word);
|
|
prev = f;
|
|
// finish drains everything.
|
|
for (int i = 0; i < TAIL; i++) {
|
|
vk_completion_add(prev + 1, note, (void *)(uintptr_t)(prev + 1));
|
|
prev = b->submit(handles, 2);
|
|
}
|
|
b->finish();
|
|
CHECK(b->fence_done() == prev, "fence_done %llu after finish, newest %llu", (unsigned long long)b->fence_done(),
|
|
(unsigned long long)prev);
|
|
CHECK(ran_now() == SUBMITS + TAIL, "%ld of %d callbacks ran after finish", ran_now(), SUBMITS + TAIL);
|
|
for (int i = 0; i < SUBMITS + TAIL; i++) {
|
|
uint64_t want = *first + (uint64_t)i + (i >= SUBMITS); // the word submission sits between the two runs
|
|
CHECK(ran[i] == want, "callback %d ran for fence %llu, expected %llu", i, (unsigned long long)ran[i],
|
|
(unsigned long long)want);
|
|
}
|
|
CHECK(!on_main, "%ld callbacks ran on the submitting thread", on_main);
|
|
*last = prev;
|
|
return 0;
|
|
}
|
|
|
|
static uint8_t to_byte(float f) { return (uint8_t)(f * 255.0f + 0.5f); }
|
|
|
|
static int test_present(const GfxBackend *b, uint8_t *guest, int *w_out, int *h_out) {
|
|
pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0x100000, .size = 0x100000,
|
|
.game_mem = guest + 0x100000};
|
|
CHECK(b->pool_init(&pool), "pool_init of a CPU-visible pool");
|
|
tex_t t;
|
|
memset(&t, 0, sizeof t);
|
|
t.magic = NVN_TEX_MAGIC;
|
|
t.d.target = NVN_TEXTURE_TARGET_2D, t.d.format = NVN_FORMAT_RGBA8, t.d.width = TEX_W, t.d.height = TEX_H;
|
|
t.d.depth = 1, t.d.levels = 1;
|
|
nvn_tex_layout(&t.d, &t.L);
|
|
CHECK(t.L.size <= pool.size, "texture storage %#llx exceeds the pool", (unsigned long long)t.L.size);
|
|
t.pool = &pool, t.off = 0, t.mem = pool.mem, t.serial = 1;
|
|
const VkTexImage *img = vk_texture_get(&t);
|
|
CHECK(img && img->format == VK_FORMAT_R8G8B8A8_UNORM, "vk_texture_get of an RGBA8 texture");
|
|
vk_texture_gpu_written(&t); // the clear below is its content, not the storage
|
|
const float rgba[4] = {0.2f, 0.4f, 0.6f, 1.0f};
|
|
VkClearColorValue cv;
|
|
memcpy(cv.float32, rgba, sizeof cv.float32);
|
|
VkImageSubresourceRange rng = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
VkCommandBuffer cb = vk_upload_begin();
|
|
vkCmdClearColorImage(cb, img->image, VK_IMAGE_LAYOUT_GENERAL, &cv, 1, &rng);
|
|
vk_upload_end();
|
|
|
|
uint64_t frames = gfx_stats().frames;
|
|
// the exact colour check needs the bilinear blit: FSR's approximate reciprocals move a flat colour by 1/255
|
|
plat_setenv("TOTK_UPSCALER", "bilinear");
|
|
vk_present_capture(1);
|
|
const int crop[4] = {0, 0, TEX_W, TEX_H};
|
|
b->present(&t, crop, 0);
|
|
b->finish(); // the queued present is submitted and, with capture on, read back
|
|
const uint8_t *px;
|
|
int w, h;
|
|
VkFormat fmt;
|
|
CHECK(vk_present_captured(&px, &w, &h, &fmt), "the present was not captured");
|
|
CHECK(gfx_stats().frames == frames + 1, "frame counter %llu after one present from %llu",
|
|
(unsigned long long)gfx_stats().frames, (unsigned long long)frames);
|
|
uint8_t want[4] = {0, 0, 0, 255};
|
|
if (fmt == VK_FORMAT_B8G8R8A8_UNORM) want[0] = to_byte(rgba[2]), want[1] = to_byte(rgba[1]), want[2] = to_byte(rgba[0]);
|
|
else if (fmt == VK_FORMAT_R8G8B8A8_UNORM) want[0] = to_byte(rgba[0]), want[1] = to_byte(rgba[1]), want[2] = to_byte(rgba[2]);
|
|
else CHECK(0, "swapchain format %d is not UNORM RGBA/BGRA", (int)fmt);
|
|
uint32_t wv, black = 0xff000000u; // opaque black in both byte orders
|
|
memcpy(&wv, want, 4);
|
|
int r[4]; // the picture's rectangle; black bars around it when the window's shape differs (gfx_present.c)
|
|
gfx_fit_rect(TEX_W, TEX_H, w, h, r);
|
|
const uint32_t *p = (const uint32_t *)px;
|
|
for (long k = 0, n = (long)w * h; k < n; k++) {
|
|
int x = (int)(k % w), y = (int)(k / w);
|
|
uint32_t e = x >= r[0] && x < r[0] + r[2] && y >= r[1] && y < r[1] + r[3] ? wv : black;
|
|
CHECK(p[k] == e, "presented pixel %ld (%d, %d) is %08x, expected %08x", k, x, y, p[k], e);
|
|
}
|
|
vk_present_capture(0);
|
|
b->texture_release(&t);
|
|
b->pool_fini(&pool);
|
|
*w_out = w, *h_out = h;
|
|
return 0;
|
|
}
|
|
|
|
// ---- clear part: a hand-built op stream through submit, read back with read_texels.
|
|
|
|
static uint8_t *put(uint8_t *p, uint32_t op, const void *payload, uint32_t n) {
|
|
op_hdr h = {op, (uint32_t)((sizeof h + n + 7) & ~7u)};
|
|
memset(p, 0, h.size);
|
|
memcpy(p, &h, sizeof h);
|
|
if (n) memcpy(p + sizeof h, payload, n);
|
|
return p + h.size;
|
|
}
|
|
|
|
static void init_tex(tex_t *t, pool_t *pool, int64_t off, int format) {
|
|
memset(t, 0, sizeof *t);
|
|
t->magic = NVN_TEX_MAGIC;
|
|
t->d.target = NVN_TEXTURE_TARGET_2D, t->d.format = format, t->d.width = TEX_W, t->d.height = TEX_H;
|
|
t->d.depth = 1, t->d.levels = 1;
|
|
nvn_tex_layout(&t->d, &t->L);
|
|
t->pool = pool, t->pool_flags = (uint16_t)pool->flags, t->off = off, t->mem = pool->mem + off, t->serial = 100 + (uint64_t)off;
|
|
}
|
|
|
|
// Pixel of the RGBA8 target after the stream: the scissored second clear inside the rectangle, the first elsewhere.
|
|
#define SC_X 40
|
|
#define SC_Y 24
|
|
#define SC_W 64
|
|
#define SC_H 32
|
|
|
|
static int test_clear(const GfxBackend *b, uint8_t *guest, int *n_inside) {
|
|
pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0x400000, .size = 0x200000,
|
|
.game_mem = guest + 0x400000};
|
|
CHECK(b->pool_init(&pool), "pool_init of a CPU-visible pool for the clear part");
|
|
static tex_t color, depth; // payload texture pointers live as long as the submission
|
|
init_tex(&color, &pool, 0, NVN_FORMAT_RGBA8);
|
|
init_tex(&depth, &pool, 0x100000, NVN_FORMAT_DEPTH32F);
|
|
CHECK(color.L.size <= 0x100000 && depth.L.size <= 0x100000, "clear-part textures exceed their pool halves");
|
|
|
|
static _Alignas(8) uint8_t stream[4096];
|
|
uint8_t *p = stream;
|
|
p = put(p, OP_BEGIN, NULL, 0);
|
|
p_set_rt rt;
|
|
memset(&rt, 0, sizeof rt);
|
|
rt.n = 1, rt.color[0] = &color, rt.depth = &depth;
|
|
p = put(p, OP_SET_RT, &rt, sizeof rt);
|
|
const p_clear_color c0 = {0, 0xf, {0.2f, 0.4f, 0.6f, 1.0f}};
|
|
p = put(p, OP_CLEAR_COLOR, &c0, sizeof c0); // whole target outside a pass: vkCmdClearColorImage
|
|
const p_clear_ds d0 = {0.5f, 1, 0, 0xff};
|
|
p = put(p, OP_CLEAR_DS, &d0, sizeof d0); // vkCmdClearDepthStencilImage
|
|
const p_scissor sc = {SC_X, SC_Y, SC_W, SC_H};
|
|
p = put(p, OP_SCISSOR, &sc, sizeof sc);
|
|
const p_clear_color c1 = {0, 0xf, {1.0f, 0.0f, 0.0f, 1.0f}};
|
|
p = put(p, OP_CLEAR_COLOR, &c1, sizeof c1); // scissored: a pass and vkCmdClearAttachments
|
|
const p_clear_ds d1 = {0.25f, 1, 0, 0xff};
|
|
p = put(p, OP_CLEAR_DS, &d1, sizeof d1);
|
|
const p_scissor full = {0, 0, 1 << 20, 1 << 20};
|
|
p = put(p, OP_SCISSOR, &full, sizeof full); // leaves the replay's scissor unbounded for later submissions
|
|
p = put(p, OP_END, NULL, 0);
|
|
CHECK(p <= stream + sizeof stream, "op stream overflow");
|
|
|
|
uint64_t handle = (uint64_t)(uintptr_t)stream;
|
|
uint64_t f = b->submit(&handle, 1);
|
|
CHECK(b->fence_wait(f, 5000000000ull), "fence_wait of the clear submission timed out");
|
|
|
|
static uint32_t px[TEX_W * TEX_H];
|
|
static float dpx[TEX_W * TEX_H];
|
|
NVNcopyRegion r = {0, 0, 0, TEX_W, TEX_H, 1};
|
|
b->read_texels(&color, NULL, &r, (uint8_t *)px, TEX_W * 4, TEX_W * TEX_H * 4);
|
|
b->read_texels(&depth, NULL, &r, (uint8_t *)dpx, TEX_W * 4, TEX_W * TEX_H * 4);
|
|
const uint8_t out_b[4] = {to_byte(0.2f), to_byte(0.4f), to_byte(0.6f), 255}, in_b[4] = {255, 0, 0, 255};
|
|
uint32_t out_v, in_v;
|
|
memcpy(&out_v, out_b, 4), memcpy(&in_v, in_b, 4);
|
|
int inside = 0;
|
|
for (int y = 0; y < TEX_H; y++)
|
|
for (int x = 0; x < TEX_W; x++) {
|
|
int in = x >= SC_X && x < SC_X + SC_W && y >= SC_Y && y < SC_Y + SC_H;
|
|
inside += in;
|
|
uint32_t want = in ? in_v : out_v;
|
|
float dwant = in ? 0.25f : 0.5f;
|
|
CHECK(px[y * TEX_W + x] == want, "colour texel (%d,%d) is %08x, expected %08x", x, y, px[y * TEX_W + x], want);
|
|
CHECK(dpx[y * TEX_W + x] == dwant, "depth texel (%d,%d) is %g, expected %g", x, y, dpx[y * TEX_W + x], dwant);
|
|
}
|
|
b->texture_release(&color);
|
|
b->texture_release(&depth);
|
|
b->pool_fini(&pool);
|
|
*n_inside = inside;
|
|
return 0;
|
|
}
|
|
|
|
// ---- draw part: the pipeline self-test, then a hand-built stream that clears an RGBA8 target, binds a census
|
|
// vertex + fragment pair and draws one triangle; a census compute shader is dispatched. The census (probe_census_dir,
|
|
// tools/shader_census.py) is read the way vk_pipeline_selftest reads it: a pair is a fragment shader and a vertex
|
|
// shader of the same source file.
|
|
|
|
void vk_exec_draw_shutdown(void); // vk_exec_draw.c: the stand-in resources; the device must be idle
|
|
void vk_exec_xfer_shutdown(void); // vk_exec_xfer.c: the occlusion query pool; the device must be idle
|
|
|
|
typedef struct { const char *key, *c1, *src; size_t c1_len, src_len; int stage; uint32_t local[3]; } DrawEnt;
|
|
|
|
static const char *dr_field(const char *from, const char *to, const char *name, size_t *len) {
|
|
char pat[32];
|
|
snprintf(pat, sizeof pat, "\"%s\": \"", name);
|
|
const char *p = strstr(from, pat);
|
|
if (!p || p >= to) return NULL;
|
|
p += strlen(pat);
|
|
const char *e = memchr(p, '"', (size_t)(to - p));
|
|
if (!e) return NULL;
|
|
*len = (size_t)(e - p);
|
|
return p;
|
|
}
|
|
|
|
static int dr_cmp(const void *x, const void *y) {
|
|
const DrawEnt *a = x, *b = y;
|
|
size_t n = a->src_len < b->src_len ? a->src_len : b->src_len;
|
|
int c = memcmp(a->src, b->src, n);
|
|
if (c) return c;
|
|
if (a->src_len != b->src_len) return a->src_len < b->src_len ? -1 : 1;
|
|
return a->stage - b->stage;
|
|
}
|
|
|
|
static int dr_parse(const char *jobs, DrawEnt *out, int max) {
|
|
int n = 0;
|
|
for (const char *p = jobs; n < max && (p = strstr(p, "\"job\": {")); p += 8) {
|
|
const char *end = strstr(p, "\"origin\": ");
|
|
if (!end) break;
|
|
size_t kl, sl, cl, rl;
|
|
const char *k = dr_field(p, end, "key", &kl), *s = dr_field(p, end, "stage", &sl), *c = dr_field(p, end, "c1", &cl),
|
|
*r = dr_field(p, end, "source", &rl);
|
|
if (!k || !s || !c || !r || kl != 64) continue;
|
|
DrawEnt *e = &out[n++];
|
|
e->key = k, e->c1 = c, e->c1_len = cl / 2, e->src = r, e->src_len = rl;
|
|
e->stage = !strncmp(s, "vs", sl) ? GFX_STAGE_VS : !strncmp(s, "ps", sl) ? GFX_STAGE_FS : GFX_STAGE_CS;
|
|
e->local[0] = e->local[1] = e->local[2] = 1;
|
|
const char *l = strstr(p, "\"local\": [");
|
|
if (l && l < end) {
|
|
l += 10;
|
|
for (int i = 0; i < 3; i++) {
|
|
while (*l == ' ' || *l == '\n' || *l == '\r' || *l == ',') l++;
|
|
e->local[i] = (uint32_t)strtoul(l, (char **)&l, 10);
|
|
}
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
static char *dr_read(const char *path, long *size) {
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) return NULL;
|
|
fseek(f, 0, SEEK_END);
|
|
long n = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
char *b = malloc((size_t)n + 1);
|
|
if (b && fread(b, 1, (size_t)n, f) != (size_t)n) free(b), b = NULL;
|
|
fclose(f);
|
|
if (b) b[n] = 0, *size = n;
|
|
return b;
|
|
}
|
|
|
|
static int dr_hex(char c) { return c <= '9' ? c - '0' : (c | 32) - 'a' + 10; }
|
|
|
|
// A program of one census shader as the game hands it to NVN: control section, code with its 0x30-byte header.
|
|
static prog_t *dr_prog(const char *root, const DrawEnt *e[], int n) {
|
|
static uint64_t serial = 0x7000;
|
|
prog_t *p = calloc(1, sizeof *p);
|
|
p->magic = NVN_PROG_MAGIC, p->serial = ++serial;
|
|
for (int i = 0; i < n; i++) {
|
|
char path[PLAT_PATH_MAX * 2 + 128];
|
|
snprintf(path, sizeof path, "%s/code/%.64s.code", root, e[i]->key);
|
|
long body;
|
|
char *raw = dr_read(path, &body);
|
|
if (!raw) return free(p), NULL;
|
|
size_t skip = e[i]->stage == GFX_STAGE_CS ? 0 : 0x30, total = skip + (size_t)body;
|
|
uint8_t *code = calloc(1, total), *ctl = calloc(1, 0x880);
|
|
memcpy(code + skip, raw, (size_t)body);
|
|
free(raw);
|
|
uint32_t c1_off = (uint32_t)(total - e[i]->c1_len), length = (uint32_t)total;
|
|
uint32_t nvn_stage = e[i]->stage == GFX_STAGE_VS ? 0 : e[i]->stage == GFX_STAGE_FS ? 1 : 5;
|
|
memcpy(ctl + 0x700, &c1_off, 4), memcpy(ctl + 0x704, &length, 4), memcpy(ctl + 0x714, &nvn_stage, 4);
|
|
if (nvn_stage == 5) memcpy(ctl + 0x748, e[i]->local, 12);
|
|
p->sd[i].data = (uint64_t)(uintptr_t)code, p->sd[i].control = ctl;
|
|
p->nstage++;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
#define DR_ATTRS 8
|
|
#define DR_STRIDE (DR_ATTRS * 16)
|
|
|
|
static int layouts_equal; // texels compared equal between the two layouts (0: not compared)
|
|
static int ind_draw_texels, ind_dispatches; // indirect draws' texels compared, indirect dispatches run
|
|
|
|
// The draw part's stream up to the draw: target cleared, viewport, program, three RGBA32F vertices at 0x80000 of the
|
|
// pool, identity uniforms at 0x90000.
|
|
static uint8_t *draw_prefix(uint8_t *q, prog_t *prog, const pool_t *pool, tex_t *color) {
|
|
q = put(q, OP_BEGIN, NULL, 0);
|
|
p_set_rt rt;
|
|
memset(&rt, 0, sizeof rt);
|
|
rt.n = 1, rt.color[0] = color;
|
|
q = put(q, OP_SET_RT, &rt, sizeof rt);
|
|
const p_clear_color c0 = {0, 0xf, {0.2f, 0.4f, 0.6f, 1.0f}};
|
|
q = put(q, OP_CLEAR_COLOR, &c0, sizeof c0);
|
|
p_viewports vp;
|
|
memset(&vp, 0, sizeof vp);
|
|
vp.n = 1, vp.v[0][2] = TEX_W, vp.v[0][3] = TEX_H, vp.v[0][5] = 1.0f;
|
|
q = put(q, OP_VIEWPORTS, &vp, sizeof vp);
|
|
const p_bind_program bp = {prog, 3};
|
|
q = put(q, OP_BIND_PROGRAM, &bp, sizeof bp);
|
|
p_bind_vattr va;
|
|
memset(&va, 0, sizeof va);
|
|
va.n = DR_ATTRS;
|
|
for (int a = 0; a < DR_ATTRS; a++) {
|
|
va_st s = {NVN_FORMAT_RGBA32F, (uint32_t)(16 * a), 0};
|
|
memcpy(&va.st[a], &s, sizeof s);
|
|
}
|
|
q = put(q, OP_BIND_VATTR, &va, sizeof va);
|
|
p_bind_vstream vs;
|
|
memset(&vs, 0, sizeof vs);
|
|
vs.n = 1, vs.st[0].stride = DR_STRIDE;
|
|
q = put(q, OP_BIND_VSTREAM, &vs, sizeof vs);
|
|
p_bind_vbuf vb;
|
|
memset(&vb, 0, sizeof vb);
|
|
vb.n = 1, vb.r[0].address = (uint64_t)(uintptr_t)pool->mem + 0x80000, vb.r[0].size = 3 * DR_STRIDE;
|
|
q = put(q, OP_BIND_VBUF, &vb, sizeof vb);
|
|
for (int stage = 0; stage < 2; stage++)
|
|
for (int i = 0; i < 4; i++) {
|
|
p_bind_buf bu = {stage, i, (uint64_t)(uintptr_t)pool->mem + 0x90000, 4096};
|
|
q = put(q, OP_BIND_UBO, &bu, sizeof bu);
|
|
}
|
|
return q;
|
|
}
|
|
|
|
// ---- pass marks part (vk_crumbs.c): one submission with a pass of 2 draws into the draw part's target, a dispatch and
|
|
// a pass of 3 draws into a second target. Once its fence is done, the buffer marks must name the last pass as both
|
|
// started and finished, the ring records must hold what the three were, and vk_pass_marks_format (the text
|
|
// vk_device_fault_report logs) must name it. Without VK_AMD_buffer_marker or with TOTK_PASS_CRUMBS=0 it is skipped:
|
|
// NVIDIA's checkpoints can only be read once the device is lost.
|
|
static char marks_text[2048];
|
|
static int marks_skipped;
|
|
|
|
static int test_marks(const GfxBackend *b, pool_t *pool, tex_t *color, prog_t *gfx, prog_t *cs) {
|
|
uint32_t s0, f0;
|
|
if (!vk_pass_marks_read(&s0, &f0)) {
|
|
marks_skipped = 1;
|
|
return 0;
|
|
}
|
|
static tex_t second;
|
|
init_tex(&second, pool, 0x40000, NVN_FORMAT_RGBA8);
|
|
CHECK(0x40000 + second.L.size <= 0x80000, "pass-marks target exceeds its pool slot");
|
|
static _Alignas(8) uint8_t stream[8192];
|
|
uint8_t *q = draw_prefix(stream, gfx, pool, color);
|
|
const p_draw d = {NVN_DRAW_PRIMITIVE_TRIANGLES, 0, 3, 0, 0};
|
|
for (int i = 0; i < 2; i++) q = put(q, OP_DRAW, &d, sizeof d);
|
|
const p_bind_program bc = {cs, 1 << 5};
|
|
q = put(q, OP_BIND_PROGRAM, &bc, sizeof bc);
|
|
const p_dispatch dp = {2, 1, 1, 0};
|
|
q = put(q, OP_DISPATCH, &dp, sizeof dp);
|
|
const p_bind_program bg = {gfx, 3};
|
|
q = put(q, OP_BIND_PROGRAM, &bg, sizeof bg);
|
|
p_set_rt rt;
|
|
memset(&rt, 0, sizeof rt);
|
|
rt.n = 1, rt.color[0] = &second;
|
|
q = put(q, OP_SET_RT, &rt, sizeof rt);
|
|
for (int i = 0; i < 3; i++) q = put(q, OP_DRAW, &d, sizeof d);
|
|
q = put(q, OP_END, NULL, 0);
|
|
CHECK(q <= stream + sizeof stream, "pass-marks op stream overflow");
|
|
uint64_t handle = (uint64_t)(uintptr_t)stream;
|
|
uint64_t f = b->submit(&handle, 1);
|
|
CHECK(b->fence_wait(f, 20000000000ull), "fence_wait of the pass-marks submission timed out");
|
|
uint32_t started = 0, finished = 0;
|
|
vk_pass_marks_read(&started, &finished);
|
|
CHECK(started && started == finished, "after the fence the GPU last started mark %u and finished mark %u (before "
|
|
"the submission: %u and %u)", started, finished, s0, f0);
|
|
const VkPassMark *p1 = vk_pass_mark_record(finished), *dm = vk_pass_mark_record(finished - 1),
|
|
*p0 = vk_pass_mark_record(finished - 2);
|
|
CHECK(p1 && p1->kind == VK_MARK_PASS && p1->fence == f && p1->step == 1,
|
|
"the last finished mark %u is not pass 1 of submission %llu", finished, (unsigned long long)f);
|
|
CHECK(p1->w == TEX_W && p1->h == TEX_H && p1->d == 1 && p1->ncolor == 1 &&
|
|
p1->color[0] == VK_FORMAT_R8G8B8A8_UNORM && p1->depth == VK_FORMAT_UNDEFINED,
|
|
"pass 1 has %ux%u, %u layers, %u colour targets, format %d, depth %d", p1->w, p1->h, p1->d, p1->ncolor,
|
|
(int)p1->color[0], (int)p1->depth);
|
|
CHECK(p1->draws == 3 && p1->first[0] && p1->first[1] && p1->first[0] == p1->last[0] && p1->first[1] == p1->last[1],
|
|
"pass 1 has %u draws (3 expected) and shaders %p %p ... %p %p", p1->draws, (const void *)p1->first[0],
|
|
(const void *)p1->first[1], (const void *)p1->last[0], (const void *)p1->last[1]);
|
|
CHECK(dm && dm->kind == VK_MARK_DISPATCH && dm->fence == f && dm->step == 0 && dm->w == 2 && dm->h == 1 &&
|
|
dm->d == 1 && dm->first[0],
|
|
"the mark before pass 1 is not dispatch 0 of 2x1x1 groups of submission %llu", (unsigned long long)f);
|
|
CHECK(p0 && p0->kind == VK_MARK_PASS && p0->fence == f && p0->step == 0 && p0->draws == 2 &&
|
|
p0->first[0] == p1->first[0],
|
|
"the mark before the dispatch is not pass 0 of submission %llu with 2 draws", (unsigned long long)f);
|
|
vk_pass_marks_format("VK_AMD_buffer_marker", started, finished, marks_text, sizeof marks_text);
|
|
char want[128];
|
|
snprintf(want, sizeof want, "last finished %u: pass 1 of submission %llu: %ux%u, 1 layer, colour formats %d, ",
|
|
finished, (unsigned long long)f, TEX_W, TEX_H, (int)VK_FORMAT_R8G8B8A8_UNORM);
|
|
CHECK(strstr(marks_text, want) && strstr(marks_text, ", 3 draws; first vs "),
|
|
"the report text lacks \"%s\" or the draw count:\n%s", want, marks_text);
|
|
b->texture_release(&second);
|
|
return 0;
|
|
}
|
|
|
|
static int test_draw(const GfxBackend *b, uint8_t *guest, int *n_drawn, int *n_tries, int *n_cs) {
|
|
char root[PLAT_PATH_MAX * 2], path[PLAT_PATH_MAX * 2 + 64];
|
|
CHECK(probe_census_dir(root, sizeof root), "no build/v%d/shaders/jobs.json above the exe (run tools/shader_census.py)",
|
|
rt_game_version);
|
|
snprintf(path, sizeof path, "%s/jobs.json", root);
|
|
long size;
|
|
char *jobs = dr_read(path, &size);
|
|
DrawEnt *e = malloc(sizeof *e * 20000);
|
|
int n = jobs && e ? dr_parse(jobs, e, 20000) : 0;
|
|
CHECK(n > 0, "no census entries");
|
|
qsort(e, (size_t)n, sizeof *e, dr_cmp);
|
|
int (*pairs)[2] = malloc(sizeof *pairs * (size_t)n), np = 0, *cs = malloc(sizeof *cs * (size_t)n), nc = 0;
|
|
for (int i = 0; i < n;) {
|
|
int j = i, v0 = -1, nv = 0, nf = 0;
|
|
for (; j < n && e[j].src_len == e[i].src_len && !memcmp(e[j].src, e[i].src, e[i].src_len); j++) {
|
|
if (e[j].stage == GFX_STAGE_VS && v0 < 0) v0 = j;
|
|
nv += e[j].stage == GFX_STAGE_VS;
|
|
if (e[j].stage == GFX_STAGE_CS) cs[nc++] = j;
|
|
}
|
|
for (int k = i; k < j && nv; k++)
|
|
if (e[k].stage == GFX_STAGE_FS) pairs[np][0] = v0 + nf++ % nv, pairs[np][1] = k, np++;
|
|
i = j;
|
|
}
|
|
CHECK(np > 0 && nc > 0, "census has %d pairs and %d compute shaders", np, nc);
|
|
|
|
pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0x700000, .size = 0xC0000,
|
|
.game_mem = guest + 0x700000};
|
|
CHECK(b->pool_init(&pool), "pool_init of a CPU-visible pool for the draw part");
|
|
static tex_t color;
|
|
init_tex(&color, &pool, 0, NVN_FORMAT_RGBA8);
|
|
CHECK(color.L.size <= 0x80000, "draw-part target exceeds its pool slot");
|
|
// Vertex data at 0x80000: three vertices of DR_ATTRS RGBA32F attributes, attribute 0 the position. Uniform data at
|
|
// 0x90000: identity matrices back to back, so that a vertex shader multiplying the position by one passes it through.
|
|
float *vtx = (float *)(pool.mem + 0x80000);
|
|
static const float pos[3][4] = {{-0.8f, -0.8f, 0.0f, 1.0f}, {0.8f, -0.8f, 0.0f, 1.0f}, {0.0f, 0.8f, 0.0f, 1.0f}};
|
|
for (int v = 0; v < 3; v++)
|
|
for (int a = 0; a < DR_ATTRS; a++)
|
|
for (int c = 0; c < 4; c++) vtx[(v * DR_ATTRS + a) * 4 + c] = a == 0 ? pos[v][c] : c == 3 ? 1.0f : 0.5f;
|
|
float *ub = (float *)(pool.mem + 0x90000);
|
|
for (int m = 0; m < 256; m++)
|
|
for (int i = 0; i < 16; i++) ub[m * 16 + i] = i % 5 == 0 ? 1.0f : 0.0f;
|
|
|
|
const uint8_t clear_b[4] = {to_byte(0.2f), to_byte(0.4f), to_byte(0.6f), 255};
|
|
uint32_t clear_v;
|
|
memcpy(&clear_v, clear_b, 4);
|
|
static uint32_t px[TEX_W * TEX_H];
|
|
static _Alignas(8) uint8_t stream[8192];
|
|
int tries = 0, drawn = 0;
|
|
prog_t *hit = NULL;
|
|
for (int t = 0; t < 150 && !drawn; t++) {
|
|
const int *pr = pairs[(long long)t * np / (np < 150 ? np : 150)];
|
|
if (t && pr == pairs[(long long)(t - 1) * np / (np < 150 ? np : 150)]) continue;
|
|
const DrawEnt *both[2] = {&e[pr[0]], &e[pr[1]]};
|
|
prog_t *prog = dr_prog(root, both, 2);
|
|
CHECK(prog, "census code file of %.16s is missing", both[0]->key);
|
|
tries++;
|
|
uint8_t *q = draw_prefix(stream, prog, &pool, &color);
|
|
const p_draw d = {NVN_DRAW_PRIMITIVE_TRIANGLES, 0, 3, 0, 0};
|
|
q = put(q, OP_DRAW, &d, sizeof d);
|
|
q = put(q, OP_END, NULL, 0);
|
|
CHECK(q <= stream + sizeof stream, "draw op stream overflow");
|
|
uint64_t handle = (uint64_t)(uintptr_t)stream;
|
|
uint64_t f = b->submit(&handle, 1);
|
|
CHECK(b->fence_wait(f, 20000000000ull), "fence_wait of the draw submission timed out");
|
|
const NVNcopyRegion r = {0, 0, 0, TEX_W, TEX_H, 1};
|
|
b->read_texels(&color, NULL, &r, (uint8_t *)px, TEX_W * 4, TEX_W * TEX_H * 4);
|
|
int diff = 0;
|
|
for (int k = 0; k < TEX_W * TEX_H; k++) diff += px[k] != clear_v;
|
|
if (diff) drawn = diff, hit = prog;
|
|
if (diff && vk_caps.gpl) { // the same draw through the shared set 0 layout: texel-equal
|
|
static uint32_t px2[TEX_W * TEX_H];
|
|
int was = vk_pipeline_set_indep(0);
|
|
f = b->submit(&handle, 1);
|
|
CHECK(b->fence_wait(f, 20000000000ull), "fence_wait of the shared-layout draw timed out");
|
|
b->read_texels(&color, NULL, &r, (uint8_t *)px2, TEX_W * 4, TEX_W * TEX_H * 4);
|
|
vk_pipeline_set_indep(was);
|
|
int ne = 0;
|
|
for (int k = 0; k < TEX_W * TEX_H; k++) ne += px[k] != px2[k];
|
|
CHECK(!ne, "%d of %d texels differ between independent and shared layouts", ne, TEX_W * TEX_H);
|
|
layouts_equal = TEX_W * TEX_H;
|
|
}
|
|
}
|
|
CHECK(drawn, "none of %d census pairs changed a pixel of the cleared target", tries);
|
|
|
|
// DrawElementsIndirect (OP_DRAW_IDX_IND) with the pair that drew: indices {0, 1, 2} at 0xA0000; the arguments are
|
|
// copied by the GPU (COPY_B2B, barrier) to 0xA0200 right before the draw, so the replay must read them on the GPU.
|
|
// {3, 1, 0, 0, 0} draws the same triangle (texel-equal to the direct draw); count 0 draws nothing.
|
|
uint32_t *ix = (uint32_t *)(pool.mem + 0xA0000), *args = (uint32_t *)(pool.mem + 0xA0100);
|
|
ix[0] = 0, ix[1] = 1, ix[2] = 2;
|
|
for (int pass = 0; pass < 2; pass++) {
|
|
const uint32_t want[5] = {pass ? 0u : 3u, 1, 0, 0, 0};
|
|
memcpy(args, want, sizeof want);
|
|
memset(pool.mem + 0xA0200, 0, 20);
|
|
uint8_t *q = draw_prefix(stream, hit, &pool, &color);
|
|
const p_copy_b2b cp = {(uint64_t)(uintptr_t)args, (uint64_t)(uintptr_t)(pool.mem + 0xA0200), 20};
|
|
q = put(q, OP_COPY_B2B, &cp, sizeof cp);
|
|
const p_barrier bar = {0, 0};
|
|
q = put(q, OP_BARRIER, &bar, sizeof bar);
|
|
const p_draw_idx_ind di = {NVN_DRAW_PRIMITIVE_TRIANGLES, NVN_INDEX_TYPE_UNSIGNED_INT, (uint64_t)(uintptr_t)ix,
|
|
(uint64_t)(uintptr_t)(pool.mem + 0xA0200)};
|
|
q = put(q, OP_DRAW_IDX_IND, &di, sizeof di);
|
|
q = put(q, OP_END, NULL, 0);
|
|
uint64_t handle = (uint64_t)(uintptr_t)stream;
|
|
uint64_t f = b->submit(&handle, 1);
|
|
CHECK(b->fence_wait(f, 20000000000ull), "fence_wait of the indirect draw timed out");
|
|
static uint32_t pxi[TEX_W * TEX_H];
|
|
const NVNcopyRegion r = {0, 0, 0, TEX_W, TEX_H, 1};
|
|
b->read_texels(&color, NULL, &r, (uint8_t *)pxi, TEX_W * 4, TEX_W * TEX_H * 4);
|
|
int ne = 0;
|
|
for (int k = 0; k < TEX_W * TEX_H; k++) ne += pxi[k] != (pass ? clear_v : px[k]);
|
|
CHECK(!ne, "indirect draw with count %u: %d of %d texels differ from the %s", want[0], ne, TEX_W * TEX_H,
|
|
pass ? "cleared target" : "direct draw");
|
|
ind_draw_texels += TEX_W * TEX_H;
|
|
}
|
|
|
|
// Compute: a few census compute shaders, one dispatch each, resources unbound (zero buffers and stand-in images).
|
|
int dispatched = 0;
|
|
prog_t *cs_prog = NULL;
|
|
for (int t = 0; t < 4 && t < nc; t++) {
|
|
const DrawEnt *one[1] = {&e[cs[(long long)t * nc / (nc < 4 ? nc : 4)]]};
|
|
prog_t *prog = dr_prog(root, one, 1);
|
|
CHECK(prog, "census code file of %.16s is missing", one[0]->key);
|
|
cs_prog = prog;
|
|
uint8_t *q = stream;
|
|
q = put(q, OP_BEGIN, NULL, 0);
|
|
const p_bind_program bp = {prog, 1 << 5};
|
|
q = put(q, OP_BIND_PROGRAM, &bp, sizeof bp);
|
|
const p_dispatch dp = {1, 1, 1, 0};
|
|
q = put(q, OP_DISPATCH, &dp, sizeof dp);
|
|
// DispatchComputeIndirect: group counts {1, 1, 1} copied by the GPU to 0xA0500 first
|
|
const uint32_t groups[3] = {1, 1, 1};
|
|
memcpy(pool.mem + 0xA0400, groups, sizeof groups);
|
|
memset(pool.mem + 0xA0500, 0, sizeof groups);
|
|
const p_copy_b2b cp = {(uint64_t)(uintptr_t)(pool.mem + 0xA0400), (uint64_t)(uintptr_t)(pool.mem + 0xA0500), 12};
|
|
q = put(q, OP_COPY_B2B, &cp, sizeof cp);
|
|
const p_barrier bar = {0, 0};
|
|
q = put(q, OP_BARRIER, &bar, sizeof bar);
|
|
const p_dispatch_ind dpi = {(uint64_t)(uintptr_t)(pool.mem + 0xA0500), 0};
|
|
q = put(q, OP_DISPATCH_IND, &dpi, sizeof dpi);
|
|
q = put(q, OP_END, NULL, 0);
|
|
uint64_t handle = (uint64_t)(uintptr_t)stream;
|
|
uint64_t f = b->submit(&handle, 1);
|
|
CHECK(b->fence_wait(f, 20000000000ull), "fence_wait of the dispatch submission timed out");
|
|
dispatched++;
|
|
ind_dispatches++;
|
|
}
|
|
if (test_marks(b, &pool, &color, hit, cs_prog)) return 1;
|
|
b->texture_release(&color);
|
|
b->pool_fini(&pool);
|
|
free(pairs), free(cs), free(e), free(jobs);
|
|
*n_drawn = drawn, *n_tries = tries, *n_cs = dispatched;
|
|
return 0;
|
|
}
|
|
|
|
// ---- xfer part: copies, texture clear, event and counter through submit.
|
|
|
|
#define XF_W 64
|
|
#define XF_H 64
|
|
#define XF_BYTES (XF_W * XF_H * 4)
|
|
|
|
// Texture in the xfer pool at `off`.
|
|
static int xf_tex(tex_t *t, pool_t *pool, int64_t off, uint64_t serial) {
|
|
init_tex(t, pool, off, NVN_FORMAT_RGBA8);
|
|
t->d.width = XF_W, t->d.height = XF_H;
|
|
nvn_tex_layout(&t->d, &t->L);
|
|
t->serial = serial;
|
|
return t->L.size <= 0x20000;
|
|
}
|
|
|
|
static int xf_submit(const GfxBackend *b, uint8_t *stream, uint8_t *end) {
|
|
end = put(end, OP_END, NULL, 0);
|
|
(void)stream;
|
|
uint64_t handle = (uint64_t)(uintptr_t)stream;
|
|
uint64_t f = b->submit(&handle, 1);
|
|
return b->fence_wait(f, 5000000000ull);
|
|
}
|
|
|
|
static int test_xfer(const GfxBackend *b, uint8_t *guest, int *bytes) {
|
|
pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0x800000, .size = 0x100000,
|
|
.game_mem = guest + 0x800000};
|
|
CHECK(b->pool_init(&pool), "pool_init of a CPU-visible pool for the xfer part");
|
|
uint8_t *m = pool.mem;
|
|
static tex_t ta, tb, tc;
|
|
CHECK(xf_tex(&ta, &pool, 0x40000, 201) && xf_tex(&tb, &pool, 0x60000, 202) && xf_tex(&tc, &pool, 0x80000, 203),
|
|
"xfer textures exceed their pool slots");
|
|
// Linear buffers: B2B source 0x0, target 0x1000 (4 KiB); texture source 0x2000, read-backs 0x6000 and 0xA000.
|
|
for (int i = 0; i < 4096; i++) m[i] = (uint8_t)(i * 7 + 3), m[0x1000 + i] = 0;
|
|
uint32_t *img = (uint32_t *)(m + 0x2000);
|
|
for (int i = 0; i < XF_W * XF_H; i++) img[i] = 0x01010101u * (uint32_t)(i & 0xff) + (uint32_t)i * 0x00010000u;
|
|
memset(m + 0x6000, 0, XF_BYTES), memset(m + 0xA000, 0, XF_BYTES);
|
|
volatile uint32_t *event = (volatile uint32_t *)(m + 0xE000);
|
|
NVNcounterData *ctr = (NVNcounterData *)(m + 0xE100), *sp = (NVNcounterData *)(m + 0xE200);
|
|
*event = 0, ctr->counter = 99, ctr->timestamp = 0;
|
|
sp->counter = 99, sp->timestamp = 0;
|
|
|
|
static _Alignas(8) uint8_t stream[4096];
|
|
uint8_t *p = stream;
|
|
p = put(p, OP_BEGIN, NULL, 0);
|
|
const p_copy_b2b bb = {(uint64_t)(uintptr_t)m, (uint64_t)(uintptr_t)(m + 0x1000), 4096};
|
|
p = put(p, OP_COPY_B2B, &bb, sizeof bb);
|
|
p_copy_b2t up;
|
|
memset(&up, 0, sizeof up);
|
|
up.src = (uint64_t)(uintptr_t)(m + 0x2000), up.tex = &ta, up.r = (NVNcopyRegion){0, 0, 0, XF_W, XF_H, 1};
|
|
p = put(p, OP_COPY_B2T, &up, sizeof up);
|
|
p_copy_b2t down = up;
|
|
down.src = (uint64_t)(uintptr_t)(m + 0x6000);
|
|
p = put(p, OP_COPY_T2B, &down, sizeof down);
|
|
p_copy_t2t tt;
|
|
memset(&tt, 0, sizeof tt);
|
|
tt.src = &ta, tt.dst = &tb, tt.sr = tt.dr = up.r;
|
|
p = put(p, OP_COPY_T2T, &tt, sizeof tt);
|
|
down.tex = &tb, down.src = (uint64_t)(uintptr_t)(m + 0xA000);
|
|
p = put(p, OP_COPY_T2B, &down, sizeof down);
|
|
p_clear_tex ct;
|
|
memset(&ct, 0, sizeof ct);
|
|
ct.tex = &tc, ct.r = (NVNcopyRegion){0, 0, 0, XF_W, XF_H, 1}, ct.mask = 0xf;
|
|
ct.c.f[0] = 0.2f, ct.c.f[1] = 0.4f, ct.c.f[2] = 0.6f, ct.c.f[3] = 1.0f;
|
|
p = put(p, OP_CLEAR_TEX, &ct, sizeof ct);
|
|
const p_signal_event ev1 = {event, NVN_EVENT_SIGNAL_MODE_WRITE, 7}, ev2 = {event, NVN_EVENT_SIGNAL_MODE_ADD, 5};
|
|
p = put(p, OP_SIGNAL_EVENT, &ev1, sizeof ev1);
|
|
p = put(p, OP_SIGNAL_EVENT, &ev2, sizeof ev2);
|
|
const p_report_counter rc = {NVN_COUNTER_TYPE_TIMESTAMP, (uint64_t)(uintptr_t)ctr};
|
|
p = put(p, OP_REPORT_COUNTER, &rc, sizeof rc);
|
|
// Samples passed: a reset, no draw, a report: {0, time} through the occlusion-query path.
|
|
const p_reset_counter rs = {NVN_COUNTER_TYPE_SAMPLES_PASSED};
|
|
p = put(p, OP_RESET_COUNTER, &rs, sizeof rs);
|
|
const p_report_counter rsp = {NVN_COUNTER_TYPE_SAMPLES_PASSED, (uint64_t)(uintptr_t)sp};
|
|
p = put(p, OP_REPORT_COUNTER, &rsp, sizeof rsp);
|
|
CHECK(p <= stream + sizeof stream - sizeof(op_hdr), "xfer op stream overflow");
|
|
CHECK(xf_submit(b, stream, p), "fence_wait of the xfer submission timed out");
|
|
|
|
for (int i = 0; i < 4096; i++) CHECK(m[0x1000 + i] == m[i], "B2B byte %d is %02x, expected %02x", i, m[0x1000 + i], m[i]);
|
|
for (int i = 0; i < XF_W * XF_H; i++) {
|
|
CHECK(((uint32_t *)(m + 0x6000))[i] == img[i], "B2T then T2B texel %d is %08x, expected %08x", i,
|
|
((uint32_t *)(m + 0x6000))[i], img[i]);
|
|
CHECK(((uint32_t *)(m + 0xA000))[i] == img[i], "T2T then T2B texel %d is %08x, expected %08x", i,
|
|
((uint32_t *)(m + 0xA000))[i], img[i]);
|
|
}
|
|
static uint32_t px[XF_W * XF_H];
|
|
const NVNcopyRegion r = {0, 0, 0, XF_W, XF_H, 1};
|
|
b->read_texels(&ta, NULL, &r, (uint8_t *)px, XF_W * 4, XF_BYTES);
|
|
for (int i = 0; i < XF_W * XF_H; i++) CHECK(px[i] == img[i], "read_texels of the copied texture, texel %d is %08x", i, px[i]);
|
|
b->read_texels(&tc, NULL, &r, (uint8_t *)px, XF_W * 4, XF_BYTES);
|
|
const uint8_t cl[4] = {to_byte(0.2f), to_byte(0.4f), to_byte(0.6f), 255};
|
|
uint32_t clv;
|
|
memcpy(&clv, cl, 4);
|
|
for (int i = 0; i < XF_W * XF_H; i++) CHECK(px[i] == clv, "CLEAR_TEX texel %d is %08x, expected %08x", i, px[i], clv);
|
|
CHECK(*event == 12, "event word is %u after WRITE 7 and ADD 5", *event);
|
|
CHECK(ctr->counter == 0 && ctr->timestamp != 0, "counter report is {%llu, %llu}", (unsigned long long)ctr->counter,
|
|
(unsigned long long)ctr->timestamp);
|
|
CHECK(sp->counter == 0 && sp->timestamp != 0, "samples-passed report after a reset and no draw is {%llu, %llu}",
|
|
(unsigned long long)sp->counter, (unsigned long long)sp->timestamp);
|
|
b->texture_release(&ta);
|
|
b->texture_release(&tb);
|
|
b->texture_release(&tc);
|
|
b->pool_fini(&pool);
|
|
*bytes = 4096 + 2 * XF_BYTES;
|
|
return 0;
|
|
}
|
|
|
|
// c0 as vk_c0_fill builds it (CPU only): compute storage and uniform tables, dispatch size, unbound slots zero; the
|
|
// vertex stage's point size clamp and base instance.
|
|
static int test_c0(void) {
|
|
static p_bind_buf ssbo[16], ubo[16];
|
|
static uint8_t bank[VK_C0_BYTES];
|
|
ssbo[1] = (p_bind_buf){5, 1, 0x12340000, 0x400};
|
|
ubo[4] = (p_bind_buf){5, 4, 0x55660000, 0x100};
|
|
const uint32_t dim[3] = {3, 2, 1};
|
|
uint32_t u[4];
|
|
uint64_t a;
|
|
vk_c0_fill(bank, 5, ssbo, ubo, dim, 7);
|
|
memcpy(u, bank + 0x200, 12);
|
|
CHECK(u[0] == 3 && u[1] == 2 && u[2] == 1, "c0 dispatch size is %u %u %u", u[0], u[1], u[2]);
|
|
memcpy(&a, bank + 0x320, 8), memcpy(u, bank + 0x328, 4);
|
|
CHECK(a == 0x12340000 && u[0] == 0x400, "c0 0x320 (storage slot 1) is {%#llx, %#x}", (unsigned long long)a, u[0]);
|
|
memcpy(&a, bank + 0x250, 8), memcpy(u, bank + 0x258, 4);
|
|
CHECK(a == 0x55660000 && u[0] == 0x100, "c0 0x250 (uniform slot 4) is {%#llx, %#x}", (unsigned long long)a, u[0]);
|
|
for (int i = 0; i < 16; i++) CHECK(!bank[0x310 + i], "c0 0x310 (unbound storage slot 0) byte %d is %02x", i, bank[0x310 + i]);
|
|
CHECK(!bank[0x644] && !bank[0x610], "c0 of a compute stage has graphics words");
|
|
vk_c0_fill(bank, 0, ssbo, ubo, dim, 61);
|
|
float ps;
|
|
memcpy(&ps, bank + 0x610, 4), memcpy(u, bank + 0x644, 4);
|
|
CHECK(ps == 2047.0f && u[0] == 61, "vertex c0: point size %g at 0x610, base instance %u at 0x644", ps, u[0]);
|
|
memcpy(&a, bank + 0x120, 8);
|
|
CHECK(a == 0x12340000 && !bank[0x200], "vertex c0: storage slot 1 at 0x120 is %#llx", (unsigned long long)a);
|
|
return 0;
|
|
}
|
|
|
|
// A texture in a physical pool's memory that nothing wrote reads zero, as Switch pool memory starts zeroed
|
|
// (TOTK_FIX_ZERO; the driver may return zeros without it, then this is a guard).
|
|
static int test_zero(const GfxBackend *b, uint8_t *guest, int *bytes) {
|
|
pool_t phys = {.magic = NVN_POOL_MAGIC, .flags = 0x189, .mem = (uint8_t *)(uintptr_t)RT_GPU_VA_BASE, .size = 0x100000,
|
|
.game_mem = guest + 0xA00000};
|
|
pool_t cpu = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0x800000, .size = 0x100000, .game_mem = guest + 0x800000};
|
|
CHECK(b->pool_init(&phys), "pool_init of a physical pool");
|
|
CHECK(b->pool_init(&cpu), "pool_init of a CPU-visible pool for the zero part");
|
|
static tex_t t;
|
|
init_tex(&t, &phys, 0, NVN_FORMAT_RGBA8);
|
|
t.d.width = 64, t.d.height = 64;
|
|
nvn_tex_layout(&t.d, &t.L);
|
|
t.serial = 301;
|
|
memset(cpu.mem, 0xab, 64 * 64 * 4);
|
|
static _Alignas(8) uint8_t stream[512];
|
|
uint8_t *p = put(stream, OP_BEGIN, NULL, 0);
|
|
p_copy_b2t down;
|
|
memset(&down, 0, sizeof down);
|
|
down.src = (uint64_t)(uintptr_t)cpu.mem, down.tex = &t, down.r = (NVNcopyRegion){0, 0, 0, 64, 64, 1};
|
|
p = put(p, OP_COPY_T2B, &down, sizeof down);
|
|
CHECK(xf_submit(b, stream, p), "fence_wait of the zero submission timed out");
|
|
for (int i = 0; i < 64 * 64 * 4; i++) CHECK(!cpu.mem[i], "unwritten texel byte %d of a physical-pool texture is %02x", i, cpu.mem[i]);
|
|
b->texture_release(&t);
|
|
b->pool_fini(&cpu);
|
|
b->pool_fini(&phys);
|
|
*bytes = 64 * 64 * 4;
|
|
return 0;
|
|
}
|
|
|
|
// ClearBuffer / ReportValue (OP_CLEAR_BUF), Downsample (a linear OP_COPY_T2T to half size) and the stencil view
|
|
// aspect. Fills: 64 bytes in a CPU-visible pool, a 4-byte report over the first word in the same submission (GPU order:
|
|
// the report wins), and 64 bytes in a CPU-invisible physical pool copied out with COPY_B2B. Downsample: a 64x64 RGBA8
|
|
// texture to 32x32, each texel the 2x2 average within 1. Stencil: views of a D24S8 texture pick the stencil aspect in
|
|
// STENCIL mode and depth otherwise, and a stencil-aspect sampled view of the image is created.
|
|
static int test_nvnx(const GfxBackend *b, uint8_t *guest, int *bytes) {
|
|
pool_t cpu = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0x800000, .size = 0x100000, .game_mem = guest + 0x800000};
|
|
pool_t phys = {.magic = NVN_POOL_MAGIC, .flags = 0x189, .mem = (uint8_t *)(uintptr_t)RT_GPU_VA_BASE, .size = 0x100000,
|
|
.game_mem = guest + 0xA00000};
|
|
CHECK(b->pool_init(&cpu) && b->pool_init(&phys), "pool_init of the nvnx pools");
|
|
uint8_t *m = cpu.mem;
|
|
memset(m, 0x11, 0x400);
|
|
static tex_t src, dst;
|
|
init_tex(&src, &cpu, 0x40000, NVN_FORMAT_RGBA8);
|
|
src.d.width = 64, src.d.height = 64, src.serial = 401;
|
|
nvn_tex_layout(&src.d, &src.L);
|
|
init_tex(&dst, &cpu, 0x60000, NVN_FORMAT_RGBA8);
|
|
dst.d.width = 32, dst.d.height = 32, dst.serial = 402;
|
|
nvn_tex_layout(&dst.d, &dst.L);
|
|
CHECK(src.L.size <= 0x20000 && dst.L.size <= 0x20000, "downsample textures exceed their pool slots");
|
|
uint8_t *img = m + 0x1000, *out = m + 0x8000;
|
|
for (int i = 0; i < 64 * 64 * 4; i++) img[i] = (uint8_t)((i * 37 + (i >> 8) * 11) & 0xff);
|
|
memset(out, 0, 32 * 32 * 4);
|
|
|
|
static _Alignas(8) uint8_t stream[2048];
|
|
uint8_t *p = put(stream, OP_BEGIN, NULL, 0);
|
|
const p_clear_buf f1 = {(uint64_t)(uintptr_t)m, 64, 0xdeadbeefu, 0}, rv = {(uint64_t)(uintptr_t)m, 4, 0x1234u, 0};
|
|
p = put(p, OP_CLEAR_BUF, &f1, sizeof f1);
|
|
p = put(p, OP_CLEAR_BUF, &rv, sizeof rv);
|
|
const p_clear_buf f2 = {RT_GPU_VA_BASE + 0x100, 64, 0xa5a5a5a5u, 0};
|
|
p = put(p, OP_CLEAR_BUF, &f2, sizeof f2);
|
|
const p_copy_b2b back = {RT_GPU_VA_BASE + 0x100, (uint64_t)(uintptr_t)(m + 0x200), 64};
|
|
p = put(p, OP_COPY_B2B, &back, sizeof back);
|
|
p_copy_b2t up;
|
|
memset(&up, 0, sizeof up);
|
|
up.src = (uint64_t)(uintptr_t)img, up.tex = &src, up.r = (NVNcopyRegion){0, 0, 0, 64, 64, 1};
|
|
p = put(p, OP_COPY_B2T, &up, sizeof up);
|
|
p_copy_t2t ds; // as nvnCommandBufferDownsample records it
|
|
memset(&ds, 0, sizeof ds);
|
|
ds.src = &src, ds.dst = &dst, ds.sr = (NVNcopyRegion){0, 0, 0, 64, 64, 1}, ds.dr = (NVNcopyRegion){0, 0, 0, 32, 32, 1};
|
|
ds.flags = NVN_COPY_FLAGS_LINEAR_FILTER_BIT;
|
|
p = put(p, OP_COPY_T2T, &ds, sizeof ds);
|
|
p_copy_b2t down;
|
|
memset(&down, 0, sizeof down);
|
|
down.src = (uint64_t)(uintptr_t)out, down.tex = &dst, down.r = (NVNcopyRegion){0, 0, 0, 32, 32, 1};
|
|
p = put(p, OP_COPY_T2B, &down, sizeof down);
|
|
CHECK(xf_submit(b, stream, p), "fence_wait of the nvnx submission timed out");
|
|
|
|
uint32_t w0;
|
|
memcpy(&w0, m, 4);
|
|
CHECK(w0 == 0x1234u, "ReportValue word after the fill is %#x", w0);
|
|
for (int i = 1; i < 16; i++) {
|
|
uint32_t w;
|
|
memcpy(&w, m + 4 * i, 4);
|
|
CHECK(w == 0xdeadbeefu, "ClearBuffer word %d is %#x", i, w);
|
|
}
|
|
CHECK(m[64] == 0x11, "ClearBuffer wrote past its 64 bytes (%02x)", m[64]);
|
|
for (int i = 0; i < 64; i++) CHECK(m[0x200 + i] == 0xa5, "physical-pool fill byte %d is %02x", i, m[0x200 + i]);
|
|
int worst = 0;
|
|
for (int y = 0; y < 32; y++)
|
|
for (int x = 0; x < 32; x++)
|
|
for (int ch = 0; ch < 4; ch++) {
|
|
int sum = 0;
|
|
for (int dy = 0; dy < 2; dy++)
|
|
for (int dx = 0; dx < 2; dx++) sum += img[((2 * y + dy) * 64 + 2 * x + dx) * 4 + ch];
|
|
int d = abs(out[(y * 32 + x) * 4 + ch] - (sum + 2) / 4);
|
|
if (d > worst) worst = d;
|
|
}
|
|
CHECK(worst <= 1, "downsampled texel off the 2x2 average by %d", worst);
|
|
|
|
static tex_t dsx;
|
|
init_tex(&dsx, &cpu, 0x80000, NVN_FORMAT_DEPTH24_STENCIL8);
|
|
dsx.d.width = 64, dsx.d.height = 64, dsx.serial = 403;
|
|
nvn_tex_layout(&dsx.d, &dsx.L);
|
|
const VkTexImage *di = vk_texture_get(&dsx);
|
|
CHECK(di && (di->aspect & VK_IMAGE_ASPECT_STENCIL_BIT) && (di->aspect & VK_IMAGE_ASPECT_DEPTH_BIT), "D24S8 image with both aspects");
|
|
view_t sv, dv;
|
|
memset(&sv, 0, sizeof sv), memset(&dv, 0, sizeof dv);
|
|
sv.set_dsmode = 1, sv.dsmode = NVN_TEXTURE_DEPTH_STENCIL_MODE_STENCIL;
|
|
dv.set_dsmode = 1, dv.dsmode = NVN_TEXTURE_DEPTH_STENCIL_MODE_DEPTH;
|
|
CHECK(vk_exec_view_aspect(di, &sv, 1) == VK_IMAGE_ASPECT_STENCIL_BIT && vk_exec_view_aspect(di, &dv, 1) == VK_IMAGE_ASPECT_DEPTH_BIT &&
|
|
vk_exec_view_aspect(di, &sv, 0) == VK_IMAGE_ASPECT_DEPTH_BIT, "view aspects of a D24S8 texture");
|
|
VkImageViewCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = di->image,
|
|
.viewType = VK_IMAGE_VIEW_TYPE_2D, .format = di->format,
|
|
.subresourceRange = {VK_IMAGE_ASPECT_STENCIL_BIT, 0, 1, 0, 1}};
|
|
VkImageView view = VK_NULL_HANDLE;
|
|
CHECK(vkCreateImageView(vk_ctx()->device, &ci, NULL, &view) == VK_SUCCESS, "stencil-aspect view of the D24S8 image");
|
|
vkDestroyImageView(vk_ctx()->device, view, NULL);
|
|
b->texture_release(&src);
|
|
b->texture_release(&dst);
|
|
b->texture_release(&dsx);
|
|
b->pool_fini(&cpu);
|
|
b->pool_fini(&phys);
|
|
*bytes = 64 + 64 + 32 * 32 * 4;
|
|
return 0;
|
|
}
|
|
|
|
// VRAM mirror (vk_memory.c; skipped in import mode): a pool given its mirror as on a first buffer use, after which
|
|
// buffer copies and fills read and write the mirror. The first use copies the whole pool; CPU writes between
|
|
// submissions reach the mirror through write watch; GPU writes reach guest memory by the fence, and only the bytes
|
|
// written: CPU bytes beside a GPU fill keep their values.
|
|
static int test_mirror(const GfxBackend *b, uint8_t *guest, int *bytes) {
|
|
*bytes = 0;
|
|
if (!vk_mirror_on) return 0;
|
|
pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0xC00000, .size = 0x100000,
|
|
.game_mem = guest + 0xC00000};
|
|
CHECK(b->pool_init(&pool), "pool_init of a CPU-visible pool for the mirror part");
|
|
uint8_t *m = pool.mem;
|
|
for (int i = 0; i < 4096; i++) m[i] = (uint8_t)(i * 13 + 1);
|
|
memset(m + 0x1000, 0xEE, 0x5000);
|
|
CHECK(vk_mem_mirror_probe((uint64_t)(uintptr_t)m), "the pool got no VRAM mirror");
|
|
static _Alignas(8) uint8_t stream[1024];
|
|
uint64_t a = (uint64_t)(uintptr_t)m;
|
|
// 1: B2B in the mirror after its first-use copy, and a fill; both written back.
|
|
uint8_t *p = put(stream, OP_BEGIN, NULL, 0);
|
|
const p_copy_b2b c1 = {a, a + 0x1000, 4096};
|
|
const p_clear_buf f1 = {a + 0x2000, 0x800, 0x5a5a5a5au, 0};
|
|
p = put(p, OP_COPY_B2B, &c1, sizeof c1);
|
|
p = put(p, OP_CLEAR_BUF, &f1, sizeof f1);
|
|
CHECK(xf_submit(b, stream, p), "fence_wait of the first mirror submission timed out");
|
|
for (int i = 0; i < 4096; i++) CHECK(m[0x1000 + i] == m[i], "mirror B2B byte %d is %02x, expected %02x", i, m[0x1000 + i], m[i]);
|
|
for (int i = 0; i < 0x800; i++) CHECK(m[0x2000 + i] == 0x5a, "mirror fill byte %d is %02x", i, m[0x2000 + i]);
|
|
for (int i = 0x800; i < 0x1000; i++) CHECK(m[0x2000 + i] == 0xEE, "byte %#x past the mirror fill is %02x", i, m[0x2000 + i]);
|
|
// 2: the CPU rewrites the source; the next submission copies the new bytes.
|
|
for (int i = 0; i < 4096; i++) m[i] = (uint8_t)(i * 7 + 5);
|
|
p = put(stream, OP_BEGIN, NULL, 0);
|
|
const p_copy_b2b c2 = {a, a + 0x3000, 4096};
|
|
p = put(p, OP_COPY_B2B, &c2, sizeof c2);
|
|
CHECK(xf_submit(b, stream, p), "fence_wait of the second mirror submission timed out");
|
|
for (int i = 0; i < 4096; i++)
|
|
CHECK(m[0x3000 + i] == m[i], "B2B byte %d after a CPU write is %02x, expected %02x (write not mirrored)", i,
|
|
m[0x3000 + i], m[i]);
|
|
// 3: CPU bytes in the page of a GPU fill, written before the submission, survive the write-back.
|
|
memset(m + 0x4000, 0x11, 0x800);
|
|
p = put(stream, OP_BEGIN, NULL, 0);
|
|
const p_clear_buf f3 = {a + 0x4800, 0x800, 0x77777777u, 0};
|
|
p = put(p, OP_CLEAR_BUF, &f3, sizeof f3);
|
|
CHECK(xf_submit(b, stream, p), "fence_wait of the third mirror submission timed out");
|
|
for (int i = 0; i < 0x800; i++) CHECK(m[0x4000 + i] == 0x11, "CPU byte %#x beside a fill is %02x", i, m[0x4000 + i]);
|
|
for (int i = 0; i < 0x800; i++) CHECK(m[0x4800 + i] == 0x77, "fill byte %#x is %02x", i, m[0x4800 + i]);
|
|
// 4: two small flushes at the ends of the pool copy their own pages, not what lies between them.
|
|
int fl_bad = vk_mem_flush_add_probe();
|
|
CHECK(!fl_bad, "flush range list check %d failed", fl_bad);
|
|
p = put(stream, OP_BEGIN, NULL, 0);
|
|
const p_copy_b2b c4 = {a, a + 0x3000, 4096};
|
|
p = put(p, OP_COPY_B2B, &c4, sizeof c4);
|
|
CHECK(xf_submit(b, stream, p), "fence_wait of the mirror submission before the flushes timed out");
|
|
uint64_t up0 = vk_mem_mirror_uploaded();
|
|
nvn_pool_flushed(&pool, 0x6000, 0x1000);
|
|
nvn_pool_flushed(&pool, 0xF0000, 0x1000);
|
|
p = put(stream, OP_BEGIN, NULL, 0);
|
|
p = put(p, OP_COPY_B2B, &c4, sizeof c4);
|
|
CHECK(xf_submit(b, stream, p), "fence_wait of the mirror submission after the flushes timed out");
|
|
// A submission's copies reach the counter with the next one, and c4's own write-back adds its destination page.
|
|
CHECK(xf_submit(b, stream, p), "fence_wait of the mirror submission that counts the flushes timed out");
|
|
uint64_t up = vk_mem_mirror_uploaded() - up0;
|
|
if (!rt_knob("FLUSH_SPAN", "0"))
|
|
CHECK(up >= 0x2000 && up < 0x10000, "two flushed pages copied %#llx bytes, 0xeb000 would be the span",
|
|
(unsigned long long)up);
|
|
b->pool_fini(&pool);
|
|
*bytes = 4096 + 0x1000 + 4096 + 0x1000;
|
|
return 0;
|
|
}
|
|
|
|
// A pool of zero bytes (TotK makes some now and then; MacNativeGames hit one on 1.2.1) initialises as an empty pool: no
|
|
// Vulkan buffer or memory (a buffer or import of 0 bytes is invalid usage) and no pool index entry, so a lookup at its
|
|
// address still finds the pool that starts there, before and after the empty pool's Finalize.
|
|
static int test_empty_pool(const GfxBackend *b, uint8_t *guest) {
|
|
pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0xE00000, .size = 0x100000,
|
|
.game_mem = guest + 0xE00000};
|
|
pool_t empty = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0xE00000, .size = 0,
|
|
.game_mem = guest + 0xE00000};
|
|
CHECK(b->pool_init(&pool), "pool_init of a CPU-visible pool for the empty-pool part");
|
|
long errors = vk_validation_errors_total();
|
|
CHECK(b->pool_init(&empty), "pool_init of a zero-byte pool");
|
|
uint64_t a = (uint64_t)(uintptr_t)pool.mem;
|
|
VkMemSpan whole, at, after;
|
|
CHECK(vk_mem_resolve(a + 16, 16, &whole) && whole.buffer, "the pool beside an empty pool is not resolved");
|
|
CHECK(vk_mem_resolve(a, 0, &at) && at.buffer == whole.buffer && at.offset == whole.offset - 16,
|
|
"a lookup at the start of a pool finds the empty pool there");
|
|
CHECK(vk_validation_errors_total() == errors, "the zero-byte pool raised %ld validation errors",
|
|
vk_validation_errors_total() - errors);
|
|
b->pool_fini(&empty);
|
|
CHECK(vk_mem_resolve(a + 16, 16, &after) && after.buffer == whole.buffer,
|
|
"the empty pool's Finalize took the pool beside it out of the index");
|
|
b->pool_fini(&pool);
|
|
return 0;
|
|
}
|
|
|
|
int probe_vkexec(void) {
|
|
vk_hdr_off(); // the present checks read 8-bit swapchain pixels
|
|
layouts_equal = 0;
|
|
main_tid = plat_thread_id();
|
|
const GfxBackend *b = &gfx_vulkan_backend;
|
|
uint64_t heap = 0;
|
|
CHECK(mem_set_heap_size(16ull << 20, &heap) == 0 && heap, "SetHeapSize 16 MiB");
|
|
// Out-of-memory waits (oom.c): the first two staging buffers, the first texture image and the first draw-data chunk
|
|
// fail; every part below passes through the wait or the retry that follows.
|
|
rt_oom_force(RT_OOM_STAGING, 2), rt_oom_force(RT_OOM_IMAGE, 1), rt_oom_force(RT_OOM_CHUNK, 1);
|
|
uint8_t *guest = (uint8_t *)(uintptr_t)heap;
|
|
uint64_t first = 0, last = 0;
|
|
int w = 0, h = 0, inside = 0, xfer_bytes = 0;
|
|
int bad = test_sync(b, guest, &first, &last) || test_present(b, guest, &w, &h);
|
|
int sync_bad = bad;
|
|
if (!bad) bad = test_clear(b, guest, &inside);
|
|
int draw_bad = 0, drawn = 0, tries = 0, dispatched = 0;
|
|
if (!bad) bad = draw_bad = vk_pipeline_selftest() || test_draw(b, guest, &drawn, &tries, &dispatched);
|
|
if (!bad) bad = test_xfer(b, guest, &xfer_bytes);
|
|
int zero_bytes = 0;
|
|
if (!bad) bad = test_c0() || test_zero(b, guest, &zero_bytes);
|
|
int nvnx_bytes = 0, mirror_bytes = 0;
|
|
if (!bad) bad = test_nvnx(b, guest, &nvnx_bytes);
|
|
if (!bad) bad = test_mirror(b, guest, &mirror_bytes);
|
|
if (!bad) bad = test_empty_pool(b, guest);
|
|
b->finish();
|
|
vk_pipeline_shutdown(); // the pipelines and the draw part's stand-ins go before the device
|
|
vk_exec_draw_shutdown();
|
|
vk_exec_xfer_shutdown();
|
|
vk_backend_shutdown();
|
|
long errors = vk_validation_errors_total();
|
|
int oom_left[3] = {rt_oom_forced_left(RT_OOM_STAGING), rt_oom_forced_left(RT_OOM_IMAGE), rt_oom_forced_left(RT_OOM_CHUNK)};
|
|
if (!bad && !errors && (oom_left[0] || oom_left[1] || oom_left[2])) {
|
|
printf("probe-vkexec: FAILED (forced allocation failures never reached: staging %d, image %d, chunk %d)\n",
|
|
oom_left[0], oom_left[1], oom_left[2]);
|
|
return 1;
|
|
}
|
|
if (bad || errors) {
|
|
printf("probe-vkexec: FAILED (%s, %ld validation errors)\n",
|
|
sync_bad ? "a sync check failed" : draw_bad ? "a draw check failed" : bad ? "a clear or xfer check failed" : "checks passed", errors);
|
|
return 1;
|
|
}
|
|
printf("probe-vkexec: sync part passed (%d submissions, fences %llu..%llu, %d completion callbacks in submit order "
|
|
"on the completion thread, guest word visible after fence_wait, finish drained, present %dx%d exact, "
|
|
"0 validation errors)\n",
|
|
SUBMITS + TAIL + 1, (unsigned long long)first, (unsigned long long)last, SUBMITS + TAIL, w, h);
|
|
printf("probe-vkexec: clear part passed (RGBA8 + D32 %dx%d: image clears outside a pass, scissored attachment "
|
|
"clears in a pass on %d texels, read back exact, 0 validation errors)\n",
|
|
TEX_W, TEX_H, inside);
|
|
printf("probe-vkexec: draw part passed (pipeline self-test; one triangle of census pair %d changed %d of %d texels of the "
|
|
"cleared RGBA8 target; %d census compute shaders dispatched; 0 validation errors)\n",
|
|
tries, drawn, TEX_W * TEX_H, dispatched);
|
|
if (layouts_equal)
|
|
printf("probe-vkexec: layout part passed (the same draw through independent sets 0 and 1 and the shared set 0: "
|
|
"%d of %d texels equal)\n", layouts_equal, TEX_W * TEX_H);
|
|
else
|
|
printf("probe-vkexec: layout part skipped (no graphics pipeline library: shared set 0 only)\n");
|
|
printf("probe-vkexec: xfer part passed (B2B between CPU-visible pools, B2T + T2B round trip and T2T of a %dx%d RGBA8 "
|
|
"texture exact, CLEAR_TEX exact, event WRITE + ADD, timestamp and samples-passed reports in guest memory after "
|
|
"fence_wait, %d bytes compared, 0 validation errors)\n",
|
|
XF_W, XF_H, xfer_bytes);
|
|
printf("probe-vkexec: indirect part passed (DrawElementsIndirect with GPU-copied arguments: count 3 texel-equal to the "
|
|
"direct draw, count 0 draws nothing, %d texels compared; %d census compute shaders dispatched indirectly)\n",
|
|
ind_draw_texels, ind_dispatches);
|
|
printf("probe-vkexec: nvnx part passed (ClearBuffer then ReportValue in GPU order, a CPU-invisible pool fill copied out, "
|
|
"Downsample 64x64 -> 32x32 within 1 of the 2x2 average, stencil view aspect of D24S8, %d bytes compared)\n",
|
|
nvnx_bytes);
|
|
printf("probe-vkexec: c0 and zero parts passed (c0 tables, dispatch size, point size and base instance at their offsets, "
|
|
"unbound slots zero; %d bytes of an unwritten physical-pool texture read zero, TOTK_FIX_ZERO=%s)\n",
|
|
zero_bytes, cfg_value("TOTK_FIX_ZERO"));
|
|
if (vk_mirror_on)
|
|
printf("probe-vkexec: mirror part passed (B2B and fill in a VRAM mirror written back by the fence, a CPU write "
|
|
"between submissions mirrored, CPU bytes beside a fill kept, %d bytes compared)\n", mirror_bytes);
|
|
else
|
|
printf("probe-vkexec: mirror part skipped (memory mode import)\n");
|
|
if (marks_skipped)
|
|
printf("probe-vkexec: pass marks part skipped (no VK_AMD_buffer_marker, or TOTK_PASS_CRUMBS=0)\n");
|
|
else
|
|
printf("probe-vkexec: pass marks part passed (pass, dispatch, pass in one submission: the GPU's last started and "
|
|
"finished mark is the last pass, ring records exact); the report reads:\n%s\n", marks_text);
|
|
printf("probe-vkexec: empty pool part passed (a zero-byte pool at the start of another: no Vulkan objects, lookups "
|
|
"there find the other pool before and after its Finalize)\n");
|
|
printf("probe-vkexec: out-of-memory part passed (2 failed staging buffers waited for, 1 failed texture image retried, "
|
|
"1 failed draw-data chunk waited for)\n");
|
|
return 0;
|
|
}
|