[Build] Convert buildshaders to custom build rules

This commit is contained in:
Herman S.
2026-03-04 11:36:47 +09:00
parent a09292c98f
commit 4bd93883ac
9 changed files with 411 additions and 228 deletions

View File

@@ -943,13 +943,6 @@ class BuildCommand(BaseBuildCommand):
def execute(self, args, pass_args, cwd):
print(f"Building {args['config']}...\n")
# Generate shader bytecode before building
shader_result = build_shaders()
if shader_result != 0:
print(f"{bcolors.FAIL}ERROR: Shader generation failed{bcolors.ENDC}")
return shader_result
result = super(BuildCommand, self).execute(args, pass_args, cwd)
print_status(ResultStatus.SUCCESS if not result else ResultStatus.FAILURE)
@@ -981,7 +974,9 @@ class BuildShadersCommand(Command):
def build_shaders(targets=None):
"""Builds shader bytecode. Called by BuildShadersCommand and BuildCommand.
"""Builds shader bytecode. Called by BuildShadersCommand.
Delegates to the per-file compile scripts in tools/build/.
Args:
targets: List of targets ("dxbc", "spirv"), or None/empty for all.
@@ -1026,226 +1021,47 @@ def build_shaders(targets=None):
targets = []
all_targets = len(targets) == 0
# XeSL ("Xenia Shading Language") means shader files that can be
# compiled as multiple languages from a single file. Whenever possible,
# this is achieved without the involvement of the build script, using
# just conditionals, macros and functions in shaders, however, in some
# cases, that's necessary (such as to prepend `#version` in GLSL, as
# well as to enable `#include` in GLSL, to include `xesl.xesli` itself,
# without writing the same `#if` / `#extension` / `#endif` in every
# shader). Also, not all shading languages provide a built-in
# preprocessor definition for identification of them, so
# `SHADING_LANGUAGE_*_XE` is also defined via the build arguments.
# `SHADING_LANGUAGE_*_XE` is set regardless of whether the file is XeSL
# or a raw source file in a specific language, as XeSL headers may be
# used in language-specific sources.
valid_stages = ["vs", "hs", "ds", "gs", "ps", "cs"]
compile_spirv = os.path.join(self_path, "tools", "build", "compile_shader_spirv.py")
compile_dxbc = os.path.join(self_path, "tools", "build", "compile_shader_dxbc.py")
# Direct3D DXBC (Windows only).
if (all_targets or "dxbc" in targets) and sys.platform == "win32":
print("Building Direct3D 12 Shader Model 5.1 DXBC shaders...")
# Get the FXC path.
fxc = os.environ.get("FXC_PATH")
if not fxc:
# Fall back to searching Windows Kits
fxc = glob(os.path.join(os.environ.get("ProgramFiles(x86)", ""),
"Windows Kits", "10", "bin", "*", "x64", "fxc.exe"))
if not fxc:
print("ERROR: could not find fxc! Set FXC_PATH environment variable or install Windows SDK.")
return 1
fxc = fxc[-1] # Highest version is last
else:
print(f"Using FXC from environment variable: {fxc}")
# Build DXBC.
dxbc_stages = ["vs", "hs", "ds", "gs", "ps", "cs"]
for src_path in src_paths:
src_name = os.path.basename(src_path)
if ((not src_name.endswith(".hlsl") and
not src_name.endswith(".xesl")) or
len(src_name) <= 8 or src_name[-8] != "."):
continue
dxbc_identifier = src_name[:-5].replace(".", "_")
dxbc_stage = dxbc_identifier[-2:]
if dxbc_stage not in dxbc_stages:
identifier = src_name[:-5].replace(".", "_")
if identifier[-2:] not in valid_stages:
continue
print(f"- {src_path} > d3d12_5_1")
dxbc_dir_path = os.path.join(os.path.dirname(src_path),
"bytecode/d3d12_5_1")
os.makedirs(dxbc_dir_path, exist_ok=True)
dxbc_file_path_base = os.path.join(dxbc_dir_path, dxbc_identifier)
# Not enabling treating warnings as errors (/WX) because it
# overrides #pragma warning, and the FXAA shader triggers a
# bug in FXC causing an uninitialized variable warning if
# early exit from a function is done.
# FXC writes errors and warnings to stderr, not stdout, but
# stdout receives generic status messages that only add
# clutter in this case.
# Check if using DXC or FXC based on executable name
is_dxc = "dxc" in fxc.lower()
# Start with base command - use wine on non-Windows platforms
if sys.platform != "win32":
compiler_args = ["wine", fxc]
else:
compiler_args = [fxc]
src_dir = os.path.dirname(src_path)
if is_dxc:
# DXC only supports SM 6.0+, cannot compile SM 5.1
print("WARNING: DXC doesn't support SM 5.1, using SM 6.0")
compiler_args.extend([
"-T", f"{dxbc_stage}_6_0",
"-HV", "2017",
"-D", "SHADING_LANGUAGE_HLSL_XE=1",
"-I", src_dir,
"-Fh", f"{dxbc_file_path_base}.h",
"-Vn", dxbc_identifier,
"-nologo",
src_path,
])
else:
# FXC uses traditional syntax.
# Do NOT use /all_resources_bound — it alters the generated
# DXBC bytecode (changes dcl_globalFlags, register allocation
# and instruction scheduling) which causes rendering artifacts
# such as blue lightning/flash glitches at scaled resolutions.
compiler_args.extend([
"/D", "SHADING_LANGUAGE_HLSL_XE=1",
"/I", src_dir,
"/Fh", f"{dxbc_file_path_base}.h",
"/T", f"{dxbc_stage}_5_1",
"/Vn", dxbc_identifier,
"/O3",
"/Qstrip_reflect",
"/Qstrip_debug",
"/Qstrip_priv",
"/Gfp",
"/nologo",
src_path,
])
if subprocess.call(compiler_args, stdout=subprocess.DEVNULL) != 0:
print(f"ERROR: failed to compile DXBC shader: {src_path}")
return 1
output = os.path.join(src_dir, "bytecode", "d3d12_5_1", f"{identifier}.h")
print(f"- {src_path} > d3d12_5_1")
result = subprocess.call([sys.executable, compile_dxbc, src_path, output])
if result != 0:
return result
# Vulkan SPIR-V.
if all_targets or "spirv" in targets:
print("Building Vulkan SPIR-V shaders...")
# Get the SPIR-V tool paths.
vulkan_sdk_path = os.environ.get("VULKAN_SDK")
if not vulkan_sdk_path:
print("ERROR: VULKAN_SDK environment variable is not set")
if sys.platform == "win32":
print("Please install Vulkan SDK from:")
print("https://sdk.lunarg.com/sdk/download/latest/windows/vulkan-sdk.exe")
else:
print("Please install Vulkan SDK and set VULKAN_SDK environment variable")
return 1
if not os.path.exists(vulkan_sdk_path):
print(f"ERROR: could not find the Vulkan SDK at {vulkan_sdk_path}")
return 1
vulkan_bin_path = os.path.join(vulkan_sdk_path, "bin")
if not os.path.exists(vulkan_bin_path):
print("ERROR: could not find the Vulkan SDK binaries")
return 1
glslang = os.path.join(vulkan_bin_path, "glslangValidator")
if not has_bin(glslang):
print("ERROR: could not find glslangValidator")
return 1
spirv_opt = os.path.join(vulkan_bin_path, "spirv-opt")
if not has_bin(spirv_opt):
print("ERROR: could not find spirv-opt")
return 1
spirv_dis = os.path.join(vulkan_bin_path, "spirv-dis")
if not has_bin(spirv_dis):
print("ERROR: could not find spirv-dis")
return 1
# Build SPIR-V.
spirv_stages = {
"vs": "vert", "hs": "tesc", "ds": "tese",
"gs": "geom", "ps": "frag", "cs": "comp",
}
spirv_xesl_wrapper = (
"#version 460\n"
"#extension GL_EXT_control_flow_attributes : require\n"
"#extension GL_EXT_samplerless_texture_functions : require\n"
"#extension GL_GOOGLE_include_directive : require\n"
"#include \"%s\"\n"
)
for src_path in src_paths:
src_name = os.path.basename(src_path)
src_is_xesl = src_name.endswith(".xesl")
if ((not src_is_xesl and not src_name.endswith(".glsl")) or
if ((not src_name.endswith(".glsl") and
not src_name.endswith(".xesl")) or
len(src_name) <= 8 or src_name[-8] != "."):
continue
spirv_identifier = src_name[:-5].replace(".", "_")
spirv_stage = spirv_stages.get(spirv_identifier[-2:], None)
if spirv_stage is None:
identifier = src_name[:-5].replace(".", "_")
if identifier[-2:] not in valid_stages:
continue
print(f"- {src_path} > vulkan_spirv")
src_dir = os.path.dirname(src_path)
spirv_dir_path = os.path.join(src_dir, "bytecode/vulkan_spirv")
os.makedirs(spirv_dir_path, exist_ok=True)
spirv_file_path_base = os.path.join(spirv_dir_path, spirv_identifier)
spirv_glslang_file_path = f"{spirv_file_path_base}.glslang.spv"
glslang_arguments = [glslang,
"--stdin" if src_is_xesl else src_path,
"-DSHADING_LANGUAGE_GLSL_XE=1",
"-S", spirv_stage,
"-o", spirv_glslang_file_path,
"-V"]
if src_is_xesl:
glslang_arguments.append(f"-I{src_dir}")
if subprocess.run(
glslang_arguments,
input=(spirv_xesl_wrapper % src_name) if src_is_xesl else None,
text=True).returncode != 0:
print("ERROR: failed to build a SPIR-V shader")
return 1
spirv_file_path = f"{spirv_file_path_base}.spv"
if subprocess.call([spirv_opt, "-O", "-O", "--canonicalize-ids",
spirv_glslang_file_path, "-o", spirv_file_path]) != 0:
print("ERROR: failed to optimize a SPIR-V shader")
return 1
os.remove(spirv_glslang_file_path)
spirv_dis_file_path = f"{spirv_file_path_base}.txt"
if subprocess.call([spirv_dis, "-o", spirv_dis_file_path,
spirv_file_path]) != 0:
print("ERROR: failed to disassemble a SPIR-V shader")
return 1
# Generate the header from the disassembly and the binary.
with open(f"{spirv_file_path_base}.h", "w") as out_file:
out_file.write("// Generated with `xb buildshaders`.\n#if 0\n")
with open(spirv_dis_file_path, "r") as spirv_dis_file:
spirv_dis_data = spirv_dis_file.read()
if len(spirv_dis_data) > 0:
out_file.write(spirv_dis_data)
if spirv_dis_data[-1] != "\n":
out_file.write("\n")
out_file.write("#endif\n\nconst uint32_t %s[] = {" % spirv_identifier)
with open(spirv_file_path, "rb") as spirv_file:
index = 0
c = spirv_file.read(4)
while len(c) != 0:
if len(c) != 4:
print("ERROR: a SPIR-V shader is misaligned")
return 1
if index % 6 == 0:
out_file.write("\n ")
else:
out_file.write(" ")
index += 1
out_file.write("0x%08X," % int.from_bytes(c, sys.byteorder))
c = spirv_file.read(4)
out_file.write("\n};\n")
os.remove(spirv_dis_file_path)
os.remove(spirv_file_path)
output = os.path.join(src_dir, "bytecode", "vulkan_spirv", f"{identifier}.h")
print(f"- {src_path} > vulkan_spirv")
result = subprocess.call([sys.executable, compile_spirv, src_path, output])
if result != 0:
return result
return 0
@@ -1976,12 +1792,6 @@ class DevenvCommand(Command):
else:
print("IDE not detected. CMakeLists.txt is in the project root.")
print("\n- generating shaders...")
shader_result = build_shaders()
if shader_result != 0:
print_error("Shader generation failed")
return shader_result
print("\n- running cmake configure...")
run_cmake_configure()