#!/usr/bin/env python3 """embed_spirv.py — compile GLSL shaders to SPIR-V (glslc) and emit one C header. Each input shader (*.vert / *.frag) is compiled to SPIR-V and written into the output header as a `static const uint32_t spv__[]` array plus its byte length, so the Vulkan backend can create shader modules with zero runtime file dependencies (keeps the self-contained exe). Stage is inferred from the file extension. Usage: embed_spirv.py --glslc --out shader1.vert shader2.frag ... """ import argparse, os, subprocess, sys, struct, tempfile STAGE = {".vert": "vert", ".frag": "frag", ".comp": "comp"} def ident(path): base = os.path.basename(path) name, ext = os.path.splitext(base) return "%s_%s" % (name.replace("-", "_"), STAGE[ext]), STAGE[ext] def main(): ap = argparse.ArgumentParser() ap.add_argument("--glslc", required=True) ap.add_argument("--out", required=True) ap.add_argument("shaders", nargs="+") a = ap.parse_args() os.makedirs(os.path.dirname(a.out), exist_ok=True) out = ["/* Auto-generated by embed_spirv.py — do not edit. */", "#ifndef PSX_VK_SHADERS_SPV_H", "#define PSX_VK_SHADERS_SPV_H", "#include ", ""] # Scratch dir for the intermediate .spv files. It MUST NOT be the shader # source dir: every runtime target (psx-runtime, psx-oracle, each game exe) # runs this script over the SAME shaders, and under `ninja -j N` two of them # overlap. Writing ".spv" next to the source made them race — one # process removed the file the other had just written, and the loser died # with FileNotFoundError on a shader it had compiled successfully. Keeping # the scratch per-invocation makes concurrent runs independent, and the # TemporaryDirectory also stops a crash from leaving .spv litter in the # source tree. with tempfile.TemporaryDirectory(prefix="psx_spirv_") as scratch: for sh in a.shaders: name, stage = ident(sh) spv = os.path.join(scratch, os.path.basename(sh) + ".spv") cmd = [a.glslc, "--target-env=vulkan1.1", "-O", sh, "-o", spv] r = subprocess.run(cmd, capture_output=True, text=True) if r.returncode != 0: sys.stderr.write("glslc failed for %s:\n%s\n" % (sh, r.stderr)) return 1 with open(spv, "rb") as f: data = f.read() os.remove(spv) if len(data) % 4 != 0: sys.stderr.write("SPIR-V for %s not 4-byte aligned\n" % sh) return 1 words = struct.unpack("<%dI" % (len(data) // 4), data) out.append("static const uint32_t spv_%s[] = {" % name) line = " " for w in words: tok = "0x%08x," % w if len(line) + len(tok) > 96: out.append(line); line = " " line += tok if line.strip(): out.append(line) out.append("};") out.append("static const uint32_t spv_%s_size = sizeof(spv_%s);" % (name, name)) out.append("") out.append("#endif") with open(a.out, "w") as f: f.write("\n".join(out)) return 0 if __name__ == "__main__": sys.exit(main())