diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d0bdceb86..4d04b3a70 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -65,13 +65,6 @@ jobs: path: third_party key: ${{ runner.os }}-submodules-${{ hashFiles('.gitmodules') }}-${{ hashFiles('.git/modules/third_party/*/HEAD') }} - - name: Cache Vulkan SDK - id: cache-vulkan-sdk-linux - uses: actions/cache@v3 - with: - path: ~/vulkan-sdk - key: ${{ runner.os }}-vulkan-sdk-latest - - name: Setup run: | UBUNTU_BASE=$(lsb_release -cs) @@ -79,17 +72,7 @@ jobs: sudo apt-add-repository "deb http://apt.llvm.org/${UBUNTU_BASE}/ llvm-toolchain-${UBUNTU_BASE}-21 main" sudo apt-get -y update - sudo apt-get -y install mesa-vulkan-drivers valgrind libc++-dev libc++abi-dev libgtk-3-dev libsdl2-dev libvulkan-dev libx11-xcb-dev clang-21 lld-21 ninja-build cmake libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-cursor0 libxcb-shape0 libfontconfig1-dev libxtst-dev - - # Install Vulkan SDK - if [ '${{ steps.cache-vulkan-sdk-linux.outputs.cache-hit }}' != 'true' ]; then - wget -qO vulkan-sdk.tar.xz https://sdk.lunarg.com/sdk/download/latest/linux/vulkan-sdk.tar.xz - mkdir -p ~/vulkan-sdk - tar -xf vulkan-sdk.tar.xz -C ~/vulkan-sdk - fi - VULKAN_SDK_VERSION=$(ls ~/vulkan-sdk) - echo "VULKAN_SDK=$HOME/vulkan-sdk/$VULKAN_SDK_VERSION/x86_64" >> $GITHUB_ENV - echo "$HOME/vulkan-sdk/$VULKAN_SDK_VERSION/x86_64/bin" >> $GITHUB_PATH + sudo apt-get -y install valgrind libc++-dev libc++abi-dev libgtk-3-dev libsdl2-dev libx11-xcb-dev clang-21 lld-21 ninja-build cmake libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-cursor0 libxcb-shape0 libfontconfig1-dev libxtst-dev # Install Qt 6.10.1 via aqtinstall if [ '${{ steps.cache-qt-linux.outputs.cache-hit }}' != 'true' ]; then @@ -117,11 +100,6 @@ jobs: fi echo "$HOME/linuxdeploy" >> $GITHUB_PATH - # Verify shader tools are available - which glslangValidator && glslangValidator --version || echo "Warning: glslangValidator not found" - which spirv-opt && spirv-opt --version || echo "Warning: spirv-opt not found" - which spirv-dis || echo "Warning: spirv-dis not found" - # Update submodules if not cached if [ '${{ steps.cache-submodules.outputs.cache-hit }}' != 'true' ]; then git submodule update --init --depth=1 -j$(getconf _NPROCESSORS_ONLN) $(grep -oP '(?<=path = )(?!third_party\/DirectXShaderCompiler).+' .gitmodules) @@ -217,15 +195,6 @@ jobs: fetch-depth: 0 submodules: false - - name: Cache Vulkan SDK - id: cache-vulkan-sdk - uses: actions/cache@v3 - with: - path: C:\VulkanSDK - key: ${{ runner.os }}-vulkan-sdk-${{ hashFiles('**/vulkan-sdk.exe') }} - restore-keys: | - ${{ runner.os }}-vulkan-sdk- - - name: Cache Qt6 id: cache-qt uses: actions/cache@v3 @@ -244,42 +213,6 @@ jobs: - name: Setup run: | - # Install Vulkan SDK for headers + loader - if (Test-Path -Path "C:\VulkanSDK") { - echo "Vulkan SDK found in cache." - } else { - Invoke-WebRequest -Uri "https://sdk.lunarg.com/sdk/download/latest/windows/vulkan-sdk.exe" -OutFile "vulkan-sdk.exe" - Start-Process -FilePath "vulkan-sdk.exe" -ArgumentList "--accept-licenses", "--default-answer", "--confirm-command", "install" -Wait - } - $env:VULKAN_SDK = "C:\VulkanSDK\$(Get-ChildItem -Path 'C:\VulkanSDK' -Directory | Select-Object -First 1 -ExpandProperty Name)" - $env:PATH = "$env:VULKAN_SDK\Bin;$env:PATH" - echo "VULKAN_SDK=$env:VULKAN_SDK" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - echo "$env:VULKAN_SDK\Bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - # Verify shader tools are available - $spirvOptPath = "$env:VULKAN_SDK\Bin\spirv-opt.exe" - if (Test-Path $spirvOptPath) { - & $spirvOptPath --version - echo "spirv-opt found at: $spirvOptPath" - } else { - echo "Warning: spirv-opt.exe not found at expected location" - } - - $glslangPath = "$env:VULKAN_SDK\Bin\glslangValidator.exe" - if (Test-Path $glslangPath) { - & $glslangPath --version - echo "glslangValidator found at: $glslangPath" - } else { - echo "Warning: glslangValidator.exe not found at expected location" - } - - $spirvDisPath = "$env:VULKAN_SDK\Bin\spirv-dis.exe" - if (Test-Path $spirvDisPath) { - echo "spirv-dis found at: $spirvDisPath" - } else { - echo "Warning: spirv-dis.exe not found at expected location" - } - # Verify FXC is available (from Windows SDK) $fxcPaths = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\fxc.exe" -ErrorAction SilentlyContinue | Sort-Object FullName if ($fxcPaths) { diff --git a/.gitmodules b/.gitmodules index ed4fda155..a320af715 100644 --- a/.gitmodules +++ b/.gitmodules @@ -110,3 +110,11 @@ [submodule "third_party/xbyak_aarch64"] path = third_party/xbyak_aarch64 url = https://github.com/fujitsu/xbyak_aarch64.git +[submodule "third_party/SPIRV-Headers"] + path = third_party/SPIRV-Headers + url = https://github.com/KhronosGroup/SPIRV-Headers.git + branch = vulkan-sdk-1.4.341 +[submodule "third_party/SPIRV-Tools"] + path = third_party/SPIRV-Tools + url = https://github.com/KhronosGroup/SPIRV-Tools.git + branch = vulkan-sdk-1.4.341 diff --git a/cmake/XeniaHelpers.cmake b/cmake/XeniaHelpers.cmake index f1d9b141f..8f71f32f3 100644 --- a/cmake/XeniaHelpers.cmake +++ b/cmake/XeniaHelpers.cmake @@ -113,19 +113,17 @@ endfunction() # xe_shader_rules_spirv(target shader_dir) # -# Wires up SPIR-V shader compilation (glslang + spirv-opt + spirv-dis, -# via tools/build/compile_shader_spirv.py) as a prerequisite of target. -# Sources are *.xesl / *.glsl with a stage suffix (vs/hs/ds/gs/ps/cs); -# outputs land in /bytecode/vulkan_spirv/.h. A stamp -# file drives incremental rebuilds when any source or the compile -# script changes. +# Wires up SPIR-V shader compilation via the in-tree xenia-shader-cc host +# tool (glslang-based) as a prerequisite of target. Sources are *.xesl / +# *.glsl with a stage suffix (vs/hs/ds/gs/ps/cs); outputs land in +# /bytecode/vulkan_spirv/.h. A stamp file drives +# incremental rebuilds when any source or the tool changes. function(xe_shader_rules_spirv target shader_dir) get_filename_component(shader_dir "${shader_dir}" ABSOLUTE) file(GLOB _sources "${shader_dir}/*.xesl" "${shader_dir}/*.glsl" "${shader_dir}/*.xesli" "${shader_dir}/*.glsli") set(_stamp "${CMAKE_CURRENT_BINARY_DIR}/${target}_spirv.stamp") - set(_script "${PROJECT_SOURCE_DIR}/tools/build/compile_shader_spirv.py") set(_valid_stages vs hs ds gs ps cs) set(_commands) set(_bytecode_dir "${shader_dir}/bytecode/vulkan_spirv") @@ -143,13 +141,14 @@ function(xe_shader_rules_spirv target shader_dir) if(NOT _stage IN_LIST _valid_stages) continue() endif() - list(APPEND _commands COMMAND ${Python3_EXECUTABLE} "${_script}" "${src}" "${_bytecode_dir}/${_id}.h") + list(APPEND _commands COMMAND $ + "${src}" "${_bytecode_dir}/${_id}.h") endforeach() add_custom_command( OUTPUT "${_stamp}" ${_commands} COMMAND ${CMAKE_COMMAND} -E touch "${_stamp}" - DEPENDS ${_sources} "${_script}" + DEPENDS ${_sources} xenia-shader-cc COMMENT "Compiling SPIR-V shaders for ${target}..." VERBATIM ) diff --git a/docs/building.md b/docs/building.md index f79f2819a..1562f0692 100644 --- a/docs/building.md +++ b/docs/building.md @@ -14,8 +14,6 @@ drivers. * Windows 11 SDK version 10.0.22000.0 (for Visual Studio 2022, this or any newer version) * [Python 3.6+ 64-bit](https://www.python.org/downloads/) * Ensure Python is in PATH. -* [Vulkan SDK](https://sdk.lunarg.com/sdk/download/latest/windows/vulkan-sdk.exe) - * The build script will automatically detect it if installed at `C:\VulkanSDK` * Qt 6.10.1 (for the UI) * Install using aqtinstall: ``` @@ -70,15 +68,6 @@ xb gentests # Run tests: xb test - -# Run GPU tests: -xb gputest - - -## Other: - -# Generate SPIR-V binaries and header files: -xb genspirv --> #### Cross-compiling (Windows ARM64 ↔ x64) diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 3441999c5..c23688873 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -156,12 +156,14 @@ add_library(fmt STATIC target_include_directories(fmt PUBLIC fmt/include) # ============================================================================== -# glslang-spirv (not needed on macOS — no Vulkan/SPIR-V backend) +# glslang-spirv (runtime: SPIR-V emission primitives for Xenia's own +# SpirvShaderTranslator; not needed on macOS — no Vulkan/SPIR-V backend) # ============================================================================== if(NOT APPLE) file(GLOB _glslang_spirv_cpp "glslang/SPIRV/*.cpp") file(GLOB _glslang_spirv_h "glslang/SPIRV/*.h") file(GLOB _glslang_spirv_hpp "glslang/SPIRV/*.hpp11") + # Excluding GlslangToSpv keeps MachineIndependent out of the runtime link. list(FILTER _glslang_spirv_cpp EXCLUDE REGEX "GlslangToSpv\\.cpp$") add_library(glslang-spirv STATIC ${_glslang_spirv_cpp} @@ -172,6 +174,100 @@ if(NOT APPLE) target_include_directories(glslang-spirv PUBLIC glslang) endif() +# ============================================================================== +# SPIRV-Headers + SPIRV-Tools (host: SPIR-V optimization + disassembly +# support for glslang's ENABLE_OPT path; used by the xenia-shader-cc tool) +# ============================================================================== +# Point SPIRV-Tools at our in-tree SPIRV-Headers checkout instead of the +# external/spirv-headers path it expects by default. +set(SPIRV-Headers_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/SPIRV-Headers" + CACHE INTERNAL "") +set(SPIRV_SKIP_TESTS ON CACHE INTERNAL "") +set(SPIRV_SKIP_EXECUTABLES ON CACHE INTERNAL "") +set(SKIP_SPIRV_TOOLS_INSTALL ON CACHE INTERNAL "") +set(SPIRV_WERROR OFF CACHE INTERNAL "") +set(SPIRV_BUILD_FUZZER OFF CACHE INTERNAL "") +add_subdirectory(SPIRV-Tools EXCLUDE_FROM_ALL) + +# SPIRV-Tools unconditionally forces /W2 /WX on MSVC via its own +# target_compile_options, which conflicts with the directory-level /W0 and +# produces D9025 "overriding /W0 with /W2" for every translation unit. Strip +# the /W* and /WX flags it added so only the directory-level /W0 survives. +if(MSVC) + foreach(_spvt_target + SPIRV-Tools-static SPIRV-Tools-shared SPIRV-Tools-opt + SPIRV-Tools-link SPIRV-Tools-reduce SPIRV-Tools-diff) + if(TARGET ${_spvt_target}) + get_target_property(_opts ${_spvt_target} COMPILE_OPTIONS) + if(_opts) + list(FILTER _opts EXCLUDE REGEX "^/[Ww]([0-9]|all|X)$") + set_target_properties(${_spvt_target} PROPERTIES COMPILE_OPTIONS "${_opts}") + endif() + endif() + endforeach() +endif() + +# ============================================================================== +# glslang (host: full frontend + SPIR-V emission for the xenia-shader-cc +# build-time tool; all platforms, including macOS) +# ============================================================================== +# Read glslang's own version file to generate build_info.h in sync with the +# pinned submodule, matching what glslang's own CMakeLists does. +include(${CMAKE_CURRENT_SOURCE_DIR}/glslang/parse_version.cmake) +parse_version(${CMAKE_CURRENT_SOURCE_DIR}/glslang/CHANGES.md GLSLANG) +set(major ${GLSLANG_VERSION_MAJOR}) +set(minor ${GLSLANG_VERSION_MINOR}) +set(patch ${GLSLANG_VERSION_PATCH}) +set(flavor ${GLSLANG_VERSION_FLAVOR}) +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/glslang/build_info.h.tmpl + ${CMAKE_CURRENT_BINARY_DIR}/glslang-build-info/glslang/build_info.h + @ONLY) + +file(GLOB _glslang_mi + "glslang/glslang/MachineIndependent/*.cpp" + "glslang/glslang/MachineIndependent/preprocessor/*.cpp" + "glslang/glslang/GenericCodeGen/*.cpp") +file(GLOB _glslang_spirv_all "glslang/SPIRV/*.cpp") +add_library(glslang STATIC + ${_glslang_mi} + ${_glslang_spirv_all} + glslang/glslang/ResourceLimits/ResourceLimits.cpp +) +if(WIN32) + target_sources(glslang PRIVATE glslang/glslang/OSDependent/Windows/ossource.cpp) +else() + target_sources(glslang PRIVATE glslang/glslang/OSDependent/Unix/ossource.cpp) +endif() +target_include_directories(glslang PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/glslang + ${CMAKE_CURRENT_BINARY_DIR}/glslang-build-info) +target_compile_features(glslang PUBLIC cxx_std_17) +# Enable glslang's SpvTools.cpp optimizer/disassembler bridge to SPIRV-Tools. +target_compile_definitions(glslang PRIVATE ENABLE_OPT=1) +target_link_libraries(glslang PUBLIC SPIRV-Tools-opt SPIRV-Tools-static) +# glslang relies on exceptions internally. +if(MSVC) + target_compile_options(glslang PRIVATE /EHsc) +endif() +if(NOT WIN32) + find_package(Threads) + if(Threads_FOUND) + target_link_libraries(glslang PUBLIC Threads::Threads) + endif() +endif() + +# ============================================================================== +# xenia-shader-cc (build-time host tool: GLSL/XeSL -> SPIR-V -> .h) +# ============================================================================== +add_executable(xenia-shader-cc ${PROJECT_SOURCE_DIR}/tools/build/shader_cc.cc) +target_link_libraries(xenia-shader-cc PRIVATE glslang SPIRV-Tools-static) +target_include_directories(xenia-shader-cc PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/glslang/StandAlone) +if(MSVC) + target_compile_options(xenia-shader-cc PRIVATE /EHsc) +endif() + # ============================================================================== # imgui # ============================================================================== diff --git a/third_party/SPIRV-Headers b/third_party/SPIRV-Headers new file mode 160000 index 000000000..01e057791 --- /dev/null +++ b/third_party/SPIRV-Headers @@ -0,0 +1 @@ +Subproject commit 01e0577914a75a2569c846778c2f93aa8e6feddd diff --git a/third_party/SPIRV-Tools b/third_party/SPIRV-Tools new file mode 160000 index 000000000..7f2d9ee92 --- /dev/null +++ b/third_party/SPIRV-Tools @@ -0,0 +1 @@ +Subproject commit 7f2d9ee926f98fc77a3ed1e1e0f113b8c9c49458 diff --git a/tools/build/compile_shader_spirv.py b/tools/build/compile_shader_spirv.py deleted file mode 100644 index 2f7eb262c..000000000 --- a/tools/build/compile_shader_spirv.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python3 - -"""Compiles a single shader to SPIR-V and generates a C header. - -Usage: compile_shader_spirv.py - -Pipeline: - 1. glslangValidator -> unoptimized .spv - 2. spirv-opt -> optimized .spv - 3. spirv-dis -> disassembly .txt - 4. Generate .h with disassembly comment + uint32_t array -""" - -import os -import struct -import subprocess -import sys - - -SPIRV_STAGES = { - "vs": "vert", "hs": "tesc", "ds": "tese", - "gs": "geom", "ps": "frag", "cs": "comp", -} - -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" -) - - -def find_vulkan_tools(): - """Find Vulkan SDK tools via VULKAN_SDK env or PATH.""" - vulkan_sdk = os.environ.get("VULKAN_SDK") - if vulkan_sdk: - bin_dir = os.path.join(vulkan_sdk, "bin") - if os.path.isdir(bin_dir): - return ( - os.path.join(bin_dir, "glslangValidator"), - os.path.join(bin_dir, "spirv-opt"), - os.path.join(bin_dir, "spirv-dis"), - ) - - # Fall back to PATH - return ("glslangValidator", "spirv-opt", "spirv-dis") - - -def parse_stage(filename): - """Extract the 2-char shader stage from filename like 'foo.cs.xesl'.""" - basename = os.path.splitext(filename)[0] # 'foo.cs' - identifier = basename.replace(".", "_") # 'foo_cs' - stage_key = identifier[-2:] - if stage_key not in SPIRV_STAGES: - return None, None - return stage_key, SPIRV_STAGES[stage_key] - - -def main(): - if len(sys.argv) != 3: - print(f"Usage: {sys.argv[0]} ", file=sys.stderr) - return 1 - - input_path = sys.argv[1] - output_path = sys.argv[2] - - src_name = os.path.basename(input_path) - src_dir = os.path.dirname(input_path) - src_is_xesl = src_name.endswith(".xesl") - - stage_key, spirv_stage = parse_stage(src_name) - if spirv_stage is None: - print(f"ERROR: cannot determine shader stage from: {src_name}", file=sys.stderr) - return 1 - - # Compute identifier (matches what Lua does: basename with dots -> underscores) - identifier = os.path.splitext(src_name)[0].replace(".", "_") - - glslang, spirv_opt, spirv_dis = find_vulkan_tools() - - # Create output directory if needed. - os.makedirs(os.path.dirname(output_path), exist_ok=True) - - # Temp file paths next to output. - base = os.path.splitext(output_path)[0] - glslang_spv = base + ".glslang.spv" - opt_spv = base + ".spv" - dis_txt = base + ".txt" - - try: - # Step 1: glslangValidator - glslang_args = [ - glslang, - "--stdin" if src_is_xesl else input_path, - "-DSHADING_LANGUAGE_GLSL_XE=1", - "-S", spirv_stage, - "-o", glslang_spv, - "-V", - ] - if src_is_xesl: - glslang_args.append(f"-I{src_dir}") - - stdin_data = (XESL_WRAPPER % src_name) if src_is_xesl else None - result = subprocess.run(glslang_args, input=stdin_data, text=True, - stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) - if result.returncode != 0: - print(f"ERROR: glslangValidator failed for {src_name}", file=sys.stderr) - if result.stderr: - sys.stderr.write(result.stderr) - return 1 - - # Step 2: spirv-opt - result = subprocess.run([ - spirv_opt, "-O", "-O", "--canonicalize-ids", - glslang_spv, "-o", opt_spv, - ], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) - if result.returncode != 0: - print(f"ERROR: spirv-opt failed for {src_name}", file=sys.stderr) - if result.stderr: - sys.stderr.write(result.stderr) - return 1 - - # Step 3: spirv-dis - result = subprocess.run([spirv_dis, "-o", dis_txt, opt_spv], - stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) - if result.returncode != 0: - print(f"ERROR: spirv-dis failed for {src_name}", file=sys.stderr) - if result.stderr: - sys.stderr.write(result.stderr) - return 1 - - # Step 4: Generate header - with open(output_path, "w") as out: - out.write("// Generated with `xb buildshaders`.\n#if 0\n") - with open(dis_txt, "r") as dis_file: - dis_data = dis_file.read() - if dis_data: - out.write(dis_data) - if dis_data[-1] != "\n": - out.write("\n") - out.write("#endif\n\nconst uint32_t %s[] = {" % identifier) - with open(opt_spv, "rb") as spv_file: - index = 0 - while True: - word = spv_file.read(4) - if len(word) == 0: - break - if len(word) != 4: - print("ERROR: SPIR-V binary is misaligned", file=sys.stderr) - return 1 - if index % 6 == 0: - out.write("\n ") - else: - out.write(" ") - index += 1 - value = struct.unpack("[]`. +// +// Usage: xenia-shader-cc +// +// Stage (vs/hs/ds/gs/ps/cs) is parsed from the filename stem, e.g. foo.cs.glsl +// -> compute. XeSL sources are compiled with the XeSL wrapper (#version 460 + +// Google include directive + #include of the file itself) and with includes +// resolved relative to the input file's directory. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DirStackFileIncluder.h" +#include "SPIRV/GlslangToSpv.h" +#include "glslang/Public/ResourceLimits.h" +#include "glslang/Public/ShaderLang.h" +#include "spirv-tools/libspirv.hpp" +#include "spirv-tools/optimizer.hpp" + +namespace { + +struct StageInfo { + const char* key; + EShLanguage language; +}; + +constexpr StageInfo kStages[] = { + {"vs", EShLangVertex}, {"hs", EShLangTessControl}, + {"ds", EShLangTessEvaluation}, {"gs", EShLangGeometry}, + {"ps", EShLangFragment}, {"cs", EShLangCompute}, +}; + +constexpr const char* kXeslWrapperPrefix = + "#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"; + +bool ParseStage(const std::string& filename, EShLanguage* out, + std::string* stage_key_out) { + // Strip extension, then take last 2 chars after the final dot. + auto dot = filename.rfind('.'); + if (dot == std::string::npos || dot < 3 || filename[dot - 3] != '.') { + return false; + } + std::string key = filename.substr(dot - 2, 2); + for (const auto& stage : kStages) { + if (key == stage.key) { + *out = stage.language; + *stage_key_out = key; + return true; + } + } + return false; +} + +std::string IdentifierFromFilename(const std::string& filename) { + // foo.bar.cs.glsl -> foo_bar_cs + auto last_dot = filename.rfind('.'); + std::string stem = filename.substr(0, last_dot); + std::replace(stem.begin(), stem.end(), '.', '_'); + return stem; +} + +bool ReadFile(const std::filesystem::path& path, std::string* out) { + std::ifstream in(path, std::ios::binary); + if (!in) { + std::fprintf(stderr, "failed to open %s\n", path.string().c_str()); + return false; + } + std::ostringstream buf; + buf << in.rdbuf(); + *out = buf.str(); + return true; +} + +std::string DisassembleSpirv(const std::vector& spirv) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); + std::string text; + const uint32_t options = SPV_BINARY_TO_TEXT_OPTION_INDENT | + SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES; + if (!tools.Disassemble(spirv, &text, options)) { + text.clear(); + } + return text; +} + +bool WriteSpirvHeader(const std::filesystem::path& path, + const std::string& identifier, + const std::vector& spirv) { + std::error_code ec; + std::filesystem::create_directories(path.parent_path(), ec); + std::ofstream out(path, std::ios::binary); + if (!out) { + std::fprintf(stderr, "failed to open output %s\n", path.string().c_str()); + return false; + } + out << "// Generated by xenia-shader-cc. Do not edit.\n#if 0\n"; + std::string disasm = DisassembleSpirv(spirv); + if (!disasm.empty()) { + out << disasm; + if (disasm.back() != '\n') { + out << '\n'; + } + } + out << "#endif\n\n"; + out << "const uint32_t " << identifier << "[] = {"; + for (size_t i = 0; i < spirv.size(); ++i) { + out << (i % 6 == 0 ? "\n " : " "); + char word[16]; + std::snprintf(word, sizeof(word), "0x%08X,", spirv[i]); + out << word; + } + out << "\n};\n"; + return bool(out); +} + +} // namespace + +int main(int argc, char** argv) { + if (argc != 3) { + std::fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + std::filesystem::path input_path = argv[1]; + std::filesystem::path output_path = argv[2]; + std::string input_filename = input_path.filename().string(); + + EShLanguage stage; + std::string stage_key; + if (!ParseStage(input_filename, &stage, &stage_key)) { + std::fprintf(stderr, + "cannot determine shader stage from filename: %s\n", + input_filename.c_str()); + return 1; + } + + std::string source; + if (!ReadFile(input_path, &source)) { + return 1; + } + + // .xesl files rely on a wrapper prepending #version / extensions and then + // #include'ing the file itself. glslang needs the include directive to come + // from an actual source string, not from disk, so we synthesize the wrapper + // and let the includer fetch the real file. + const bool is_xesl = input_filename.size() > 5 && + input_filename.compare(input_filename.size() - 5, 5, + ".xesl") == 0; + std::string wrapper; + std::string wrapper_name; + if (is_xesl) { + wrapper = kXeslWrapperPrefix; + wrapper += "#include \""; + wrapper += input_filename; + wrapper += "\"\n"; + } + + glslang::InitializeProcess(); + + glslang::TShader shader(stage); + const char* source_strings[1]; + const char* source_names[1]; + int source_lengths[1]; + if (is_xesl) { + source_strings[0] = wrapper.c_str(); + source_lengths[0] = static_cast(wrapper.size()); + wrapper_name = "xesl_wrapper"; + source_names[0] = wrapper_name.c_str(); + } else { + source_strings[0] = source.c_str(); + source_lengths[0] = static_cast(source.size()); + source_names[0] = input_filename.c_str(); + } + shader.setStringsWithLengthsAndNames(source_strings, source_lengths, + source_names, 1); + shader.setPreamble("#define SHADING_LANGUAGE_GLSL_XE 1\n"); + // Match the old `glslangValidator -V` default: Vulkan 1.0 / SPV 1.0, which + // stays compatible with the Vulkan 1.0 devices the runtime still supports. + shader.setEnvInput(glslang::EShSourceGlsl, stage, glslang::EShClientVulkan, + 100); + shader.setEnvClient(glslang::EShClientVulkan, glslang::EShTargetVulkan_1_0); + shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_0); + + DirStackFileIncluder includer; + std::string input_dir = input_path.parent_path().string(); + if (!input_dir.empty()) { + includer.pushExternalLocalDirectory(input_dir); + } + + const int default_version = 460; + const EShMessages messages = static_cast( + EShMsgDefault | EShMsgSpvRules | EShMsgVulkanRules); + + if (!shader.parse(GetDefaultResources(), default_version, false, messages, + includer)) { + std::fprintf(stderr, "glslang parse failed for %s:\n%s%s", + input_path.string().c_str(), shader.getInfoLog(), + shader.getInfoDebugLog()); + glslang::FinalizeProcess(); + return 1; + } + + glslang::TProgram program; + program.addShader(&shader); + if (!program.link(messages)) { + std::fprintf(stderr, "glslang link failed for %s:\n%s%s", + input_path.string().c_str(), program.getInfoLog(), + program.getInfoDebugLog()); + glslang::FinalizeProcess(); + return 1; + } + + // Emit raw SPIR-V from glslang; optimization is done explicitly below to + // match the previous `spirv-opt -O -O --canonicalize-ids` behavior. + glslang::SpvOptions spv_options; + spv_options.generateDebugInfo = false; + spv_options.stripDebugInfo = true; + spv_options.disableOptimizer = true; + spv_options.validate = false; + + std::vector spirv; + glslang::GlslangToSpv(*program.getIntermediate(stage), spirv, &spv_options); + + glslang::FinalizeProcess(); + + if (spirv.empty()) { + std::fprintf(stderr, "GlslangToSpv produced empty output for %s\n", + input_path.string().c_str()); + return 1; + } + + { + spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_2); + optimizer.SetMessageConsumer( + [](spv_message_level_t, const char*, const spv_position_t&, + const char* message) { std::fprintf(stderr, "%s\n", message); }); + optimizer.RegisterPerformancePasses(); + optimizer.RegisterPerformancePasses(); + optimizer.RegisterPass(spvtools::CreateCanonicalizeIdsPass()); + std::vector optimized; + if (!optimizer.Run(spirv.data(), spirv.size(), &optimized)) { + std::fprintf(stderr, "spirv-opt failed for %s\n", + input_path.string().c_str()); + return 1; + } + spirv = std::move(optimized); + } + + std::string identifier = IdentifierFromFilename(input_filename); + if (!WriteSpirvHeader(output_path, identifier, spirv)) { + return 1; + } + return 0; +} diff --git a/xenia-build.py b/xenia-build.py index fbd290788..99ae8d09a 100755 --- a/xenia-build.py +++ b/xenia-build.py @@ -82,7 +82,6 @@ def import_subprocess_environment(args): "WindowsSdkDir", "PROGRAMFILES", "ProgramFiles(x86)", - "VULKAN_SDK" "CC", "CXX", ) @@ -155,62 +154,6 @@ vs_version = import_vs_environment() default_branch = "canary_experimental" -def setup_vulkan_sdk(): - """Setup Vulkan SDK environment variables if not already set. - - Returns: - True if Vulkan SDK is available and valid, False otherwise. - """ - # Check if VULKAN_SDK is already set and valid - existing_vulkan_sdk = os.environ.get("VULKAN_SDK") - if existing_vulkan_sdk: - # Validate that the path exists - if os.path.exists(existing_vulkan_sdk): - # Check if spirv-opt is accessible - if has_bin("spirv-opt"): - print(f"VULKAN_SDK is set to {existing_vulkan_sdk}") - return True - print_warning(f"VULKAN_SDK is set to {existing_vulkan_sdk} but spirv-opt not found in PATH") - else: - print_warning(f"VULKAN_SDK is set to {existing_vulkan_sdk} but directory does not exist") - return False - - if sys.platform != "win32": - # On Linux, find spirv-opt in PATH and set VULKAN_SDK based on its location - spirv_opt_path = get_bin("spirv-opt") - if spirv_opt_path: - # spirv-opt is typically in $VULKAN_SDK/bin/, so get parent directory - spirv_bin_dir = os.path.dirname(spirv_opt_path) - vulkan_sdk = os.path.dirname(spirv_bin_dir) - os.environ["VULKAN_SDK"] = vulkan_sdk - print(f"Found Vulkan SDK at {vulkan_sdk} (from spirv-opt location)") - return True - return False - - # Windows: Check if Vulkan SDK is installed at the default location - vulkan_base = "C:\\VulkanSDK" - if not os.path.exists(vulkan_base): - return False - - # Get the first (latest) version directory - try: - subdirs = [d for d in os.listdir(vulkan_base) - if os.path.isdir(os.path.join(vulkan_base, d))] - if not subdirs: - return False - - vulkan_sdk = os.path.join(vulkan_base, subdirs[0]) - vulkan_bin = os.path.join(vulkan_sdk, "Bin") - - os.environ["VULKAN_SDK"] = vulkan_sdk - os.environ["PATH"] = f"{vulkan_bin}{os.pathsep}{os.environ['PATH']}" - - print(f"Found Vulkan SDK at {vulkan_sdk}") - return True - except Exception: - return False - - def setup_qt(target_arch=None): """Setup Qt environment variables if not already set. @@ -337,9 +280,6 @@ def main(): # Add self to the root search path. sys.path.insert(0, self_path) - # Setup Vulkan SDK and check if available - vulkan_sdk_available = setup_vulkan_sdk() - # Augment path to include our fancy things. os.environ["PATH"] += os.pathsep + os.pathsep.join([ self_path, @@ -1077,13 +1017,6 @@ class BaseBuildCommand(Command): f"\nSee: https://github.com/has207/xenia-edge/blob/{default_branch}/docs/building.md") return 1 - if sys.platform != "darwin" and not os.environ.get("VULKAN_SDK"): - print_error("Vulkan SDK not found!" - "\nPlease install Vulkan SDK from:" - "\nhttps://sdk.lunarg.com/sdk/download/latest/windows/vulkan-sdk.exe" - f"\nSee: https://github.com/has207/xenia-edge/blob/{default_branch}/docs/building.md") - return 1 - target_arch = args.get("target_arch") if not args["no_configure"]: print("- running cmake configure...")