Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/dxc/Support/HLSLOptions.td
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,8 @@ def fspv_use_legacy_buffer_matrix_order: Flag<["-"], "fspv-use-legacy-buffer-mat
HelpText<"Assume the legacy matrix order (row major) when accessing raw buffers (e.g., ByteAdddressBuffer)">;
def fspv_reflect: Flag<["-"], "fspv-reflect">, Group<spirv_Group>, Flags<[CoreOption, DriverOption]>,
HelpText<"Emit additional SPIR-V instructions to aid reflection">;
def fspv_allow_import: Flag<["-"], "fspv-allow-import">, Group<spirv_Group>, Flags<[CoreOption, DriverOption]>,
HelpText<"Allow undefined external functions in library targets and emit Import linkage decorations for them">;
def fspv_debug_EQ : Joined<["-"], "fspv-debug=">, Group<spirv_Group>, Flags<[CoreOption, DriverOption]>,
HelpText<"Specify whitelist of debug info category (file -> source -> line, tool, vulkan-with-source)">;
def fspv_extension_EQ : Joined<["-"], "fspv-extension=">, Group<spirv_Group>, Flags<[CoreOption, DriverOption]>,
Expand Down
1 change: 1 addition & 0 deletions include/dxc/Support/SPIRVOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ struct SpirvCodeGenOptions {
bool enable16BitTypes = false;
bool finiteMathOnly = false;
bool enableReflect = false;
bool allowImport = false;
bool invertY = false; // Additive inverse
bool invertW = false; // Multiplicative inverse
bool noWarnEmulatedFeatures = false;
Expand Down
3 changes: 3 additions & 0 deletions lib/DxcSupport/HLSLOptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,8 @@ int ReadDxcOpts(const OptTable *optionTable, unsigned flagsToInclude,
Args.hasFlag(OPT_fspv_use_legacy_buffer_matrix_order, OPT_INVALID, false);
opts.SpirvOptions.enableReflect =
Args.hasFlag(OPT_fspv_reflect, OPT_INVALID, false);
opts.SpirvOptions.allowImport =
Args.hasFlag(OPT_fspv_allow_import, OPT_INVALID, false);
opts.SpirvOptions.noWarnIgnoredFeatures =
Args.hasFlag(OPT_Wno_vk_ignored_features, OPT_INVALID, false);
opts.SpirvOptions.noWarnEmulatedFeatures =
Expand Down Expand Up @@ -1296,6 +1298,7 @@ int ReadDxcOpts(const OptTable *optionTable, unsigned flagsToInclude,
Args.hasFlag(OPT_fspv_flatten_resource_arrays, OPT_INVALID, false) ||
Args.hasFlag(OPT_fspv_reduce_load_size, OPT_INVALID, false) ||
Args.hasFlag(OPT_fspv_reflect, OPT_INVALID, false) ||
Args.hasFlag(OPT_fspv_allow_import, OPT_INVALID, false) ||
Args.hasFlag(OPT_fspv_fix_func_call_arguments, OPT_INVALID, false) ||
Args.hasFlag(OPT_fspv_print_all, OPT_INVALID, false) ||
Args.hasFlag(OPT_fspv_use_emulated_heap, OPT_INVALID, false) ||
Expand Down
6 changes: 6 additions & 0 deletions tools/clang/include/clang/SPIRV/SpirvFunction.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ class SpirvFunction {
void addVariable(SpirvVariable *);
void addBasicBlock(SpirvBasicBlock *);

// True if this is a function declaration (header-only, no body), as
// emitted under -fspv-allow-import for undefined external functions.
// Per SPIR-V Logical Layout (sec. 2.4), declarations precede
// definitions in the function section.
bool isDeclaration() const { return basicBlocks.empty(); }

/// Adds the given instruction as the first instruction of this SPIR-V
/// function body.
void addFirstInstruction(SpirvInstruction *inst) {
Expand Down
8 changes: 8 additions & 0 deletions tools/clang/lib/SPIRV/DeclResultIdMapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1824,6 +1824,14 @@ SpirvFunction *DeclResultIdMapper::getOrRegisterFn(const FunctionDecl *fn) {
if (fn->getAttr<HLSLExportAttr>()) {
spvBuilder.decorateLinkage(nullptr, spirvFunction, fn->getName(),
spv::LinkageType::Export, fn->getLocation());
} else if (spirvOptions.allowImport && !fn->isImplicit()) {
// -fspv-allow-import is gated to lib_* profiles at SpirvEmitter
// construction, so reaching here implies a library compilation.
const FunctionDecl *defn = nullptr;
if (!fn->isDefined(defn)) {
spvBuilder.decorateLinkage(nullptr, spirvFunction, fn->getName(),
spv::LinkageType::Import, fn->getLocation());
}
}

// No need to dereference to get the pointer. Function returns that are
Expand Down
24 changes: 24 additions & 0 deletions tools/clang/lib/SPIRV/EmitVisitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,23 @@ void EmitVisitor::emitDebugLine(spv::Op op, const SourceLocation &loc,
(op != spv::Op::OpReturn && op != spv::Op::OpFunction))
return;

// Body-less Import prototype: suppress OpLine for the *entire* emit
// window — including the OpLine that initInstruction would emit
// immediately before OpFunction itself. spirv-val's current layout
// classification (OpLine -> FunctionDefinitions outside Types)
// advances the section past FunctionDeclarations on any OpLine seen
// while in FunctionDeclarations: the OpLines between OpFunction and
// OpFunctionParameter trip the prototype's *own* OpFunctionEnd, and
// — more subtly — the OpLine that DXC emits *between* two
// consecutive prototypes (after the previous OpFunctionEnd, before
// this prototype's OpFunction) trips the *next* prototype's
// OpFunctionEnd. Skipping every OpLine inside the prototype's emit
// window keeps the binary structurally-valid AND validator-clean.
// Prototypes are dropped during link-time import resolution anyway,
// so the lost source-location info on them costs nothing.
if (inImportPrototype)
return;

// Based on SPIR-V spec, OpSelectionMerge must immediately precede either an
// OpBranchConditional or OpSwitch instruction. Similarly OpLoopMerge must
// immediately precede either an OpBranch or OpBranchConditional instruction.
Expand Down Expand Up @@ -522,6 +539,12 @@ bool EmitVisitor::visit(SpirvFunction *fn, Phase phase) {

if (fn->isEntryFunctionWrapper())
inEntryFunctionWrapper = true;
// Body-less functions are Import prototypes (under
// -fspv-allow-import). Suppress OpLine for instructions inside
// their header — see the `inImportPrototype` flag in EmitVisitor.h
// for the rationale.
if (fn->isDeclaration())
inImportPrototype = true;

// Emit OpFunction
initInstruction(spv::Op::OpFunction, fn->getSourceLocation());
Expand All @@ -547,6 +570,7 @@ bool EmitVisitor::visit(SpirvFunction *fn, Phase phase) {
initInstruction(spv::Op::OpFunctionEnd, /* SourceLocation */ {});
finalizeInstruction(&mainBinary);
inEntryFunctionWrapper = false;
inImportPrototype = false;
}

return true;
Expand Down
13 changes: 12 additions & 1 deletion tools/clang/lib/SPIRV/EmitVisitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ class EmitVisitor : public Visitor {
debugMainFileId(0), debugInfoExtInstId(0), debugLineStart(0),
debugLineEnd(0), debugColumnStart(0), debugColumnEnd(0),
lastOpWasMergeInst(false), inEntryFunctionWrapper(false),
hlslVersion(0) {}
inImportPrototype(false), hlslVersion(0) {}

~EmitVisitor();

Expand Down Expand Up @@ -493,6 +493,17 @@ class EmitVisitor : public Visitor {
bool lastOpWasMergeInst;
// True if currently it enters an entry function wrapper.
bool inEntryFunctionWrapper;
// True while emitting a body-less Import-linkage function prototype
// (the form -fspv-allow-import emits for undefined external functions).
// Used by emitDebugLine to suppress OpLine instructions inside the
// prototype's header — they're structurally legal per SPIR-V spec but
// confuse spirv-val's layout pass into advancing past
// FunctionDeclarations on the first OpLine, which then flags the
// prototype's own OpFunctionEnd as a "declaration after definition"
// violation. See `external/SPIRV-Tools` upstream issue: OpLine in
// FunctionDeclarations layout section is misclassified as
// FunctionDefinitions.
bool inImportPrototype;
// Map of filename string id to the id of its DebugSource instruction. When
// generating OpSource instruction without a result id, use 1 to remember it
// was generated.
Expand Down
60 changes: 57 additions & 3 deletions tools/clang/lib/SPIRV/SpirvEmitter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,10 @@ SpirvEmitter::SpirvEmitter(CompilerInstance &ci)
spvContext.setCurrentShaderModelKind(shaderModel->GetKind());
spvContext.setMajorVersion(shaderModel->GetMajor());
spvContext.setMinorVersion(shaderModel->GetMinor());
isLibProfile = shaderModel->IsLib();

if (spirvOptions.allowImport && !isLibProfile)
emitError("-fspv-allow-import requires a lib_* target profile", {});
spirvOptions.signaturePacking =
ci.getCodeGenOpts().HLSLSignaturePackingStrategy ==
(unsigned)hlsl::DXIL::PackingStrategy::Optimized;
Expand Down Expand Up @@ -963,8 +967,15 @@ void SpirvEmitter::HandleTranslationUnit(ASTContext &context) {
!dsetbindingsToCombineImageSampler.empty() ||
spirvOptions.signaturePacking;

// SPIR-V modules with Import linkage attributes are not yet linked, so
// SPIRV-Tools' optimizer/legalizer passes (which assume every reachable
// function has a body) are not meaningful and would crash on the
// body-less Import prototypes. The expectation is that the user runs
// those passes after spirv-link has resolved the imports.
const bool skipPostCodegenPasses = spirvOptions.allowImport;

// Run legalization passes
if (spirvOptions.codeGenHighLevel) {
if (spirvOptions.codeGenHighLevel || skipPostCodegenPasses) {
beforeHlslLegalization = needsLegalization;
} else {
if (needsLegalization) {
Expand Down Expand Up @@ -1561,6 +1572,16 @@ bool SpirvEmitter::handleNodePayloadArrayType(const ParmVarDecl *decl,
void SpirvEmitter::doFunctionDecl(const FunctionDecl *decl) {
// Forward declaration of a function inside another.
if (!decl->isThisDeclarationADefinition()) {
// In library targets with -fspv-allow-import, a function that is
// declared but never defined anywhere in the translation unit is
// emitted as a body-less prototype with Import linkage. The Import
// decoration itself is added by getOrRegisterFn().
const FunctionDecl *defn = nullptr;
if (isLibProfile && spirvOptions.allowImport &&
!decl->isImplicit() && !decl->isDefined(defn)) {
emitImportFunctionPrototype(decl);
return;
}
addFunctionToWorkQueue(spvContext.getCurrentShaderModelKind(), decl,
/*isEntryFunction*/ false);
return;
Expand Down Expand Up @@ -3232,8 +3253,17 @@ SpirvInstruction *SpirvEmitter::processCall(const CallExpr *callExpr) {
// Note that we always want the definition because Stmts/Exprs in the
// function body reference the parameters in the definition.
if (!callee) {
emitError("found undefined function", callExpr->getExprLoc());
return nullptr;
// In library targets with -fspv-allow-import, calls to undefined
// functions are emitted as references to an Import-linkage prototype
// so the resulting SPIR-V module can be linked against an external
// definition.
if (isLibProfile && spirvOptions.allowImport) {
callee = callExpr->getDirectCallee();
}
if (!callee) {
emitError("found undefined function", callExpr->getExprLoc());
return nullptr;
}
}

const auto paramTypeMatchesArgType = [](QualType paramType,
Expand Down Expand Up @@ -15811,6 +15841,30 @@ void SpirvEmitter::addFunctionToWorkQueue(hlsl::DXIL::ShaderKind shaderKind,
}
}

void SpirvEmitter::emitImportFunctionPrototype(const FunctionDecl *decl) {
// The work queue is idempotent in addFunctionToWorkQueue, so doFunctionDecl
// is only invoked once per external prototype — no further dedup needed.
// getOrRegisterFn applies the Import LinkageAttributes decoration when the
// function has no definition and -fspv-allow-import is set.
SpirvFunction *func = declIdMapper.getOrRegisterFn(decl);

const QualType retType =
declIdMapper.getTypeAndCreateCounterForPotentialAliasVar(decl);
spvBuilder.beginFunction(retType, decl->getLocStart(), getFnName(decl),
decl->hasAttr<HLSLPreciseAttr>(),
decl->hasAttr<NoInlineAttr>(), func);

// OpFunctionParameter for each parameter; no basic blocks (Import
// linkage requires the function body to be absent).
for (uint32_t i = 0; i < decl->getNumParams(); ++i) {
const ParmVarDecl *paramDecl = decl->getParamDecl(i);
declIdMapper.createFnParam(paramDecl, i + 1,
/*decorateIntrinsicAttrs*/ true);
}

spvBuilder.endFunction();
}

SpirvInstruction *
SpirvEmitter::processTraceRayInline(const CXXMemberCallExpr *expr) {
const auto object = expr->getImplicitObjectArgument();
Expand Down
11 changes: 11 additions & 0 deletions tools/clang/lib/SPIRV/SpirvEmitter.h
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,12 @@ class SpirvEmitter : public ASTConsumer {
const clang::FunctionDecl *,
bool isEntryFunction);

/// \brief Emits a body-less SpirvFunction (OpFunction / OpFunctionParameters
/// / OpFunctionEnd, no basic blocks) for the given declaration. Used to
/// model external functions for SPIR-V Import linkage when the user passes
/// -fspv-allow-import on a library target.
void emitImportFunctionPrototype(const clang::FunctionDecl *decl);

/// \brief Helper function to run SPIRV-Tools optimizer's performance passes.
/// Runs the SPIRV-Tools optimizer on the given SPIR-V module |mod|, and
/// gets the info/warning/error messages via |messages|.
Expand Down Expand Up @@ -1583,6 +1589,11 @@ class SpirvEmitter : public ASTConsumer {
/// option to spirv-val because of illegal function parameter scope.
bool beforeHlslLegalization;

/// True when the overall compilation profile is `lib_*`. Stays true even
/// while emitting an entry function within the library, where
/// `spvContext.isLib()` flips to the entry's per-stage shader model kind.
bool isLibProfile = false;

/// Mapping from methods to the decls to represent their implicit object
/// parameters
///
Expand Down
18 changes: 16 additions & 2 deletions tools/clang/lib/SPIRV/SpirvModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,23 @@ bool SpirvModule::invokeVisitor(Visitor *visitor, bool reverseOrder) {
if (!debugInstructions[i]->invokeVisitor(visitor))
return false;

// SPIR-V Logical Layout (sec. 2.4) requires all function
// declarations (header-only) to precede any function definitions
// (with bodies). Under -fspv-allow-import we emit body-less
// prototypes for undefined external functions, but they're
// discovered in source order — interleaved with bodies coming
// from the same translation unit (e.g. helpers pulled in via
// #include). Two-pass emit keeps declarations first, definitions
// second; spirv-val rejects the binary otherwise with "Function
// declarations must appear before function definitions".
for (auto fn : functions)
if (!fn->invokeVisitor(visitor, reverseOrder))
return false;
if (fn->isDeclaration())
if (!fn->invokeVisitor(visitor, reverseOrder))
return false;
for (auto fn : functions)
if (!fn->isDeclaration())
if (!fn->invokeVisitor(visitor, reverseOrder))
return false;
}

if (!visitor->visit(this, Visitor::Phase::Done))
Expand Down
38 changes: 38 additions & 0 deletions tools/clang/test/CodeGenSPIRV/lib.fn.export.callables.hlsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// RUN: %dxc -T lib_6_x -fspv-target-env=universal1.5 %s -spirv | FileCheck %s

// Library with multiple exported callable functions and no entry point: a
// SPIR-V module suitable for spirv-link consumption. Mirrors the
// material-graph "node" shape: every callable takes a struct as its first
// in-parameter, returns through an `out` parameter, and ranges over scalar
// and vector floats plus a few common intrinsics.

// CHECK-DAG: OpCapability Shader
// CHECK-DAG: OpCapability Linkage

// No entry points should be emitted for an export-only library.
// CHECK-NOT: OpEntryPoint

// CHECK-DAG: OpDecorate %add_f1 LinkageAttributes "add_f1" Export
// CHECK-DAG: OpDecorate %add_f4 LinkageAttributes "add_f4" Export
// CHECK-DAG: OpDecorate %dot_f3 LinkageAttributes "dot_f3" Export
// CHECK-DAG: OpDecorate %normalize_f3 LinkageAttributes "normalize_f3" Export
// CHECK-DAG: OpDecorate %saturate_f1 LinkageAttributes "saturate_f1" Export
// CHECK-DAG: OpDecorate %lerp_f4 LinkageAttributes "lerp_f4" Export

struct ShadingContext {
uint3 dispatch_thread_id;
uint num_invocations;
};

export void add_f1(in ShadingContext ctx, in float a, in float b, out float result) { result = a + b; }
export void add_f4(in ShadingContext ctx, in float4 a, in float4 b, out float4 result) { result = a + b; }

export void dot_f3(in ShadingContext ctx, in float3 a, in float3 b, out float result) { result = dot(a, b); }

export void normalize_f3(in ShadingContext ctx, in float3 a, out float3 result) { result = normalize(a); }

export void saturate_f1(in ShadingContext ctx, in float a, out float result) { result = saturate(a); }

export void lerp_f4(in ShadingContext ctx, in float4 a, in float4 b, in float t, out float4 result) {
result = lerp(a, b, t);
}
18 changes: 18 additions & 0 deletions tools/clang/test/CodeGenSPIRV/lib.fn.import.export.hlsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// RUN: %dxc -T lib_6_x -fspv-target-env=universal1.5 -fspv-allow-import %s -spirv | FileCheck %s

// Library target with both Export and Import linkage in the same module:
// `caller` is exported and forwards through an undefined `helper` which
// must be Import-linked.

// CHECK-DAG: OpCapability Linkage
// CHECK-DAG: OpDecorate %caller LinkageAttributes "caller" Export
// CHECK-DAG: OpDecorate %helper LinkageAttributes "helper" Import

// No entry points should be emitted.
// CHECK-NOT: OpEntryPoint

float helper(float x);

export float caller(float v) {
return helper(v);
}
29 changes: 29 additions & 0 deletions tools/clang/test/CodeGenSPIRV/lib.fn.import.hlsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// RUN: %dxc -T lib_6_x -E main -fspv-target-env=universal1.5 -fspv-allow-import %s -spirv | FileCheck %s

// Calls to undefined external functions in a library target compile to a
// body-less OpFunction prototype decorated with Import linkage, so the
// resulting SPIR-V module can be linked against a separately compiled
// definition.

// CHECK-DAG: OpCapability Linkage

// The Import prototype must be referenced as the callee of OpFunctionCall.
// CHECK-DAG: OpDecorate %external_helper LinkageAttributes "external_helper" Import

// CHECK: OpFunctionCall %void %external_helper

// The prototype itself must have parameters but no entry block.
// CHECK: %external_helper = OpFunction %void None
// CHECK-NEXT: OpFunctionParameter
// CHECK-NEXT: OpFunctionParameter
// CHECK-NEXT: OpFunctionEnd

void external_helper(float a, out float result);

[shader("compute")]
[numthreads(1, 1, 1)]
void main(uint3 dtid : SV_DispatchThreadID)
{
float result;
external_helper(1.0, result);
}
Loading
Loading