Files
NeHe-SDL_GPU/scripts/compile_shaders.py

260 lines
9.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""
SPDX-FileCopyrightText: (C) 2025 a dinosaur
SPDX-License-Identifier: Zlib
"""
import os
import shutil
import sys
from collections import namedtuple
from pathlib import Path
import subprocess
import platform
from typing import Iterable
def compile_metal_shaders(
sources: list[str | Path], library: str | Path,
cflags: list[str] = None, sdk="macosx", debug: bool = False, cwd: Path | None = None) -> None:
"""Build a Metal shader library from a list of Metal shaders
:param sources: List of Metal shader source paths
:param library: Path to the Metal library to build
:param cflags: Optional list of additional compiler parameters
:param sdk: Name of the Xcode platform SDK to use (default: "macosx"),
can be "macosx", "iphoneos", "iphonesimulator", "appletvos", or "appletvsimulator"
:param debug: Generate a symbol file that can be used to debug the shader library
:param cwd: Optionally set the current working directory for the compiler
"""
if cflags is None:
cflags = []
def xcrun_find_program(name: str):
return subprocess.run(["xcrun", "-sdk", sdk, "-f", name],
capture_output=True, check=True).stdout.decode("utf-8").strip()
# Find Metal compilers
metal = xcrun_find_program("metal")
if debug:
metal_dsymutil = xcrun_find_program("metal-dsymutil")
else:
metallib = xcrun_find_program("metallib")
# Compile each source to an AIR (Apple Intermediate Representation)
cflags = cflags + ["-frecord-sources"]
air_objects = [f"{str(s).removesuffix('.metal')}.air" for s in sources]
for src, obj in zip(sources, air_objects):
subprocess.run([metal, *cflags, "-c", src, "-o", obj], cwd=cwd, check=True)
try:
# Build the Metal library
if debug:
subprocess.run([metal, "-frecord-sources", "-o", library, *air_objects], cwd=cwd, check=True)
subprocess.run([metal_dsymutil, "-flat", "-remove-source", library], cwd=cwd, check=True)
else:
subprocess.run([metallib, "-o", library, *air_objects], cwd=cwd, check=True)
finally:
# Clean up AIR objects
for obj in air_objects:
cwd.joinpath(obj).unlink()
Shader = namedtuple("Shader", ["source", "output"])
def shaders_suffixes(shaders: Iterable[Shader],
in_suffix: str | None, out_suffix: str | None) -> Iterable[Shader]:
"""Add file extensions to the source and outputs of a list of shaders
:param shaders: The list of Shader tuples
:param in_suffix: Optional file extension to append to source
:param out_suffix: Optional file extension to append to output
:return: Generator with the modified shaders
"""
for s in shaders:
yield Shader(
f"{s.source}.{in_suffix}" if in_suffix else s.source,
f"{s.output}.{out_suffix}" if out_suffix else s.output)
def shader_prepend_ext(shader: Shader, prep_ext: str) -> Shader:
"""Prepend `prep_ext` to the shader's output path's extension
:param shader: Shader to modify
:param prep_ext: Extension to prepend (without the dot)
:return: The modified shader
"""
out = Path(shader.output)
return Shader(shader.source, out.with_suffix(f".{prep_ext}{out.suffix}"))
def compile_spirv_shaders(shaders: Iterable[Shader], suffix: str = "spv",
glslang: str | None = None, dxc: str | None = None, cwd: Path | None = None) -> None:
"""Compile shaders to SPIR-V using glslang for GLSL shaders and DXC for HLSL shaders
If a GLSL version exists it will be built with glslang,
else building HLSL with DXC will be attempted.
:param shaders: The list of shader source paths to compile
:param glslang: Optional path to glslang executable, if `None` defaults to "glslang"
:param dxc: Optional path to dxc excutable, if `None` defaults to "dxc"
:param cwd: Optionally set the current working directory for the compiler
"""
for glsl, hlsl in zip(
shaders_suffixes(shaders, "glsl", suffix),
shaders_suffixes(shaders, "hlsl", suffix)):
if cwd.joinpath(glsl.source).exists():
flags = ["--quiet"]
compile_glsl_spirv_shader(shader_prepend_ext(glsl, "vtx"), "vert",
flags=[*flags, "-DVERTEX"], glslang=glslang, cwd=cwd)
compile_glsl_spirv_shader(shader_prepend_ext(glsl, "frg"), "frag",
flags=flags, glslang=glslang, cwd=cwd)
else:
compile_hlsl_spirv_shader(shader_prepend_ext(hlsl, "vtx"), "vert",
flags=["-DVULKAN", "-DVERTEX"], dxc=dxc, cwd=cwd)
compile_hlsl_spirv_shader(shader_prepend_ext(hlsl, "frg"), "frag",
flags=["-DVULKAN"], dxc=dxc, cwd=cwd)
def compile_glsl_spirv_shader(shader: Shader, type: str, flags: list[str] = None,
glslang: str | None = None, cwd: Path | None = None) -> None:
"""Compile GLSL shaders to SPIR-V using glslang
:param shader: The shaders to compile
:param type: Type of shader to compile
:param flags: List of additional flags to pass to glslang
:param glslang: Optional path to glslang executable, if `None` defaults to "glslang"
:param cwd: Optionally set the current working directory for the compiler
"""
if glslang is None:
glslang = "glslang"
if flags is None:
flags = []
flags += ["-V", "-S", type, "-o", shader.output, shader.source]
subprocess.run([glslang, *flags], cwd=cwd, check=True)
def compile_hlsl_spirv_shader(shader: Shader, type: str, flags: list[str] = None,
dxc: str | None = None, cwd: Path | None = None) -> None:
"""Compile HLSL shaders to SPIR-V using DXC
:param shader: The shaders to compile
:param type: Type of shader to compile
:param flags: List of additional flags to pass to DXC
:param dxc: Optional path to dxc excutable, if `None` defaults to "dxc"
:param cwd: Optionally set the current working directory for the compiler
"""
if dxc is None:
dxc = "dxc"
if flags is None:
flags = []
entry, shader_type = {
"vert": ("VertexMain", "vs_6_0"),
"frag": ("FragmentMain", "ps_6_0") }[type]
cflags = ["-spirv", *flags, "-E", entry, "-T", shader_type]
subprocess.run([dxc, *cflags, "-Fo", shader.output, shader.source], cwd=cwd, check=True)
def compile_d3d12_shaders(shaders: Iterable[Shader], build_dxbc: bool = False,
dxc: str | None = None, cwd: Path | None = None) -> None:
for shader in shaders_suffixes(shaders, "hlsl", "dxb"):
compile_dxil_shader(shader_prepend_ext(shader, "vtx"), "vert",
flags=["-DD3D12", "-DVERTEX"], dxc=dxc, cwd=cwd)
compile_dxil_shader(shader_prepend_ext(shader, "pxl"), "frag",
flags=["-DD3D12"], dxc=dxc, cwd=cwd)
if build_dxbc: # FXC is only available thru the Windows SDK
for shader in shaders_suffixes(shaders, "hlsl", "fxb"):
compile_dxbc_shader(shader_prepend_ext(shader, "vtx"), "vert",
flags=["/DD3D12", "/DVERTEX"], cwd=cwd)
compile_dxbc_shader(shader_prepend_ext(shader, "pxl"), "frag",
flags=["/DD3D12"], cwd=cwd)
def compile_dxil_shader(shader: Shader, type: str, flags: list[str] | None = None,
dxc: str | None = None, cwd: Path | None = None) -> None:
"""Compile an HLSL shaders to DXIL using DXC
:param shader: Path to the shader source to compile
:param type: Type of shader to compile
:param flags: Optional list of flags to pass to DXC
:param dxc: Optional path to dxc excutable, if `None` defaults to "dxc"
:param cwd: Optionally set the current working directory for the compiler
"""
if flags is None:
flags = []
if dxc is None:
dxc = "dxc"
entry, shader_type = {
"vert": ("VertexMain", "vs_6_0"),
"frag": ("PixelMain", "ps_6_0") }[type]
cflags = [*flags, "-E", entry, "-T", shader_type]
subprocess.run([dxc, *cflags, "-Fo", shader.output, shader.source], cwd=cwd, check=True)
def compile_dxbc_shader(shader: Shader, type: str, flags: list[str] | None = None,
cwd: Path | None = None) -> None:
"""Compile an HLSL shader to DXBC using FXC
:param shader: Path to the shader source to compile
:param type: Type of shader to compile
:param flags: Optional list of flags to pass to FXC
:param cwd: Optionally set the current working directory for the compiler
"""
if flags is None:
flags = []
entry, shader_type = {
"vert": ("VertexMain", "vs_5_1"),
"frag": ("PixelMain", "ps_5_1") }[type]
cflags = [*flags, "/E", entry, "/T", shader_type]
subprocess.run(["fxc", *cflags, "/Fo", shader.output, shader.source], cwd=cwd, check=True)
def compile_shaders() -> None:
build_spirv = True
build_metal = True
build_dxil = True
build_dxbc = False
lessons = [ "lesson2", "lesson3", "lesson6", "lesson7", "lesson8", "lesson9" ]
src_dir = Path("src/shaders")
dest_dir = Path("data/shaders")
system = platform.system()
shaders = [Shader(src_dir / lesson, dest_dir / lesson) for lesson in lessons]
root = Path(sys.argv[0]).resolve().parent.parent
root.joinpath(dest_dir).mkdir(parents=True, exist_ok=True)
# Try to find cross-platform shader compilers
glslang = shutil.which("glslang")
dxc = shutil.which("dxc", path=f"/opt/dxc/bin:{os.environ.get('PATH', os.defpath)}")
if build_spirv:
# Build SPIR-V shaders for Vulkan
compile_spirv_shaders(shaders, glslang=glslang, dxc=dxc, cwd=root)
if build_metal:
# Build Metal shaders on macOS
if system == "Darwin":
compile_platform = "macos"
sdk_platform = "macosx"
min_version = "10.11"
for shader in shaders_suffixes(shaders, "metal", "metallib"):
compile_metal_shaders(
sources=[shader.source],
library=shader.output,
cflags=["-Wall", "-O3",
f"-std={compile_platform}-metal1.1",
f"-m{sdk_platform}-version-min={min_version}"],
sdk=sdk_platform,
cwd=root)
# Build HLSL shaders on Windows or when DXC is available
is_windows = system == "Windows"
if (build_dxil or (is_windows and build_dxbc)) and (is_windows or dxc is not None):
compile_d3d12_shaders(shaders, build_dxbc=build_dxbc and is_windows, dxc=dxc, cwd=root)
if __name__ == "__main__":
compile_shaders()