rendering: formalize unity-style shader pass contracts

This commit is contained in:
2026-04-07 00:34:28 +08:00
parent 7216ad9138
commit 87533e08f6
13 changed files with 1133 additions and 164 deletions

View File

@@ -24,6 +24,20 @@ void WriteTextFile(const std::filesystem::path& path, const std::string& content
ASSERT_TRUE(static_cast<bool>(output));
}
const ShaderPassTagEntry* FindPassTag(const ShaderPass* pass, const char* name) {
if (pass == nullptr || name == nullptr) {
return nullptr;
}
for (const ShaderPassTagEntry& tag : pass->tags) {
if (tag.name == name) {
return &tag;
}
}
return nullptr;
}
TEST(ShaderLoader, GetResourceType) {
ShaderLoader loader;
EXPECT_EQ(loader.GetResourceType(), ResourceType::Shader);
@@ -640,11 +654,13 @@ TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringBuildsGenericHlslVar
const ShaderPass* pass = shader->FindPass("ForwardLit");
ASSERT_NE(pass, nullptr);
ASSERT_EQ(pass->tags.Size(), 2u);
ASSERT_EQ(pass->tags.Size(), 3u);
EXPECT_EQ(pass->tags[0].name, "Queue");
EXPECT_EQ(pass->tags[0].value, "Geometry");
EXPECT_EQ(pass->tags[1].name, "LightMode");
EXPECT_EQ(pass->tags[1].value, "ForwardLit");
EXPECT_EQ(pass->tags[1].name, "LOD");
EXPECT_EQ(pass->tags[1].value, "200");
EXPECT_EQ(pass->tags[2].name, "LightMode");
EXPECT_EQ(pass->tags[2].value, "ForwardLit");
EXPECT_TRUE(pass->resources.Empty());
ASSERT_EQ(pass->keywordDeclarations.Size(), 2u);
EXPECT_EQ(pass->keywordDeclarations[0].type, ShaderKeywordDeclarationType::MultiCompile);
@@ -708,6 +724,134 @@ TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringBuildsGenericHlslVar
fs::remove_all(shaderRoot);
}
TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringParsesPassStateAndFallback) {
namespace fs = std::filesystem;
const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_single_source_pass_state";
const fs::path shaderPath = shaderRoot / "single_source_state.shader";
fs::remove_all(shaderRoot);
fs::create_directories(shaderRoot);
WriteTextFile(
shaderPath,
R"(Shader "SingleSourceStateful"
{
Fallback "Legacy/Diffuse"
SubShader
{
Pass
{
Name "ForwardLit"
Cull Front
ZWrite Off
ZTest LEqual
Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha
ColorMask RGB
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
float4 Vert() : SV_POSITION { return 0; }
float4 Frag() : SV_TARGET { return 1; }
ENDHLSL
}
}
}
)");
ShaderLoader loader;
LoadResult result = loader.Load(shaderPath.string().c_str());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
auto* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
EXPECT_EQ(shader->GetFallback(), "Legacy/Diffuse");
const ShaderPass* pass = shader->FindPass("ForwardLit");
ASSERT_NE(pass, nullptr);
EXPECT_TRUE(pass->hasFixedFunctionState);
EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Front);
EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable);
EXPECT_TRUE(pass->fixedFunctionState.depthTestEnable);
EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual);
EXPECT_TRUE(pass->fixedFunctionState.blendEnable);
EXPECT_EQ(pass->fixedFunctionState.srcBlend, MaterialBlendFactor::SrcAlpha);
EXPECT_EQ(pass->fixedFunctionState.dstBlend, MaterialBlendFactor::InvSrcAlpha);
EXPECT_EQ(pass->fixedFunctionState.srcBlendAlpha, MaterialBlendFactor::One);
EXPECT_EQ(pass->fixedFunctionState.dstBlendAlpha, MaterialBlendFactor::InvSrcAlpha);
EXPECT_EQ(pass->fixedFunctionState.colorWriteMask, 0x7u);
delete shader;
fs::remove_all(shaderRoot);
}
TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactFromSingleSourceAuthoringPreservesPassStateAndFallback) {
namespace fs = std::filesystem;
const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_single_source_artifact_pass_state";
const fs::path shaderDir = projectRoot / "Assets" / "Shaders";
const fs::path shaderPath = shaderDir / "single_source_state.shader";
fs::remove_all(projectRoot);
fs::create_directories(shaderDir);
WriteTextFile(
shaderPath,
R"(Shader "ArtifactSingleSourceStateful"
{
Fallback "Legacy/Cutout"
SubShader
{
Pass
{
Name "ForwardLit"
Cull Off
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
ColorMask RGBA
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
float4 Vert() : SV_POSITION { return 0; }
float4 Frag() : SV_TARGET { return 1; }
ENDHLSL
}
}
}
)");
AssetDatabase database;
database.Initialize(projectRoot.string().c_str());
AssetDatabase::ResolvedAsset resolvedAsset;
ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/single_source_state.shader", ResourceType::Shader, resolvedAsset));
ASSERT_TRUE(resolvedAsset.artifactReady);
ShaderLoader loader;
LoadResult result = loader.Load(resolvedAsset.artifactMainPath.CStr());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
auto* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
EXPECT_EQ(shader->GetFallback(), "Legacy/Cutout");
const ShaderPass* pass = shader->FindPass("ForwardLit");
ASSERT_NE(pass, nullptr);
EXPECT_TRUE(pass->hasFixedFunctionState);
EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::None);
EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable);
EXPECT_TRUE(pass->fixedFunctionState.blendEnable);
EXPECT_EQ(pass->fixedFunctionState.srcBlend, MaterialBlendFactor::SrcAlpha);
EXPECT_EQ(pass->fixedFunctionState.dstBlend, MaterialBlendFactor::InvSrcAlpha);
EXPECT_EQ(pass->fixedFunctionState.colorWriteMask, 0xFu);
delete shader;
database.Shutdown();
fs::remove_all(projectRoot);
}
TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringParsesMultiCompileLocalKeywords) {
namespace fs = std::filesystem;
@@ -770,6 +914,82 @@ TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringParsesMultiCompileLo
fs::remove_all(shaderRoot);
}
TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringParsesFallbackAndFixedFunctionStateInheritance) {
namespace fs = std::filesystem;
const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_single_source_fixed_state";
const fs::path shaderPath = shaderRoot / "single_source_fixed_state.shader";
fs::remove_all(shaderRoot);
fs::create_directories(shaderRoot);
WriteTextFile(
shaderPath,
R"(Shader "SingleSourceFixedState"
{
Fallback "Legacy Shaders/Diffuse"
SubShader
{
Tags { "Queue" = "Transparent" "RenderType" = "Transparent" }
LOD 310
Cull Front
ZWrite Off
Pass
{
Name "ForwardLit"
Tags { "LightMode" = "ForwardLit" }
Blend SrcAlpha OneMinusSrcAlpha
ColorMask RGB
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
float4 Vert() : SV_POSITION { return 0; }
float4 Frag() : SV_TARGET { return 1; }
ENDHLSL
}
}
}
)");
ShaderLoader loader;
LoadResult result = loader.Load(shaderPath.string().c_str());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
auto* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
EXPECT_EQ(shader->GetFallback(), "Legacy Shaders/Diffuse");
const ShaderPass* pass = shader->FindPass("ForwardLit");
ASSERT_NE(pass, nullptr);
EXPECT_TRUE(pass->hasFixedFunctionState);
EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Front);
EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable);
EXPECT_TRUE(pass->fixedFunctionState.depthTestEnable);
EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual);
EXPECT_TRUE(pass->fixedFunctionState.blendEnable);
EXPECT_EQ(pass->fixedFunctionState.srcBlend, MaterialBlendFactor::SrcAlpha);
EXPECT_EQ(pass->fixedFunctionState.dstBlend, MaterialBlendFactor::InvSrcAlpha);
EXPECT_EQ(pass->fixedFunctionState.srcBlendAlpha, MaterialBlendFactor::SrcAlpha);
EXPECT_EQ(pass->fixedFunctionState.dstBlendAlpha, MaterialBlendFactor::InvSrcAlpha);
EXPECT_EQ(pass->fixedFunctionState.colorWriteMask, 0x7);
const ShaderPassTagEntry* queueTag = FindPassTag(pass, "Queue");
ASSERT_NE(queueTag, nullptr);
EXPECT_EQ(queueTag->value, "Transparent");
const ShaderPassTagEntry* lodTag = FindPassTag(pass, "LOD");
ASSERT_NE(lodTag, nullptr);
EXPECT_EQ(lodTag->value, "310");
const ShaderPassTagEntry* lightModeTag = FindPassTag(pass, "LightMode");
ASSERT_NE(lightModeTag, nullptr);
EXPECT_EQ(lightModeTag->value, "ForwardLit");
delete shader;
fs::remove_all(shaderRoot);
}
TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringRejectsBackendPragma) {
namespace fs = std::filesystem;