Files
XCEngine/tests/Resources/Shader/test_shader_dependencies.cpp

1228 lines
45 KiB
C++
Raw Normal View History

#include <XCEngine/Core/Asset/AssetDatabase.h>
#include <XCEngine/Core/Asset/ArtifactContainer.h>
#include <XCEngine/Core/Asset/ArtifactFormats.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Core/Containers/Array.h>
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/Rendering/Internal/ShaderVariantUtils.h>
#include <XCEngine/RHI/D3D12/D3D12Shader.h>
#include <XCEngine/Resources/Shader/ShaderLoader.h>
#include <d3dcompiler.h>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory>
#include <string>
#include <system_error>
#include <vector>
namespace {
class ScopedCurrentPath {
public:
explicit ScopedCurrentPath(const std::filesystem::path& path)
: m_previousPath(std::filesystem::current_path()) {
std::filesystem::current_path(path);
}
ScopedCurrentPath(const ScopedCurrentPath&) = delete;
ScopedCurrentPath& operator=(const ScopedCurrentPath&) = delete;
~ScopedCurrentPath() {
std::error_code ec;
std::filesystem::current_path(m_previousPath, ec);
}
private:
std::filesystem::path m_previousPath;
};
bool ContainsPathSuffix(
const XCEngine::Containers::Array<XCEngine::Containers::String>& paths,
const char* suffix) {
for (size_t index = 0; index < paths.Size(); ++index) {
const std::string path = paths[index].CStr();
if (path.ends_with(suffix)) {
return true;
}
}
return false;
}
bool WriteTextFile(const std::filesystem::path& path, const char* text) {
std::filesystem::create_directories(path.parent_path());
std::ofstream stream(path, std::ios::binary);
if (!stream) {
return false;
}
stream << text;
return true;
}
std::vector<std::string> SplitTabFields(const std::string& line) {
std::vector<std::string> fields;
size_t fieldStart = 0;
while (fieldStart <= line.size()) {
const size_t tabPos = line.find('\t', fieldStart);
if (tabPos == std::string::npos) {
fields.push_back(line.substr(fieldStart));
break;
}
fields.push_back(line.substr(fieldStart, tabPos - fieldStart));
fieldStart = tabPos + 1;
}
return fields;
}
bool LoadSourceDbShaderRecord(
const std::filesystem::path& projectRoot,
const char* relativePath,
std::vector<std::string>& outFields) {
std::ifstream input(projectRoot / "Library/assets.db");
if (!input.is_open()) {
std::cerr << "failed to open source asset db\n";
return false;
}
std::string line;
while (std::getline(input, line)) {
if (line.empty() || line[0] == '#') {
continue;
}
std::vector<std::string> fields = SplitTabFields(line);
if (fields.size() >= 10 && fields[1] == relativePath) {
outFields = std::move(fields);
return true;
}
}
std::cerr << "source asset db did not contain record: " << relativePath << "\n";
return false;
}
XCEngine::Core::uint64 GetFileSizeValue(const std::filesystem::path& path) {
std::error_code ec;
const auto size = std::filesystem::file_size(path, ec);
return ec ? 0 : static_cast<XCEngine::Core::uint64>(size);
}
XCEngine::Core::uint64 GetFileWriteTimeValue(const std::filesystem::path& path) {
std::error_code ec;
const auto writeTime = std::filesystem::last_write_time(path, ec);
if (ec) {
return 0;
}
return static_cast<XCEngine::Core::uint64>(writeTime.time_since_epoch().count());
}
bool WriteMinimalCurrentShaderArtifact(const std::filesystem::path& artifactPath) {
XCEngine::Resources::ShaderArtifactFileHeader fileHeader = {};
XCEngine::Resources::ShaderArtifactHeader shaderHeader = {};
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
payload.Resize(sizeof(fileHeader) + sizeof(shaderHeader));
std::memcpy(payload.Data(), &fileHeader, sizeof(fileHeader));
std::memcpy(
payload.Data() + sizeof(fileHeader),
&shaderHeader,
sizeof(shaderHeader));
XCEngine::Resources::ArtifactContainerEntry entry = {};
entry.name = "main";
entry.resourceType = XCEngine::Resources::ResourceType::Shader;
entry.localID = XCEngine::Resources::kMainAssetLocalID;
entry.payload = std::move(payload);
XCEngine::Resources::ArtifactContainerWriter writer;
writer.AddEntry(std::move(entry));
XCEngine::Containers::String error;
if (!writer.WriteToFile(artifactPath.generic_string().c_str(), &error)) {
std::cerr << "failed to write minimal shader artifact: "
<< error.CStr() << "\n";
return false;
}
return true;
}
bool WriteShaderArtifactDbRecord(
const std::filesystem::path& projectRoot,
const std::vector<std::string>& sourceFields,
const std::filesystem::path& dependencyPath,
XCEngine::Core::uint64 dependencyFileSize,
XCEngine::Core::uint64 dependencyWriteTime,
const std::string& artifactKey,
const std::string& mainArtifactPath) {
if (sourceFields.size() < 10) {
return false;
}
const std::filesystem::path artifactDbPath = projectRoot / "Library/artifacts.db";
std::filesystem::create_directories(artifactDbPath.parent_path());
std::ofstream output(artifactDbPath, std::ios::out | std::ios::trunc);
if (!output.is_open()) {
std::cerr << "failed to write artifact db: " << artifactDbPath << "\n";
return false;
}
output << "# schema=2\n";
output << "# artifactKey\tassetGuid\timporter\tversion\ttype\tartifactDir\tmainArtifact\tsourceHash\tmetaHash\tsize\twriteTime\tmainLocalID\tstorageKind\tmainEntryName\t(depPath\tdepHash\tdepSize\tdepWriteTime)...\n";
output << artifactKey << '\t'
<< sourceFields[0] << '\t'
<< "ShaderImporter" << '\t'
<< "8" << '\t'
<< static_cast<XCEngine::Core::uint32>(XCEngine::Resources::ResourceType::Shader) << '\t'
<< std::filesystem::path(mainArtifactPath).parent_path().generic_string() << '\t'
<< mainArtifactPath << '\t'
<< sourceFields[7] << '\t'
<< sourceFields[6] << '\t'
<< sourceFields[8] << '\t'
<< sourceFields[9] << '\t'
<< XCEngine::Resources::kMainAssetLocalID << '\t'
<< static_cast<XCEngine::Core::uint32>(
XCEngine::Resources::ArtifactStorageKind::SingleFileContainer) << '\t'
<< "main" << '\t'
<< dependencyPath.lexically_normal().generic_string() << '\t'
<< "dependency-hash-not-used-by-currentness-check" << '\t'
<< dependencyFileSize << '\t'
<< dependencyWriteTime << '\n';
return static_cast<bool>(output);
}
bool CollectDependencies(
const std::filesystem::path& shaderPath,
XCEngine::Containers::Array<XCEngine::Containers::String>& dependencies) {
XCEngine::Resources::ShaderLoader loader;
return loader.CollectSourceDependencies(shaderPath.generic_string().c_str(), dependencies);
}
bool CollectDependencies(
const std::filesystem::path& shaderPath,
XCEngine::Containers::Array<XCEngine::Containers::String>& dependencies,
XCEngine::Containers::String& error) {
XCEngine::Resources::ShaderLoader loader;
return loader.CollectSourceDependencies(shaderPath.generic_string().c_str(), dependencies, &error);
}
std::string NarrowWideString(const std::wstring& value) {
std::string result;
result.reserve(value.size());
for (wchar_t ch : value) {
result.push_back(static_cast<char>(ch));
}
return result;
}
bool RequireDependencySuffixes(
const XCEngine::Containers::Array<XCEngine::Containers::String>& dependencies,
const std::vector<const char*>& suffixes) {
for (const char* suffix : suffixes) {
if (ContainsPathSuffix(dependencies, suffix)) {
continue;
}
std::cerr << "missing dependency suffix: " << suffix << "\n";
for (const auto& dependency : dependencies) {
std::cerr << "dependency: " << dependency.CStr() << "\n";
}
return false;
}
return true;
}
bool RequireNoDependencyPrefix(
const XCEngine::Containers::Array<XCEngine::Containers::String>& dependencies,
const std::filesystem::path& prefixPath) {
const std::string prefix = prefixPath.lexically_normal().generic_string();
for (const auto& dependency : dependencies) {
const std::string path = std::filesystem::path(dependency.CStr())
.lexically_normal()
.generic_string();
if (path.rfind(prefix, 0) == 0) {
std::cerr << "dependency unexpectedly resolved under shadow package root: "
<< path << "\n";
return false;
}
}
return true;
}
bool RequireDependencySuffixUnderPrefix(
const XCEngine::Containers::Array<XCEngine::Containers::String>& dependencies,
const char* suffix,
const std::filesystem::path& prefixPath) {
const std::string prefix = prefixPath.lexically_normal().generic_string();
bool sawSuffix = false;
for (const auto& dependency : dependencies) {
const std::string path = std::filesystem::path(dependency.CStr())
.lexically_normal()
.generic_string();
if (!path.ends_with(suffix)) {
continue;
}
sawSuffix = true;
if (path.rfind(prefix, 0) == 0) {
return true;
}
std::cerr << "dependency suffix resolved outside expected prefix: "
<< path << " prefix=" << prefix << "\n";
}
if (!sawSuffix) {
std::cerr << "missing dependency suffix under prefix: " << suffix << "\n";
}
for (const auto& dependency : dependencies) {
std::cerr << "dependency: " << dependency.CStr() << "\n";
}
return false;
}
bool CompileD3D12ShaderVariant(
const std::filesystem::path& shaderPath,
const XCEngine::Resources::ShaderPass& pass,
const XCEngine::Resources::ShaderStageVariant& variant) {
XCEngine::RHI::ShaderCompileDesc compileDesc = {};
XCEngine::Rendering::Internal::ApplyShaderStageVariant(
shaderPath.generic_string().c_str(),
pass,
XCEngine::Resources::ShaderBackend::D3D12,
variant,
compileDesc);
std::vector<std::string> macroNames;
std::vector<std::string> macroDefinitions;
std::vector<D3D_SHADER_MACRO> macros;
macroNames.reserve(compileDesc.macros.size());
macroDefinitions.reserve(compileDesc.macros.size());
macros.reserve(compileDesc.macros.size() + 1u);
for (const XCEngine::RHI::ShaderCompileMacro& macro : compileDesc.macros) {
macroNames.push_back(NarrowWideString(macro.name));
macroDefinitions.push_back(NarrowWideString(macro.definition));
}
for (size_t macroIndex = 0; macroIndex < macroNames.size(); ++macroIndex) {
D3D_SHADER_MACRO d3dMacro = {};
d3dMacro.Name = macroNames[macroIndex].c_str();
d3dMacro.Definition =
macroDefinitions[macroIndex].empty() ? "1" : macroDefinitions[macroIndex].c_str();
macros.push_back(d3dMacro);
}
macros.push_back({ nullptr, nullptr });
const std::string entryPoint = NarrowWideString(compileDesc.entryPoint);
const std::string profile = NarrowWideString(compileDesc.profile);
XCEngine::RHI::D3D12Shader compiledShader;
if (!compiledShader.Compile(
compileDesc.source.data(),
compileDesc.source.size(),
compileDesc.fileName.empty() ? nullptr : compileDesc.fileName.c_str(),
macros.data(),
entryPoint.empty() ? nullptr : entryPoint.c_str(),
profile.empty() ? nullptr : profile.c_str())) {
std::cerr << "D3D12 compile failed for entry=" << entryPoint << " profile=" << profile << "\n";
return false;
}
if (compiledShader.GetBytecodeSize() == 0) {
std::cerr << "D3D12 compile produced empty bytecode for entry=" << entryPoint << "\n";
return false;
}
return true;
}
bool RequirePackageShaderLoadAndD3D12Compile(const std::filesystem::path& shaderPath) {
XCEngine::Resources::ShaderLoader loader;
XCEngine::Resources::LoadResult result =
loader.Load(shaderPath.generic_string().c_str());
if (!result || result.resource == nullptr) {
std::cerr << "failed to load package compile shader: "
<< result.errorMessage.CStr() << "\n";
return false;
}
std::unique_ptr<XCEngine::Resources::Shader> shader(
static_cast<XCEngine::Resources::Shader*>(result.resource));
const XCEngine::Resources::ShaderPass* pass = shader->FindPass("PackageCompile");
if (pass == nullptr) {
std::cerr << "loaded package compile shader is missing PackageCompile pass\n";
return false;
}
const XCEngine::Resources::ShaderStageVariant* vertexVariant =
shader->FindVariant(
"PackageCompile",
XCEngine::Resources::ShaderType::Vertex,
XCEngine::Resources::ShaderBackend::Generic);
const XCEngine::Resources::ShaderStageVariant* fragmentVariant =
shader->FindVariant(
"PackageCompile",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::Generic);
if (vertexVariant == nullptr || fragmentVariant == nullptr) {
std::cerr << "loaded package compile shader is missing graphics variants\n";
return false;
}
const std::string vertexSource = vertexVariant->sourceCode.CStr();
if (vertexSource.find("Packages/") != std::string::npos ||
vertexSource.find("TransformObjectToHClip") == std::string::npos ||
vertexSource.find("XC_UNIVERSAL_CORE_INCLUDED") == std::string::npos) {
std::cerr << "package include was not expanded into compile-ready source\n";
return false;
}
return CompileD3D12ShaderVariant(shaderPath, *pass, *vertexVariant) &&
CompileD3D12ShaderVariant(shaderPath, *pass, *fragmentVariant);
}
bool RequirePackageDependencyCurrentPathFallback(
const std::filesystem::path& projectRoot) {
std::error_code cleanupError;
std::filesystem::remove_all(projectRoot, cleanupError);
const char* includePath =
"Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/CurrentPathFallbackOnly_20260425.hlsl";
const char* resolvedSuffix =
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/CurrentPathFallbackOnly_20260425.hlsl";
const std::filesystem::path shaderPath =
projectRoot / "current_path_fallback.shader";
const std::filesystem::path dependencyPath =
projectRoot / resolvedSuffix;
const std::string shaderSource =
std::string("Shader \"Tests/CurrentPathFallback\"\n") +
"{\n"
" HLSLINCLUDE\n"
" #include \"" + includePath + "\"\n"
" ENDHLSL\n"
" SubShader\n"
" {\n"
" }\n"
"}\n";
if (!WriteTextFile(shaderPath, shaderSource.c_str()) ||
!WriteTextFile(dependencyPath, "float4 CurrentPathFallbackOnlyValue() { return 1.0f.xxxx; }\n")) {
std::cerr << "failed to prepare current-path package fallback fixtures\n";
return false;
}
XCEngine::Containers::Array<XCEngine::Containers::String> dependencies;
XCEngine::Containers::String dependencyError;
{
ScopedCurrentPath scopedCurrentPath(projectRoot);
if (!CollectDependencies(shaderPath, dependencies, dependencyError)) {
std::cerr << "current-path package fallback dependency collection failed: "
<< dependencyError.CStr() << "\n";
return false;
}
}
return RequireDependencySuffixUnderPrefix(
dependencies,
resolvedSuffix,
projectRoot);
}
bool RequireShaderArtifactLoads(
const std::filesystem::path& projectRoot,
const XCEngine::Containers::String& artifactMainPath,
const char* label) {
if (artifactMainPath.Empty()) {
std::cerr << label << " artifact path is empty\n";
return false;
}
const std::filesystem::path absoluteArtifactPath =
projectRoot / artifactMainPath.CStr();
if (!std::filesystem::exists(absoluteArtifactPath)) {
std::cerr << label << " artifact file does not exist: "
<< absoluteArtifactPath << "\n";
return false;
}
XCEngine::Resources::ShaderLoader loader;
XCEngine::Resources::LoadResult result =
loader.Load(absoluteArtifactPath.generic_string().c_str());
if (!result || result.resource == nullptr) {
std::cerr << label << " artifact failed to load: "
<< result.errorMessage.CStr() << "\n";
return false;
}
delete static_cast<XCEngine::Resources::Shader*>(result.resource);
return true;
}
bool RequireAssetDatabaseImportFailureDiagnostic(const std::filesystem::path& projectRoot) {
const std::filesystem::path shaderPath =
projectRoot / "Assets/MissingPackageInclude.shader";
const char* shaderSource = R"(Shader "Tests/AssetDatabaseMissingPackageInclude"
{
SubShader
{
Pass
{
Name "AssetDatabaseMissingPackageInclude"
HLSLPROGRAM
#pragma vertex MainVS
#pragma fragment MainPS
#include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingAssetDatabaseFile.hlsl"
float4 MainVS(float3 positionOS : POSITION) : SV_POSITION
{
return float4(positionOS, 1.0f);
}
float4 MainPS() : SV_TARGET
{
return 1.0f.xxxx;
}
ENDHLSL
}
}
}
)";
if (!WriteTextFile(shaderPath, shaderSource)) {
std::cerr << "failed to write AssetDatabase missing include shader fixture: "
<< shaderPath << "\n";
return false;
}
XCEngine::Resources::AssetDatabase assetDatabase;
assetDatabase.Initialize(projectRoot.generic_string().c_str());
XCEngine::Resources::AssetDatabase::ResolvedAsset resolvedAsset;
XCEngine::Resources::AssetDatabase::MaintenanceStats stats;
const bool imported = assetDatabase.ReimportAsset(
"Assets/MissingPackageInclude.shader",
resolvedAsset,
&stats);
const std::string errorText = assetDatabase.GetLastErrorMessage().CStr();
assetDatabase.Shutdown();
if (imported) {
std::cerr << "AssetDatabase unexpectedly imported shader with missing package include\n";
return false;
}
if (errorText.find(
"requested=Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingAssetDatabaseFile.hlsl") ==
std::string::npos ||
errorText.find(
"normalized=Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingAssetDatabaseFile.hlsl") ==
std::string::npos ||
errorText.find("source=") == std::string::npos ||
errorText.find("normalizedCandidate=") == std::string::npos ||
errorText.find("searchedAnchors=[sourceRoot=") == std::string::npos ||
errorText.find("includeChain=") == std::string::npos ||
errorText.find("builtinPackageRoot=engine/assets/builtin/shaders/Packages") ==
std::string::npos) {
std::cerr << "AssetDatabase missing package include diagnostic is not actionable: "
<< errorText << "\n";
return false;
}
return true;
}
bool RequirePackageEditProducesNewShaderArtifact(
const std::filesystem::path& projectRoot) {
std::error_code cleanupError;
std::filesystem::remove_all(projectRoot, cleanupError);
std::filesystem::create_directories(projectRoot / "Assets");
ScopedCurrentPath scopedCurrentPath(projectRoot);
const char* relativeShaderPath = "Assets/PackageArtifactEdit.shader";
const std::filesystem::path shaderPath = projectRoot / relativeShaderPath;
const std::filesystem::path dependencyPath =
projectRoot /
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/ArtifactEditTest.hlsl";
const char* shaderSource = R"(Shader "Tests/PackageArtifactEdit"
{
HLSLINCLUDE
#include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/ArtifactEditTest.hlsl"
ENDHLSL
SubShader
{
}
}
)";
if (!WriteTextFile(shaderPath, shaderSource) ||
!WriteTextFile(dependencyPath, "float4 PackageArtifactEditValue() { return 1.0f.xxxx; }\n")) {
std::cerr << "failed to prepare package edit artifact fixtures\n";
return false;
}
XCEngine::Resources::AssetDatabase assetDatabase;
assetDatabase.Initialize(projectRoot.generic_string().c_str());
XCEngine::Resources::AssetDatabase::ResolvedAsset firstArtifact;
if (!assetDatabase.EnsureArtifact(
relativeShaderPath,
XCEngine::Resources::ResourceType::Shader,
firstArtifact)) {
std::cerr << "initial package-edit shader import failed: "
<< assetDatabase.GetLastErrorMessage().CStr() << "\n";
assetDatabase.Shutdown();
return false;
}
if (!firstArtifact.imported ||
!RequireShaderArtifactLoads(projectRoot, firstArtifact.artifactMainPath, "initial package-edit shader")) {
std::cerr << "initial package-edit shader did not produce a valid new artifact\n";
assetDatabase.Shutdown();
return false;
}
const std::string firstArtifactPath = firstArtifact.artifactMainPath.CStr();
if (!WriteTextFile(
dependencyPath,
"float4 PackageArtifactEditValue() { return float4(0.25f, 0.5f, 0.75f, 1.0f); }\n"
"float4 PackageArtifactEditValueAfterEdit() { return 2.0f.xxxx; }\n")) {
std::cerr << "failed to edit package dependency fixture\n";
assetDatabase.Shutdown();
return false;
}
XCEngine::Resources::AssetDatabase::ResolvedAsset secondArtifact;
if (!assetDatabase.EnsureArtifact(
relativeShaderPath,
XCEngine::Resources::ResourceType::Shader,
secondArtifact)) {
std::cerr << "package-edit shader reimport failed after dependency edit: "
<< assetDatabase.GetLastErrorMessage().CStr() << "\n";
assetDatabase.Shutdown();
return false;
}
const std::string secondArtifactPath = secondArtifact.artifactMainPath.CStr();
if (!secondArtifact.imported ||
secondArtifactPath.empty() ||
secondArtifactPath == firstArtifactPath ||
!RequireShaderArtifactLoads(projectRoot, secondArtifact.artifactMainPath, "edited package shader")) {
std::cerr << "package edit did not produce a distinct valid shader artifact; first="
<< firstArtifactPath << " second=" << secondArtifactPath << "\n";
assetDatabase.Shutdown();
return false;
}
XCEngine::Resources::AssetDatabase::ResolvedAsset thirdArtifact;
if (!assetDatabase.EnsureArtifact(
relativeShaderPath,
XCEngine::Resources::ResourceType::Shader,
thirdArtifact)) {
std::cerr << "package-edit shader unchanged ensure failed: "
<< assetDatabase.GetLastErrorMessage().CStr() << "\n";
assetDatabase.Shutdown();
return false;
}
const std::string thirdArtifactPath = thirdArtifact.artifactMainPath.CStr();
if (thirdArtifact.imported || thirdArtifactPath != secondArtifactPath) {
std::cerr << "unchanged package dependency did not reuse current shader artifact; second="
<< secondArtifactPath << " third=" << thirdArtifactPath << "\n";
assetDatabase.Shutdown();
return false;
}
assetDatabase.Shutdown();
return true;
}
bool RequirePackageDependencyStaleRecordTriggersShaderReimport(
const std::filesystem::path& projectRoot) {
std::error_code cleanupError;
std::filesystem::remove_all(projectRoot, cleanupError);
const char* relativeShaderPath = "Assets/StalePackageDependency.shader";
const std::filesystem::path shaderPath = projectRoot / relativeShaderPath;
const std::filesystem::path dependencyPath =
projectRoot /
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/StaleArtifactDependency.hlsl";
const std::string artifactKey = "0123456789abcdef0123456789abcdef";
const std::string mainArtifactPath =
"Library/Artifacts/01/" + artifactKey + ".xcshader";
const char* shaderSource = R"(Shader "Tests/StalePackageDependency"
{
SubShader
{
Pass
{
Name "StalePackageDependency"
HLSLPROGRAM
#pragma vertex MainVS
#pragma fragment MainPS
#include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingAfterInvalidation.hlsl"
float4 MainVS(float3 positionOS : POSITION) : SV_POSITION
{
return float4(positionOS, 1.0f);
}
float4 MainPS() : SV_TARGET
{
return 1.0f.xxxx;
}
ENDHLSL
}
}
}
)";
if (!WriteTextFile(shaderPath, shaderSource) ||
!WriteTextFile(dependencyPath, "float4 StaleArtifactDependencyValue() { return 1.0f.xxxx; }\n")) {
std::cerr << "failed to prepare stale package dependency fixtures\n";
return false;
}
{
XCEngine::Resources::AssetDatabase assetDatabase;
assetDatabase.Initialize(projectRoot.generic_string().c_str());
assetDatabase.Refresh();
assetDatabase.Shutdown();
}
std::vector<std::string> sourceFields;
if (!LoadSourceDbShaderRecord(projectRoot, relativeShaderPath, sourceFields)) {
return false;
}
if (!WriteMinimalCurrentShaderArtifact(projectRoot / mainArtifactPath)) {
std::cerr << "failed to write stale package dependency artifact fixture\n";
return false;
}
const XCEngine::Core::uint64 currentDependencySize = GetFileSizeValue(dependencyPath);
const XCEngine::Core::uint64 currentDependencyWriteTime = GetFileWriteTimeValue(dependencyPath);
if (!WriteShaderArtifactDbRecord(
projectRoot,
sourceFields,
dependencyPath,
currentDependencySize,
currentDependencyWriteTime,
artifactKey,
mainArtifactPath)) {
return false;
}
{
XCEngine::Resources::AssetDatabase assetDatabase;
assetDatabase.Initialize(projectRoot.generic_string().c_str());
XCEngine::Resources::AssetDatabase::ResolvedAsset resolvedAsset;
if (!assetDatabase.EnsureArtifact(
relativeShaderPath,
XCEngine::Resources::ResourceType::Shader,
resolvedAsset)) {
std::cerr << "current package dependency record unexpectedly triggered import: "
<< assetDatabase.GetLastErrorMessage().CStr() << "\n";
assetDatabase.Shutdown();
return false;
}
if (resolvedAsset.imported ||
resolvedAsset.artifactMainPath.CStr()[0] == '\0') {
std::cerr << "current package dependency record did not use existing artifact\n";
assetDatabase.Shutdown();
return false;
}
assetDatabase.Shutdown();
}
const XCEngine::Core::uint64 staleWriteTime =
currentDependencyWriteTime == 1 ? 2 : 1;
if (!WriteShaderArtifactDbRecord(
projectRoot,
sourceFields,
dependencyPath,
currentDependencySize,
staleWriteTime,
artifactKey,
mainArtifactPath)) {
return false;
}
{
XCEngine::Resources::AssetDatabase assetDatabase;
assetDatabase.Initialize(projectRoot.generic_string().c_str());
XCEngine::Resources::AssetDatabase::ResolvedAsset resolvedAsset;
const bool ensured = assetDatabase.EnsureArtifact(
relativeShaderPath,
XCEngine::Resources::ResourceType::Shader,
resolvedAsset);
const std::string errorText = assetDatabase.GetLastErrorMessage().CStr();
assetDatabase.Shutdown();
if (ensured) {
std::cerr << "stale package dependency record did not trigger shader reimport\n";
return false;
}
if (errorText.find(
"Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingAfterInvalidation.hlsl") ==
std::string::npos) {
std::cerr << "stale package dependency reimport did not reach shader loader diagnostic: "
<< errorText << "\n";
return false;
}
}
return true;
}
bool RequireIncludeFailureDiagnostic(
const char* label,
const XCEngine::Containers::String& error,
const std::string& requestedPath) {
const std::string errorText = error.CStr();
if (errorText.find("requested=" + requestedPath) == std::string::npos ||
errorText.find("normalized=") == std::string::npos ||
errorText.find("source=") == std::string::npos ||
errorText.find("normalizedCandidate=") == std::string::npos ||
errorText.find("searchedAnchors=[sourceRoot=") == std::string::npos ||
errorText.find("resourceRoot=") == std::string::npos ||
errorText.find("currentPath=") == std::string::npos ||
errorText.find("includeChain=") == std::string::npos ||
errorText.find("builtinPackageRoot=engine/assets/builtin/shaders/Packages") == std::string::npos) {
std::cerr << label << " diagnostic is not actionable: " << errorText << "\n";
return false;
}
return true;
}
} // namespace
int main() {
const std::filesystem::path tempRoot =
std::filesystem::temp_directory_path() /
"xcengine_shader_dependency_tests";
const std::filesystem::path shaderPath = tempRoot / "package_include.shader";
const char* shaderSource = R"(Shader "Tests/PackageInclude"
{
SubShader
{
Pass
{
Name "PackageInclude"
HLSLPROGRAM
#pragma vertex MainVS
#pragma fragment MainPS
#include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl"
float4 MainVS(float3 positionOS : POSITION) : SV_POSITION
{
return TransformObjectToHClip(positionOS);
}
float4 MainPS() : SV_TARGET
{
return 1.0f.xxxx;
}
ENDHLSL
}
}
}
)";
if (!WriteTextFile(shaderPath, shaderSource)) {
std::cerr << "failed to write shader fixture: " << shaderPath << "\n";
return 1;
}
XCEngine::Containers::Array<XCEngine::Containers::String> dependencies;
if (!CollectDependencies(shaderPath, dependencies)) {
std::cerr << "CollectSourceDependencies failed\n";
return 1;
}
if (!RequireDependencySuffixes(
dependencies,
{
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl",
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl",
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl",
})) {
return 1;
}
const std::filesystem::path originalCurrentPath = std::filesystem::current_path();
const std::filesystem::path shadowPackageRoot =
tempRoot / "engine/assets/builtin/shaders/Packages";
const std::filesystem::path shadowCorePath =
shadowPackageRoot /
"com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl";
if (!WriteTextFile(shadowCorePath, "float4 ShadowRootSelected() { return 1.0f.xxxx; }\n")) {
std::cerr << "failed to write shadow package include fixture: " << shadowCorePath << "\n";
return 1;
}
std::filesystem::current_path(tempRoot);
dependencies.Clear();
if (!CollectDependencies(shaderPath, dependencies)) {
std::filesystem::current_path(originalCurrentPath);
std::cerr << "CollectSourceDependencies failed while current directory contained a shadow package root\n";
return 1;
}
std::filesystem::current_path(originalCurrentPath);
if (!RequireDependencySuffixes(
dependencies,
{
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl",
}) ||
!RequireNoDependencyPrefix(dependencies, shadowPackageRoot)) {
return 1;
}
const std::filesystem::path currentPathFallbackProjectRoot =
tempRoot / "current_path_fallback_project";
if (!RequirePackageDependencyCurrentPathFallback(currentPathFallbackProjectRoot)) {
return 1;
}
const std::filesystem::path packageCompileShaderPath =
tempRoot / "package_compile.shader";
const char* packageCompileShaderSource = R"(Shader "Tests/PackageCompile"
{
SubShader
{
Pass
{
Name "PackageCompile"
HLSLPROGRAM
#pragma vertex MainVS
#pragma fragment MainPS
#include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float3 positionOS : POSITION;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
};
Varyings MainVS(Attributes input)
{
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS);
return output;
}
float4 MainPS(Varyings input) : SV_TARGET
{
return input.positionCS;
}
ENDHLSL
}
}
}
)";
if (!WriteTextFile(packageCompileShaderPath, packageCompileShaderSource)) {
std::cerr << "failed to write package compile shader fixture: " << packageCompileShaderPath << "\n";
return 1;
}
if (!RequirePackageShaderLoadAndD3D12Compile(packageCompileShaderPath)) {
return 1;
}
const std::filesystem::path backslashPackageIncludeShaderPath =
tempRoot / "backslash_package_include.shader";
const char* backslashPackageIncludeShaderSource = R"(Shader "Tests/BackslashPackageInclude"
{
SubShader
{
Pass
{
Name "BackslashPackageInclude"
HLSLPROGRAM
#pragma vertex MainVS
#pragma fragment MainPS
#include "Packages\com.xcengine.render-pipelines.universal\ShaderLibrary\Core.hlsl"
float4 MainVS(float3 positionOS : POSITION) : SV_POSITION
{
return TransformObjectToHClip(positionOS);
}
float4 MainPS() : SV_TARGET
{
return 1.0f.xxxx;
}
ENDHLSL
}
}
}
)";
if (!WriteTextFile(backslashPackageIncludeShaderPath, backslashPackageIncludeShaderSource)) {
std::cerr << "failed to write backslash package include shader fixture: " << backslashPackageIncludeShaderPath << "\n";
return 1;
}
dependencies.Clear();
if (!CollectDependencies(backslashPackageIncludeShaderPath, dependencies)) {
std::cerr << "CollectSourceDependencies failed for backslash package include\n";
return 1;
}
if (!RequireDependencySuffixes(
dependencies,
{
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl",
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl",
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl",
})) {
return 1;
}
const std::filesystem::path allPublicIncludesShaderPath =
tempRoot / "all_public_package_includes.shader";
const char* allPublicIncludesShaderSource = R"(Shader "Tests/AllPublicPackageIncludes"
{
SubShader
{
Pass
{
Name "AllPublicPackageIncludes"
HLSLPROGRAM
#pragma vertex MainVS
#pragma fragment MainPS
#include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl"
#include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
#include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/TextureSampling.hlsl"
#include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Packing.hlsl"
#include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Input.hlsl"
#include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
#include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/ShaderPass.hlsl"
#include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/RealtimeLights.hlsl"
float4 MainVS(float3 positionOS : POSITION) : SV_POSITION
{
return TransformObjectToHClip(positionOS);
}
float4 MainPS() : SV_TARGET
{
return XcLinearToSrgb(1.0f.xxx).xyzz;
}
ENDHLSL
}
}
}
)";
if (!WriteTextFile(allPublicIncludesShaderPath, allPublicIncludesShaderSource)) {
std::cerr << "failed to write all public package include shader fixture: " << allPublicIncludesShaderPath << "\n";
return 1;
}
dependencies.Clear();
if (!CollectDependencies(allPublicIncludesShaderPath, dependencies)) {
std::cerr << "CollectSourceDependencies failed for all public package includes\n";
return 1;
}
if (!RequireDependencySuffixes(
dependencies,
{
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/ShaderVariables.hlsl",
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl",
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/TextureSampling.hlsl",
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Color.hlsl",
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.core/ShaderLibrary/Packing.hlsl",
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Core.hlsl",
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/Input.hlsl",
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl",
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/ShaderPass.hlsl",
"engine/assets/builtin/shaders/Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/RealtimeLights.hlsl",
})) {
return 1;
}
const std::filesystem::path missingIncludeShaderPath =
tempRoot / "missing_package_include.shader";
const char* missingIncludeShaderSource = R"(Shader "Tests/MissingPackageInclude"
{
SubShader
{
Pass
{
Name "MissingPackageInclude"
HLSLPROGRAM
#pragma vertex MainVS
#pragma fragment MainPS
#include "Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingFile.hlsl"
float4 MainVS(float3 positionOS : POSITION) : SV_POSITION
{
return float4(positionOS, 1.0f);
}
float4 MainPS() : SV_TARGET
{
return 1.0f.xxxx;
}
ENDHLSL
}
}
}
)";
if (!WriteTextFile(missingIncludeShaderPath, missingIncludeShaderSource)) {
std::cerr << "failed to write missing include shader fixture: " << missingIncludeShaderPath << "\n";
return 1;
}
dependencies.Clear();
XCEngine::Containers::String dependencyError;
if (CollectDependencies(missingIncludeShaderPath, dependencies, dependencyError)) {
std::cerr << "CollectSourceDependencies unexpectedly accepted a missing package include\n";
return 1;
}
if (!RequireIncludeFailureDiagnostic(
"missing package include",
dependencyError,
"Packages/com.xcengine.render-pipelines.universal/ShaderLibrary/MissingFile.hlsl")) {
return 1;
}
const std::filesystem::path escapedPackageShaderPath =
tempRoot / "escaped_package_include.shader";
const char* escapedPackageShaderSource = R"(Shader "Tests/EscapedPackageInclude"
{
SubShader
{
Pass
{
Name "EscapedPackageInclude"
HLSLPROGRAM
#pragma vertex MainVS
#pragma fragment MainPS
#include "Packages/../forward-lit.shader"
float4 MainVS(float3 positionOS : POSITION) : SV_POSITION
{
return float4(positionOS, 1.0f);
}
float4 MainPS() : SV_TARGET
{
return 1.0f.xxxx;
}
ENDHLSL
}
}
}
)";
if (!WriteTextFile(escapedPackageShaderPath, escapedPackageShaderSource)) {
std::cerr << "failed to write escaped package include shader fixture: " << escapedPackageShaderPath << "\n";
return 1;
}
dependencies.Clear();
dependencyError.Clear();
if (CollectDependencies(escapedPackageShaderPath, dependencies, dependencyError)) {
std::cerr << "CollectSourceDependencies unexpectedly accepted a package include outside Packages root\n";
return 1;
}
if (!RequireIncludeFailureDiagnostic(
"escaped package include",
dependencyError,
"Packages/../forward-lit.shader")) {
return 1;
}
const std::filesystem::path absoluteIncludeTargetPath =
tempRoot / "absolute_include_target.hlsl";
if (!WriteTextFile(absoluteIncludeTargetPath, "float4 AbsoluteIncludeValue() { return 1.0f.xxxx; }\n")) {
std::cerr << "failed to write absolute include target fixture: " << absoluteIncludeTargetPath << "\n";
return 1;
}
const std::filesystem::path absoluteIncludeShaderPath =
tempRoot / "absolute_include.shader";
const std::string absoluteIncludePath = absoluteIncludeTargetPath.generic_string();
const std::string absoluteIncludeShaderSource =
"Shader \"Tests/AbsoluteInclude\"\n"
"{\n"
" SubShader\n"
" {\n"
" Pass\n"
" {\n"
" Name \"AbsoluteInclude\"\n"
" HLSLPROGRAM\n"
" #pragma vertex MainVS\n"
" #pragma fragment MainPS\n"
" #include \"" + absoluteIncludePath + "\"\n" +
" float4 MainVS(float3 positionOS : POSITION) : SV_POSITION\n"
" {\n"
" return float4(positionOS, 1.0f);\n"
" }\n"
" float4 MainPS() : SV_TARGET\n"
" {\n"
" return AbsoluteIncludeValue();\n"
" }\n"
" ENDHLSL\n"
" }\n"
" }\n"
"}\n";
if (!WriteTextFile(absoluteIncludeShaderPath, absoluteIncludeShaderSource.c_str())) {
std::cerr << "failed to write absolute include shader fixture: " << absoluteIncludeShaderPath << "\n";
return 1;
}
dependencies.Clear();
dependencyError.Clear();
if (CollectDependencies(absoluteIncludeShaderPath, dependencies, dependencyError)) {
std::cerr << "CollectSourceDependencies unexpectedly accepted an absolute include\n";
return 1;
}
if (!RequireIncludeFailureDiagnostic(
"absolute include",
dependencyError,
absoluteIncludePath)) {
return 1;
}
const std::filesystem::path assetDatabaseProjectRoot =
tempRoot / "asset_database_project";
std::error_code cleanupError;
std::filesystem::remove_all(assetDatabaseProjectRoot, cleanupError);
if (!RequireAssetDatabaseImportFailureDiagnostic(assetDatabaseProjectRoot)) {
return 1;
}
const std::filesystem::path artifactInvalidationProjectRoot =
tempRoot / "artifact_invalidation_project";
if (!RequirePackageDependencyStaleRecordTriggersShaderReimport(
artifactInvalidationProjectRoot)) {
return 1;
}
const std::filesystem::path packageEditProjectRoot =
tempRoot / "package_edit_artifact_project";
if (!RequirePackageEditProducesNewShaderArtifact(packageEditProjectRoot)) {
return 1;
}
return 0;
}