mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
Every texture initialization got a 128-byte host copy for recordings to point at, kept for the whole run. The game initializes its transient targets again every frame (about 29 new copies a frame on 1.4.3), so host memory grew by about 44 MiB per minute of play, standing still too, until the commit limit ended long sessions (#670). Each pin now counts the recording copies whose ops name it: counted in flatten_recording, uncounted when a copy or a per-submission clone is freed. A pin with no copy left is freed NVN_PIN_AGE (1200) submits after its last use, so a recording still in progress keeps its pins, and never while the replay holds it as a current render target, which carries over to the next submission and is compared by pointer. TOTK_PIN_SWEEP=0 keeps every copy. A 300 s standing-still run on the field save: host memory without VRAM grew 5 MiB, against about 220 MiB before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
935 lines
51 KiB
C
935 lines
51 KiB
C
// totk.exe --probe-nvn: the NVN front end and the null back end, called the way the
|
|
// game calls them: through the real nvnp_* procs with a synthetic Cpu (AAPCS64 arguments in x0..x7 and v0..v7), on the
|
|
// guest main thread, on the null back end whatever TOTK_GPU says. Checks, in order:
|
|
// - pool, buffer, texture, sampler, sync and event objects against MacNativeGames' semantics (hle_nvn.c), the
|
|
// texture storage sizes against the original-SDK goldens of MNG tests/texture_auto_layout/probe.c;
|
|
// - every op kind recorded through the CommandBuffer procs and parsed back with nvn_op_first/next/payload, and
|
|
// CallCommands inlining a recording;
|
|
// - the null back end's CPU-visible side effects (event write and add, counter report, buffer copy, uniform update,
|
|
// in stream order across handles) landing in guest memory at submit, and not before;
|
|
// - the proc table: every name of nvn_procs.inc resolves to its own synthetic address, an unknown name to NULL;
|
|
// - named fatal stubs stop with their name, each in a child process (this executable again, TOTK_PROBE_NVN_CHILD).
|
|
// The 40 layout goldens themselves are run by --probe-nvn-tex against nvn_tex.c; here four of them go through the procs.
|
|
// Ported in part from MacNativeGames games/totk/tests/texture_auto_layout/probe.c @ b75be8cf (MIT, Copyright (c) 2026
|
|
// jhaizhou; origin d2r-mobile): the golden metadata, which holds no game bytes.
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "gfx.h"
|
|
#include "host.h"
|
|
#include "nvn_api.h"
|
|
#include "platform/platform.h"
|
|
|
|
#define CHILD_ENV "TOTK_PROBE_NVN_CHILD"
|
|
|
|
#define NVN_PROC(n) void nvnp_##n(Cpu *c);
|
|
#include "nvn_procs.inc"
|
|
#undef NVN_PROC
|
|
|
|
typedef struct { const char *name; void (*fn)(Cpu *); } ProcRow;
|
|
static const ProcRow procs[] = {
|
|
#define NVN_PROC(n) {#n, nvnp_##n},
|
|
#include "nvn_procs.inc"
|
|
#undef NVN_PROC
|
|
};
|
|
#define NPROCS ((int)(sizeof procs / sizeof *procs))
|
|
|
|
// The named fatal stubs checked in child processes: proc names that no task implements (NvRm interop of the NVX
|
|
// extension), so the check does not go stale when the stub list shrinks.
|
|
static const char *const fatal_procs[] = {"nvnSyncGetNvRmSyncNVX", "nvnTextureGetNvRmSurfaceNVX"};
|
|
#define NFATAL ((int)(sizeof fatal_procs / sizeof *fatal_procs))
|
|
|
|
static int failures, checks, n_objects, n_ops, n_effects;
|
|
#define CHECK(cond, ...) \
|
|
do { checks++; if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
|
|
|
|
// ---- calling a proc
|
|
static Cpu cpu;
|
|
static uint64_t call(void (*f)(Cpu *), const uint64_t a[8]) {
|
|
memcpy(cpu.x, a, 8 * sizeof *a);
|
|
f(&cpu);
|
|
return cpu.x[0];
|
|
}
|
|
#define CALL(n, ...) call(nvnp_##n, (const uint64_t[8]){__VA_ARGS__})
|
|
#define U(p) ((uint64_t)(uintptr_t)(p))
|
|
#define I(x) ((uint64_t)(int64_t)(x))
|
|
static void fv(int i, float f) { // a float argument: low 32 bits of vi
|
|
uint32_t b;
|
|
memcpy(&b, &f, 4);
|
|
cpu.v[i] = b;
|
|
}
|
|
|
|
// ---- the objects, in place like the game's (each type is its NVN reserved size)
|
|
#define OBJ(T, n) static T n __attribute__((aligned(16)))
|
|
OBJ(NVNdevice, dev);
|
|
OBJ(NVNdeviceBuilder, devb);
|
|
OBJ(NVNmemoryPool, pool); // CPU-visible, game storage
|
|
OBJ(NVNmemoryPool, gpool); // physical, CPU_NO_ACCESS: GPU window
|
|
OBJ(NVNmemoryPool, vpool); // virtual
|
|
OBJ(NVNmemoryPoolBuilder, poolb);
|
|
OBJ(NVNqueue, queue);
|
|
OBJ(NVNqueueBuilder, queueb);
|
|
static uint8_t *pmem, *gmem;
|
|
static const uint64_t PROBE_POOL_SIZE = 0x40000, PROBE_GPOOL_SIZE = 0x20000;
|
|
|
|
static pool_t *P(void) { return (pool_t *)(void *)&pool; }
|
|
static pool_t *G(void) { return (pool_t *)(void *)&gpool; }
|
|
|
|
// ---- pools
|
|
static uint64_t make_pool(NVNmemoryPool *p, void *storage, uint64_t size, uint32_t flags) {
|
|
CALL(nvnMemoryPoolBuilderSetDefaults, U(&poolb));
|
|
CALL(nvnMemoryPoolBuilderSetDevice, U(&poolb), U(&dev));
|
|
CALL(nvnMemoryPoolBuilderSetStorage, U(&poolb), U(storage), size);
|
|
CALL(nvnMemoryPoolBuilderSetFlags, U(&poolb), flags);
|
|
return (uint8_t)CALL(nvnMemoryPoolInitialize, U(p), U(&poolb));
|
|
}
|
|
|
|
static void probe_device_and_pools(void) {
|
|
CALL(nvnDeviceBuilderSetDefaults, U(&devb));
|
|
CHECK((uint8_t)CALL(nvnDeviceInitialize, U(&dev), U(&devb)) == 1, "DeviceInitialize");
|
|
int abi[2] = {0, 0};
|
|
CALL(nvnDeviceGetInteger, U(&dev), 0, U(&abi[0]));
|
|
CALL(nvnDeviceGetInteger, U(&dev), 1, U(&abi[1]));
|
|
CHECK(abi[0] == 55 && abi[1] >= 15, "device ABI %d.%d", abi[0], abi[1]);
|
|
|
|
pmem = mem_kernel_alloc(PROBE_POOL_SIZE, 0x1000);
|
|
gmem = mem_kernel_alloc(PROBE_GPOOL_SIZE, 0x1000);
|
|
CHECK((uint8_t)make_pool(&pool, pmem, PROBE_POOL_SIZE, NVN_MEMORY_POOL_FLAGS_CPU_CACHED | NVN_MEMORY_POOL_FLAGS_GPU_CACHED) == 1,
|
|
"CPU-visible pool");
|
|
CHECK(P()->magic == NVN_POOL_MAGIC && P()->mem == pmem && P()->size == PROBE_POOL_SIZE, "pool fields");
|
|
CHECK(CALL(nvnMemoryPoolGetFlags, U(&pool)) == (NVN_MEMORY_POOL_FLAGS_CPU_CACHED | NVN_MEMORY_POOL_FLAGS_GPU_CACHED),
|
|
"pool flags");
|
|
CHECK(CALL(nvnMemoryPoolMap, U(&pool)) == U(pmem) && CALL(nvnMemoryPoolGetBufferAddress, U(&pool)) == U(pmem),
|
|
"pool map and address are the guest storage");
|
|
CHECK(CALL(nvnMemoryPoolGetSize, U(&pool)) == PROBE_POOL_SIZE, "pool size");
|
|
CHECK(nvn_pool_find(U(pmem) + 0x100, 0x200) == P() && !nvn_pool_find(U(pmem) + PROBE_POOL_SIZE - 8, 16), "pool index");
|
|
n_objects += 6;
|
|
|
|
// refused: no storage, PHYSICAL | VIRTUAL, a size that is not a 64 KiB multiple (MNG totk_vpool.c)
|
|
static NVNmemoryPool bad;
|
|
CHECK(make_pool(&bad, NULL, 0x10000, NVN_MEMORY_POOL_FLAGS_CPU_CACHED) == 0, "a pool without storage is refused");
|
|
CHECK(make_pool(&bad, gmem, PROBE_GPOOL_SIZE, NVN_MEMORY_POOL_FLAGS_PHYSICAL | NVN_MEMORY_POOL_FLAGS_VIRTUAL) == 0,
|
|
"PHYSICAL | VIRTUAL is refused");
|
|
CHECK(make_pool(&bad, gmem, 0x1800, NVN_MEMORY_POOL_FLAGS_PHYSICAL | NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS) == 0,
|
|
"a physical pool of 0x1800 bytes is refused");
|
|
n_objects += 3;
|
|
|
|
// a CPU-invisible physical pool lives in the GPU window, has no CPU view, and is not in the host address space
|
|
CHECK((uint8_t)make_pool(&gpool, gmem, PROBE_GPOOL_SIZE, NVN_MEMORY_POOL_FLAGS_PHYSICAL | NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS |
|
|
NVN_MEMORY_POOL_FLAGS_GPU_CACHED) == 1, "physical pool");
|
|
CHECK(nvn_gpu_only(U(G()->mem)) && G()->game_mem == gmem && G()->span == PROBE_GPOOL_SIZE, "physical pool placed in the GPU window");
|
|
CHECK(CALL(nvnMemoryPoolMap, U(&gpool)) == 0, "a CPU_NO_ACCESS pool has no CPU view");
|
|
CHECK(CALL(nvnMemoryPoolGetBufferAddress, U(&gpool)) == U(G()->mem), "GPU address of the physical pool");
|
|
|
|
// a virtual pool: requests are validated before any mapping changes
|
|
CHECK((uint8_t)make_pool(&vpool, NULL, PROBE_POOL_SIZE, NVN_MEMORY_POOL_FLAGS_VIRTUAL | NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS) == 1,
|
|
"virtual pool");
|
|
nvn_mapping_t map[2] = {{(const NVNmemoryPool *)&gpool, 0, 0x10000, 0x10000}, {NULL, 0, 0x10000, 0x10000}};
|
|
CHECK((uint8_t)CALL(nvnMemoryPoolMapVirtual, U(&vpool), 1, U(map)) == 1, "map 64 KiB into the virtual pool");
|
|
CHECK((uint8_t)CALL(nvnMemoryPoolMapVirtual, U(&vpool), 2, U(map)) == 1, "map then unmap");
|
|
nvn_mapping_t bad_map[3] = {{(const NVNmemoryPool *)&gpool, 0, 0x100, 0x10000}, // unaligned
|
|
{(const NVNmemoryPool *)&gpool, 0, 0x30000, 0x20000}, // past the virtual pool
|
|
{(const NVNmemoryPool *)&pool, 0, 0, 0x10000}}; // source is not physical
|
|
for (int i = 0; i < 3; i++)
|
|
CHECK((uint8_t)CALL(nvnMemoryPoolMapVirtual, U(&vpool), 1, U(&bad_map[i])) == 0, "invalid mapping request %d accepted", i);
|
|
n_objects += 9;
|
|
|
|
// Finalize returns a GPU window range, so the pool can be initialized again
|
|
uint64_t gva = U(((pool_t *)(void *)&vpool)->mem);
|
|
CALL(nvnMemoryPoolFinalize, U(&vpool));
|
|
CHECK(((pool_t *)(void *)&vpool)->magic == 0 && !nvn_gpu_only(U(((pool_t *)(void *)&vpool)->mem)), "virtual pool finalized");
|
|
CHECK((uint8_t)make_pool(&vpool, NULL, PROBE_POOL_SIZE, NVN_MEMORY_POOL_FLAGS_VIRTUAL | NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS) == 1 &&
|
|
U(((pool_t *)(void *)&vpool)->mem) == gva, "the freed window range is reused");
|
|
CALL(nvnMemoryPoolFinalize, U(&vpool));
|
|
n_objects += 2;
|
|
}
|
|
|
|
// ---- buffers
|
|
OBJ(NVNbufferBuilder, bufb);
|
|
OBJ(NVNbuffer, buf);
|
|
OBJ(NVNbuffer, gbuf);
|
|
|
|
static void probe_buffers(void) {
|
|
CALL(nvnBufferBuilderSetDevice, U(&bufb), U(&dev));
|
|
CALL(nvnBufferBuilderSetDefaults, U(&bufb));
|
|
CALL(nvnBufferBuilderSetStorage, U(&bufb), U(&pool), 0x100, 0x200);
|
|
CHECK((uint8_t)CALL(nvnBufferInitialize, U(&buf), U(&bufb)) == 1, "BufferInitialize");
|
|
CHECK(CALL(nvnBufferGetAddress, U(&buf)) == U(pmem) + 0x100 && CALL(nvnBufferMap, U(&buf)) == U(pmem) + 0x100,
|
|
"buffer address and map are pool storage + offset (MNG hle_nvn.c:237, 248)");
|
|
CHECK(CALL(nvnBufferGetSize, U(&buf)) == 0x200 && CALL(nvnBufferGetMemoryOffset, U(&buf)) == 0x100 &&
|
|
CALL(nvnBufferGetMemoryPool, U(&buf)) == U(&pool), "buffer size, offset and pool");
|
|
CALL(nvnBufferFlushMappedRange, U(&buf), 0, 0x200);
|
|
|
|
CALL(nvnBufferBuilderSetStorage, U(&bufb), U(&pool), I(PROBE_POOL_SIZE - 0x100), 0x200);
|
|
CHECK((uint8_t)CALL(nvnBufferInitialize, U(&buf), U(&bufb)) == 0, "a buffer past the end of its pool is refused");
|
|
CALL(nvnBufferBuilderSetStorage, U(&bufb), U(&pool), 0x100, 0x200);
|
|
CALL(nvnBufferInitialize, U(&buf), U(&bufb));
|
|
CALL(nvnBufferFinalize, U(&buf));
|
|
CHECK(((const uint32_t *)(const void *)&buf)[0] == 0, "BufferFinalize clears the object");
|
|
|
|
CALL(nvnBufferBuilderSetStorage, U(&bufb), U(&gpool), 0x1000, 0x400);
|
|
CHECK((uint8_t)CALL(nvnBufferInitialize, U(&gbuf), U(&bufb)) == 1, "buffer in a GPU-window pool");
|
|
CHECK(CALL(nvnBufferMap, U(&gbuf)) == 0 && CALL(nvnBufferGetAddress, U(&gbuf)) == U(G()->mem) + 0x1000,
|
|
"a GPU-window buffer has no CPU view and a window address");
|
|
n_objects += 7;
|
|
}
|
|
|
|
// ---- textures and views
|
|
OBJ(NVNtextureBuilder, texb);
|
|
OBJ(NVNtexture, tex1);
|
|
OBJ(NVNtexture, tex2);
|
|
OBJ(NVNtexture, tex3);
|
|
OBJ(NVNtextureView, view);
|
|
|
|
static void set_tex(int fmt, int w, int h, int levels, uint64_t off) {
|
|
CALL(nvnTextureBuilderSetDevice, U(&texb), U(&dev));
|
|
CALL(nvnTextureBuilderSetDefaults, U(&texb));
|
|
CALL(nvnTextureBuilderSetTarget, U(&texb), NVN_TEXTURE_TARGET_2D);
|
|
CALL(nvnTextureBuilderSetSize2D, U(&texb), I(w), I(h));
|
|
CALL(nvnTextureBuilderSetLevels, U(&texb), I(levels));
|
|
CALL(nvnTextureBuilderSetFormat, U(&texb), I(fmt));
|
|
CALL(nvnTextureBuilderSetStorage, U(&texb), U(&pool), off);
|
|
}
|
|
|
|
// Four of the 40 original-SDK goldens of MNG texture_auto_layout/probe.c: {format, width, height, levels, size}
|
|
static const struct { int format, width, height, levels; uint64_t size; } layout_goldens[] = {
|
|
{0x25, 40, 9, 6, 6144}, {0x25, 40, 128, 8, 37888}, {0x48, 160, 160, 8, 57344}, {0x48, 160, 516, 10, 222208}};
|
|
|
|
static void probe_textures(void) {
|
|
for (size_t i = 0; i < sizeof layout_goldens / sizeof *layout_goldens; i++) {
|
|
set_tex(layout_goldens[i].format, layout_goldens[i].width, layout_goldens[i].height, layout_goldens[i].levels, 0);
|
|
uint64_t size = CALL(nvnTextureBuilderGetStorageSize, U(&texb));
|
|
CHECK(size == layout_goldens[i].size, "storage size of %#x %dx%d: %llu, expected %llu", layout_goldens[i].format,
|
|
layout_goldens[i].width, layout_goldens[i].height, (unsigned long long)size,
|
|
(unsigned long long)layout_goldens[i].size);
|
|
CHECK(CALL(nvnTextureBuilderGetStorageAlignment, U(&texb)) == 512, "storage alignment");
|
|
n_objects += 2;
|
|
}
|
|
|
|
set_tex(NVN_FORMAT_RGBA8, 40, 128, 8, 0x1000);
|
|
CHECK((uint8_t)CALL(nvnTextureInitialize, U(&tex1), U(&texb)) == 1, "TextureInitialize");
|
|
const tex_t *t1 = (const tex_t *)(const void *)&tex1;
|
|
CHECK(t1->magic == NVN_TEX_MAGIC && t1->serial && t1->mem == pmem + 0x1000 && t1->L.size == 37888, "texture fields");
|
|
CHECK(CALL(nvnTextureGetWidth, U(&tex1)) == 40 && CALL(nvnTextureGetHeight, U(&tex1)) == 128 &&
|
|
CALL(nvnTextureGetDepth, U(&tex1)) == 1 && CALL(nvnTextureGetStorageSize, U(&tex1)) == 37888, "texture queries");
|
|
CHECK(CALL(nvnTextureGetStorageClass, U(&tex1)) == CALL(nvnTextureBuilderGetStorageClass, U(&texb)), "storage class");
|
|
tex_t live;
|
|
CHECK(nvn_tex_live_at(pmem + 0x1000, &live) && live.serial == t1->serial, "the live texture table resolves the storage");
|
|
|
|
// a second texture and a third, in the other pools of the layout
|
|
set_tex(NVN_FORMAT_RGBA_DXT5_SRGB, 160, 160, 8, 0x10000);
|
|
CHECK((uint8_t)CALL(nvnTextureInitialize, U(&tex2), U(&texb)) == 1, "BC3 texture");
|
|
CHECK(CALL(nvnTextureGetStorageSize, U(&tex2)) == 57344, "BC3 160x160 storage size");
|
|
CHECK(CALL(nvnTextureCompare, U(&tex1), U(&tex1)) == 1 && CALL(nvnTextureCompare, U(&tex1), U(&tex2)) == 0, "TextureCompare");
|
|
|
|
// packaged texture data is copied into the pool storage at Initialize
|
|
set_tex(NVN_FORMAT_RGBA8, 4, 4, 1, 0x20000);
|
|
uint64_t need = CALL(nvnTextureBuilderGetStorageSize, U(&texb));
|
|
static uint8_t pkg[4096];
|
|
CHECK(need > 0 && need <= sizeof pkg, "packaged texture size %llu", (unsigned long long)need);
|
|
for (size_t i = 0; i < sizeof pkg; i++) pkg[i] = (uint8_t)(i * 7 + 3);
|
|
CALL(nvnTextureBuilderSetPackagedTextureData, U(&texb), U(pkg));
|
|
CHECK(CALL(nvnTextureBuilderGetPackagedTextureData, U(&texb)) == U(pkg), "packaged data pointer");
|
|
CHECK((uint8_t)CALL(nvnTextureInitialize, U(&tex3), U(&texb)) == 1, "packaged texture");
|
|
CHECK(!memcmp(pmem + 0x20000, pkg, (size_t)need), "packaged data copied into the pool storage");
|
|
|
|
// views: levels and layers, and the byte offset of a level (hand-derived in nvn_tex_probe.c)
|
|
CALL(nvnTextureViewSetDefaults, U(&view));
|
|
CALL(nvnTextureViewSetLevels, U(&view), 1, 3);
|
|
CALL(nvnTextureViewSetLayers, U(&view), 2, 4);
|
|
int layers[2] = {0, 0};
|
|
CHECK((uint8_t)CALL(nvnTextureViewGetLayers, U(&view), U(&layers[0]), U(&layers[1])) == 1 && layers[0] == 2 && layers[1] == 4,
|
|
"view layers");
|
|
CALL(nvnTextureViewSetLayers, U(&view), 0, 1);
|
|
CHECK(CALL(nvnTextureGetViewOffset, U(&tex1), U(&view)) == 24576, "offset of level 1 of the RGBA8 40x128 texture");
|
|
CHECK(CALL(nvnTextureGetViewOffset, U(&tex1), 0) == 0, "no view, no offset");
|
|
// depth-stencil mode: unset reads DEPTH and false; STENCIL is kept, read back and compared
|
|
OBJ(NVNtextureView, sview);
|
|
int dsm = -1;
|
|
CALL(nvnTextureViewSetDefaults, U(&sview));
|
|
CHECK((uint8_t)CALL(nvnTextureViewGetDepthStencilMode, U(&sview), U(&dsm)) == 0 && dsm == NVN_TEXTURE_DEPTH_STENCIL_MODE_DEPTH,
|
|
"default view depth-stencil mode");
|
|
CHECK((uint8_t)CALL(nvnTextureViewCompare, U(&sview), U(&view)) == 0, "views with other levels compare unequal");
|
|
OBJ(NVNtextureView, sview2);
|
|
CALL(nvnTextureViewSetDefaults, U(&sview2));
|
|
CALL(nvnTextureViewSetDepthStencilMode, U(&sview), NVN_TEXTURE_DEPTH_STENCIL_MODE_STENCIL);
|
|
CHECK((uint8_t)CALL(nvnTextureViewGetDepthStencilMode, U(&sview), U(&dsm)) == 1 && dsm == NVN_TEXTURE_DEPTH_STENCIL_MODE_STENCIL,
|
|
"view depth-stencil mode STENCIL");
|
|
CHECK((uint8_t)CALL(nvnTextureViewCompare, U(&sview), U(&sview2)) == 0, "a STENCIL view differs from a default one");
|
|
CALL(nvnTextureViewSetDepthStencilMode, U(&sview2), NVN_TEXTURE_DEPTH_STENCIL_MODE_STENCIL);
|
|
CHECK((uint8_t)CALL(nvnTextureViewCompare, U(&sview), U(&sview2)) == 1, "two STENCIL views compare equal");
|
|
CHECK(((const view_t *)(const void *)&sview)->set_dsmode && ((const view_t *)(const void *)&sview)->dsmode == 1 &&
|
|
!((const view_t *)(const void *)&sview)->set_levels, "STENCIL view bits");
|
|
|
|
// descriptor pools
|
|
OBJ(NVNtexturePool, tp);
|
|
CHECK((uint8_t)CALL(nvnTexturePoolInitialize, U(&tp), U(&pool), 0x30000, 16) == 1, "texture pool");
|
|
CALL(nvnTexturePoolRegisterTexture, U(&tp), 3, U(&tex1), 0);
|
|
|
|
CALL(nvnTextureFinalize, U(&tex3));
|
|
CHECK(((const tex_t *)(const void *)&tex3)->magic == 0 && !nvn_tex_live_at(pmem + 0x20000, &live), "TextureFinalize");
|
|
n_objects += 15;
|
|
}
|
|
|
|
// ---- samplers
|
|
OBJ(NVNsamplerBuilder, sampb);
|
|
OBJ(NVNsampler, samp);
|
|
|
|
static void probe_samplers(void) {
|
|
CALL(nvnSamplerBuilderSetDevice, U(&sampb), U(&dev));
|
|
CALL(nvnSamplerBuilderSetDefaults, U(&sampb));
|
|
uint32_t mn = 99, mg = 99, ws = 99, wt = 99, wr = 99;
|
|
CALL(nvnSamplerBuilderGetMinMagFilter, U(&sampb), U(&mn), U(&mg));
|
|
CALL(nvnSamplerBuilderGetWrapMode, U(&sampb), U(&ws), U(&wt), U(&wr));
|
|
CHECK(mn == NVN_MIN_FILTER_NEAREST_MIPMAP_LINEAR && mg == NVN_MAG_FILTER_LINEAR && ws == NVN_WRAP_MODE_REPEAT &&
|
|
wt == NVN_WRAP_MODE_REPEAT && wr == NVN_WRAP_MODE_REPEAT, "sampler defaults are the GL ones");
|
|
|
|
CALL(nvnSamplerBuilderSetMinMagFilter, U(&sampb), NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR, NVN_MAG_FILTER_NEAREST);
|
|
CALL(nvnSamplerBuilderSetWrapMode, U(&sampb), NVN_WRAP_MODE_CLAMP, NVN_WRAP_MODE_MIRROR_CLAMP, NVN_WRAP_MODE_REPEAT);
|
|
fv(0, 0.5f);
|
|
fv(1, 7.f);
|
|
CALL(nvnSamplerBuilderSetLodClamp, U(&sampb));
|
|
fv(0, -1.5f);
|
|
CALL(nvnSamplerBuilderSetLodBias, U(&sampb));
|
|
fv(0, 4.f);
|
|
CALL(nvnSamplerBuilderSetMaxAnisotropy, U(&sampb));
|
|
float border[4] = {0.25f, 0.5f, 0.75f, 1.f};
|
|
CALL(nvnSamplerBuilderSetBorderColor, U(&sampb), U(border));
|
|
CALL(nvnSamplerBuilderSetReductionFilter, U(&sampb), 1);
|
|
CHECK((uint8_t)CALL(nvnSamplerInitialize, U(&samp), U(&sampb)) == 1, "SamplerInitialize");
|
|
const samp_t *s = (const samp_t *)(const void *)&samp;
|
|
CHECK(s->magic == NVN_SAMP_MAGIC && s->p.minf == NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR && s->p.magf == NVN_MAG_FILTER_NEAREST &&
|
|
s->p.ws == NVN_WRAP_MODE_CLAMP && s->p.wt == NVN_WRAP_MODE_MIRROR_CLAMP && s->p.wr == NVN_WRAP_MODE_REPEAT,
|
|
"sampler filters and wrap modes");
|
|
CHECK(s->p.lod_min == 0.5f && s->p.lod_max == 7.f && s->p.lod_bias == -1.5f && s->p.aniso == 4.f &&
|
|
!memcmp(s->p.border, border, 16), "sampler LOD, anisotropy and border");
|
|
CHECK(CALL(nvnSamplerGetReductionFilter, U(&samp)) == 1 && CALL(nvnSamplerBuilderGetReductionFilter, U(&sampb)) == 1,
|
|
"reduction filter");
|
|
OBJ(NVNsamplerPool, sp);
|
|
CHECK((uint8_t)CALL(nvnSamplerPoolInitialize, U(&sp), U(&pool), 0x31000, 8) == 1, "sampler pool");
|
|
CALL(nvnSamplerPoolRegisterSampler, U(&sp), 2, U(&samp));
|
|
CALL(nvnSamplerFinalize, U(&samp));
|
|
CHECK(((const samp_t *)(const void *)&samp)->magic == 0, "SamplerFinalize");
|
|
CALL(nvnSamplerInitialize, U(&samp), U(&sampb));
|
|
n_objects += 6;
|
|
}
|
|
|
|
// ---- sync and events
|
|
OBJ(NVNsync, syncs);
|
|
OBJ(NVNsync, sync_future);
|
|
OBJ(NVNevent, ev);
|
|
OBJ(NVNevent, ev2);
|
|
OBJ(NVNeventBuilder, evb);
|
|
|
|
static void probe_sync_events(void) {
|
|
CHECK((uint8_t)CALL(nvnSyncInitialize, U(&syncs), U(&dev)) == 1, "SyncInitialize");
|
|
CHECK(CALL(nvnSyncWait, U(&syncs), 1000) == NVN_SYNC_WAIT_RESULT_TIMEOUT_EXPIRED, "a sync never fenced times out at once");
|
|
CALL(nvnSyncInitialize, U(&sync_future), U(&dev));
|
|
sync_t *sf = (sync_t *)(void *)&sync_future;
|
|
sf->fenced = 1;
|
|
sf->fence = nvn_fence_last() + 100;
|
|
CHECK(CALL(nvnSyncWait, U(&sync_future), 1000000) == NVN_SYNC_WAIT_RESULT_TIMEOUT_EXPIRED,
|
|
"a fence value that was never submitted is never reached");
|
|
CALL(nvnSyncFinalize, U(&sync_future));
|
|
CHECK(sf->magic == 0, "SyncFinalize");
|
|
|
|
CALL(nvnEventBuilderSetDefaults, U(&evb));
|
|
CALL(nvnEventBuilderSetStorage, U(&evb), U(&pool), 0);
|
|
CHECK((uint8_t)CALL(nvnEventInitialize, U(&ev), U(&evb)) == 1, "EventInitialize");
|
|
CHECK(((const ev_t *)(const void *)&ev)->val == (volatile uint32_t *)pmem, "the event value lives in pool storage");
|
|
CHECK((uint32_t)CALL(nvnEventGetValue, U(&ev)) == 0, "a new event reads 0");
|
|
CALL(nvnEventSignal, U(&ev), NVN_EVENT_SIGNAL_MODE_WRITE, 7);
|
|
CHECK((uint32_t)CALL(nvnEventGetValue, U(&ev)) == 7 && *(uint32_t *)pmem == 7, "EventSignal WRITE");
|
|
CALL(nvnEventSignal, U(&ev), NVN_EVENT_SIGNAL_MODE_ADD, 3);
|
|
CHECK((uint32_t)CALL(nvnEventGetValue, U(&ev)) == 10, "EventSignal ADD");
|
|
CALL(nvnEventSignal, U(&ev), NVN_EVENT_SIGNAL_MODE_WRITE, 0);
|
|
|
|
CALL(nvnEventBuilderSetStorage, U(&evb), U(&gpool), 0);
|
|
CHECK((uint8_t)CALL(nvnEventInitialize, U(&ev2), U(&evb)) == 0, "an event in a GPU-window pool is refused");
|
|
CALL(nvnEventFinalize, U(&ev2));
|
|
|
|
// QueueBuilder defaults, then the queue
|
|
CALL(nvnQueueBuilderSetDevice, U(&queueb), U(&dev));
|
|
CALL(nvnQueueBuilderSetDefaults, U(&queueb));
|
|
CHECK(CALL(nvnQueueBuilderGetQueueMemorySize, U(&queueb)) == ((65536u + 262144u + 16384u + 4095) & ~4095u), "queue memory size");
|
|
CHECK((uint8_t)CALL(nvnQueueInitialize, U(&queue), U(&queueb)) == 1, "QueueInitialize");
|
|
|
|
OBJ(NVNwindow, win);
|
|
uint8_t before[sizeof win];
|
|
memcpy(before, &win, sizeof win);
|
|
CALL(nvnWindowFinalize, U(&win));
|
|
CHECK(!memcmp(before, &win, sizeof win), "WindowFinalize is a no-op");
|
|
n_objects += 11;
|
|
}
|
|
|
|
// ---- recording every op kind and parsing it back
|
|
OBJ(NVNcommandBuffer, cmd);
|
|
OBJ(NVNcommandBuffer, cmd2);
|
|
OBJ(NVNprogram, prog);
|
|
static uint8_t *ctlmem1, *ctlmem2, *ctlmem3;
|
|
|
|
static uint32_t expect[128];
|
|
static int nexpect;
|
|
#define REC(op, call) do { call; expect[nexpect++] = (op); } while (0)
|
|
|
|
static void begin(NVNcommandBuffer *c, uint8_t *ctl, uint64_t cmd_off) {
|
|
CHECK((uint8_t)CALL(nvnCommandBufferInitialize, U(c), U(&dev)) == 1, "CommandBufferInitialize");
|
|
CALL(nvnCommandBufferAddCommandMemory, U(c), U(&pool), cmd_off, 0x4000);
|
|
CALL(nvnCommandBufferAddControlMemory, U(c), U(ctl), 0x1000);
|
|
CALL(nvnCommandBufferBeginRecording, U(c));
|
|
}
|
|
|
|
static uint64_t record_all(void) {
|
|
NVNcommandBuffer *c = &cmd;
|
|
begin(c, ctlmem1, 0x32000);
|
|
static const float rgba[4] = {0.1f, 0.2f, 0.3f, 0.4f};
|
|
static const uint8_t ub_data[8] = {1, 2, 3, 4, 5, 6, 7, 8};
|
|
const NVNtexture *colors[1] = {&tex1};
|
|
NVNcopyRegion rg = {1, 2, 3, 4, 5, 1}, rg2 = {6, 7, 8, 9, 10, 1};
|
|
float vp[8] = {1, 2, 3, 4, 5, 6, 7, 8};
|
|
uint32_t attr[2] = {0x11223344, 0x55667788};
|
|
struct { uint32_t stride, divisor; } stream[2] = {{16, 0}, {32, 1}};
|
|
NVNbufferRange vb[2] = {{0x1000, 0x100}, {0x2000, 0x200}};
|
|
uint8_t blend[8] = {1, 2, 3, 4, 5, 6, 7, 8}, poly[4] = {9, 10, 11, 12};
|
|
uint64_t handles[3] = {0x300005, 0x400006, 0x500007};
|
|
cpu.x[30] = 0x7000001234ull;
|
|
OBJ(NVNtexturePool, tp2);
|
|
OBJ(NVNsamplerPool, sp2);
|
|
CALL(nvnTexturePoolInitialize, U(&tp2), U(&pool), 0x30000, 16);
|
|
CALL(nvnSamplerPoolInitialize, U(&sp2), U(&pool), 0x31000, 8);
|
|
CALL(nvnProgramInitialize, U(&prog), U(&dev));
|
|
CALL(nvnTextureViewSetDefaults, U(&view));
|
|
CALL(nvnTextureViewSetLevels, U(&view), 1, 3);
|
|
|
|
REC(OP_SIGNAL_EVENT, CALL(nvnCommandBufferSignalEvent, U(c), U(&ev), NVN_EVENT_SIGNAL_MODE_ADD, NVN_EVENT_SIGNAL_LOCATION_BOTTOM, 0, 0x55));
|
|
REC(OP_REPORT_COUNTER, CALL(nvnCommandBufferReportCounter, U(c), NVN_COUNTER_TYPE_SAMPLES_PASSED, 0xaabb00));
|
|
REC(OP_RESET_COUNTER, CALL(nvnCommandBufferResetCounter, U(c), NVN_COUNTER_TYPE_SAMPLES_PASSED));
|
|
REC(OP_COPY_B2B, CALL(nvnCommandBufferCopyBufferToBuffer, U(c), 0x1000, 0x2000, 0x40, 0));
|
|
REC(OP_UPDATE_UB, CALL(nvnCommandBufferUpdateUniformBuffer, U(c), 0x3000, 0x100, 0x10, 8, U(ub_data)));
|
|
REC(OP_SET_RT, CALL(nvnCommandBufferSetRenderTargets, U(c), 1, U(colors), 0, U(&tex2), 0));
|
|
REC(OP_CLEAR_COLOR, CALL(nvnCommandBufferClearColor, U(c), 2, U(rgba), 0xf));
|
|
fv(0, 0.25f);
|
|
REC(OP_CLEAR_DS, CALL(nvnCommandBufferClearDepthStencil, U(c), 1, 5, 0xff));
|
|
REC(OP_VIEWPORTS, CALL(nvnCommandBufferSetViewports, U(c), 1, 2, U(vp)));
|
|
REC(OP_SCISSOR, CALL(nvnCommandBufferSetScissor, U(c), 1, 2, 3, 4));
|
|
fv(0, 0.125f);
|
|
fv(1, 0.875f);
|
|
REC(OP_DEPTH_RANGE, CALL(nvnCommandBufferSetDepthRange, U(c)));
|
|
REC(OP_BIND_PROGRAM, CALL(nvnCommandBufferBindProgram, U(c), U(&prog), 3));
|
|
REC(OP_BIND_STATE, CALL(nvnCommandBufferBindBlendState, U(c), U(blend)));
|
|
REC(OP_BIND_STATE, CALL(nvnCommandBufferBindPolygonState, U(c), U(poly)));
|
|
REC(OP_BIND_VATTR, CALL(nvnCommandBufferBindVertexAttribState, U(c), 2, U(attr)));
|
|
REC(OP_BIND_VSTREAM, CALL(nvnCommandBufferBindVertexStreamState, U(c), 2, U(stream)));
|
|
REC(OP_BIND_VBUF, CALL(nvnCommandBufferBindVertexBuffers, U(c), 1, 2, U(vb)));
|
|
REC(OP_BIND_UBO, CALL(nvnCommandBufferBindUniformBuffer, U(c), NVN_SHADER_STAGE_FRAGMENT, 3, 0x6000, 0x80));
|
|
REC(OP_BIND_SSBO, CALL(nvnCommandBufferBindStorageBuffer, U(c), NVN_SHADER_STAGE_COMPUTE, 1, 0x7000, 0x90));
|
|
REC(OP_BIND_TEX, CALL(nvnCommandBufferBindTexture, U(c), NVN_SHADER_STAGE_FRAGMENT, 2, 0x300005));
|
|
REC(OP_BIND_TEX, CALL(nvnCommandBufferBindTexturesDeferred, U(c), NVN_SHADER_STAGE_VERTEX, 0, 3, U(handles)));
|
|
REC(OP_BIND_SAMP, CALL(nvnCommandBufferBindSeparateSampler, U(c), NVN_SHADER_STAGE_FRAGMENT, 4, 0x100000));
|
|
REC(OP_BIND_IMG, CALL(nvnCommandBufferBindImage, U(c), NVN_SHADER_STAGE_COMPUTE, 1, 7));
|
|
REC(OP_STENCIL, CALL(nvnCommandBufferSetStencilRef, U(c), NVN_FACE_FRONT, 9));
|
|
fv(0, 1.5f);
|
|
fv(1, 2.5f);
|
|
fv(2, 3.5f);
|
|
REC(OP_POLY_OFFSET, CALL(nvnCommandBufferSetPolygonOffsetClamp, U(c)));
|
|
REC(OP_DRAW, CALL(nvnCommandBufferDrawArrays, U(c), NVN_DRAW_PRIMITIVE_TRIANGLES, 0, 6));
|
|
REC(OP_DRAW_IDX, CALL(nvnCommandBufferDrawElementsInstanced, U(c), NVN_DRAW_PRIMITIVE_TRIANGLES, NVN_INDEX_TYPE_UNSIGNED_SHORT, 12,
|
|
0x8000, I(-3), 2, 5));
|
|
REC(OP_DISPATCH, CALL(nvnCommandBufferDispatchCompute, U(c), 8, 4, 2));
|
|
REC(OP_BARRIER, CALL(nvnCommandBufferBarrier, U(c), 3));
|
|
REC(OP_SET_TEX_POOL, CALL(nvnCommandBufferSetTexturePool, U(c), U(&tp2)));
|
|
REC(OP_SET_SAMP_POOL, CALL(nvnCommandBufferSetSamplerPool, U(c), U(&sp2)));
|
|
REC(OP_COPY_B2T, CALL(nvnCommandBufferCopyBufferToTexture, U(c), 0x9000, U(&tex1), U(&view), U(&rg), 0));
|
|
REC(OP_COPY_STRIDE, CALL(nvnCommandBufferSetCopyRowStride, U(c), 256));
|
|
REC(OP_BLEND_COLOR, CALL(nvnCommandBufferSetBlendColor, U(c), U(rgba)));
|
|
REC(OP_COPY_T2B, CALL(nvnCommandBufferCopyTextureToBuffer, U(c), U(&tex1), 0, U(&rg), 0xa000, 0));
|
|
REC(OP_COPY_T2T, CALL(nvnCommandBufferCopyTextureToTexture, U(c), U(&tex1), 0, U(&rg), U(&tex2), 0, U(&rg2), 0));
|
|
REC(OP_CLEAR_TEX, CALL(nvnCommandBufferClearTexture, U(c), U(&tex2), 0, U(&rg2), U(rgba), 0xf));
|
|
REC(OP_DEPTH_CLAMP, CALL(nvnCommandBufferSetDepthClamp, U(c), 1));
|
|
REC(OP_RENDER_ENABLE, CALL(nvnCommandBufferSetRenderEnable, U(c), 0));
|
|
REC(OP_RASTER_DISCARD, CALL(nvnCommandBufferSetRasterizerDiscard, U(c), 1));
|
|
REC(OP_RENDER_COND, CALL(nvnCommandBufferSetRenderEnableConditional, U(c), 1, 0xb000));
|
|
REC(OP_PRIM_RESTART, CALL(nvnCommandBufferSetPrimitiveRestart, U(c), 1, 0xffff));
|
|
REC(OP_DRAW_IDX_IND, CALL(nvnCommandBufferDrawElementsIndirect, U(c), NVN_DRAW_PRIMITIVE_TRIANGLES, NVN_INDEX_TYPE_UNSIGNED_INT,
|
|
0x8000, 0xc000));
|
|
REC(OP_DISPATCH_IND, CALL(nvnCommandBufferDispatchComputeIndirect, U(c), 0xc100));
|
|
REC(OP_CLEAR_BUF, CALL(nvnCommandBufferClearBuffer, U(c), 0xc200, 0x40, 0xdeadbeef));
|
|
REC(OP_CLEAR_BUF, CALL(nvnCommandBufferReportValue, U(c), 0x1234, 0xc300));
|
|
static const int thresholds[4] = {1, 2, 3, 4};
|
|
CALL(nvnCommandBufferSetColorReductionEnable, U(c), 1); // hints: nothing recorded
|
|
CALL(nvnCommandBufferSetColorReductionThresholds, U(c), 0, U(thresholds));
|
|
REC(OP_COPY_T2T, CALL(nvnCommandBufferDownsample, U(c), U(&tex1), U(&tex2)));
|
|
CHECK((uint8_t)CALL(nvnCommandBufferIsRecording, U(c)) == 1, "IsRecording while recording");
|
|
uint64_t h = CALL(nvnCommandBufferEndRecording, U(c));
|
|
CHECK((uint8_t)CALL(nvnCommandBufferIsRecording, U(c)) == 0, "IsRecording after EndRecording");
|
|
return h;
|
|
}
|
|
|
|
static const float *as_f(const void *p) { return p; }
|
|
|
|
static void parse_all(uint64_t h) {
|
|
CHECK(h && nvn_handle_live(h), "the recording handle is live");
|
|
const op_hdr *o = nvn_op_first(h);
|
|
CHECK(o->op == OP_BEGIN, "a recording starts with OP_BEGIN");
|
|
uint64_t bytes = 0;
|
|
int idx = 0, seen_state = 0, seen_tex = 0;
|
|
uint8_t kinds[OP_COUNT_] = {0};
|
|
const tex_t *t1 = (const tex_t *)(const void *)&tex1, *t2 = (const tex_t *)(const void *)&tex2;
|
|
for (; o->op != OP_END; o = nvn_op_next(o)) {
|
|
bytes += o->size;
|
|
CHECK(o->size >= 8 && !(o->size & 7) && o->op < OP_COUNT_, "op header op %u size %u", o->op, o->size);
|
|
if (o->op == OP_BEGIN) continue;
|
|
CHECK(o->op != OP_JUMP && o->op != OP_CALL, "a handle holds no OP_JUMP or OP_CALL");
|
|
if (idx >= nexpect || o->op != expect[idx]) {
|
|
CHECK(0, "op %d is %s, expected %s", idx, nvn_op_name(o->op), idx < nexpect ? nvn_op_name(expect[idx]) : "(end)");
|
|
return;
|
|
}
|
|
idx++;
|
|
kinds[o->op]++;
|
|
const void *a = nvn_op_payload(o);
|
|
switch (o->op) {
|
|
case OP_SIGNAL_EVENT: {
|
|
const p_signal_event *p = a;
|
|
CHECK(p->val == (volatile uint32_t *)pmem && p->mode == NVN_EVENT_SIGNAL_MODE_ADD && p->value == 0x55, "SIGNAL_EVENT payload");
|
|
break;
|
|
}
|
|
case OP_REPORT_COUNTER: {
|
|
const p_report_counter *p = a;
|
|
CHECK(p->type == NVN_COUNTER_TYPE_SAMPLES_PASSED && p->addr == 0xaabb00, "REPORT_COUNTER payload");
|
|
break;
|
|
}
|
|
case OP_RESET_COUNTER: CHECK(((const p_reset_counter *)a)->type == NVN_COUNTER_TYPE_SAMPLES_PASSED, "RESET_COUNTER payload"); break;
|
|
case OP_COPY_B2B: {
|
|
const p_copy_b2b *p = a;
|
|
CHECK(p->src == 0x1000 && p->dst == 0x2000 && p->size == 0x40, "COPY_B2B payload");
|
|
break;
|
|
}
|
|
case OP_UPDATE_UB: {
|
|
const p_update_ub *p = a;
|
|
static const uint8_t want[8] = {1, 2, 3, 4, 5, 6, 7, 8};
|
|
CHECK(p->dst == 0x3010 && p->size == 8 && !memcmp(p->data, want, 8) && o->size == 8 + 16 + 8, "UPDATE_UB payload");
|
|
break;
|
|
}
|
|
case OP_SET_RT: {
|
|
const p_set_rt *p = a;
|
|
CHECK(p->n == 1 && p->color[0] && p->color[0] != t1 && p->color[0]->serial == t1->serial && p->depth &&
|
|
p->depth != t2 && p->depth->serial == t2->serial && !p->has_cview[0] && !p->has_dview,
|
|
"SET_RT payload: textures are pinned host copies");
|
|
break;
|
|
}
|
|
case OP_CLEAR_COLOR: {
|
|
const p_clear_color *p = a;
|
|
CHECK(p->index == 2 && p->mask == 0xf && p->rgba[0] == 0.1f && p->rgba[3] == 0.4f, "CLEAR_COLOR payload");
|
|
break;
|
|
}
|
|
case OP_CLEAR_DS: {
|
|
const p_clear_ds *p = a;
|
|
CHECK(p->depth == 0.25f && p->depth_mask == 1 && p->stencil == 5 && p->stencil_mask == 0xff, "CLEAR_DS payload");
|
|
break;
|
|
}
|
|
case OP_VIEWPORTS: {
|
|
const p_viewports *p = a;
|
|
CHECK(p->first == 1 && p->n == 2 && p->v[0][0] == 1 && p->v[0][3] == 4 && p->v[1][0] == 5 && p->v[1][3] == 8, "VIEWPORTS payload");
|
|
break;
|
|
}
|
|
case OP_SCISSOR: {
|
|
const p_scissor *p = a;
|
|
CHECK(p->x == 1 && p->y == 2 && p->w == 3 && p->h == 4, "SCISSOR payload");
|
|
break;
|
|
}
|
|
case OP_DEPTH_RANGE: CHECK(as_f(a)[0] == 0.125f && as_f(a)[1] == 0.875f, "DEPTH_RANGE payload"); break;
|
|
case OP_BIND_PROGRAM: {
|
|
const p_bind_program *p = a;
|
|
CHECK(p->prog == (const prog_t *)(const void *)&prog && p->stages == 3, "BIND_PROGRAM payload");
|
|
break;
|
|
}
|
|
case OP_BIND_STATE: {
|
|
const p_bind_state *p = a;
|
|
if (seen_state++ == 0) CHECK(p->kind == 0 && p->bytes[0] == 1 && p->bytes[7] == 8, "BIND_STATE blend payload");
|
|
else CHECK(p->kind == 4 && p->bytes[0] == 9 && p->bytes[3] == 12, "BIND_STATE polygon payload");
|
|
break;
|
|
}
|
|
case OP_BIND_VATTR: {
|
|
const p_bind_vattr *p = a;
|
|
CHECK(p->first == 0 && p->n == 2 && p->st[0] == 0x11223344 && p->st[1] == 0x55667788, "BIND_VATTR payload");
|
|
break;
|
|
}
|
|
case OP_BIND_VSTREAM: {
|
|
const p_bind_vstream *p = a;
|
|
CHECK(p->n == 2 && p->st[0].stride == 16 && p->st[1].stride == 32 && p->st[1].divisor == 1, "BIND_VSTREAM payload");
|
|
break;
|
|
}
|
|
case OP_BIND_VBUF: {
|
|
const p_bind_vbuf *p = a;
|
|
CHECK(p->first == 1 && p->n == 2 && p->r[0].address == 0x1000 && p->r[1].size == 0x200, "BIND_VBUF payload");
|
|
break;
|
|
}
|
|
case OP_BIND_UBO: {
|
|
const p_bind_buf *p = a;
|
|
CHECK(p->stage == NVN_SHADER_STAGE_FRAGMENT && p->index == 3 && p->addr == 0x6000 && p->size == 0x80, "BIND_UBO payload");
|
|
break;
|
|
}
|
|
case OP_BIND_SSBO: {
|
|
const p_bind_buf *p = a;
|
|
CHECK(p->stage == NVN_SHADER_STAGE_COMPUTE && p->index == 1 && p->addr == 0x7000 && p->size == 0x90, "BIND_SSBO payload");
|
|
break;
|
|
}
|
|
case OP_BIND_TEX: {
|
|
const p_bind_handles *p = a;
|
|
if (seen_tex++ == 0)
|
|
CHECK(p->stage == NVN_SHADER_STAGE_FRAGMENT && p->first == 2 && p->n == 1 && p->h[0] == 0x300005 && p->src == 0,
|
|
"BIND_TEX immediate payload");
|
|
else
|
|
CHECK(p->stage == NVN_SHADER_STAGE_VERTEX && p->first == 0 && p->n == 3 && p->h[2] == 0x500007 && p->src != 0,
|
|
"BIND_TEX deferred payload keeps the game's array pointer");
|
|
break;
|
|
}
|
|
case OP_BIND_SAMP: {
|
|
const p_bind_handles *p = a;
|
|
CHECK(p->first == 4 && p->n == 1 && p->h[0] == 0x100000, "BIND_SAMP payload");
|
|
break;
|
|
}
|
|
case OP_BIND_IMG: {
|
|
const p_bind_handles *p = a;
|
|
CHECK(p->stage == NVN_SHADER_STAGE_COMPUTE && p->first == 1 && p->n == 1 && p->h[0] == 7, "BIND_IMG payload");
|
|
break;
|
|
}
|
|
case OP_STENCIL: {
|
|
const p_stencil *p = a;
|
|
CHECK(p->face == NVN_FACE_FRONT && p->ref == 9 && p->which == 2, "STENCIL payload");
|
|
break;
|
|
}
|
|
case OP_POLY_OFFSET: {
|
|
const p_poly_offset *p = a;
|
|
CHECK(p->factor == 1.5f && p->units == 2.5f && p->clamp == 3.5f, "POLY_OFFSET payload");
|
|
break;
|
|
}
|
|
case OP_DRAW: {
|
|
const p_draw *p = a;
|
|
CHECK(p->prim == NVN_DRAW_PRIMITIVE_TRIANGLES && p->first == 0 && p->count == 6 && p->instances == 1, "DRAW payload");
|
|
break;
|
|
}
|
|
case OP_DRAW_IDX: {
|
|
const p_draw_idx *p = a;
|
|
CHECK(p->itype == NVN_INDEX_TYPE_UNSIGNED_SHORT && p->count == 12 && p->base_vertex == -3 && p->base_instance == 2 &&
|
|
p->instances == 5 && p->ibuf == 0x8000, "DRAW_IDX payload");
|
|
break;
|
|
}
|
|
case OP_DISPATCH: {
|
|
const p_dispatch *p = a;
|
|
CHECK(p->x == 8 && p->y == 4 && p->z == 2 && p->caller == 0x7000001234ull, "DISPATCH payload");
|
|
break;
|
|
}
|
|
case OP_BARRIER: CHECK(((const p_barrier *)a)->bits == 3, "BARRIER payload"); break;
|
|
case OP_SET_TEX_POOL:
|
|
case OP_SET_SAMP_POOL: CHECK(((const p_set_pool *)a)->pool != NULL, "SET_*_POOL payload"); break;
|
|
case OP_COPY_B2T: {
|
|
const p_copy_b2t *p = a;
|
|
CHECK(p->src == 0x9000 && p->tex && p->tex->serial == t1->serial && p->has_view && p->view.set_levels && p->view.base_level == 1 &&
|
|
p->r.xoffset == 1 && p->r.width == 4 && p->r.height == 5, "COPY_B2T payload");
|
|
break;
|
|
}
|
|
case OP_COPY_STRIDE: CHECK(((const p_copy_stride *)a)->stride == 256, "COPY_STRIDE payload"); break;
|
|
case OP_BLEND_COLOR: CHECK(((const p_blend_color *)a)->rgba[2] == 0.3f, "BLEND_COLOR payload"); break;
|
|
case OP_COPY_T2B: {
|
|
const p_copy_b2t *p = a;
|
|
CHECK(p->src == 0xa000 && p->tex && p->tex->serial == t1->serial && !p->has_view && p->r.width == 4, "COPY_T2B payload");
|
|
break;
|
|
}
|
|
case OP_COPY_T2T: {
|
|
const p_copy_t2t *p = a;
|
|
if (p->flags == NVN_COPY_FLAGS_LINEAR_FILTER_BIT) { // Downsample: whole level 0 of each, filtered
|
|
CHECK(p->src && p->src->serial == t1->serial && p->dst && p->dst->serial == t2->serial && !p->has_sv && !p->has_dv &&
|
|
p->sr.width == 40 && p->sr.height == 128 && p->dr.width == 160 && p->dr.height == 160 && p->sr.depth == 1,
|
|
"Downsample payload");
|
|
break;
|
|
}
|
|
CHECK(p->src && p->src->serial == t1->serial && p->dst && p->dst->serial == t2->serial && p->sr.width == 4 && p->dr.width == 9,
|
|
"COPY_T2T payload");
|
|
break;
|
|
}
|
|
case OP_CLEAR_TEX: {
|
|
const p_clear_tex *p = a;
|
|
CHECK(p->tex && p->tex->serial == t2->serial && !p->is_uint && p->mask == 0xf && p->c.f[1] == 0.2f && p->r.xoffset == 6, "CLEAR_TEX payload");
|
|
break;
|
|
}
|
|
case OP_DEPTH_CLAMP: CHECK(((const p_flag *)a)->value == 1, "DEPTH_CLAMP payload"); break;
|
|
case OP_RENDER_ENABLE: CHECK(((const p_flag *)a)->value == 0, "RENDER_ENABLE payload"); break;
|
|
case OP_RASTER_DISCARD: CHECK(((const p_flag *)a)->value == 1, "RASTER_DISCARD payload"); break;
|
|
case OP_RENDER_COND: {
|
|
const p_render_cond *p = a;
|
|
CHECK(p->mode == 1 && p->addr == 0xb000, "RENDER_COND payload");
|
|
break;
|
|
}
|
|
case OP_PRIM_RESTART: {
|
|
const p_prim_restart *p = a;
|
|
CHECK(p->enable == 1 && p->index == 0xffff, "PRIM_RESTART payload");
|
|
break;
|
|
}
|
|
case OP_DRAW_IDX_IND: {
|
|
const p_draw_idx_ind *p = a;
|
|
CHECK(p->prim == NVN_DRAW_PRIMITIVE_TRIANGLES && p->itype == NVN_INDEX_TYPE_UNSIGNED_INT && p->ibuf == 0x8000 && p->ind == 0xc000,
|
|
"DRAW_IDX_IND payload");
|
|
break;
|
|
}
|
|
case OP_DISPATCH_IND: CHECK(((const p_dispatch_ind *)a)->ind == 0xc100, "DISPATCH_IND payload"); break;
|
|
case OP_CLEAR_BUF: {
|
|
const p_clear_buf *p = a;
|
|
CHECK((p->addr == 0xc200 && p->size == 0x40 && p->value == 0xdeadbeef) ||
|
|
(p->addr == 0xc300 && p->size == 4 && p->value == 0x1234), "CLEAR_BUF payload (ClearBuffer, ReportValue)");
|
|
break;
|
|
}
|
|
default: CHECK(0, "unexpected op %s", nvn_op_name(o->op)); break;
|
|
}
|
|
}
|
|
bytes += o->size; // OP_END
|
|
CHECK(idx == nexpect, "%d of %d ops parsed", idx, nexpect);
|
|
CHECK(bytes == nvn_handle_bytes(h), "the copy holds %llu bytes, the walk %llu", (unsigned long long)nvn_handle_bytes(h),
|
|
(unsigned long long)bytes);
|
|
int missing = 0;
|
|
for (uint32_t k = OP_SIGNAL_EVENT; k < OP_COUNT_; k++) missing += !kinds[k];
|
|
CHECK(!missing, "%d op kinds were not recorded", missing);
|
|
n_ops = OP_COUNT_ - OP_SIGNAL_EVENT;
|
|
}
|
|
|
|
// A recording naming tex3 as its render target; *pin: the texture pin in its SET_RT payload.
|
|
static uint64_t record_target(uint8_t *ctl, const tex_t **pin) {
|
|
const NVNtexture *colors[1] = {&tex3};
|
|
begin(&cmd2, ctl, 0x3a000);
|
|
CALL(nvnCommandBufferSetRenderTargets, U(&cmd2), 1, U(colors), 0, 0, 0);
|
|
uint64_t h = CALL(nvnCommandBufferEndRecording, U(&cmd2));
|
|
*pin = NULL;
|
|
for (const op_hdr *o = nvn_op_first(h); o->op != OP_END; o = nvn_op_next(o))
|
|
if (o->op == OP_SET_RT) *pin = ((const p_set_rt *)nvn_op_payload(o))->color[0];
|
|
return h;
|
|
}
|
|
|
|
// Texture pins are freed NVN_PIN_AGE submits after the last recording copy naming them is released, unless the replay
|
|
// keeps them as its current render targets; a texture named again gets a new pin.
|
|
static void probe_pins(void) {
|
|
uint8_t *ctl = mem_kernel_alloc(0x1000, 16);
|
|
set_tex(NVN_FORMAT_RGBA8, 16, 16, 1, 0x20000);
|
|
CHECK((uint8_t)CALL(nvnTextureInitialize, U(&tex3), U(&texb)) == 1, "pin probe texture");
|
|
uint64_t serial = ((const tex_t *)(const void *)&tex3)->serial;
|
|
uint64_t live0, freed0, live, freed;
|
|
nvn_pins_stats(&live0, &freed0);
|
|
const tex_t *pin;
|
|
uint64_t h = record_target(ctl, &pin);
|
|
nvn_pins_stats(&live, &freed);
|
|
CHECK(pin && pin->serial == serial && live == live0 + 1, "a recording naming a new texture makes one pin");
|
|
uint64_t s = nvn_submit_count();
|
|
nvn_pins_sweep(s + 10 * NVN_PIN_AGE, NULL, 0);
|
|
nvn_pins_stats(&live, &freed);
|
|
CHECK(live == live0 + 1 && freed == freed0 && nvn_handle_live(h), "a pin a live recording names stays");
|
|
CALL(nvnCommandBufferAddControlMemory, U(&cmd2), U(ctl), 0x1000); // retires h: the pin has no copy left
|
|
CHECK(!nvn_handle_live(h), "the pin's recording is retired");
|
|
nvn_pins_sweep(s + NVN_PIN_AGE - 1, NULL, 0);
|
|
nvn_pins_stats(&live, &freed);
|
|
CHECK(live == live0 + 1 && freed == freed0, "a pin stays NVN_PIN_AGE submits after its last copy went");
|
|
nvn_pins_sweep(s + NVN_PIN_AGE, &pin, 1);
|
|
nvn_pins_stats(&live, &freed);
|
|
CHECK(live == live0 + 1 && freed == freed0, "the replay's current render target is kept");
|
|
nvn_pins_sweep(s + 2 * NVN_PIN_AGE, NULL, 0);
|
|
nvn_pins_stats(&live, &freed);
|
|
CHECK(live == live0 && freed == freed0 + 1, "an unnamed, unkept pin is freed (live %llu of %llu, freed %llu of %llu)",
|
|
(unsigned long long)live, (unsigned long long)live0, (unsigned long long)freed, (unsigned long long)freed0 + 1);
|
|
h = record_target(ctl, &pin);
|
|
nvn_pins_stats(&live, &freed);
|
|
CHECK(pin && pin->serial == serial && live == live0 + 1, "naming the texture again makes a new pin");
|
|
CALL(nvnCommandBufferAddControlMemory, U(&cmd2), U(ctl), 0x1000);
|
|
CALL(nvnCommandBufferFinalize, U(&cmd2));
|
|
CALL(nvnTextureFinalize, U(&tex3));
|
|
}
|
|
|
|
static void probe_commands(void) {
|
|
ctlmem1 = mem_kernel_alloc(0x1000, 16);
|
|
ctlmem2 = mem_kernel_alloc(0x1000, 16);
|
|
ctlmem3 = mem_kernel_alloc(0x1000, 16);
|
|
uint64_t h = record_all();
|
|
parse_all(h);
|
|
|
|
// CallCommands inlines the called recording at the call position
|
|
begin(&cmd2, ctlmem2, 0x34000);
|
|
CALL(nvnCommandBufferBarrier, U(&cmd2), 1);
|
|
CALL(nvnCommandBufferCallCommands, U(&cmd2), 1, U(&h));
|
|
CALL(nvnCommandBufferBarrier, U(&cmd2), 2);
|
|
uint64_t h2 = CALL(nvnCommandBufferEndRecording, U(&cmd2));
|
|
int n = 0, barriers = 0, inner = 0, forbidden = 0;
|
|
for (const op_hdr *o = nvn_op_first(h2); o->op != OP_END; o = nvn_op_next(o)) {
|
|
forbidden += o->op == OP_CALL || o->op == OP_JUMP;
|
|
if (o->op != OP_BEGIN) n++;
|
|
if (o->op == OP_BARRIER) barriers++;
|
|
inner++;
|
|
}
|
|
CHECK(!forbidden && n == nexpect + 2 && barriers == 3 && inner == nexpect + 3, "CallCommands inlined: %d ops (expected %d), %d barriers",
|
|
n, nexpect + 2, barriers);
|
|
CHECK(nvn_op_first(h2)->op == OP_BEGIN && nvn_op_next(nvn_op_first(h2))->op == OP_BARRIER, "the first op before the call stays first");
|
|
|
|
// adding the control memory again retires the recordings made in it
|
|
CALL(nvnCommandBufferAddControlMemory, U(&cmd2), U(ctlmem2), 0x1000);
|
|
CHECK(!nvn_handle_live(h2) && nvn_handle_live(h), "reusing control memory retires only the handles recorded in it");
|
|
CALL(nvnCommandBufferFinalize, U(&cmd2));
|
|
probe_pins();
|
|
n_objects += 5;
|
|
}
|
|
|
|
// ---- the null back end's side effects, at submit and in stream order
|
|
static void probe_null_effects(void) {
|
|
// layout in the CPU-visible pool; offsets are bytes from pmem
|
|
enum { OFF_COUNTER_A = 0x100, OFF_COUNTER_B = 0x200, OFF_UB = 0x300, OFF_DST = 0x400, OFF_B2B_DST = 0x500 };
|
|
uint8_t *ev_val = pmem;
|
|
static const uint8_t d1[16] = {0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f};
|
|
static const uint8_t d2[8] = {0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7};
|
|
uint8_t keep[16];
|
|
memset(pmem + OFF_COUNTER_B, 0xaa, 16);
|
|
memset(pmem + OFF_B2B_DST, 0x5c, 16);
|
|
memcpy(keep, pmem + OFF_B2B_DST, 16);
|
|
*(uint32_t *)ev_val = 0;
|
|
uint64_t base = U(pmem), gbase = U(G()->mem);
|
|
|
|
NVNcommandBuffer *c = &cmd;
|
|
begin(c, ctlmem3, 0x36000);
|
|
CALL(nvnCommandBufferResetCounter, U(c), NVN_COUNTER_TYPE_SAMPLES_PASSED);
|
|
CALL(nvnCommandBufferSignalEvent, U(c), U(&ev), NVN_EVENT_SIGNAL_MODE_WRITE, NVN_EVENT_SIGNAL_LOCATION_TOP, 0, 0x11);
|
|
CALL(nvnCommandBufferSignalEvent, U(c), U(&ev), NVN_EVENT_SIGNAL_MODE_ADD, NVN_EVENT_SIGNAL_LOCATION_BOTTOM, 0, 5);
|
|
CALL(nvnCommandBufferUpdateUniformBuffer, U(c), base + OFF_UB, 64, 4, 16, U(d1));
|
|
CALL(nvnCommandBufferCopyBufferToBuffer, U(c), base + OFF_UB + 4, base + OFF_DST, 16, 0);
|
|
CALL(nvnCommandBufferReportCounter, U(c), NVN_COUNTER_TYPE_TIMESTAMP, base + OFF_COUNTER_A);
|
|
CALL(nvnCommandBufferReportCounter, U(c), NVN_COUNTER_TYPE_SAMPLES_PASSED, base + OFF_COUNTER_B);
|
|
// GPU-window addresses hold no host memory: these have no effect and must not stop the run
|
|
CALL(nvnCommandBufferCopyBufferToBuffer, U(c), base + OFF_UB + 4, gbase + 0x10, 16, 0);
|
|
CALL(nvnCommandBufferCopyBufferToBuffer, U(c), gbase + 0x10, base + OFF_B2B_DST, 16, 0);
|
|
CALL(nvnCommandBufferReportCounter, U(c), NVN_COUNTER_TYPE_TIMESTAMP, gbase + 0x20);
|
|
CALL(nvnCommandBufferUpdateUniformBuffer, U(c), gbase, 64, 8, 8, U(d2));
|
|
CALL(nvnCommandBufferDrawArrays, U(c), NVN_DRAW_PRIMITIVE_TRIANGLES, 0, 3); // ignored
|
|
CALL(nvnCommandBufferBarrier, U(c), 1);
|
|
uint64_t ha = CALL(nvnCommandBufferEndRecording, U(c));
|
|
|
|
begin(&cmd2, ctlmem2, 0x38000);
|
|
CALL(nvnCommandBufferUpdateUniformBuffer, U(&cmd2), base + OFF_DST, 64, 0, 8, U(d2)); // after the first copy
|
|
CALL(nvnCommandBufferSignalEvent, U(&cmd2), U(&ev), NVN_EVENT_SIGNAL_MODE_WRITE, NVN_EVENT_SIGNAL_LOCATION_TOP, 0, 0x99);
|
|
uint64_t hb = CALL(nvnCommandBufferEndRecording, U(&cmd2));
|
|
|
|
CHECK(*(uint32_t *)ev_val == 0 && !pmem[OFF_UB + 4] && !pmem[OFF_DST] && pmem[OFF_COUNTER_B] == 0xaa,
|
|
"recording has no side effects");
|
|
|
|
uint64_t t0 = rt_ticks(), subs = nvn_submit_count(), fence0 = nvn_fence_last();
|
|
uint64_t hs[2] = {ha, hb};
|
|
CALL(nvnQueueSubmitCommands, U(&queue), 2, U(hs));
|
|
uint64_t t1 = rt_ticks();
|
|
CALL(nvnQueueFenceSync, U(&queue), U(&syncs), NVN_SYNC_CONDITION_ALL_GPU_COMMANDS_COMPLETE, 0);
|
|
uint64_t wait = CALL(nvnSyncWait, U(&syncs), 1000000000ull);
|
|
CHECK(wait == NVN_SYNC_WAIT_RESULT_ALREADY_SIGNALED || wait == NVN_SYNC_WAIT_RESULT_CONDITION_SATISFIED, "SyncWait after the submit: %llu",
|
|
(unsigned long long)wait);
|
|
CHECK(nvn_submit_count() == subs + 1 && nvn_fence_last() == fence0 + 1 && gfx_backend()->fence_done() == nvn_fence_last(),
|
|
"one submit advances the fence once (%llu -> %llu)", (unsigned long long)fence0, (unsigned long long)nvn_fence_last());
|
|
|
|
// event: WRITE 0x11, ADD 5, then handle 2's WRITE 0x99
|
|
CHECK((uint32_t)CALL(nvnEventGetValue, U(&ev)) == 0x99, "event value %#x after the stream, expected 0x99", *(uint32_t *)ev_val);
|
|
// uniform update lands at buffer + offset, nothing around it moves
|
|
CHECK(!memcmp(pmem + OFF_UB + 4, d1, 16) && !pmem[OFF_UB + 3] && !pmem[OFF_UB + 20], "uniform update");
|
|
// buffer copy in stream order: handle 1's copy, then handle 2's update over its first 8 bytes
|
|
CHECK(!memcmp(pmem + OFF_DST, d2, 8) && !memcmp(pmem + OFF_DST + 8, d1 + 8, 8) && !pmem[OFF_DST + 16],
|
|
"buffer copy and later update in stream order");
|
|
// counters: NVNcounterData {counter, timestamp}; the null GPU reports 0 and a timestamp in 19.2 MHz ticks
|
|
uint64_t ca[2], cb[2];
|
|
memcpy(ca, pmem + OFF_COUNTER_A, 16);
|
|
memcpy(cb, pmem + OFF_COUNTER_B, 16);
|
|
CHECK(ca[0] == 0 && ca[1] + 1 >= t0 && ca[1] <= t1 + 1, "timestamp counter {%llu, %llu} outside [%llu, %llu]", (unsigned long long)ca[0],
|
|
(unsigned long long)ca[1], (unsigned long long)t0, (unsigned long long)t1);
|
|
CHECK(cb[0] == 0 && cb[1] + 1 >= t0 && cb[1] <= t1 + 1, "samples-passed counter {%llu, %llu}", (unsigned long long)cb[0],
|
|
(unsigned long long)cb[1]);
|
|
CHECK(!memcmp(pmem + OFF_B2B_DST, keep, 16), "a copy out of the GPU window left the target alone");
|
|
n_effects = 6;
|
|
|
|
CALL(nvnCommandBufferFinalize, U(&cmd2));
|
|
CALL(nvnCommandBufferFinalize, U(&cmd));
|
|
n_objects += 2;
|
|
}
|
|
|
|
static void probe_main(Cpu *unused) {
|
|
(void)unused;
|
|
probe_device_and_pools();
|
|
probe_buffers();
|
|
probe_textures();
|
|
probe_samplers();
|
|
probe_sync_events();
|
|
probe_commands();
|
|
probe_null_effects();
|
|
CALL(nvnQueueFinalize, U(&queue));
|
|
CALL(nvnDeviceFinalize, U(&dev));
|
|
}
|
|
|
|
// ---- the proc table
|
|
static void probe_proc_table(void) {
|
|
int dup = 0;
|
|
uint64_t addr[NPROCS];
|
|
for (int i = 0; i < NPROCS; i++) {
|
|
addr[i] = nvn_proc_address(procs[i].name);
|
|
const char *reg = addr[i] ? hostfn_name(addr[i]) : NULL;
|
|
char want[160];
|
|
snprintf(want, sizeof want, "nvn:%s", procs[i].name);
|
|
CHECK(addr[i] && reg && !strcmp(reg, want), "proc %s resolves to %s", procs[i].name, reg ? reg : "nothing");
|
|
}
|
|
for (int i = 0; i < NPROCS; i++)
|
|
for (int k = i + 1; k < NPROCS; k++) dup += addr[i] && addr[i] == addr[k];
|
|
CHECK(dup == 0, "%d procs share a synthetic address", dup);
|
|
CHECK(NPROCS >= 534, "the proc list has %d names, the loader asks for 534", NPROCS);
|
|
CHECK(nvn_proc_address("nvnNoSuchProc") == 0 && nvn_proc_address("") == 0 && nvn_proc_address(NULL) == 0,
|
|
"an unknown name returns NULL");
|
|
// nvnDeviceGetProcAddress(device, name), the way the loader calls it
|
|
CHECK(CALL(nvnDeviceGetProcAddress, U(&dev), U("nvnBufferGetSize")) == nvn_proc_address("nvnBufferGetSize") &&
|
|
CALL(nvnDeviceGetProcAddress, U(&dev), U("nvnNoSuchProc")) == 0, "nvnDeviceGetProcAddress");
|
|
}
|
|
|
|
// ---- named fatal stubs, each in a child process
|
|
static uint32_t run_child(const char *mode, char *out, size_t cap) {
|
|
int code = plat_run_self(CHILD_ENV, mode, out, cap, 0);
|
|
if (code < 0) rt_fatal("probe-nvn: cannot start the %s child", mode);
|
|
return (uint32_t)code;
|
|
}
|
|
|
|
static int run_as_child(const char *name) {
|
|
for (int i = 0; i < NPROCS; i++)
|
|
if (!strcmp(procs[i].name, name)) {
|
|
procs[i].fn(&cpu);
|
|
printf("child %s: the call returned\n", name);
|
|
return 0;
|
|
}
|
|
rt_fatal("probe-nvn: no proc %s", name);
|
|
}
|
|
|
|
static void probe_children(void) {
|
|
static char out[16384];
|
|
for (int i = 0; i < NFATAL; i++) {
|
|
uint32_t code = run_child(fatal_procs[i], out, sizeof out);
|
|
char want[160];
|
|
snprintf(want, sizeof want, "NVN %s is not implemented", fatal_procs[i]);
|
|
CHECK(code == 70 && strstr(out, "FATAL: ") && strstr(out, want), "child %s: exit %u, output: %s", fatal_procs[i], code, out);
|
|
const char *line = strstr(out, "FATAL: ");
|
|
int len = line ? (int)strcspn(line, "\r\n") : 0;
|
|
printf("fatal stub %-30s child exit %u: %.*s\n", fatal_procs[i], code, len, line ? line : "(no FATAL line)");
|
|
}
|
|
}
|
|
|
|
int probe_nvn(void) {
|
|
const char *child = getenv(CHILD_ENV);
|
|
if (child && *child) return run_as_child(child);
|
|
// The probe checks the null back end whatever TOTK_GPU says; the choice is made once, at the first object call.
|
|
((RtConfig *)rt_config())->gpu = "null";
|
|
nvn_procs_register();
|
|
hostfn_seal();
|
|
kern_run_on_guest_main(probe_main);
|
|
probe_proc_table();
|
|
probe_children();
|
|
if (failures) {
|
|
printf("probe-nvn: FAILED (%d of %d checks)\n", failures, checks);
|
|
return 1;
|
|
}
|
|
printf("probe-nvn: passed (%d checks: %d object cases, %d op kinds recorded and parsed, %d procs resolved, %d side effects on the %s "
|
|
"back end, %d fatal stubs in child processes)\n", checks, n_objects, n_ops, NPROCS, n_effects, gfx_backend()->name, NFATAL);
|
|
return 0;
|
|
}
|