`examples/<name>/` was a tested surface in name only. The source-quality job had no examples check at all, and scripts/smoke_examples.sh accepted any process that had not printed one of its error markers: a project that died during startup, exited after a single frame, or crashed without a marker was reported as PASS. Nothing stopped a malformed or unregistered example from reaching dev, which is how 14 examples arrived without READMEs and 20 were never registered in the overview table. Source side - scripts/check_examples.py classifies every directory under examples/ as either runnable (main.nut plus a config.nut that assigns the engine `config` table) or as a reviewed non-runnable sample listed in NON_RUNNABLE_EXAMPLES with a reason. Both kinds must own a README.md and appear in examples/README.md. Stale exemptions and overview links to missing directories fail as well. - `make check/examples` and a new source-quality CI step run the gate. - scripts/tests/test_check_examples.py covers it and pins each failure mode against fixtures. Runtime side - scripts/smoke_examples.sh now enforces liveness, not just marker absence: every runnable example must still be running after MIN_RUN_SECONDS (default 2s; CI passes 2s), and a process that dies inside that window fails whatever its exit code. A clean self-exit after the window passes with an explicit note instead of being silently accepted as a pass. - The Linux CI step sets MIN_RUN_SECONDS=2 and keeps RUN_SECONDS=6, so the job's wall clock is unchanged (13.7 min of its 20 min budget on dev). Windows / GBK - scripts/utf8_stdio.py re-encodes stdout/stderr as UTF-8 and every scripts/*.py program calls it as the first statement of its __main__ block. On a Chinese Windows a redirected stream falls back to the ANSI code page (cp936), and printing a character that code page cannot encode (arrows, superscripts, bullet separators, emoji - all common in this repository) raised UnicodeEncodeError and turned a checker into a traceback. - The mirror image is input: read_text() without encoding= decoded UTF-8 repository text as cp936 and raised UnicodeDecodeError. - scripts/tests/test_utf8_stdio.py enforces both conventions for every script and test module; text file I/O and child-process decoding now pass encoding="utf-8" explicitly. - Unit-test invocations run with `python3 -X utf8` so the runner's own output never follows the host code page. Violations the new gates found on top of dev, fixed here: 14 examples shipped without a README (fluid3d, mesh-impact-lab, mesh-modifier-lab, spline-tube-lab on the earlier dev tip, plus pcg-depth-of-field, pcg-detail-overwrite, pcg-layer-culling, pcg-location-system, pcg-runtime-orchestration, pcg-scene-player-culling, pcg-snow-wind, pcg-thunder-strike, pcg-ui-scaler and pcg-underwater added by #397), 20 were missing from the overview table (those 14 plus archspace-editor, geometry-stroke-lab, interior-mapping, layered-animation, triplanar-decal, terrain-stamping), and scripts/export_meshret_onnx.py had no UTF-8 stdio guard. Verified locally: `python -m ruff check scripts` (All checks passed); scripts/check_test_manifest.py; scripts/check_quality_metadata.py; scripts/check_architecture_contracts.py --base HEAD; scripts/check_nodiscard.py; `make -n check/examples`; scripts/check_examples.py (125 examples: 118 runnable, 7 reviewed non-runnable); and `python -X utf8 -m unittest discover -s scripts/tests -p "test_*.py"` (155 tests, OK). The runtime half of the contract needs a Linux headless build, so it is verified by the CI examples smoke step rather than locally.
144 lines
5.0 KiB
Python
144 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Compile the EVEngine GLSL shader sources to WGSL C-arrays for the WebGPU
|
|
backend.
|
|
|
|
For every `*.vert` / `*.frag` / `*.glsl` under src/modules/graphics/shaders,
|
|
this script runs the classic GLSL -> SPIR-V toolchain (glslc, or
|
|
glslangValidator as fallback) and then converts the SPIR-V to WGSL with Tint,
|
|
emitting a `<name>_wgsl.inc` file holding a `static const uint32_t` array (UTF-8
|
|
WGSL is embedded word-aligned via uint32_t storage, mirroring the existing
|
|
`*_spv.inc` convention).
|
|
|
|
Existing files are only rewritten when the WGSL content actually changes, so the
|
|
committed `.inc` files stay stable across builds.
|
|
|
|
Usage:
|
|
python3 scripts/compile_webgpu_shaders.py --shaders-dir <dir> [--tint <path>] [--glslc <path>]
|
|
|
|
Environment overrides: EVENGINE_GLSLC, EVENGINE_TINT.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
import utf8_stdio
|
|
|
|
STAGE_EXT = {".vert": "vertex", ".frag": "fragment", ".glsl": "fragment"}
|
|
|
|
|
|
def find_tool(name, env_name, extra_hints):
|
|
if env_name and os.environ.get(env_name):
|
|
return os.environ[env_name]
|
|
p = shutil.which(name)
|
|
if p:
|
|
return p
|
|
for hint in extra_hints:
|
|
if hint and os.path.exists(hint):
|
|
return hint
|
|
return None
|
|
|
|
|
|
def emit_wgsl_inc(src_path: Path, wgsl: bytes) -> Path:
|
|
out = src_path.with_suffix(src_path.suffix + "_wgsl.inc")
|
|
stem = src_path.stem.replace("-", "_").replace(".", "_")
|
|
array_name = f"{stem}_wgsl"
|
|
|
|
# Pack the WGSL source so the C++ side can memcpy it as an opaque blob.
|
|
padded = wgsl + b"\x00" * (-len(wgsl) % 4)
|
|
words = struct.unpack(f"<{len(padded) // 4}I", padded)
|
|
lines = [f"static const uint32_t {array_name}[] = {{"]
|
|
for i in range(0, len(words), 8):
|
|
lines.append(" " + ", ".join(f"0x{w:08x}" for w in words[i : i + 8]) + ",")
|
|
lines.append("};")
|
|
lines.append(f"static const size_t {array_name}_count = {len(words)};")
|
|
content = "\n".join(lines) + "\n"
|
|
|
|
if out.exists() and out.read_bytes() == content.encode("utf-8"):
|
|
return out
|
|
out.write_text(content, encoding="utf-8")
|
|
print(f"[webgpu-shaders] wrote {out}")
|
|
return out
|
|
|
|
|
|
def compile_one(glsl_path: Path, glslc, glslang, tint, scratch: Path):
|
|
ext = glsl_path.suffix.lower()
|
|
stage = STAGE_EXT.get(ext)
|
|
if stage is None:
|
|
return
|
|
|
|
# Fragment includes (parallax_map.glsl / tex_cell_bomb.glsl / tonemap.glsl)
|
|
# are pulled into a main shader via #include; they have no entry point and
|
|
# cannot be compiled standalone. Skip them.
|
|
try:
|
|
src = glsl_path.read_text(encoding="utf-8", errors="replace")
|
|
except OSError:
|
|
return
|
|
if "void main" not in src:
|
|
print(f"[webgpu-shaders] SKIP {glsl_path.name}: include fragment (no main)")
|
|
return
|
|
|
|
spv_path = scratch / (glsl_path.stem + ".spv")
|
|
|
|
# 1) GLSL -> SPIR-V
|
|
if glslc:
|
|
cmd = [glslc, f"-fshader-stage={stage}", "-o", str(spv_path), str(glsl_path)]
|
|
elif glslang:
|
|
cmd = [glslang, f"-S{stage}", "-V", "-o", str(spv_path), str(glsl_path)]
|
|
else:
|
|
print(f"[webgpu-shaders] SKIP {glsl_path.name}: no glslc/glslangValidator on PATH")
|
|
return
|
|
subprocess.run(cmd, check=True, capture_output=True)
|
|
|
|
# 2) SPIR-V -> WGSL
|
|
if not tint:
|
|
print(f"[webgpu-shaders] SKIP {glsl_path.name}: no tint on PATH")
|
|
return
|
|
wgsl_path = scratch / (glsl_path.stem + ".wgsl")
|
|
subprocess.run([tint, str(spv_path), "--format", "wgsl", "-o", str(wgsl_path)],
|
|
check=True, capture_output=True)
|
|
wgsl = wgsl_path.read_bytes()
|
|
emit_wgsl_inc(glsl_path, wgsl)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--shaders-dir", required=True)
|
|
ap.add_argument("--tint", default="")
|
|
ap.add_argument("--glslc", default="")
|
|
args = ap.parse_args()
|
|
|
|
shaders_dir = Path(args.shaders_dir)
|
|
if not shaders_dir.is_dir():
|
|
print(f"[webgpu-shaders] missing shaders dir {shaders_dir}")
|
|
return 1
|
|
|
|
glslc = args.glslc or find_tool("glslc", "EVENGINE_GLSLC", [])
|
|
glslang = args.glslc or find_tool("glslangValidator", "EVENGINE_GLSLANG", [])
|
|
tint = args.tint or find_tool("tint", "EVENGINE_TINT", [])
|
|
|
|
if not (glslc or glslang):
|
|
print("[webgpu-shaders] WARNING: no GLSL compiler (glslc/glslangValidator); "
|
|
"WebGPU shader .inc files will not be (re)generated.")
|
|
if not tint:
|
|
print("[webgpu-shaders] WARNING: no tint found; WGSL .inc files will not be regenerated.")
|
|
|
|
with tempfile.TemporaryDirectory(prefix="eve_wgsl_") as td:
|
|
scratch = Path(td)
|
|
for glsl_path in sorted(shaders_dir.glob("*")):
|
|
if glsl_path.suffix.lower() in STAGE_EXT:
|
|
try:
|
|
compile_one(glsl_path, glslc, glslang, tint, scratch)
|
|
except (subprocess.CalledProcessError, OSError) as e:
|
|
print(f"[webgpu-shaders] ERROR {glsl_path.name}: {e}", file=sys.stderr)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
utf8_stdio.enable_utf8_stdio()
|
|
sys.exit(main())
|