LLVM SPIR-V Codegen
The experimental backend lives in
src/backends/common/spirv_llvm/. It lowers a Luisa AST Function to LLVM IR
and asks LLVM's native SPIR-V target to emit a
spirv64-unknown-vulkan1.2 module.
This is not the native XIR-to-SPIR-V path. The two Vulkan codegen selections are mutually exclusive:
- CMake:
LUISA_COMPUTE_ENABLE_VK_AST_LLVM_SPIRV=ONandLUISA_COMPUTE_ENABLE_VK_XIR_SPIRV=OFF; - XMake:
lc_vk_backend_use_ast_llvm_spirv=true, withlc_llvm_pathpointing to an LLVM installation/build prefix or itsllvm-config.
The public entry is deliberately LLVM-header-free:
#include <spirv_llvm/spirv_llvm.h>
auto result = lc::llvm_codegen::compile_spirv(kernel, option);
Do not expose an LLVM type through spirv_llvm.h or
llvm_codegen_result.h; consumers of the static facade must not inherit LLVM
include directories or unrelated LLVM preprocessor definitions.
Current support boundary
The Vulkan descriptor interface is preflighted before AST lowering by
validate_llvm_vulkan_resource_model. At present it rejects:
- every kernel argument, including ordinary value arguments and direct buffers;
- textures, bindless arrays, acceleration structures, custom/indirect resources, and their global heaps;
- shader printing.
Consequently the current end-to-end Vulkan contract is intentionally narrow: no-argument, no-print compute kernels whose operations are implemented by the visitor. The successful interface contains only the fixed 16-entry immutable sampler property. Do not describe the visitor's partial resource stubs as runtime support, and do not bypass this preflight merely because LLVM happens to emit some SPIR-V for an address-space global.
Unsupported AST operations call LUISA_NOT_IMPLEMENTED or otherwise fail
closed. Returning zero, undef, or a no-op is not an acceptable way to claim
support. When expanding the backend, implement the LLVM IR lowering, extend
the property preflight, validate the Vulkan runtime binding shape, and add an
end-to-end SPIR-V validator test together.
Source map
| Files | Responsibility |
|---|---|
spirv_llvm.h/.cpp |
LLVM-free facade and once-only SPIR-V target registration |
llvm_codegen_result.h |
SPIR-V words, properties, printer records, and global bindless flags returned to Vulkan |
llvm_codegen_stack_data.h/.cpp |
Per-compilation state and mutex-protected reusable state pool |
llvm_codegen_utility.h/.cpp |
Type/constant/function lowering, module legalization, target-machine emission, post-processing, and validation |
llvm_state_visitor.h/.cpp |
AST expression and statement lowering through llvm::IRBuilder<> |
llvm_compat.h |
API-detection boundary for LLVM-version differences such as nullable terminator lookup |
vulkan_binding_properties.h |
Pure property planner and fail-closed resource-model support check |
CMakeLists.txt / xmake.lua |
Component-aware LLVM discovery and static-facade linkage |
Compilation pipeline
LLVMCodegenUtility::CompileSPIRV owns the complete path:
- Create a fresh LLVM context/module/builder and register LLVM's SPIR-V target
exactly once with
std::call_once. - Initialize a
spirv64-unknown-vulkan1.2target machine and its data layout. - Detect prospective bindless/property usage from the AST, validate the current Vulkan resource-model boundary, and freeze the property plan before visiting the AST.
- Lower the kernel and reachable callables to LLVM IR. A compute entry is
named
main, has no function parameters, and carrieshlsl.shader=computeplushlsl.numthreadsattributes. - Before target emission, recursively scalarize aggregate loads/stores,
lower aggregate returns to
voidplus an out pointer, scalarize again, and verify the LLVM module. - Emit object bytes through LLVM's legacy pass manager. The expected output is raw SPIR-V words; an ELF result is not silently accepted as valid SPIR-V and will fail the final validator (there is no ELF section extractor yet).
- Strip LLVM's
Addresses/Linkagecapabilities and linkage decorations, and convertOpPtrAccessChainforms to their logical-addressing counterparts. - Validate the returned module with SPIRV-Tools under
SPV_ENV_VULKAN_1_2.
Unlike the native path, this backend currently has no SPIRV-Tools optimizer
stage and does not produce the native exact per-argument role plan. Vulkan
serializes it as LLVM_SPIRV, embeds constants directly in the module, and
uses the backend's conservative SPIR-V artifact feature requirements.
Function and control-flow rules
- Vulkan entry points have no LLVM function parameters. The current code has provisional global-variable lowering for arguments, but the property preflight rejects those kernels until a real descriptor ABI exists.
- Callable arguments remain LLVM function parameters.
- Save and restore the builder insertion point, current function, and variable map around recursive callable generation.
- Probe incomplete blocks through
llvm_compat.h::terminator_or_null; do not guess the API from an LLVM major version. - Verify the full module before invoking LLVM target passes. Per-function verification warnings are useful during construction but do not replace the module check.
- Aggregate legalization is mandatory because the LLVM SPIR-V target cannot reliably legalize the aggregate memory/return forms produced here.
Target initialization
InitializeLLVMSPIRVTarget registers global LLVM state in dependency order:
LLVMInitializeSPIRVTargetInfo();
LLVMInitializeSPIRVTarget();
LLVMInitializeSPIRVTargetMC();
LLVMInitializeSPIRVAsmPrinter();
Use the declarations from <llvm/Support/TargetSelect.h>. Hand-written
declarations can acquire the wrong language linkage. Keep the std::call_once
guard because shader compilation may be concurrent.
The target triple is deliberately spirv64: the supported LLVM revision's
spirv32 path fails in pointer-cast legalization. Vulkan logical addressing is
restored by the checked post-processing step and then enforced by validation;
raw EmitSPIRV() output is not the public contract.
CMake integration
The target is luisa-compute-spirv-llvm. It is created only when Vulkan and
LUISA_COMPUTE_ENABLE_VK_AST_LLVM_SPIRV are enabled. The Vulkan plugin also
links luisa-compute-spirv, because the shared artifact codec still needs the
native/common SPIR-V validation and feature-reconciliation utilities.
src/backends/common/spirv_llvm/CMakeLists.txt must:
find_package(LLVM CONFIG REQUIRED)without allowingLLVMConfig.cmaketo overwrite the project'sCMAKE_MSVC_RUNTIME_LIBRARYdefault;- require an exact
SPIRVtoken inLLVM_TARGETS_TO_BUILDand locatellvm/IR/IntrinsicsSPIRV.hin LLVM's reported include directories; - keep LLVM includes and ordinary definitions private;
- compile-check
_GLIBCXX_USE_CXX11_ABIand propagate only that ABI macro through the C++ facade when LLVM reports it; - request
core,support,bitwriter,transformutils,analysis,codegen,target,mc,spirvcodegen,spirvdesc,spirvinfo, andspirvanalysisthrough LLVM's component mapper; - use
llvm_config(... USE_SHARED ...)when a monolithicLLVMtarget exists so target-specific archives omitted by the dylib remain linked; - reject incompatible Windows CRT families or Debug iterator modes, and stage and install a monolithic LLVM DLL when that is the selected import.
The facade is static, so implementation link dependencies must still reach the final Vulkan plugin/test link. Do not make LLVM headers public as a workaround for a link failure.
XMake integration
The target is lc-spirv-llvm. Configuration requires an explicit
lc_llvm_path. Keep lc_vk_backend_use_xir_spirv=false when selecting it:
the top-level configuration rejects an explicit conflict, while the option
hook forces lc_enable_xir on and normalizes the native option off. The
SPIR-V targets and tests are created only when the Vulkan backend itself is
enabled.
Treat the selected llvm-config as the source of truth:
- accept an executable path, install prefix, build prefix, or common source-tree build layout;
- parse
--quote-pathsoutput withos.argv, including all-Ipaths from--cppflagsso generated intrinsics headers in development trees are found; - require an exact
SPIRVtoken from--targets-built; - query
--shared-modefor the complete component set before selecting--link-sharedor--link-static; - use static LLVM components on Windows because
llvm-configreports DLL filenames rather than MSVC import libraries for shared mode; - propagate shared-library rpaths on Unix and the required component system libraries on every platform;
- verify the libstdc++ ABI and, on Windows, the CRT family and Debug/non-Debug mode before compiling the facade.
Vulkan artifact boundary
The LLVM result reuses hlsl::Property and the common Vulkan artifact codec,
but it is a distinct LLVM_SPIRV dialect:
- do not apply native XIR capability reconciliation or exact accel-role rules to LLVM artifacts;
- constants are embedded in SPIR-V, so there is no constant-UBO payload;
- saved arguments use the legacy/unspecified resource-role sentinel;
- loaded modules are still integrity-checked and Vulkan-validated before pipeline creation.
Tests
test_spirv_llvm_facade is registered only when the LLVM facade target exists.
It compiles a no-argument kernel through the public header, checks the fixed
sampler property, and independently validates/disassembles the returned module
for Vulkan 1.2. test_vk_shader_binary_contract separately covers the common
artifact boundary under the LUISA_AST_LLVM_TO_SPIRV dialect define.
When adding support, include at least:
- a pure property-preflight rejection/acceptance test;
- a public-facade compile test;
- independent
SPV_ENV_VULKAN_1_2validation; - a Vulkan artifact round trip when properties or feature contracts change;
- a runtime Vulkan test before claiming descriptor or dispatch support.
Diagnostics and pitfalls
EmitSPIRV()writesllvm_ir_debug.llin the process working directory.- Missing SPIR-V intrinsics or target components are configuration errors, not reasons to guess library filenames or add every LLVM archive.
getDeclarationIfExists()may return null. Handle that at the operation's semantic boundary; never call through a null intrinsic declaration.- Do not call
EmitSPIRV()directly from Vulkan. Only the public facade runs required post-processing and Vulkan validation. - Do not report an AST opcode as supported merely because a visitor case exists; it must survive resource preflight, LLVM verification, SPIR-V emission, Vulkan validation, artifact loading, and runtime dispatch.