diff --git a/docs/building.md b/docs/building.md index c4c73f5d..bcc23dd6 100644 --- a/docs/building.md +++ b/docs/building.md @@ -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 diff --git a/docs/maxwell.md b/docs/maxwell.md index eff48f7e..b173af4b 100644 --- a/docs/maxwell.md +++ b/docs/maxwell.md @@ -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.** diff --git a/runtime/vk_pipeline.c b/runtime/vk_pipeline.c index 8dfd5984..40ac2e11 100644 --- a/runtime/vk_pipeline.c +++ b/runtime/vk_pipeline.c @@ -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) { diff --git a/tests/fs_fill_inputs_check.py b/tests/fs_fill_inputs_check.py new file mode 100644 index 00000000..0ba792bf --- /dev/null +++ b/tests/fs_fill_inputs_check.py @@ -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 ] +""" +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 +#include +#include +#include +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 " ", 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())