// nx2navi.exe --probe-meshfb: the mesh shader fallback (mesh_fb.c, vk_mesh_fb.c, vk_exec_draw.c "mesh fallback").
//
// Draw part, no game files: two mesh programs of the runtime's own in the shape the translator gives one
// (shaders/meshfb_probe_*.mesh, .task), one without a task stage and one fed by a task program, drawn through the
// replay into an RGBA8 target with direct and with indirect workgroup counts, and both in one pass after each other.
// Each picture is drawn twice, through VK_EXT_mesh_shader and through the fallback, and the two are compared texel by
// texel; without mesh shaders on the device only the fallback draws, and the probe says that nothing was compared.
//
// Module part, with NX2NAVI_PROBE_MESHFB_DIR=
: every "" of /list.txt names /.in (a stored
// shader input, gfx_shader.c in_store: control section and code) and /.spv (its translation). The compute
// module and, for a mesh program, a vertex module are written to /out/.cs.spv and .vs.spv for spirv-val
// (work the caller does; tests of game shaders stay outside the repository). Where /.res.json (the
// translation's reflection) is there too, the compute pipeline of the module is built on this device, as its first
// draw would build it.
#include
#include
#include
#include "cfg.h"
#include "gfx.h"
#include "host.h"
#include "mesh_fb.h"
#include "nvn_api.h"
#include "nvn_tex.h"
#include "vk.h"
#include "vk_exec.h"
#include "vk_mesh_fb.h"
#include "platform/platform.h"
#include "meshfb_probe_f_spv.h"
#include "meshfb_probe_m_spv.h"
#include "meshfb_probe_t_spv.h"
#include "meshfb_probe_tm_spv.h"
#define TEX_W 256
#define TEX_H 144
#define CHECK(cond, ...) \
do { \
if (!(cond)) { \
printf("probe-meshfb: FAILED: "); \
printf(__VA_ARGS__); \
printf("\n"); \
return 1; \
} \
} while (0)
void vk_exec_probe_shader(const uint8_t *ctl, const GfxShader *s); // vk_exec_draw.c
void vk_exec_draw_shutdown(void);
void vk_exec_xfer_shutdown(void);
// ---- module part
static uint8_t *read_all(const char *path, size_t *size) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *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 = (size_t)n;
return b;
}
static int write_all(const char *path, const void *data, size_t n) {
FILE *f = fopen(path, "wb");
if (!f) return 0;
int ok = fwrite(data, 1, n, f) == n;
fclose(f);
return ok;
}
static int modules_part(const char *dir) {
char path[1024];
size_t list_n = 0;
snprintf(path, sizeof path, "%s/list.txt", dir);
char *list = (char *)read_all(path, &list_n);
CHECK(list, "no %s", path);
int n_task = 0, n_mesh = 0, n_fail = 0, n_pipe = 0, n_pipe_fail = 0;
double pipe_ms = 0, pipe_worst = 0;
uint64_t pipe_worst_words = 0;
const GfxBackend *b = &gfx_vulkan_backend;
pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x22};
int device = 0;
for (char *name = strtok(list, "\r\n"); name; name = strtok(NULL, "\r\n")) {
size_t in_n = 0, spv_n = 0;
snprintf(path, sizeof path, "%s/%s.in", dir, name);
uint8_t *in = read_all(path, &in_n);
snprintf(path, sizeof path, "%s/%s.spv", dir, name);
uint8_t *spv = read_all(path, &spv_n);
uint32_t h[6]; // magic, version, stage, code length, c1 length, size of the answers (gfx_shader.c InHeader)
if (!in || !spv || in_n < sizeof h) {
printf("probe-meshfb: %s: input or translation missing\n", name), n_fail++;
free(in), free(spv);
continue;
}
memcpy(h, in, sizeof h);
const uint8_t *control = in + sizeof h + h[5], *code = control + 0x880;
int stage = (int)h[2]; // GFX_STAGE_TS / GFX_STAGE_MS are the NVN stage numbers 6 and 7
char err[200] = "";
MeshFbInfo m;
uint64_t on = 0, vn = 0;
uint32_t *cs = NULL, *vs = NULL;
if (in_n < sizeof h + h[5] + 0x880 + 0x80 || (stage != 6 && stage != 7)) snprintf(err, sizeof err, "not a task or mesh input");
else if (meshfb_info(control, code, stage, &m, err, sizeof err)) {
uint32_t top = 0;
const uint32_t *w = (const uint32_t *)spv;
for (size_t i = 5; i < spv_n / 4;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len) break;
if (op == 71 && len == 4 && w[i + 2] == 33 && w[i + 3] > top) top = w[i + 3];
i += len;
}
cs = stage == 6 ? meshfb_task_cs(w, spv_n / 4, &m, top + 1, &on, err, sizeof err)
: meshfb_mesh_cs(w, spv_n / 4, &m, top + 1, m.payload_bytes / 4, &on, err, sizeof err);
if (cs && stage == 7) {
uint8_t type[32] = {0};
uint32_t mask = 0;
for (uint32_t l = 0; l < m.nlines; l++)
if (m.lines[l] >= 0x80) mask |= 1u << (m.lines[l] - 0x80) / 16;
for (int g = 0; g < 32; g++) type[g] = (uint8_t)(g % 3); // every output type gets made somewhere
vs = meshfb_vs(&m, mask, type, &vn);
if (!vs) snprintf(err, sizeof err, "no vertex module");
}
}
if (cs && (stage == 6 || vs)) {
snprintf(path, sizeof path, "%s/out/%s.cs.spv", dir, name);
int ok = write_all(path, cs, on * 4);
if (vs) snprintf(path, sizeof path, "%s/out/%s.vs.spv", dir, name), ok = ok && write_all(path, vs, vn * 4);
if (!ok) printf("probe-meshfb: %s: cannot write under %s/out\n", name, dir), n_fail++;
else if (stage == 6) n_task++;
else n_mesh++;
size_t res_n = 0;
snprintf(path, sizeof path, "%s/%s.res.json", dir, name);
char *res = (char *)read_all(path, &res_n);
if (res) { // the pipeline, with the layout the reflection gives
if (!device) { // the first pool brings the device up
uint64_t heap = 0;
CHECK(mem_set_heap_size(16ull << 20, &heap) == 0 && heap, "SetHeapSize 16 MiB");
pool.mem = pool.game_mem = (uint8_t *)(uintptr_t)heap + 0x100000, pool.size = 0x100000;
CHECK(b->pool_init(&pool), "pool_init of a CPU-visible pool");
device = 1;
}
uint32_t top = 0;
const uint32_t *w = (const uint32_t *)spv;
for (size_t i = 5; i < spv_n / 4;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len) break;
if (op == 71 && len == 4 && w[i + 2] == 33 && w[i + 3] > top) top = w[i + 3];
i += len;
}
static const int task_slots[1] = {MESHFB_CB_TASK}, mesh_slots[3] = {MESHFB_CB_TASK, MESHFB_CB_MESH, MESHFB_CB_LIST};
char *cres = stage == 6 ? meshfb_res_json(res, top + 1, task_slots, 1)
: m.has_task ? meshfb_res_json(res, top + 1, mesh_slots, 3) : meshfb_res_json(res, top + 2, mesh_slots + 1, 1);
const GfxShader *sh = cres ? gfx_shader_own(cs, on, cres) : NULL;
const VkPipeInfo *info = NULL;
uint64_t t0 = plat_now_ns();
VkPipeline p = sh ? vk_pipeline_compute(sh, &info) : VK_NULL_HANDLE;
double ms = (double)(plat_now_ns() - t0) / 1e6;
if (p && info) {
n_pipe++, pipe_ms += ms;
if (ms > pipe_worst) pipe_worst = ms, pipe_worst_words = on;
} else {
n_pipe_fail++;
printf("probe-meshfb: %s (stage %d, %llu words): %s\n", name, stage, (unsigned long long)on,
sh ? "its compute pipeline was not built" : "its reflection could not be made");
}
fflush(stdout);
free(cres), free(res);
}
} else {
printf("probe-meshfb: %s (stage %d): %s\n", name, stage, err), n_fail++;
}
free(cs), free(vs), free(in), free(spv);
}
free(list);
printf("probe-meshfb: module part: %d task and %d mesh programs converted, %d failed\n", n_task, n_mesh, n_fail);
if (device) {
printf("probe-meshfb: module part: %d compute pipelines built on this device (%.0f ms each, the longest %.0f ms for %llu "
"words), %d not built\n", n_pipe, n_pipe ? pipe_ms / n_pipe : 0.0, pipe_worst, (unsigned long long)pipe_worst_words,
n_pipe_fail);
b->pool_fini(&pool);
b->submit(NULL, 0); // the pool's buffers go when a submission after their release has completed
b->finish();
vk_pipeline_shutdown();
vk_exec_draw_shutdown();
vk_exec_xfer_shutdown();
vk_backend_shutdown();
}
return n_fail != 0 || n_pipe_fail != 0;
}
// ---- draw part
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;
}
// An NVN2 control section of a task (6), mesh (7) or fragment (1) stage as vk_exec_draw.c and mesh_fb.c read it, and
// a code block whose header maps the outputs: position and generic vector 0.
static void fake_stage(NVNshaderData *sd, uint32_t stage, int fed_by_task) {
uint8_t *ctl = calloc(1, 0x880), *code = calloc(1, 0x100);
uint32_t magic = 0x98761234u, len = 0x100, st = stage << 16;
uint32_t w58 = 32u | 1u << 24; // 32 threads, a payload of one 256-byte unit
uint32_t w5c = 3u << 24 | (fed_by_task ? 1u << 16 : 0); // triangles
uint32_t w60 = 64u | 32u << 16; // 64 vertices, 32 primitives
memcpy(ctl, &magic, 4), memcpy(ctl + 0x38, &len, 4), memcpy(ctl + 0x54, &st, 4);
if (stage >= 6) memcpy(ctl + 0x58, &w58, 4);
if (stage == 7) memcpy(ctl + 0x5c, &w5c, 4), memcpy(ctl + 0x60, &w60, 4), code[0x35] = 0xf0, code[0x36] = 0x0f;
sd->control = ctl, sd->data = (uint64_t)(uintptr_t)code;
}
static const char RES_MS[] = "{\"key\":\"probe\",\"stage\":\"ms\",\"notes\":[],\"cbufs\":[{\"binding\":1,\"slot\":3}],"
"\"sbufs\":[],\"textures\":[],\"images\":[]}";
static const char RES_TS[] = "{\"key\":\"probe\",\"stage\":\"ts\",\"notes\":[],\"cbufs\":[{\"binding\":1,\"slot\":3}],"
"\"sbufs\":[],\"textures\":[],\"images\":[]}";
static const char RES_FS[] = "{\"key\":\"probe\",\"stage\":\"ps\",\"notes\":[],\"cbufs\":[],\"sbufs\":[],\"textures\":[],"
"\"images\":[]}";
// The program of a mesh stage, its task stage if any, and the fragment stage.
static prog_t *make_prog(const GfxShader *ms, const GfxShader *ts, const GfxShader *fs) {
static uint64_t serial = 0x9000;
prog_t *p = calloc(1, sizeof *p);
p->magic = NVN_PROG_MAGIC, p->serial = ++serial;
fake_stage(&p->sd[p->nstage], 7, ts != NULL), vk_exec_probe_shader(p->sd[p->nstage++].control, ms);
if (ts) fake_stage(&p->sd[p->nstage], 6, 0), vk_exec_probe_shader(p->sd[p->nstage++].control, ts);
fake_stage(&p->sd[p->nstage], 1, 0), vk_exec_probe_shader(p->sd[p->nstage++].control, fs);
return p;
}
typedef struct { prog_t *prog; uint32_t count; int indirect; uint32_t grid[2]; float tint[4]; } MeshDraw;
// Uniform data in the pool, 0x1000 apart per draw: the mesh stage's block at 0x90000, the task stage's at 0x98000; the
// indirect records at 0xa0000.
static int draw_picture(const GfxBackend *b, pool_t *pool, tex_t *color, const MeshDraw *d, int n, uint32_t *px) {
static _Alignas(8) uint8_t stream[16384];
uint8_t *q = put(stream, 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_scissor full = {0, 0, 1 << 20, 1 << 20};
q = put(q, OP_SCISSOR, &full, sizeof full);
for (int i = 0; i < n; i++) {
uint8_t *mu = pool->mem + 0x90000 + 0x1000 * i, *tu = pool->mem + 0x98000 + 0x1000 * i;
uint32_t *rec = (uint32_t *)(pool->mem + 0xa0000 + 0x100 * i);
uint32_t grid[4] = {d[i].grid[0], d[i].grid[1], 0, 0}, shift[4] = {1, 0, 0, 0};
memcpy(mu, d[i].tint, 16), memcpy(mu + 16, grid, 16), memcpy(tu, shift, 16);
rec[0] = d[i].count, rec[1] = 0, rec[2] = 0;
const p_bind_program bp = {d[i].prog, 0xc2};
q = put(q, OP_BIND_PROGRAM, &bp, sizeof bp);
const p_bind_buf bm = {7, 0, (uint64_t)(uintptr_t)mu, 256}, bt = {6, 0, (uint64_t)(uintptr_t)tu, 256};
q = put(q, OP_BIND_UBO, &bm, sizeof bm);
q = put(q, OP_BIND_UBO, &bt, sizeof bt);
if (d[i].indirect) {
const p_draw_mesh_ind di = {(uint64_t)(uintptr_t)rec, 0, 0, 0, 0};
q = put(q, OP_DRAW_MESH_IND, &di, sizeof di);
} else {
const p_draw_mesh dm = {0, d[i].count, 0};
q = put(q, OP_DRAW_MESH, &dm, sizeof dm);
}
}
q = put(q, OP_END, NULL, 0);
CHECK(q <= 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, 60000000000ull), "fence_wait of a mesh draw submission timed out");
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);
return 0;
}
static int drawn_texels(const uint32_t *px) {
int n = 0;
for (int i = 0; i < TEX_W * TEX_H; i++) n += px[i] != px[0]; // texel 0 is in the margin no cell covers
return n;
}
// Texels of a and b that differ, and the largest difference of a channel.
static int differing(const uint32_t *a, const uint32_t *b, int *worst) {
int n = 0;
*worst = 0;
for (int i = 0; i < TEX_W * TEX_H; i++) {
if (a[i] == b[i]) continue;
n++;
for (int c = 0; c < 4; c++) {
int d = (int)(a[i] >> 8 * c & 0xff) - (int)(b[i] >> 8 * c & 0xff);
if (d < 0) d = -d;
if (d > *worst) *worst = d;
}
}
return n;
}
static int draw_part(const GfxBackend *b, uint8_t *guest) {
pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0x100000, .size = 0xC0000,
.game_mem = guest + 0x100000};
CHECK(b->pool_init(&pool), "pool_init of a CPU-visible pool");
VkCtx *c = vk_ctx();
CHECK(c, "no Vulkan device");
static tex_t color;
memset(&color, 0, sizeof color);
color.magic = NVN_TEX_MAGIC;
color.d.target = NVN_TEXTURE_TARGET_2D, color.d.format = NVN_FORMAT_RGBA8, color.d.width = TEX_W, color.d.height = TEX_H;
color.d.depth = 1, color.d.levels = 1;
nvn_tex_layout(&color.d, &color.L);
CHECK(color.L.size <= 0x80000, "the target exceeds its pool slot");
color.pool = &pool, color.pool_flags = (uint16_t)pool.flags, color.off = 0, color.mem = pool.mem, color.serial = 4000;
const GfxShader *fs = gfx_shader_own(meshfb_probe_f_spv, sizeof meshfb_probe_f_spv / 4, RES_FS);
const GfxShader *ms = gfx_shader_own(meshfb_probe_m_spv, sizeof meshfb_probe_m_spv / 4, RES_MS);
const GfxShader *ts = gfx_shader_own(meshfb_probe_t_spv, sizeof meshfb_probe_t_spv / 4, RES_TS);
const GfxShader *tm = gfx_shader_own(meshfb_probe_tm_spv, sizeof meshfb_probe_tm_spv / 4, RES_MS);
CHECK(fs && ms && ts && tm, "a probe shader is malformed");
prog_t *plain = make_prog(ms, NULL, fs), *tasked = make_prog(tm, ts, fs);
// 24 cells of a 6 x 4 grid by 24 mesh workgroups; 40 cells of an 8 x 5 grid by 10 task workgroups that start 0 to
// 4 mesh workgroups each
const MeshDraw cases[][2] = {
{{plain, 24, 0, {6, 4}, {1, 1, 1, 1}}},
{{plain, 24, 1, {6, 4}, {1, 1, 1, 1}}},
{{tasked, 10, 0, {8, 5}, {1, 1, 1, 1}}},
{{tasked, 10, 1, {8, 5}, {1, 1, 1, 1}}},
{{plain, 24, 1, {6, 4}, {1, 1, 1, 1}}, {tasked, 10, 1, {8, 5}, {0.5f, 1, 0.25f, 1}}},
{{tasked, 10, 0, {8, 5}, {1, 1, 1, 1}}, {plain, 24, 0, {6, 4}, {1, 0.5f, 0.5f, 1}}},
};
static const char *const names[] = {"mesh, direct", "mesh, indirect", "task + mesh, direct", "task + mesh, indirect",
"mesh then task + mesh in one pass, indirect", "task + mesh then mesh in one pass, direct"};
enum { CASES = sizeof cases / sizeof cases[0] };
static uint32_t px[2][CASES][TEX_W * TEX_H];
int native = c->has_mesh_shader, was = c->mesh_fallback;
for (int way = native ? 0 : 1; way < 2; way++) {
c->mesh_fallback = way;
for (int k = 0; k < CASES; k++)
if (draw_picture(b, &pool, &color, cases[k], cases[k][1].prog ? 2 : 1, px[way][k])) return 1;
}
c->mesh_fallback = was;
for (int k = 0; k < CASES; k++) {
int fb = drawn_texels(px[1][k]);
CHECK(fb > TEX_W * TEX_H / 20, "%s: the fallback drew %d texels", names[k], fb);
if (!native) {
printf("probe-meshfb: %s: the fallback drew %d of %d texels (no mesh shaders here: not compared)\n", names[k], fb,
TEX_W * TEX_H);
continue;
}
int worst, diff = differing(px[0][k], px[1][k], &worst), nat = drawn_texels(px[0][k]);
CHECK(nat > TEX_W * TEX_H / 20, "%s: the mesh pipeline drew %d texels", names[k], nat);
CHECK(worst <= 1, "%s: %d texels differ between the mesh pipeline and the fallback, by up to %d of 255 (the mesh "
"pipeline drew %d texels, the fallback %d)", names[k], diff, worst, nat, fb);
printf("probe-meshfb: %s: %d texels drawn by the mesh pipeline, %d by the fallback, %d differ (by %d of 255 at most)\n",
names[k], nat, fb, diff, worst);
}
int w01, w23;
CHECK(!differing(px[1][0], px[1][1], &w01) && !differing(px[1][2], px[1][3], &w23),
"the fallback's direct and indirect pictures differ");
CHECK(!vk_meshfb_short(), "%llu of the draws above did not fit the fallback's buffers", (unsigned long long)vk_meshfb_short());
// More workgroups than the mesh buffer has slots for: what fits is drawn, the rest is dropped and counted. The
// cells past the grid are outside the picture, so it is the 24-cell picture again.
uint32_t slots = (uint32_t)(vk_meshfb_mesh_words() / 540), over = slots + 1000;
const MeshDraw big = {plain, over, 1, {6, 4}, {1, 1, 1, 1}};
static uint32_t bpx[TEX_W * TEX_H];
c->mesh_fallback = 1;
int bad = draw_picture(b, &pool, &color, &big, 1, bpx);
c->mesh_fallback = was;
if (bad) return 1;
b->finish();
int wb, db = differing(bpx, px[1][0], &wb);
CHECK(!db, "a draw of %u workgroups, %u more than the mesh buffer holds: %d texels differ from the 24-cell picture", over,
over - slots, db);
CHECK(vk_meshfb_short() == 1, "a draw of %u workgroups with room for %u was counted as %llu draws that did not fit", over, slots,
(unsigned long long)vk_meshfb_short());
printf("probe-meshfb: a draw of %u workgroups with room for %u: the picture of the first 24 is whole, the draw was counted\n",
over, slots);
// As the game runs it: pipelines built in the background, and a program's modules made on the fallback's own
// thread. New shader objects of the same programs are drawn until the picture is whole: the draws before that are
// skipped, and none may leave a partial picture behind for good.
const GfxShader *fs2 = gfx_shader_own(meshfb_probe_f_spv, sizeof meshfb_probe_f_spv / 4, RES_FS);
const GfxShader *ms2 = gfx_shader_own(meshfb_probe_m_spv, sizeof meshfb_probe_m_spv / 4, RES_MS);
const GfxShader *ts2 = gfx_shader_own(meshfb_probe_t_spv, sizeof meshfb_probe_t_spv / 4, RES_TS);
const GfxShader *tm2 = gfx_shader_own(meshfb_probe_tm_spv, sizeof meshfb_probe_tm_spv / 4, RES_MS);
CHECK(fs2 && ms2 && ts2 && tm2, "a probe shader is malformed");
prog_t *plain2 = make_prog(ms2, NULL, fs2), *tasked2 = make_prog(tm2, ts2, fs2);
const MeshDraw again[2] = {{tasked2, 10, 0, {8, 5}, {1, 1, 1, 1}}, {plain2, 24, 0, {6, 4}, {1, 0.5f, 0.5f, 1}}};
vk_pipeline_async_enable();
c->mesh_fallback = 1;
int tries = 0, left = -1, wa;
while (left && tries < 500) {
if (tries++) plat_sleep_ms(10);
if (draw_picture(b, &pool, &color, again, 2, bpx)) return 1;
left = differing(bpx, px[1][5], &wa);
}
c->mesh_fallback = was;
CHECK(!left, "with background builds the picture still differs in %d texels after %d submissions", left, tries);
printf("probe-meshfb: with background builds the picture was whole at submission %d (the earlier ones skipped their draws)\n",
tries);
b->texture_release(&color);
b->pool_fini(&pool);
return 0;
}
int probe_meshfb(void) {
const char *dir = cfg_get("NX2NAVI_PROBE_MESHFB_DIR");
if (dir && *dir) return modules_part(dir);
vk_hdr_off();
const GfxBackend *b = &gfx_vulkan_backend;
uint64_t heap = 0;
CHECK(mem_set_heap_size(16ull << 20, &heap) == 0 && heap, "SetHeapSize 16 MiB");
int bad = draw_part(b, (uint8_t *)(uintptr_t)heap);
b->finish();
vk_pipeline_shutdown();
vk_exec_draw_shutdown();
vk_exec_xfer_shutdown();
vk_backend_shutdown();
long errors = vk_validation_errors_total();
if (bad || errors) {
printf("probe-meshfb: FAILED (%s, %ld validation errors)\n", bad ? "a draw check failed" : "checks passed", errors);
return 1;
}
printf("probe-meshfb: passed (0 validation errors)\n");
return 0;
}