TL;DR
DiligentCore resolves shader #include "X" directives two different ways:
- At compile time, DXC/FXC maintain include-stack context. Once an include file has been loaded
as e.g. Lighting/LightingTypes.hlsli, a nested #include "LightingConfig.h" inside that
file is resolved relative to the loaded include path, so the compiler can request
Lighting/LightingConfig.h.
- At scan time, DiligentCore's own
ProcessShaderIncludesImpl — a hand-rolled text scanner
used to compute the BY_CONTENT render-state-cache hash (and for hot-reload dependency
tracking and archiver include enumeration) — passes the literal include string to the
stream factory with no include-stack / parent-directory resolution, so the same nested
include is looked up as bare LightingConfig.h.
For shaders with nested directory-relative includes (RTXPT's PathTracer/Lighting/… tree), DXC/FXC
compiles cleanly while the scanner throws Failed to load shader source file 'LightingConfig.h'.
This is a latent DiligentCore bug. RTXPT works around it by hashing the
cache BY_NAME (which never invokes the scanner). The proper fix is to make
ProcessShaderIncludesImpl resolve nested includes against their parent include file's path,
matching DXC/FXC include-stack behavior.
Symptom
With the render-state cache in RENDER_STATE_CACHE_FILE_HASH_MODE_BY_CONTENT (the default):
ERROR: Failed to create input stream for source file LightingConfig.h
ERROR: Failed to load shader source file 'LightingConfig.h'
[std::runtime_error / std::pair exceptions]
ERROR: Failed to process includes in file 'PathTracer/Lighting/LightsBaker.hlsl': Unknown error
The shader compiles fine (DXC/FXC resolve the nested include) — only the content-hash step,
which re-scans the source to enumerate includes, fails. In the current RTXPT chain,
LightsBaker.hlsl includes Lighting/LightingTypes.hlsli; then LightingTypes.hlsli includes
the sibling LightingConfig.h. DXC/FXC resolve the second include as Lighting/LightingConfig.h,
but the scanner tries bare LightingConfig.h against the flat search dirs
shaders;shaders\PathTracer, neither of which contains the file at that bare name.
The two resolution paths
DXC/FXC keep include-stack context for nested includes
Diligent builds a source string and passes it to DXC/FXC with an empty source name; the emitted
#line 1 "PathTracer/Lighting/LightsBaker.hlsl" marker is useful for diagnostics, but it is not
a reliable mechanism for resolving a top-level sibling include relative to ShaderCI.FilePath.
The RTXPT case that compiles is nested: LightsBaker.hlsl includes
Lighting/LightingTypes.hlsli, which is reachable from the shaders\PathTracer search dir. Once
the compiler has loaded that include file, a sibling include inside it,
#include "LightingConfig.h", is resolved relative to the loaded include path, so DXC/FXC ask
their include handlers for a path equivalent to Lighting/LightingConfig.h.
DxcIncludeHandlerImpl::LoadSource
(DXCompiler.cpp:185) and
D3DIncludeImpl::Open
(ShaderD3DBase.cpp:65) do not compute that parent directory themselves; they open the path
requested by the compiler. The DXC handler additionally strips a leading .\ if DXC provides one:
// DXCompiler.cpp:210-214
if (fileName.size() > 2 && fileName[0] == '.' && (fileName[1] == '\\' || fileName[1] == '/'))
fileName.erase(0, 2);
m_pStreamFactory->CreateInputStream(fileName.c_str(), &pSourceStream);
shaders\PathTracer (search dir) + Lighting/LightingConfig.h → found. ✔
The scanner passes the literal include text verbatim
ProcessShaderIncludesImpl
(ShaderToolsCommon.cpp:428) is a text scanner. It first opens Lighting/LightingTypes.hlsli;
inside that file, FindIncludes extracts the raw string between the quotes
(LightingConfig.h) and, when it recurses, sets the child's path to that bare name,
discarding the parent include path Lighting/:
// ShaderToolsCommon.cpp:439-448
[&](const std::string& FilePath, size_t Start, size_t End)
{
if (!Includes.insert(FilePath).second)
return;
ShaderCreateInfo IncludeCI{ShaderCI};
IncludeCI.FilePath = FilePath.c_str(); // "LightingConfig.h" — parent include path thrown away
IncludeCI.Source = nullptr;
IncludeCI.SourceLength = 0;
ProcessShaderIncludesImpl(IncludeCI, Includes, IncludeHandler);
}
ReadShaderSourceFile then calls CreateInputStream("LightingConfig.h"), which only sees the
factory's flat search dirs (shaders;shaders\PathTracer), none of which contain Lighting/, and
throws (ShaderToolsCommon.cpp:243-245). ✘
The crux
The parent include path is available — during the failing nested recursion,
ShaderCI.FilePath holds Lighting/LightingTypes.hlsli — but the scanner never combines it with
the nested include name. So:
|
Include resolution rule |
| DXC/FXC |
include-stack-aware: a nested include is resolved relative to the loaded include file path, then search paths |
| Diligent scanner |
literal include string only; no include-stack / parent-path context |
They agree only when every nested include is reachable from a search dir by the exact spelling
used in the source (e.g. Tutorial26's flat assets). RTXPT uses nested directory-relative includes,
so DXC/FXC compile cleanly while the scanner — invoked only to compute the BY_CONTENT cache hash —
fails.
It is a genuine latent bug, not just a quirk: any project with directory-relative shader
includes hits it under content hashing, hot-reload dependency tracking
(ShaderReloadFactory/RenderStateCache::Reload), or the archiver's include enumeration.
Scope / impact
Affected whenever ProcessShaderIncludesImpl runs over shaders with directory-relative includes:
RENDER_STATE_CACHE_FILE_HASH_MODE_BY_CONTENT — content hashing (the failure above).
- Hot reload —
ProcessShaderIncludes builds the include→dependency set for Reload().
- Archiver — include enumeration when packaging shader source.
Not affected: actual DXC/FXC compilation (resolves correctly), and BY_NAME hashing.
Proposed fix
Make ProcessShaderIncludesImpl resolve each nested include against the path of the currently
scanned include file before recursing — matching DXC/FXC include-stack behavior. DiligentCore
already provides the needed helpers in BasicFileSystem.hpp (already included by
ShaderToolsCommon.cpp): FileSystem::GetPathComponents, FileSystem::IsPathAbsolute,
FileSystem::SimplifyPath, FileSystem::SlashSymbol.
Minimal version (only safe for parent-relative include trees)
In the FindIncludes recursion lambda (ShaderToolsCommon.cpp:439-448), a simple parent-directory
join fixes trees where includes are consistently parent-relative. It is not safe for mixed
search-dir-relative includes such as RTXPT's first Lighting/LightingTypes.hlsli include; use the
robust version below for the engine fix.
[&](const std::string& IncludeName, size_t /*Start*/, size_t /*End*/)
{
// Resolve the include relative to the path of the currently scanned file.
// Without this, a nested include like "LightingConfig.h" inside
// Lighting/LightingTypes.hlsli loses the "Lighting/" parent path and
// cannot be found via the flat search dirs.
std::string ResolvedPath = IncludeName;
if (ShaderCI.FilePath != nullptr && !FileSystem::IsPathAbsolute(IncludeName.c_str()))
{
std::string ParentDir;
FileSystem::GetPathComponents(ShaderCI.FilePath, &ParentDir, nullptr);
if (!ParentDir.empty())
{
ResolvedPath = FileSystem::SimplifyPath(
(ParentDir + FileSystem::SlashSymbol + IncludeName).c_str(),
FileSystem::SlashSymbol);
}
}
if (!Includes.insert(ResolvedPath).second)
return;
ShaderCreateInfo IncludeCI{ShaderCI};
IncludeCI.FilePath = ResolvedPath.c_str(); // resolved, not bare -> next level's parent dir is correct
IncludeCI.Source = nullptr;
IncludeCI.SourceLength = 0;
ProcessShaderIncludesImpl(IncludeCI, Includes, IncludeHandler);
}
Because the recursion now stores the resolved path in IncludeCI.FilePath, deeper
parent-relative includes compose correctly: A/B/file.hlsl → "C/inc.h" → A/B/C/inc.h →
"deep.h" → A/B/C/deep.h.
Robust version (matches DXC/FXC include-stack lookup)
DXC/FXC's effective behavior is current-include-relative first, then include search paths. A
search-dir-relative include (e.g. RTXPT's first Lighting/LightingTypes.hlsli) should still
resolve under its original spelling, not under a blindly prepended parent directory. To match
this, probe the stream factory and fall back to the original include spelling when the
current-include-relative path can't be opened:
static std::string ResolveIncludePath(const ShaderCreateInfo& ShaderCI, const std::string& IncludeName)
{
if (ShaderCI.FilePath == nullptr || FileSystem::IsPathAbsolute(IncludeName.c_str()))
return IncludeName;
std::string ParentDir;
FileSystem::GetPathComponents(ShaderCI.FilePath, &ParentDir, nullptr);
if (ParentDir.empty())
return IncludeName;
const std::string Relative = FileSystem::SimplifyPath(
(ParentDir + FileSystem::SlashSymbol + IncludeName).c_str(), FileSystem::SlashSymbol);
// Phase 1: current-include-relative. Phase 2 (fallback): search-dir/original spelling.
if (ShaderCI.pShaderSourceStreamFactory != nullptr)
{
RefCntAutoPtr<IFileStream> pStream;
ShaderCI.pShaderSourceStreamFactory->CreateInputStream(Relative.c_str(), &pStream);
if (pStream)
return Relative;
return IncludeName; // let the existing error path report the original name if this also fails
}
return Relative;
}
Notes / risks
- Hash stability: the values inserted into
Includes change from bare names to resolved
paths, so BY_CONTENT hashes shift once. This is a one-time cache invalidation, not a
correctness problem.
- Backward compatibility: flat-asset projects (e.g. Tutorial26) still resolve, because the
robust version falls back to the original include spelling. The minimal version can break mixed
trees where a nested file is first included by a search-dir-relative path.
- Angle vs quote includes: the scanner does not distinguish
<...> from "...". The robust
(try-relative-then-bare) approach is a superset of correct behavior for both and is the safer
choice.
Key references
| What |
Location |
| Scanner that discards the parent dir (root cause) |
DiligentCore/Graphics/ShaderTools/src/ShaderToolsCommon.cpp:428-454 |
| Scanner throws on unresolved include |
DiligentCore/Graphics/ShaderTools/src/ShaderToolsCommon.cpp:224-245 |
DXC handler — opens the include path requested by DXC and strips leading .\ |
DiligentCore/Graphics/ShaderTools/src/DXCompiler.cpp:185-237 |
| FXC handler — opens the include path requested by D3DCompile |
DiligentCore/Graphics/GraphicsEngineD3DBase/src/ShaderD3DBase.cpp:57-96 |
BY_NAME hashing (never calls the scanner) |
DiligentCore/Graphics/GraphicsTools/src/RenderStateCacheImpl.cpp (HashShaderCIByFileName) |
| Path helpers for the fix |
DiligentCore/Platforms/Basic/interface/BasicFileSystem.hpp:171-197 |
RTXPT workaround (BY_NAME) |
DiligentSamples/Samples/RTXPT/src/RTXPTSample.cpp (cache create block) |
TL;DR
DiligentCore resolves shader
#include "X"directives two different ways:as e.g.
Lighting/LightingTypes.hlsli, a nested#include "LightingConfig.h"inside thatfile is resolved relative to the loaded include path, so the compiler can request
Lighting/LightingConfig.h.ProcessShaderIncludesImpl— a hand-rolled text scannerused to compute the
BY_CONTENTrender-state-cache hash (and for hot-reload dependencytracking and archiver include enumeration) — passes the literal include string to the
stream factory with no include-stack / parent-directory resolution, so the same nested
include is looked up as bare
LightingConfig.h.For shaders with nested directory-relative includes (RTXPT's
PathTracer/Lighting/…tree), DXC/FXCcompiles cleanly while the scanner throws
Failed to load shader source file 'LightingConfig.h'.This is a latent DiligentCore bug. RTXPT works around it by hashing the
cache
BY_NAME(which never invokes the scanner). The proper fix is to makeProcessShaderIncludesImplresolve nested includes against their parent include file's path,matching DXC/FXC include-stack behavior.
Symptom
With the render-state cache in
RENDER_STATE_CACHE_FILE_HASH_MODE_BY_CONTENT(the default):The shader compiles fine (DXC/FXC resolve the nested include) — only the content-hash step,
which re-scans the source to enumerate includes, fails. In the current RTXPT chain,
LightsBaker.hlslincludesLighting/LightingTypes.hlsli; thenLightingTypes.hlsliincludesthe sibling
LightingConfig.h. DXC/FXC resolve the second include asLighting/LightingConfig.h,but the scanner tries bare
LightingConfig.hagainst the flat search dirsshaders;shaders\PathTracer, neither of which contains the file at that bare name.The two resolution paths
DXC/FXC keep include-stack context for nested includes
Diligent builds a source string and passes it to DXC/FXC with an empty source name; the emitted
#line 1 "PathTracer/Lighting/LightsBaker.hlsl"marker is useful for diagnostics, but it is nota reliable mechanism for resolving a top-level sibling include relative to
ShaderCI.FilePath.The RTXPT case that compiles is nested:
LightsBaker.hlslincludesLighting/LightingTypes.hlsli, which is reachable from theshaders\PathTracersearch dir. Oncethe compiler has loaded that include file, a sibling include inside it,
#include "LightingConfig.h", is resolved relative to the loaded include path, so DXC/FXC asktheir include handlers for a path equivalent to
Lighting/LightingConfig.h.DxcIncludeHandlerImpl::LoadSource(DXCompiler.cpp:185) and
D3DIncludeImpl::Open(ShaderD3DBase.cpp:65) do not compute that parent directory themselves; they open the path
requested by the compiler. The DXC handler additionally strips a leading
.\if DXC provides one:shaders\PathTracer(search dir) +Lighting/LightingConfig.h→ found. ✔The scanner passes the literal include text verbatim
ProcessShaderIncludesImpl(ShaderToolsCommon.cpp:428) is a text scanner. It first opens
Lighting/LightingTypes.hlsli;inside that file,
FindIncludesextracts the raw string between the quotes(
LightingConfig.h) and, when it recurses, sets the child's path to that bare name,discarding the parent include path
Lighting/:ReadShaderSourceFilethen callsCreateInputStream("LightingConfig.h"), which only sees thefactory's flat search dirs (
shaders;shaders\PathTracer), none of which containLighting/, andthrows (ShaderToolsCommon.cpp:243-245). ✘
The crux
The parent include path is available — during the failing nested recursion,
ShaderCI.FilePathholdsLighting/LightingTypes.hlsli— but the scanner never combines it withthe nested include name. So:
They agree only when every nested include is reachable from a search dir by the exact spelling
used in the source (e.g. Tutorial26's flat assets). RTXPT uses nested directory-relative includes,
so DXC/FXC compile cleanly while the scanner — invoked only to compute the
BY_CONTENTcache hash —fails.
It is a genuine latent bug, not just a quirk: any project with directory-relative shader
includes hits it under content hashing, hot-reload dependency tracking
(
ShaderReloadFactory/RenderStateCache::Reload), or the archiver's include enumeration.Scope / impact
Affected whenever
ProcessShaderIncludesImplruns over shaders with directory-relative includes:RENDER_STATE_CACHE_FILE_HASH_MODE_BY_CONTENT— content hashing (the failure above).ProcessShaderIncludesbuilds the include→dependency set forReload().Not affected: actual DXC/FXC compilation (resolves correctly), and
BY_NAMEhashing.Proposed fix
Make
ProcessShaderIncludesImplresolve each nested include against the path of the currentlyscanned include file before recursing — matching DXC/FXC include-stack behavior. DiligentCore
already provides the needed helpers in
BasicFileSystem.hpp(already included byShaderToolsCommon.cpp):FileSystem::GetPathComponents,FileSystem::IsPathAbsolute,FileSystem::SimplifyPath,FileSystem::SlashSymbol.Minimal version (only safe for parent-relative include trees)
In the
FindIncludesrecursion lambda (ShaderToolsCommon.cpp:439-448), a simple parent-directoryjoin fixes trees where includes are consistently parent-relative. It is not safe for mixed
search-dir-relative includes such as RTXPT's first
Lighting/LightingTypes.hlsliinclude; use therobust version below for the engine fix.
Because the recursion now stores the resolved path in
IncludeCI.FilePath, deeperparent-relative includes compose correctly:
A/B/file.hlsl→"C/inc.h"→A/B/C/inc.h→"deep.h"→A/B/C/deep.h.Robust version (matches DXC/FXC include-stack lookup)
DXC/FXC's effective behavior is current-include-relative first, then include search paths. A
search-dir-relative include (e.g. RTXPT's first
Lighting/LightingTypes.hlsli) should stillresolve under its original spelling, not under a blindly prepended parent directory. To match
this, probe the stream factory and fall back to the original include spelling when the
current-include-relative path can't be opened:
Notes / risks
Includeschange from bare names to resolvedpaths, so
BY_CONTENThashes shift once. This is a one-time cache invalidation, not acorrectness problem.
robust version falls back to the original include spelling. The minimal version can break mixed
trees where a nested file is first included by a search-dir-relative path.
<...>from"...". The robust(try-relative-then-bare) approach is a superset of correct behavior for both and is the safer
choice.
Key references
DiligentCore/Graphics/ShaderTools/src/ShaderToolsCommon.cpp:428-454DiligentCore/Graphics/ShaderTools/src/ShaderToolsCommon.cpp:224-245.\DiligentCore/Graphics/ShaderTools/src/DXCompiler.cpp:185-237DiligentCore/Graphics/GraphicsEngineD3DBase/src/ShaderD3DBase.cpp:57-96BY_NAMEhashing (never calls the scanner)DiligentCore/Graphics/GraphicsTools/src/RenderStateCacheImpl.cpp(HashShaderCIByFileName)DiligentCore/Platforms/Basic/interface/BasicFileSystem.hpp:171-197BY_NAME)DiligentSamples/Samples/RTXPT/src/RTXPTSample.cpp(cache create block)