Fill the fragment inputs a mesh program does not write, with the point sprite's coordinate for point meshes.

A mesh pipeline whose fragment stage reads a location the mesh program never writes now gets a fragment module where
that input is a private variable set to (0, 0, 0, 1) at entry, or to (s, t, 0, 1) of gl_PointCoord when the mesh
program draws points. The fragment shaders recorded with two of the game's point-drawing mesh programs read location
5, which those programs never write; Vulkan leaves such an input undefined.

Checks: tests/fs_fill_inputs_check.py passes; 237 recorded fragment modules rewritten offline pass spirv-val; a
60 s Kakariko run with the change shows the area as before with no Vulkan errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-11 18:22:58 -05:00
parent 12d81ade9c
commit 677a69378f
4 changed files with 360 additions and 5 deletions

View File

@@ -455,6 +455,11 @@ files.
A change to how a gamepad's axes are read (`runtime/input.c` `pad_axis`: an axis counts once it has been seen at rest)
passes `python tests\pad_rest_check.py`, which builds that function with stand-ins for SDL. No game files.
A change to how a mesh pipeline's fragment module gets the inputs no mesh output feeds (`runtime/vk_pipeline.c`
`spirv_mesh_outputs`, `spirv_fs_fill_inputs`) passes `python tests\fs_fill_inputs_check.py`, which builds both
functions and runs them on mesh and fragment shaders it compiles with glslang, then validates the result. No game
files.
A change to which render passes count as drawn frame after frame (`runtime/vk_target_frames.h`: a draw into one is
skipped while its pipeline builds, a draw into any other waits) passes `python tests\target_frames_check.py`, which
builds the header alone and replays an area change after a loading screen from a player's log: the scene, probe and

View File

@@ -124,8 +124,11 @@ Grep-level look on 2026-10-22, statuses updated 2026-10-26; "saw" is what the gr
translator patch 0006 initialises the unlinked outputs. Mesh programs: a component of a position or generic vector
that the program's output map leaves out is written as (0, 0, 0, 1) by the mesh entry function
(`tools/xlate/MeshSpirv.cs`) and by the fallback's vertex stage (`runtime/mesh_fb.c` `meshfb_vs`); a whole location
the fragment stage reads and the mesh program lacks is still not padded with mesh shaders (`have = ~0u` for mesh in
`vk_pipeline.c`). **Implemented; array-span runtime validation pending.**
the fragment stage reads and the mesh program does not write is filled in the fragment module of the mesh pipeline
(`vk_pipeline.c` `spirv_fs_fill_inputs`): (0, 0, 0, 1), or for a mesh program that draws points the point sprite's
coordinate (s, t, 0, 1): the fragment shaders recorded with two of the game's point-drawing mesh programs read
location 5, which those programs never write.
**Implemented; array-span runtime validation pending.**
3. **Masked clears.** `vk_exec.c` skips colour clears with a partial channel mask (`W_CLEAR_MASK`); `clear_ds` skips stencil
clears with a partial mask (`W_STENCIL_MASK`). NX2navi issues such clears. Check the log for
`colour clear with channel mask` and `stencil clear with mask`. The hardware recipe (1.3) shows what the replay must do: colour clear = scissor 0 + channel mask, stencil clear = `value & mask` merged under the front write mask; so a draw with `colorWriteMask` / `stencilWriteMask` set to the clear mask is the exact equivalent. **Missing.**

View File

@@ -1512,6 +1512,186 @@ static uint32_t *spirv_add_outputs(const uint32_t *w, uint64_t n, uint32_t missi
return o;
}
// The locations a mesh module writes (its Output variables with a Location), and whether it draws points (execution
// mode OutputPoints). 0 when the module cannot be read.
static uint32_t spirv_mesh_outputs(const uint32_t *w, uint64_t n, int *points) {
enum { OP_EXECUTION_MODE = 16, MODE_OUTPUT_POINTS = 27 };
uint32_t bound = n >= 5 && w[3] < (1u << 22) ? w[3] : 0, mask = 0;
uint8_t *out = bound ? calloc(bound, 1) : NULL;
int8_t *loc = bound ? malloc(bound) : NULL;
*points = 0;
if (!out || !loc) return free(out), free(loc), 0;
memset(loc, -1, bound);
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len || i + len > n) break;
const uint32_t *a = w + i + 1;
if (op == OP_EXECUTION_MODE && len >= 3 && a[1] == MODE_OUTPUT_POINTS) *points = 1;
else if (op == SPV_OP_DECORATE && len >= 4 && a[1] == SPV_DEC_LOCATION && a[0] < bound && a[2] < 32) loc[a[0]] = (int8_t)a[2];
else if (op == SPV_OP_VARIABLE && len >= 4 && a[2] == SPV_SC_OUTPUT && a[1] < bound) out[a[1]] = 1;
else if (op == SPV_OP_FUNCTION) break;
i += len;
}
for (uint32_t id = 0; id < bound; id++)
if (out[id] && loc[id] >= 0) mask |= 1u << loc[id];
free(out), free(loc);
return mask;
}
// A fragment module whose inputs at the locations of `missing` read what Maxwell gives a fragment input no earlier
// stage writes: (0, 0, 0, 1), or with `points` the point sprite's coordinate (s, t, 0, 1). Each such Input variable
// (a float or a float vector) becomes a Private one, the access chains through it Private pointers, its decorations
// go, and the entry function stores the value into it before anything else. NULL when nothing is to be done or the
// module does not have the expected layout.
static uint32_t *spirv_fs_fill_inputs(const uint32_t *w, uint64_t n, uint32_t missing, int points, uint64_t *out_n) {
enum { SC_INPUT = 1, SC_PRIVATE = 6, OP_EXECUTION_MODE = 16, OP_TYPE_POINTER = 32, OP_LABEL = 248, OP_LOAD = 61,
OP_STORE = 62, OP_ACCESS_CHAIN = 65, OP_IN_BOUNDS_ACCESS_CHAIN = 66, OP_COMPOSITE_CONSTRUCT = 80,
OP_COMPOSITE_EXTRACT = 81, OP_DECORATE_ID = 332, BUILTIN_POINT_COORD = 16, MAX_FILL = 32, MAX_PRIV = 64 };
if (!missing || n < 5 || w[3] >= (1u << 22)) return NULL;
uint32_t old_bound = w[3], bound = old_bound;
uint32_t *pt_type = calloc(old_bound, 4), *vec_comp = calloc(old_bound, 4), *vec_n = calloc(old_bound, 4);
int8_t *loc = malloc(old_bound);
uint8_t *fill = calloc(old_bound, 1); // 1: a filled variable, 2: an access chain through one
uint32_t *o = NULL;
if (!pt_type || !vec_comp || !vec_n || !loc || !fill) goto done;
memset(loc, -1, old_bound);
uint32_t f32 = 0, v2 = 0, entry = 0;
uint64_t ep_at = 0, ann_end = 0, fn_at = 0;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len || i + len > n) goto done;
const uint32_t *a = w + i + 1;
if (op == SPV_OP_ENTRY_POINT && !ep_at) ep_at = i, entry = a[1];
else if (op == SPV_OP_DECORATE || op == 72 || op == OP_DECORATE_ID) {
ann_end = i + len;
if (op == SPV_OP_DECORATE && len >= 4 && a[1] == SPV_DEC_LOCATION && a[0] < old_bound && a[2] < 32)
loc[a[0]] = (int8_t)a[2];
} else if (op == SPV_OP_TYPE_FLOAT && len >= 3 && a[1] == 32) f32 = a[0];
else if (op == SPV_OP_TYPE_VECTOR && len >= 4 && a[0] < old_bound) {
vec_comp[a[0]] = a[1], vec_n[a[0]] = a[2];
if (a[1] == f32 && a[2] == 2) v2 = a[0];
} else if (op == OP_TYPE_POINTER && len >= 4 && a[0] < old_bound) pt_type[a[0]] = a[2];
else if (op == SPV_OP_FUNCTION) { fn_at = i; break; }
i += len;
}
if (!ep_at || !ann_end || !fn_at || !f32 || !entry || ann_end > fn_at) goto done;
// the variables to fill, and the Private pointer types they and the chains through them need (pass 1)
uint32_t vars[MAX_FILL], vtype[MAX_FILL], nvars = 0, priv[MAX_PRIV][2], npriv = 0; // priv: Input pointer type -> Private
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
const uint32_t *a = w + i + 1;
uint32_t ptr = 0;
if (op == SPV_OP_VARIABLE && len >= 4 && a[2] == SC_INPUT && a[1] < old_bound && a[0] < old_bound && i < fn_at) {
int l = loc[a[1]];
uint32_t t = pt_type[a[0]];
int floaty = t == f32 || (t < old_bound && vec_comp[t] == f32 && vec_n[t] >= 2 && vec_n[t] <= 4);
if (l >= 0 && (missing >> l & 1) && floaty && nvars < MAX_FILL) {
fill[a[1]] = 1, vars[nvars] = a[1], vtype[nvars++] = t, ptr = a[0];
}
} else if ((op == OP_ACCESS_CHAIN || op == OP_IN_BOUNDS_ACCESS_CHAIN) && len >= 4 && a[2] < old_bound && fill[a[2]] &&
a[1] < old_bound && a[0] < old_bound) {
fill[a[1]] = 2, ptr = a[0];
}
if (ptr) {
uint32_t k = 0;
while (k < npriv && priv[k][0] != ptr) k++;
if (k == npriv) {
if (npriv == MAX_PRIV) goto done;
priv[npriv][0] = ptr, priv[npriv++][1] = bound++;
}
}
i += len;
}
if (!nvars) goto done;
// the globals added before the first function: two constants, gl_PointCoord, and the Private pointer types not yet
// declared (a filled variable's goes right before the variable, below; an access chain's is only used in a function)
uint32_t extra[MAX_PRIV * 4 + 32], ne = 0, c0 = bound++, c1 = bound++, pc_ptr = 0, pc_var = 0;
uint8_t declared[MAX_PRIV] = {0};
extra[ne++] = 4u << 16 | SPV_OP_CONSTANT, extra[ne++] = f32, extra[ne++] = c0, extra[ne++] = 0;
extra[ne++] = 4u << 16 | SPV_OP_CONSTANT, extra[ne++] = f32, extra[ne++] = c1, extra[ne++] = 0x3f800000;
if (points) {
if (!v2) v2 = bound++, extra[ne++] = 4u << 16 | SPV_OP_TYPE_VECTOR, extra[ne++] = v2, extra[ne++] = f32, extra[ne++] = 2;
pc_ptr = bound++, pc_var = bound++;
extra[ne++] = 4u << 16 | OP_TYPE_POINTER, extra[ne++] = pc_ptr, extra[ne++] = SC_INPUT, extra[ne++] = v2;
extra[ne++] = 4u << 16 | SPV_OP_VARIABLE, extra[ne++] = pc_ptr, extra[ne++] = pc_var, extra[ne++] = SC_INPUT;
}
o = malloc((n + ne + (uint64_t)npriv * 4 + 16 + (uint64_t)nvars * 12 + 32) * 4);
if (!o) goto done;
uint32_t *p = o;
memcpy(p, w, 5 * 4), p += 5;
int in_entry = 0, stored = 0;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
const uint32_t *a = w + i + 1;
if (i == fn_at) {
for (uint32_t k = 0; k < npriv; k++)
if (!declared[k]) *p++ = 4u << 16 | OP_TYPE_POINTER, *p++ = priv[k][1], *p++ = SC_PRIVATE, *p++ = pt_type[priv[k][0]];
memcpy(p, extra, ne * 4), p += ne;
}
if (i == ep_at) { // the interface: filled variables out before SPIR-V 1.4 (Private is not listed), gl_PointCoord in
uint32_t *head = p, name_words = (uint32_t)(strnlen((const char *)(a + 2), (len - 3) * 4) / 4 + 1);
for (uint32_t k = 0; k < len; k++) {
uint32_t v = w[i + k];
if (k >= 3 + name_words && v < old_bound && fill[v] == 1 && w[1] < 0x00010400) continue;
*p++ = v;
}
if (pc_var) *p++ = pc_var;
*head = (uint32_t)(p - head) << 16 | SPV_OP_ENTRY_POINT;
i += len;
continue;
}
if ((op == SPV_OP_DECORATE || op == OP_DECORATE_ID) && len >= 3 && a[0] < old_bound && fill[a[0]] == 1) {
i += len; // Location, Flat and the like belong to Input variables
if (i == ann_end && pc_var)
*p++ = 4u << 16 | SPV_OP_DECORATE, *p++ = pc_var, *p++ = SPV_DEC_BUILTIN, *p++ = BUILTIN_POINT_COORD;
continue;
}
if (op == SPV_OP_VARIABLE && len >= 4 && a[1] < old_bound && fill[a[1]] == 1) // its pointer type first
for (uint32_t k = 0; k < npriv; k++)
if (priv[k][0] == a[0] && !declared[k])
declared[k] = 1, *p++ = 4u << 16 | OP_TYPE_POINTER, *p++ = priv[k][1], *p++ = SC_PRIVATE, *p++ = pt_type[priv[k][0]];
memcpy(p, w + i, len * 4);
if ((op == SPV_OP_VARIABLE || op == OP_ACCESS_CHAIN || op == OP_IN_BOUNDS_ACCESS_CHAIN) && len >= 4 &&
a[1] < old_bound && fill[a[1]]) {
for (uint32_t k = 0; k < npriv; k++)
if (priv[k][0] == a[0]) p[1] = priv[k][1];
if (op == SPV_OP_VARIABLE) p[3] = SC_PRIVATE;
}
p += len;
if (i + len == ann_end && pc_var) // gl_PointCoord's BuiltIn after the last decoration
*p++ = 4u << 16 | SPV_OP_DECORATE, *p++ = pc_var, *p++ = SPV_DEC_BUILTIN, *p++ = BUILTIN_POINT_COORD;
if (op == SPV_OP_FUNCTION && len >= 5) in_entry = a[1] == entry;
i += len;
if (op == OP_LABEL && in_entry && !stored) { // the entry's first block: its OpVariables, then the stores
while (i < n && (w[i] & 0xffff) == SPV_OP_VARIABLE) memcpy(p, w + i, (w[i] >> 16) * 4), p += w[i] >> 16, i += w[i] >> 16;
uint32_t sx = c0, sy = c0;
if (pc_var) {
uint32_t pc = bound++;
sx = bound++, sy = bound++;
*p++ = 4u << 16 | OP_LOAD, *p++ = v2, *p++ = pc, *p++ = pc_var;
*p++ = 5u << 16 | OP_COMPOSITE_EXTRACT, *p++ = f32, *p++ = sx, *p++ = pc, *p++ = 0;
*p++ = 5u << 16 | OP_COMPOSITE_EXTRACT, *p++ = f32, *p++ = sy, *p++ = pc, *p++ = 1;
}
for (uint32_t k = 0; k < nvars; k++) {
uint32_t t = vtype[k], c = t == f32 ? 1 : vec_n[t], comp[4] = {sx, sy, c0, c1}, val = comp[0];
if (c > 1) {
val = bound++;
*p++ = (3 + c) << 16 | OP_COMPOSITE_CONSTRUCT, *p++ = t, *p++ = val;
for (uint32_t q = 0; q < c; q++) *p++ = comp[q]; // x y z w in order: (s, t, 0, 1) or (0, 0, 0, 1)
}
*p++ = 3u << 16 | OP_STORE, *p++ = vars[k], *p++ = val;
}
stored = 1;
}
}
if (!stored) { free(o), o = NULL; goto done; }
o[3] = bound;
*out_n = (uint64_t)(p - o);
done:
free(pt_type), free(vec_comp), free(vec_n), free(loc), free(fill);
return o;
}
// Two fixes for what the translator declares but a shader does not use (NULL: nothing to change):
// - the fragment shader interlock capability and extension, which the translator adds to every fragment shader when the host
// reports interlock support (SpirvGenerator.cs) and vk_device.c does not enable; kept when the shader interlocks;
@@ -2847,24 +3027,33 @@ static VkPipeline create_graphics(const VkPipeState *s, const GKey *k, const Lay
}
if (s->fs) {
GfxShaderIo fin[32], vout[32];
uint32_t need = gfx_shader_inputs(s->fs, fin), have = mesh ? ~0u : gfx_shader_outputs(s->vs, vout), missing = 0;
// a mesh stage's outputs from its module (spirv_mesh_outputs): the translation reflects none
int points = 0;
uint32_t need = gfx_shader_inputs(s->fs, fin), missing = 0,
have = mesh ? (vcode ? spirv_mesh_outputs(vcode, vw, &points) : ~0u) : gfx_shader_outputs(s->vs, vout);
for (int i = 0; i < 32; i++) {
if (!(need >> i & 1) || fin[i].packed) continue;
if (!(have >> i & 1)) missing |= 1u << i;
else if (!mesh && vout[i].type != fin[i].type)
LOG_ONCE("vk_pipeline: varying %d is type %d in the vertex stage, %d in the fragment stage", i, vout[i].type, fin[i].type);
}
if (missing && !l->set1) {
if (missing && !l->set1 && !mesh) {
uint64_t n2 = 0;
vpatched = spirv_add_outputs(vcode, vw, missing, fin, &n2);
if (!vpatched) LOG_ONCE("vk_pipeline: cannot add missing vertex outputs (mask %#x)", missing);
else vcode = vpatched, vw = n2;
}
const uint32_t *fcode = gfx_shader_spirv(s->fs, &fw);
if (missing && mesh && fcode) { // inputs no mesh output feeds read as Maxwell gives them (spirv_fs_fill_inputs)
uint64_t n2 = 0;
uint32_t *f = spirv_fs_fill_inputs(fcode, fw, missing, points, &n2);
if (f) fcode = fpatched = f, fw = n2;
else LOG_ONCE("vk_pipeline: cannot fill fragment inputs the mesh stage lacks (mask %#x)", missing);
}
if (k->dsb) {
uint64_t n2 = 0;
uint32_t *d = spirv_dual_source(fcode, fw, &n2);
if (d) fcode = fpatched = d, fw = n2;
if (d) free(fpatched), fcode = fpatched = d, fw = n2;
else LOG_ONCE("vk_pipeline: SRC1 blend factors but the fragment shader writes no colour output 1");
}
if (l->set1 || fs_off) {

View File

@@ -0,0 +1,158 @@
"""Fragment inputs that no mesh output feeds (runtime/vk_pipeline.c spirv_mesh_outputs, spirv_fs_fill_inputs): builds
the two functions, cut from vk_pipeline.c by their text, and runs them on modules glslang makes here:
- a mesh shader that draws points and writes no generic output, and one that draws triangles and writes location 1;
- a fragment shader that reads locations 1 and 5 (5 through a component, as the translator's shaders do).
Checks that the point mesh's mask is 0 with points, the triangle mesh's is location 1; that the fragment module
rewritten for the point mesh has a PointCoord input and no Input at location 5, and for the triangle mesh keeps
location 1 as an Input; and that every rewritten module passes spirv-val. No game files.
Usage: python tests/fs_fill_inputs_check.py [--cc <clang>]
"""
import argparse
import os
import shutil
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
MESH_POINTS = '''#version 460
#extension GL_EXT_mesh_shader : require
layout(local_size_x = 1) in;
layout(points, max_vertices = 1, max_primitives = 1) out;
void main() {
SetMeshOutputsEXT(1, 1);
gl_MeshVerticesEXT[0].gl_Position = vec4(0.0, 0.0, 0.5, 1.0);
gl_MeshVerticesEXT[0].gl_PointSize = 4.0;
gl_PrimitivePointIndicesEXT[0] = 0;
}
'''
MESH_TRIANGLES = '''#version 460
#extension GL_EXT_mesh_shader : require
layout(local_size_x = 1) in;
layout(triangles, max_vertices = 3, max_primitives = 1) out;
layout(location = 1) out vec4 attr1[];
void main() {
SetMeshOutputsEXT(3, 1);
for (int i = 0; i < 3; i++) {
gl_MeshVerticesEXT[i].gl_Position = vec4(float(i), 0.0, 0.5, 1.0);
attr1[i] = vec4(1.0);
}
gl_PrimitiveTriangleIndicesEXT[0] = uvec3(0, 1, 2);
}
'''
FRAG = '''#version 460
layout(location = 1) in vec4 a1;
layout(location = 5) in vec4 a5;
layout(location = 0) out vec4 color;
void main() { color = a1 + vec4(a5.y, a5.x, 0.0, a5.w); }
'''
HARNESS = r'''
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum { SPV_OP_ENTRY_POINT = 15, SPV_OP_TYPE_INT = 21, SPV_OP_TYPE_FLOAT = 22, SPV_OP_TYPE_VECTOR = 23,
SPV_OP_TYPE_POINTER = 32, SPV_OP_CONSTANT = 43, SPV_OP_CONSTANT_COMPOSITE = 44,
SPV_OP_CONSTANT_NULL = 46, SPV_OP_FUNCTION = 54, SPV_OP_VARIABLE = 59,
SPV_OP_DECORATE = 71, SPV_DEC_BUILTIN = 11, SPV_DEC_LOCATION = 30, SPV_SC_OUTPUT = 3 };
@BODY@
static uint32_t *rd(const char *p, uint64_t *n) {
FILE *f = fopen(p, "rb");
if (!f) exit(2);
fseek(f, 0, SEEK_END);
long l = ftell(f);
fseek(f, 0, SEEK_SET);
uint32_t *w = malloc((size_t)l);
if (fread(w, 1, (size_t)l, f) != (size_t)l) exit(2);
fclose(f);
*n = (uint64_t)l / 4;
return w;
}
int main(int argc, char **argv) { // mesh.spv frag.spv out.spv: prints "<mask> <points>", writes the rewrite
(void)argc;
uint64_t mn, fn, on = 0;
uint32_t *m = rd(argv[1], &mn), *fr = rd(argv[2], &fn);
int points = 0;
uint32_t have = spirv_mesh_outputs(m, mn, &points);
uint32_t *o = spirv_fs_fill_inputs(fr, fn, ((1u << 1) | (1u << 5)) & ~have, points, &on);
printf("%u %d\n", have, points);
if (!o) return 1;
FILE *f = fopen(argv[3], "wb");
fwrite(o, 4, on, f);
fclose(f);
return 0;
}
'''
def inputs_and_builtins(spv):
w = struct.unpack('<%dI' % (len(spv) // 4), spv)
loc, builtin, sc = {}, {}, {}
i = 5
while i < len(w):
n, op = w[i] >> 16, w[i] & 0xFFFF
if op == 71 and w[i + 2] == 30:
loc[w[i + 1]] = w[i + 3]
if op == 71 and w[i + 2] == 11:
builtin[w[i + 1]] = w[i + 3]
if op == 59:
sc[w[i + 2]] = w[i + 3]
i += n
return {loc[v] for v, s in sc.items() if s == 1 and v in loc}, {builtin[v] for v, s in sc.items() if s == 1 and v in builtin}
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('--cc', help='the C compiler (default: MSYS2 UCRT64 clang on Windows, clang elsewhere)')
args = ap.parse_args()
win = os.name == 'nt'
cc = args.cc or (str(MSYS_BIN / 'clang.exe') if win and MSYS_BIN.exists() else 'clang')
glslang = str(MSYS_BIN / 'glslangValidator.exe') if win and MSYS_BIN.exists() else shutil.which('glslangValidator')
val = str(MSYS_BIN / 'spirv-val.exe') if win and MSYS_BIN.exists() else shutil.which('spirv-val')
env = dict(os.environ)
if win and MSYS_BIN.exists():
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
src = (ROOT / 'runtime' / 'vk_pipeline.c').read_text(encoding='utf-8')
body = src[src.index('// The locations a mesh module writes'):src.index('// Two fixes for what the translator declares')]
fails = 0
with tempfile.TemporaryDirectory(prefix='nx2navi-fs-fill-') as tmp:
t = Path(tmp)
for name, text, stage in (('points', MESH_POINTS, 'mesh'), ('triangles', MESH_TRIANGLES, 'mesh'), ('frag', FRAG, 'frag')):
(t / f'{name}.{stage}').write_text(text)
subprocess.check_call([glslang, '-V', '--target-env', 'vulkan1.3', '-o', str(t / f'{name}.spv'),
str(t / f'{name}.{stage}')], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
(t / 'check.c').write_text(HARNESS.replace('@BODY@', body), encoding='utf-8')
exe = t / ('check.exe' if win else 'check')
subprocess.check_call([cc, '-O2', '-w', str(t / 'check.c'), '-o', str(exe)], env=env)
for mesh, want_mask, want_points in (('points', 0, 1), ('triangles', 1 << 1, 0)):
r = subprocess.run([str(exe), str(t / f'{mesh}.spv'), str(t / 'frag.spv'), str(t / f'out_{mesh}.spv')],
capture_output=True, text=True, env=env)
mask, points = (int(x) for x in r.stdout.split())
if (mask, points) != (want_mask, want_points) or r.returncode:
print(f'FAIL {mesh}: mask {mask:#x} points {points} (want {want_mask:#x} {want_points}), rewrite {r.returncode == 0}')
fails += 1
continue
out = (t / f'out_{mesh}.spv').read_bytes()
ins, builtins = inputs_and_builtins(out)
want_ins = set() if mesh == 'points' else {1}
if ins != want_ins or (mesh == 'points') != (16 in builtins):
print(f'FAIL {mesh}: inputs {sorted(ins)} builtins {sorted(builtins)}')
fails += 1
v = subprocess.run([val, '--target-env', 'vulkan1.3', str(t / f'out_{mesh}.spv')], capture_output=True, text=True)
if v.returncode:
print(f'FAIL {mesh}: spirv-val: {(v.stdout + v.stderr).strip()}')
fails += 1
print('fs fill inputs: ok' if not fails else f'fs fill inputs: {fails} failures')
return 1 if fails else 0
if __name__ == '__main__':
sys.exit(main())