Files
XCEngine/tests/Rendering/unit/test_builtin_forward_pipeline.cpp

1663 lines
63 KiB
C++
Raw Normal View History

#include <gtest/gtest.h>
#include "Rendering/Detail/ShaderVariantUtils.h"
2026-04-05 22:02:52 +08:00
#include <XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h>
#include <XCEngine/Rendering/Passes/BuiltinDepthOnlyPass.h>
#include <XCEngine/Rendering/Passes/BuiltinObjectIdPass.h>
#include <XCEngine/Rendering/Passes/BuiltinShadowCasterPass.h>
#include <XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <XCEngine/Resources/Mesh/Mesh.h>
#include <XCEngine/Resources/Shader/Shader.h>
#include <XCEngine/Resources/Shader/ShaderLoader.h>
#include <XCEngine/RHI/RHIEnums.h>
#include <XCEngine/RHI/ShaderCompiler/SpirvShaderCompiler.h>
#include <fstream>
#include <regex>
using namespace XCEngine::Rendering::Pipelines;
using namespace XCEngine::Rendering::Passes;
using namespace XCEngine::Rendering;
using namespace XCEngine::Containers;
using namespace XCEngine::Resources;
using namespace XCEngine::RHI;
namespace {
std::string EscapeRegexLiteralForTest(const std::string& value) {
std::string escaped;
escaped.reserve(value.size() * 2u);
for (const char ch : value) {
switch (ch) {
case '\\':
case '^':
case '$':
case '.':
case '|':
case '?':
case '*':
case '+':
case '(':
case ')':
case '[':
case ']':
case '{':
case '}':
escaped.push_back('\\');
break;
default:
break;
}
escaped.push_back(ch);
}
return escaped;
}
::testing::AssertionResult SourceContainsRegisterBinding(
const std::string& source,
const std::string& declaration,
const std::string& registerClause) {
const std::regex pattern(
EscapeRegexLiteralForTest(declaration) + "\\s*:\\s*" +
EscapeRegexLiteralForTest(registerClause),
std::regex::ECMAScript);
if (std::regex_search(source, pattern)) {
return ::testing::AssertionSuccess();
}
return ::testing::AssertionFailure()
<< "Missing binding '" << declaration << " : " << registerClause << "' in source:\n"
<< source;
}
void AppendDefaultBuiltinPassResources(ShaderPass& pass) {
Array<ShaderResourceBindingDesc> bindings;
const bool resolved = TryBuildBuiltinPassDefaultResourceBindings(pass, bindings);
EXPECT_TRUE(resolved);
if (!resolved) {
return;
}
for (const ShaderResourceBindingDesc& binding : bindings) {
pass.resources.PushBack(binding);
}
}
} // namespace
TEST(BuiltinForwardPipeline_Test, UsesFloat3PositionInputLayoutForStaticMeshVertices) {
const InputLayoutDesc inputLayout = BuiltinForwardPipeline::BuildInputLayout();
ASSERT_EQ(inputLayout.elements.size(), 3u);
const InputElementDesc& position = inputLayout.elements[0];
EXPECT_EQ(position.semanticName, "POSITION");
EXPECT_EQ(position.semanticIndex, 0u);
EXPECT_EQ(position.format, static_cast<uint32_t>(Format::R32G32B32_Float));
EXPECT_EQ(position.inputSlot, 0u);
EXPECT_EQ(position.alignedByteOffset, 0u);
const InputElementDesc& normal = inputLayout.elements[1];
EXPECT_EQ(normal.semanticName, "NORMAL");
EXPECT_EQ(normal.semanticIndex, 0u);
EXPECT_EQ(normal.format, static_cast<uint32_t>(Format::R32G32B32_Float));
EXPECT_EQ(normal.inputSlot, 0u);
EXPECT_EQ(normal.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, normal)));
const InputElementDesc& texcoord = inputLayout.elements[2];
EXPECT_EQ(texcoord.semanticName, "TEXCOORD");
EXPECT_EQ(texcoord.semanticIndex, 0u);
EXPECT_EQ(texcoord.format, static_cast<uint32_t>(Format::R32G32_Float));
EXPECT_EQ(texcoord.inputSlot, 0u);
EXPECT_EQ(texcoord.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, uv0)));
}
TEST(BuiltinForwardPipeline_Test, SplitsSceneItemsIntoOpaqueAndTransparentQueueRanges) {
EXPECT_FALSE(IsTransparentRenderQueue(static_cast<int>(MaterialRenderQueue::Background)));
EXPECT_FALSE(IsTransparentRenderQueue(static_cast<int>(MaterialRenderQueue::Geometry)));
EXPECT_FALSE(IsTransparentRenderQueue(static_cast<int>(MaterialRenderQueue::AlphaTest)));
EXPECT_TRUE(IsTransparentRenderQueue(static_cast<int>(MaterialRenderQueue::Transparent)));
EXPECT_TRUE(IsTransparentRenderQueue(static_cast<int>(MaterialRenderQueue::Overlay)));
}
TEST(BuiltinForwardPipeline_Test, BuiltinForwardShaderUsesAuthoringSurfaceContract) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinForwardLitShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPropertyDesc* cutoff = shader->FindProperty("_Cutoff");
ASSERT_NE(cutoff, nullptr);
EXPECT_EQ(cutoff->type, ShaderPropertyType::Range);
EXPECT_EQ(cutoff->semantic, "AlphaCutoff");
const ShaderPass* pass = shader->FindPass("ForwardLit");
ASSERT_NE(pass, nullptr);
EXPECT_EQ(pass->resources.Size(), 8u);
EXPECT_TRUE(pass->hasFixedFunctionState);
EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Back);
EXPECT_TRUE(pass->fixedFunctionState.depthWriteEnable);
EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual);
const ShaderStageVariant* fragmentVariant = shader->FindVariant(
"ForwardLit",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::D3D12);
ASSERT_NE(fragmentVariant, nullptr);
EXPECT_EQ(
std::string(fragmentVariant->sourceCode.CStr()).find("register("),
std::string::npos);
delete shader;
}
TEST(BuiltinForwardPipeline_Test, BuiltinUnlitShaderUsesAuthoringSurfaceContract) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinUnlitShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("Unlit");
ASSERT_NE(pass, nullptr);
EXPECT_EQ(pass->resources.Size(), 4u);
EXPECT_TRUE(pass->hasFixedFunctionState);
EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Back);
EXPECT_TRUE(pass->fixedFunctionState.depthWriteEnable);
EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual);
const ShaderStageVariant* fragmentVariant = shader->FindVariant(
"Unlit",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::D3D12);
ASSERT_NE(fragmentVariant, nullptr);
EXPECT_EQ(
std::string(fragmentVariant->sourceCode.CStr()).find("register("),
std::string::npos);
delete shader;
}
2026-04-04 23:01:34 +08:00
TEST(BuiltinForwardPipeline_Test, BuiltinUnlitShaderBuildsVulkanRuntimeSourceWithResolvedBindings) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinUnlitShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
2026-04-04 23:01:34 +08:00
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("Unlit");
ASSERT_NE(pass, nullptr);
2026-04-04 23:01:34 +08:00
const ShaderStageVariant* fragmentVariant =
shader->FindVariant(
"Unlit",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::Vulkan);
ASSERT_NE(fragmentVariant, nullptr);
const std::string runtimeSource =
::XCEngine::Rendering::Detail::BuildRuntimeShaderSource(
*pass,
XCEngine::Resources::ShaderBackend::Vulkan,
*fragmentVariant);
EXPECT_TRUE(SourceContainsRegisterBinding(
runtimeSource,
"cbuffer MaterialConstants",
"register(b0, space1)"));
EXPECT_NE(runtimeSource.find("BaseColorTexture"), std::string::npos);
EXPECT_NE(runtimeSource.find("space2"), std::string::npos);
EXPECT_NE(runtimeSource.find("LinearClampSampler"), std::string::npos);
EXPECT_NE(runtimeSource.find("space3"), std::string::npos);
2026-04-05 15:44:37 +08:00
delete shader;
}
TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringUnlitBindingsToDescriptorSpaces) {
2026-04-03 17:18:46 +08:00
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinUnlitShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("Unlit");
ASSERT_NE(pass, nullptr);
const ShaderStageVariant* d3d12Fragment = shader->FindVariant(
"Unlit",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::D3D12);
ASSERT_NE(d3d12Fragment, nullptr);
ShaderCompileDesc d3d12CompileDesc = {};
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
*pass,
XCEngine::Resources::ShaderBackend::D3D12,
*d3d12Fragment,
d3d12CompileDesc);
const std::string d3d12Source(
reinterpret_cast<const char*>(d3d12CompileDesc.source.data()),
d3d12CompileDesc.source.size());
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"cbuffer MaterialConstants",
"register(b1)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"Texture2D BaseColorTexture",
"register(t0)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"SamplerState LinearClampSampler",
"register(s0)"));
const ShaderStageVariant* vulkanFragment = shader->FindVariant(
"Unlit",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::Vulkan);
ASSERT_NE(vulkanFragment, nullptr);
ShaderCompileDesc vulkanCompileDesc = {};
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
*pass,
XCEngine::Resources::ShaderBackend::Vulkan,
*vulkanFragment,
vulkanCompileDesc);
const std::string vulkanSource(
reinterpret_cast<const char*>(vulkanCompileDesc.source.data()),
vulkanCompileDesc.source.size());
EXPECT_TRUE(SourceContainsRegisterBinding(
vulkanSource,
"cbuffer MaterialConstants",
"register(b0, space1)"));
EXPECT_NE(vulkanSource.find("BaseColorTexture"), std::string::npos);
EXPECT_NE(vulkanSource.find("space2"), std::string::npos);
EXPECT_NE(vulkanSource.find("LinearClampSampler"), std::string::npos);
EXPECT_NE(vulkanSource.find("space3"), std::string::npos);
2026-04-03 17:18:46 +08:00
delete shader;
}
2026-04-03 17:18:46 +08:00
TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringForwardBindingsToDescriptorSpaces) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinForwardLitShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
2026-04-03 17:18:46 +08:00
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ForwardLit");
ASSERT_NE(pass, nullptr);
ShaderKeywordSet shadowKeywords = {};
shadowKeywords.enabledKeywords.PushBack("XC_MAIN_LIGHT_SHADOWS");
const ShaderStageVariant* d3d12Fragment = shader->FindVariant(
"ForwardLit",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::D3D12,
shadowKeywords);
ASSERT_NE(d3d12Fragment, nullptr);
ShaderCompileDesc d3d12CompileDesc = {};
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
*pass,
XCEngine::Resources::ShaderBackend::D3D12,
*d3d12Fragment,
d3d12CompileDesc);
const std::string d3d12Source(
reinterpret_cast<const char*>(d3d12CompileDesc.source.data()),
d3d12CompileDesc.source.size());
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"cbuffer PerObjectConstants",
"register(b0)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"cbuffer LightingConstants",
"register(b1)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"cbuffer MaterialConstants",
"register(b2)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"cbuffer ShadowReceiverConstants",
"register(b3)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"Texture2D BaseColorTexture",
"register(t0)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"SamplerState LinearClampSampler",
"register(s0)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"Texture2D ShadowMapTexture",
"register(t1)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"SamplerState ShadowMapSampler",
"register(s1)"));
EXPECT_EQ(d3d12Source.find("space0"), std::string::npos);
EXPECT_EQ(d3d12Source.find("space1"), std::string::npos);
const ShaderStageVariant* vulkanFragment = shader->FindVariant(
"ForwardLit",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::Vulkan,
shadowKeywords);
ASSERT_NE(vulkanFragment, nullptr);
ShaderCompileDesc vulkanCompileDesc = {};
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
*pass,
XCEngine::Resources::ShaderBackend::Vulkan,
*vulkanFragment,
vulkanCompileDesc);
const std::string vulkanSource(
reinterpret_cast<const char*>(vulkanCompileDesc.source.data()),
vulkanCompileDesc.source.size());
EXPECT_TRUE(SourceContainsRegisterBinding(
vulkanSource,
"cbuffer PerObjectConstants",
"register(b0, space0)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
vulkanSource,
"cbuffer LightingConstants",
"register(b0, space1)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
vulkanSource,
"cbuffer MaterialConstants",
"register(b0, space2)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
vulkanSource,
"cbuffer ShadowReceiverConstants",
"register(b0, space3)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
vulkanSource,
"Texture2D BaseColorTexture",
"register(t0, space4)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
vulkanSource,
"SamplerState LinearClampSampler",
"register(s0, space5)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
vulkanSource,
"Texture2D ShadowMapTexture",
"register(t0, space6)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
vulkanSource,
"SamplerState ShadowMapSampler",
"register(s0, space7)"));
delete shader;
}
TEST(BuiltinForwardPipeline_Test, OpenGLRuntimeTranspilesForwardShadowVariantToLegacyClipConventions) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinForwardLitShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ForwardLit");
ASSERT_NE(pass, nullptr);
ShaderKeywordSet shadowKeywords = {};
shadowKeywords.enabledKeywords.PushBack("XC_MAIN_LIGHT_SHADOWS");
const ShaderStageVariant* openGLFragment = shader->FindVariant(
"ForwardLit",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::OpenGL,
shadowKeywords);
ASSERT_NE(openGLFragment, nullptr);
ShaderCompileDesc compileDesc = {};
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
*pass,
XCEngine::Resources::ShaderBackend::OpenGL,
*openGLFragment,
compileDesc);
const std::string runtimeSource(
reinterpret_cast<const char*>(compileDesc.source.data()),
compileDesc.source.size());
EXPECT_NE(runtimeSource.find("const float shadowUvY = shadowNdc.y * 0.5f + 0.5f;"), std::string::npos);
EXPECT_NE(
runtimeSource.find("shadowNdc.z < -1.0f || shadowNdc.z > 1.0f"),
std::string::npos);
EXPECT_NE(
runtimeSource.find(
"const float receiverDepth = shadowNdc.z * 0.5f + 0.5f - gShadowBiasAndTexelSize.x;"),
std::string::npos);
XCEngine::RHI::CompiledSpirvShader spirvShader = {};
std::string errorMessage;
ASSERT_TRUE(
XCEngine::RHI::CompileSpirvShader(
compileDesc,
XCEngine::RHI::SpirvTargetEnvironment::Vulkan,
spirvShader,
&errorMessage))
<< errorMessage;
std::string glslSource;
ASSERT_TRUE(XCEngine::RHI::TranspileSpirvToOpenGLGLSL(spirvShader, glslSource, &errorMessage))
<< errorMessage;
EXPECT_NE(
glslSource.find("uniform sampler2D SPIRV_Cross_CombinedBaseColorTextureLinearClampSampler;"),
std::string::npos);
EXPECT_NE(
glslSource.find("uniform sampler2D SPIRV_Cross_CombinedShadowMapTextureShadowMapSampler;"),
std::string::npos);
EXPECT_NE(
glslSource.find("spvWorkaroundRowMajor(ShadowReceiverConstants.gWorldToShadowMatrix)"),
std::string::npos);
EXPECT_NE(glslSource.find("texture(SPIRV_Cross_CombinedShadowMapTextureShadowMapSampler"), std::string::npos);
delete shader;
}
TEST(BuiltinDepthStylePass_Test, OpenGLRuntimeTranspilesDepthOnlyAlphaVariantWithTexcoordAtLocation2) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinDepthOnlyShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("DepthOnly");
ASSERT_NE(pass, nullptr);
ShaderKeywordSet alphaKeywords = {};
alphaKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST");
const ShaderStageVariant* openGLVertex = shader->FindVariant(
"DepthOnly",
XCEngine::Resources::ShaderType::Vertex,
ShaderBackend::OpenGL,
alphaKeywords);
ASSERT_NE(openGLVertex, nullptr);
const ShaderStageVariant* openGLFragment = shader->FindVariant(
"DepthOnly",
XCEngine::Resources::ShaderType::Fragment,
ShaderBackend::OpenGL,
alphaKeywords);
ASSERT_NE(openGLFragment, nullptr);
auto transpileStage = [](const ShaderPass& targetPass,
const ShaderStageVariant& variant,
std::string& glslSource) {
ShaderCompileDesc compileDesc = {};
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
targetPass,
XCEngine::Resources::ShaderBackend::OpenGL,
variant,
compileDesc);
XCEngine::RHI::CompiledSpirvShader spirvShader = {};
std::string errorMessage;
EXPECT_TRUE(
XCEngine::RHI::CompileSpirvShader(
compileDesc,
XCEngine::RHI::SpirvTargetEnvironment::Vulkan,
spirvShader,
&errorMessage))
<< errorMessage;
EXPECT_TRUE(XCEngine::RHI::TranspileSpirvToOpenGLGLSL(spirvShader, glslSource, &errorMessage))
<< errorMessage;
};
std::string vertexGlsl;
transpileStage(*pass, *openGLVertex, vertexGlsl);
EXPECT_NE(vertexGlsl.find("layout(location = 2) in vec2"), std::string::npos);
std::string fragmentGlsl;
transpileStage(*pass, *openGLFragment, fragmentGlsl);
EXPECT_NE(fragmentGlsl.find("discard;"), std::string::npos);
EXPECT_NE(fragmentGlsl.find("gl_FragDepth"), std::string::npos);
delete shader;
}
TEST(BuiltinDepthStylePass_Test, OpenGLRuntimeTranspilesShadowCasterAlphaVariantWithTexcoordAtLocation2) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinShadowCasterShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ShadowCaster");
ASSERT_NE(pass, nullptr);
ShaderKeywordSet alphaKeywords = {};
alphaKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST");
const ShaderStageVariant* openGLVertex = shader->FindVariant(
"ShadowCaster",
XCEngine::Resources::ShaderType::Vertex,
ShaderBackend::OpenGL,
alphaKeywords);
ASSERT_NE(openGLVertex, nullptr);
const ShaderStageVariant* openGLFragment = shader->FindVariant(
"ShadowCaster",
XCEngine::Resources::ShaderType::Fragment,
ShaderBackend::OpenGL,
alphaKeywords);
ASSERT_NE(openGLFragment, nullptr);
auto transpileStage = [](const ShaderPass& targetPass,
const ShaderStageVariant& variant,
std::string& glslSource) {
ShaderCompileDesc compileDesc = {};
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
targetPass,
XCEngine::Resources::ShaderBackend::OpenGL,
variant,
compileDesc);
XCEngine::RHI::CompiledSpirvShader spirvShader = {};
std::string errorMessage;
EXPECT_TRUE(
XCEngine::RHI::CompileSpirvShader(
compileDesc,
XCEngine::RHI::SpirvTargetEnvironment::Vulkan,
spirvShader,
&errorMessage))
<< errorMessage;
EXPECT_TRUE(XCEngine::RHI::TranspileSpirvToOpenGLGLSL(spirvShader, glslSource, &errorMessage))
<< errorMessage;
};
std::string vertexGlsl;
transpileStage(*pass, *openGLVertex, vertexGlsl);
EXPECT_NE(vertexGlsl.find("layout(location = 2) in vec2"), std::string::npos);
std::string fragmentGlsl;
transpileStage(*pass, *openGLFragment, fragmentGlsl);
EXPECT_NE(fragmentGlsl.find("discard;"), std::string::npos);
EXPECT_NE(fragmentGlsl.find("gl_FragDepth"), std::string::npos);
2026-04-03 17:18:46 +08:00
delete shader;
}
TEST(BuiltinForwardPipeline_Test, BuiltinSkyboxShaderUsesAuthoringContract) {
2026-04-05 23:44:32 +08:00
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinSkyboxShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("Skybox");
ASSERT_NE(pass, nullptr);
EXPECT_EQ(pass->resources.Size(), 5u);
2026-04-07 04:30:26 +08:00
EXPECT_TRUE(pass->hasFixedFunctionState);
EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::None);
EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable);
EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual);
const ShaderPropertyDesc* tint = shader->FindProperty("_Tint");
ASSERT_NE(tint, nullptr);
EXPECT_EQ(tint->type, ShaderPropertyType::Color);
EXPECT_EQ(tint->semantic, "Tint");
const ShaderPropertyDesc* exposure = shader->FindProperty("_Exposure");
ASSERT_NE(exposure, nullptr);
EXPECT_EQ(exposure->type, ShaderPropertyType::Float);
EXPECT_EQ(exposure->semantic, "Exposure");
const ShaderPropertyDesc* rotation = shader->FindProperty("_Rotation");
ASSERT_NE(rotation, nullptr);
EXPECT_EQ(rotation->type, ShaderPropertyType::Float);
EXPECT_EQ(rotation->semantic, "Rotation");
const ShaderPropertyDesc* panoramic = shader->FindProperty("_MainTex");
ASSERT_NE(panoramic, nullptr);
EXPECT_EQ(panoramic->type, ShaderPropertyType::Texture2D);
EXPECT_EQ(panoramic->semantic, "SkyboxPanoramicTexture");
const ShaderPropertyDesc* cubemap = shader->FindProperty("_Tex");
ASSERT_NE(cubemap, nullptr);
EXPECT_EQ(cubemap->type, ShaderPropertyType::TextureCube);
EXPECT_EQ(cubemap->semantic, "SkyboxTexture");
2026-04-05 23:44:32 +08:00
2026-04-07 04:30:26 +08:00
delete shader;
}
2026-04-05 23:44:32 +08:00
TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoadedSkyboxShaderContract) {
2026-04-07 04:30:26 +08:00
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinSkyboxShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
2026-04-07 04:30:26 +08:00
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
2026-04-07 04:30:26 +08:00
const ShaderPass* pass = shader->FindPass("Skybox");
ASSERT_NE(pass, nullptr);
2026-04-07 04:30:26 +08:00
BuiltinPassResourceBindingPlan plan = {};
String error;
EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr();
ASSERT_EQ(plan.bindings.Size(), 5u);
EXPECT_TRUE(plan.environment.IsValid());
EXPECT_TRUE(plan.material.IsValid());
EXPECT_TRUE(plan.skyboxPanoramicTexture.IsValid());
EXPECT_TRUE(plan.skyboxTexture.IsValid());
EXPECT_TRUE(plan.linearClampSampler.IsValid());
EXPECT_EQ(plan.firstDescriptorSet, 0u);
EXPECT_EQ(plan.descriptorSetCount, 5u);
2026-04-05 23:44:32 +08:00
delete shader;
}
TEST(BuiltinForwardPipeline_Test, BuiltinFinalColorShaderUsesAuthoringContract) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinFinalColorShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("FinalColor");
ASSERT_NE(pass, nullptr);
EXPECT_EQ(pass->resources.Size(), 3u);
2026-04-07 04:30:26 +08:00
EXPECT_TRUE(pass->hasFixedFunctionState);
EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::None);
EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable);
EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::Always);
const ShaderPropertyDesc* colorScale = shader->FindProperty("_ColorScale");
ASSERT_NE(colorScale, nullptr);
EXPECT_EQ(colorScale->type, ShaderPropertyType::Color);
const ShaderPropertyDesc* exposure = shader->FindProperty("_Exposure");
ASSERT_NE(exposure, nullptr);
EXPECT_EQ(exposure->type, ShaderPropertyType::Float);
const ShaderPropertyDesc* outputTransferMode = shader->FindProperty("_OutputTransferMode");
ASSERT_NE(outputTransferMode, nullptr);
EXPECT_EQ(outputTransferMode->type, ShaderPropertyType::Float);
const ShaderPropertyDesc* toneMappingMode = shader->FindProperty("_ToneMappingMode");
ASSERT_NE(toneMappingMode, nullptr);
EXPECT_EQ(toneMappingMode->type, ShaderPropertyType::Float);
2026-04-07 04:30:26 +08:00
delete shader;
}
TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoadedFinalColorShaderContract) {
2026-04-07 04:30:26 +08:00
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinFinalColorShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("FinalColor");
ASSERT_NE(pass, nullptr);
BuiltinPassResourceBindingPlan plan = {};
String error;
EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr();
ASSERT_EQ(plan.bindings.Size(), 3u);
EXPECT_TRUE(plan.passConstants.IsValid());
EXPECT_TRUE(plan.sourceColorTexture.IsValid());
EXPECT_TRUE(plan.linearClampSampler.IsValid());
EXPECT_EQ(plan.firstDescriptorSet, 0u);
EXPECT_EQ(plan.descriptorSetCount, 3u);
delete shader;
}
TEST(BuiltinForwardPipeline_Test, BuiltinColorScalePostProcessShaderUsesAuthoringContract) {
2026-04-07 04:30:26 +08:00
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinColorScalePostProcessShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ColorScale");
ASSERT_NE(pass, nullptr);
EXPECT_EQ(pass->resources.Size(), 3u);
2026-04-07 04:30:26 +08:00
EXPECT_TRUE(pass->hasFixedFunctionState);
EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::None);
EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable);
EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::Always);
const ShaderPropertyDesc* colorScale = shader->FindProperty("_ColorScale");
ASSERT_NE(colorScale, nullptr);
EXPECT_EQ(colorScale->type, ShaderPropertyType::Color);
delete shader;
}
TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoadedColorScalePostProcessShaderContract) {
2026-04-07 04:30:26 +08:00
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinColorScalePostProcessShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ColorScale");
ASSERT_NE(pass, nullptr);
BuiltinPassResourceBindingPlan plan = {};
String error;
EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr();
ASSERT_EQ(plan.bindings.Size(), 3u);
EXPECT_TRUE(plan.passConstants.IsValid());
EXPECT_TRUE(plan.sourceColorTexture.IsValid());
EXPECT_TRUE(plan.linearClampSampler.IsValid());
EXPECT_EQ(plan.firstDescriptorSet, 0u);
EXPECT_EQ(plan.descriptorSetCount, 3u);
delete shader;
}
TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringFinalColorBindingsToDescriptorSpaces) {
2026-04-07 04:30:26 +08:00
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinFinalColorShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("FinalColor");
ASSERT_NE(pass, nullptr);
const ShaderStageVariant* d3d12Fragment = shader->FindVariant(
"FinalColor",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::D3D12);
ASSERT_NE(d3d12Fragment, nullptr);
ShaderCompileDesc d3d12CompileDesc = {};
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
*pass,
XCEngine::Resources::ShaderBackend::D3D12,
*d3d12Fragment,
d3d12CompileDesc);
const std::string d3d12Source(
reinterpret_cast<const char*>(d3d12CompileDesc.source.data()),
d3d12CompileDesc.source.size());
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"cbuffer FinalColorConstants",
"register(b0)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"Texture2D SourceColorTexture",
"register(t0)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"SamplerState LinearClampSampler",
"register(s0)"));
EXPECT_EQ(d3d12Source.find("space0"), std::string::npos);
2026-04-07 04:30:26 +08:00
EXPECT_EQ(d3d12Source.find("space1"), std::string::npos);
const ShaderStageVariant* vulkanFragment = shader->FindVariant(
"FinalColor",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::Vulkan);
ASSERT_NE(vulkanFragment, nullptr);
const std::string runtimeSource =
::XCEngine::Rendering::Detail::BuildRuntimeShaderSource(
*pass,
XCEngine::Resources::ShaderBackend::Vulkan,
*vulkanFragment);
EXPECT_TRUE(SourceContainsRegisterBinding(
runtimeSource,
"cbuffer FinalColorConstants",
"register(b0, space0)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
runtimeSource,
"Texture2D SourceColorTexture",
"register(t0, space1)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
runtimeSource,
"SamplerState LinearClampSampler",
"register(s0, space2)"));
2026-04-07 04:30:26 +08:00
ShaderCompileDesc vulkanCompileDesc = {};
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
*pass,
XCEngine::Resources::ShaderBackend::Vulkan,
*vulkanFragment,
vulkanCompileDesc);
const std::string vulkanSource(
reinterpret_cast<const char*>(vulkanCompileDesc.source.data()),
vulkanCompileDesc.source.size());
EXPECT_NE(vulkanSource.find("ApplyLinearToSRGB"), std::string::npos);
EXPECT_NE(vulkanSource.find("ApplyToneMapping"), std::string::npos);
delete shader;
}
TEST(BuiltinForwardPipeline_Test, OpenGLRuntimeTranspilesFinalColorVariantToCombinedSourceSampler) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinFinalColorShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("FinalColor");
ASSERT_NE(pass, nullptr);
const ShaderStageVariant* openGLFragment = shader->FindVariant(
"FinalColor",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::OpenGL);
ASSERT_NE(openGLFragment, nullptr);
ShaderCompileDesc compileDesc = {};
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
*pass,
XCEngine::Resources::ShaderBackend::OpenGL,
*openGLFragment,
compileDesc);
XCEngine::RHI::CompiledSpirvShader spirvShader = {};
std::string errorMessage;
ASSERT_TRUE(
XCEngine::RHI::CompileSpirvShader(
compileDesc,
XCEngine::RHI::SpirvTargetEnvironment::Vulkan,
spirvShader,
&errorMessage))
<< errorMessage;
std::string glslSource;
ASSERT_TRUE(XCEngine::RHI::TranspileSpirvToOpenGLGLSL(spirvShader, glslSource, &errorMessage))
<< errorMessage;
EXPECT_NE(glslSource.find("uniform sampler2D SPIRV_Cross_Combined"), std::string::npos);
EXPECT_NE(glslSource.find("texture(SPIRV_Cross_Combined"), std::string::npos);
EXPECT_NE(glslSource.find("FinalColorConstants"), std::string::npos);
delete shader;
}
TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringColorScaleBindingsToDescriptorSpaces) {
2026-04-07 04:30:26 +08:00
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinColorScalePostProcessShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
2026-04-07 04:30:26 +08:00
const ShaderPass* pass = shader->FindPass("ColorScale");
ASSERT_NE(pass, nullptr);
2026-04-07 04:30:26 +08:00
const ShaderStageVariant* d3d12Fragment = shader->FindVariant(
"ColorScale",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::D3D12);
ASSERT_NE(d3d12Fragment, nullptr);
ShaderCompileDesc d3d12CompileDesc = {};
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
*pass,
XCEngine::Resources::ShaderBackend::D3D12,
*d3d12Fragment,
d3d12CompileDesc);
const std::string d3d12Source(
reinterpret_cast<const char*>(d3d12CompileDesc.source.data()),
d3d12CompileDesc.source.size());
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"cbuffer PostProcessConstants",
"register(b0)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"Texture2D SourceColorTexture",
"register(t0)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
d3d12Source,
"SamplerState LinearClampSampler",
"register(s0)"));
EXPECT_EQ(d3d12Source.find("space0"), std::string::npos);
2026-04-07 04:30:26 +08:00
EXPECT_EQ(d3d12Source.find("space1"), std::string::npos);
const ShaderStageVariant* vulkanFragment = shader->FindVariant(
"ColorScale",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::Vulkan);
ASSERT_NE(vulkanFragment, nullptr);
const std::string runtimeSource =
::XCEngine::Rendering::Detail::BuildRuntimeShaderSource(
*pass,
XCEngine::Resources::ShaderBackend::Vulkan,
*vulkanFragment);
EXPECT_TRUE(SourceContainsRegisterBinding(
runtimeSource,
"cbuffer PostProcessConstants",
"register(b0, space0)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
runtimeSource,
"Texture2D SourceColorTexture",
"register(t0, space1)"));
EXPECT_TRUE(SourceContainsRegisterBinding(
runtimeSource,
"SamplerState LinearClampSampler",
"register(s0, space2)"));
2026-04-07 04:30:26 +08:00
ShaderCompileDesc vulkanCompileDesc = {};
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
*pass,
XCEngine::Resources::ShaderBackend::Vulkan,
*vulkanFragment,
vulkanCompileDesc);
const std::string vulkanSource(
reinterpret_cast<const char*>(vulkanCompileDesc.source.data()),
vulkanCompileDesc.source.size());
EXPECT_NE(vulkanSource.find("SourceColorTexture.Sample"), std::string::npos);
2026-04-07 04:30:26 +08:00
EXPECT_NE(vulkanSource.find("gColorScale"), std::string::npos);
delete shader;
}
TEST(BuiltinForwardPipeline_Test, OpenGLRuntimeTranspilesColorScaleVariantToCombinedSourceSampler) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinColorScalePostProcessShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ColorScale");
ASSERT_NE(pass, nullptr);
const ShaderStageVariant* openGLFragment = shader->FindVariant(
"ColorScale",
XCEngine::Resources::ShaderType::Fragment,
XCEngine::Resources::ShaderBackend::OpenGL);
ASSERT_NE(openGLFragment, nullptr);
ShaderCompileDesc compileDesc = {};
::XCEngine::Rendering::Detail::ApplyShaderStageVariant(
*pass,
XCEngine::Resources::ShaderBackend::OpenGL,
*openGLFragment,
compileDesc);
XCEngine::RHI::CompiledSpirvShader spirvShader = {};
std::string errorMessage;
ASSERT_TRUE(
XCEngine::RHI::CompileSpirvShader(
compileDesc,
XCEngine::RHI::SpirvTargetEnvironment::Vulkan,
spirvShader,
&errorMessage))
<< errorMessage;
std::string glslSource;
ASSERT_TRUE(XCEngine::RHI::TranspileSpirvToOpenGLGLSL(spirvShader, glslSource, &errorMessage))
<< errorMessage;
EXPECT_NE(glslSource.find("uniform sampler2D SPIRV_Cross_Combined"), std::string::npos);
EXPECT_NE(glslSource.find("texture(SPIRV_Cross_Combined"), std::string::npos);
EXPECT_NE(glslSource.find("PostProcessConstants"), std::string::npos);
delete shader;
}
TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoadedForwardShaderContract) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinForwardLitShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ForwardLit");
ASSERT_NE(pass, nullptr);
BuiltinPassResourceBindingPlan plan = {};
String error;
EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr();
EXPECT_TRUE(plan.perObject.IsValid());
2026-04-05 15:44:37 +08:00
EXPECT_TRUE(plan.lighting.IsValid());
EXPECT_TRUE(plan.material.IsValid());
2026-04-04 23:01:34 +08:00
EXPECT_TRUE(plan.shadowReceiver.IsValid());
EXPECT_TRUE(plan.baseColorTexture.IsValid());
2026-04-03 17:18:46 +08:00
EXPECT_TRUE(plan.linearClampSampler.IsValid());
2026-04-04 23:01:34 +08:00
EXPECT_TRUE(plan.shadowMapTexture.IsValid());
EXPECT_TRUE(plan.shadowMapSampler.IsValid());
EXPECT_EQ(plan.firstDescriptorSet, 0u);
2026-04-05 15:44:37 +08:00
EXPECT_EQ(plan.descriptorSetCount, 8u);
2026-04-03 17:18:46 +08:00
EXPECT_TRUE(plan.usesConstantBuffers);
EXPECT_TRUE(plan.usesTextures);
EXPECT_TRUE(plan.usesSamplers);
delete shader;
}
TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromBuiltinForwardShaderContract) {
ShaderPass pass = {};
pass.name = "ForwardLit";
ShaderPassTagEntry tag = {};
tag.name = "LightMode";
tag.value = "ForwardLit";
pass.tags.PushBack(tag);
AppendDefaultBuiltinPassResources(pass);
BuiltinPassResourceBindingPlan plan = {};
String error;
EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(pass, plan, &error)) << error.CStr();
EXPECT_TRUE(plan.perObject.IsValid());
EXPECT_TRUE(plan.lighting.IsValid());
EXPECT_TRUE(plan.material.IsValid());
EXPECT_TRUE(plan.shadowReceiver.IsValid());
EXPECT_TRUE(plan.baseColorTexture.IsValid());
EXPECT_TRUE(plan.linearClampSampler.IsValid());
EXPECT_TRUE(plan.shadowMapTexture.IsValid());
EXPECT_TRUE(plan.shadowMapSampler.IsValid());
EXPECT_EQ(plan.firstDescriptorSet, 0u);
EXPECT_EQ(plan.descriptorSetCount, 8u);
}
TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoadedUnlitShaderContract) {
2026-04-03 17:18:46 +08:00
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinUnlitShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("Unlit");
ASSERT_NE(pass, nullptr);
BuiltinPassResourceBindingPlan plan = {};
String error;
EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr();
2026-04-03 17:18:46 +08:00
EXPECT_TRUE(plan.perObject.IsValid());
EXPECT_TRUE(plan.material.IsValid());
EXPECT_TRUE(plan.baseColorTexture.IsValid());
EXPECT_TRUE(plan.linearClampSampler.IsValid());
EXPECT_EQ(plan.firstDescriptorSet, 0u);
EXPECT_EQ(plan.descriptorSetCount, 4u);
EXPECT_TRUE(plan.usesConstantBuffers);
EXPECT_TRUE(plan.usesTextures);
EXPECT_TRUE(plan.usesSamplers);
delete shader;
}
TEST(BuiltinForwardPipeline_Test, UsesLoadedForwardShaderResourceSetIndices) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinForwardLitShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ForwardLit");
ASSERT_NE(pass, nullptr);
EXPECT_EQ(pass->resources.Size(), 8u);
BuiltinPassResourceBindingPlan plan = {};
String error;
EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr();
2026-04-05 15:44:37 +08:00
ASSERT_EQ(plan.bindings.Size(), 8u);
EXPECT_EQ(plan.perObject.set, 0u);
2026-04-05 15:44:37 +08:00
EXPECT_EQ(plan.lighting.set, 1u);
EXPECT_EQ(plan.material.set, 2u);
EXPECT_EQ(plan.shadowReceiver.set, 3u);
EXPECT_EQ(plan.baseColorTexture.set, 4u);
EXPECT_EQ(plan.linearClampSampler.set, 5u);
EXPECT_EQ(plan.shadowMapTexture.set, 6u);
EXPECT_EQ(plan.shadowMapSampler.set, 7u);
delete shader;
}
TEST(BuiltinPassLayout_Test, BuildsSharedSetLayoutsFromLoadedForwardShaderResources) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinForwardLitShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ForwardLit");
ASSERT_NE(pass, nullptr);
EXPECT_EQ(pass->resources.Size(), 8u);
BuiltinPassResourceBindingPlan plan = {};
String error;
ASSERT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr();
std::vector<BuiltinPassSetLayoutMetadata> setLayouts;
ASSERT_TRUE(TryBuildBuiltinPassSetLayouts(plan, setLayouts, &error)) << error.CStr();
2026-04-05 15:44:37 +08:00
ASSERT_EQ(setLayouts.size(), 8u);
EXPECT_EQ(setLayouts[0].layout.bindingCount, 1u);
EXPECT_FALSE(setLayouts[0].shaderVisible);
EXPECT_TRUE(setLayouts[0].usesPerObject);
EXPECT_FALSE(setLayouts[0].usesMaterial);
EXPECT_EQ(setLayouts[1].layout.bindingCount, 1u);
2026-04-05 15:44:37 +08:00
EXPECT_TRUE(setLayouts[1].usesLighting);
EXPECT_FALSE(setLayouts[1].shaderVisible);
EXPECT_EQ(setLayouts[2].layout.bindingCount, 1u);
2026-04-05 15:44:37 +08:00
EXPECT_TRUE(setLayouts[2].usesMaterial);
EXPECT_FALSE(setLayouts[2].shaderVisible);
EXPECT_EQ(setLayouts[3].layout.bindingCount, 1u);
2026-04-05 15:44:37 +08:00
EXPECT_TRUE(setLayouts[3].usesShadowReceiver);
EXPECT_FALSE(setLayouts[3].shaderVisible);
EXPECT_EQ(setLayouts[4].layout.bindingCount, 1u);
2026-04-05 15:44:37 +08:00
EXPECT_TRUE(setLayouts[4].usesTexture);
EXPECT_TRUE(setLayouts[4].shaderVisible);
EXPECT_EQ(setLayouts[4].heapType, DescriptorHeapType::CBV_SRV_UAV);
EXPECT_EQ(setLayouts[5].layout.bindingCount, 1u);
2026-04-05 15:44:37 +08:00
EXPECT_TRUE(setLayouts[5].usesSampler);
EXPECT_TRUE(setLayouts[5].shaderVisible);
2026-04-05 15:44:37 +08:00
EXPECT_EQ(setLayouts[5].heapType, DescriptorHeapType::Sampler);
EXPECT_EQ(setLayouts[6].layout.bindingCount, 1u);
2026-04-05 15:44:37 +08:00
EXPECT_TRUE(setLayouts[6].usesTexture);
EXPECT_TRUE(setLayouts[6].shaderVisible);
2026-04-05 15:44:37 +08:00
EXPECT_EQ(setLayouts[7].layout.bindingCount, 1u);
EXPECT_TRUE(setLayouts[7].usesSampler);
EXPECT_TRUE(setLayouts[7].shaderVisible);
EXPECT_EQ(setLayouts[7].heapType, DescriptorHeapType::Sampler);
delete shader;
}
TEST(BuiltinDepthStylePass_Test, BuiltinDepthOnlyShaderUsesAuthoringContract) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinDepthOnlyShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("DepthOnly");
ASSERT_NE(pass, nullptr);
EXPECT_EQ(pass->resources.Size(), 4u);
EXPECT_TRUE(pass->hasFixedFunctionState);
EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Back);
EXPECT_TRUE(pass->fixedFunctionState.depthWriteEnable);
EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual);
delete shader;
}
TEST(BuiltinDepthStylePass_Test, BuiltinShadowCasterShaderUsesAuthoringContract) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinShadowCasterShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ShadowCaster");
ASSERT_NE(pass, nullptr);
EXPECT_EQ(pass->resources.Size(), 4u);
EXPECT_TRUE(pass->hasFixedFunctionState);
EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Back);
EXPECT_TRUE(pass->fixedFunctionState.depthWriteEnable);
EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual);
delete shader;
}
TEST(BuiltinObjectIdPass_Test, BuiltinObjectIdShaderUsesAuthoringContract) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinObjectIdShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ObjectId");
ASSERT_NE(pass, nullptr);
EXPECT_EQ(pass->resources.Size(), 1u);
EXPECT_TRUE(pass->hasFixedFunctionState);
EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Back);
EXPECT_TRUE(pass->fixedFunctionState.depthWriteEnable);
EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual);
delete shader;
}
TEST(BuiltinObjectIdPass_Test, BuildsBuiltinPassResourceBindingPlanFromBuiltinObjectIdShaderContract) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinObjectIdShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ObjectId");
ASSERT_NE(pass, nullptr);
BuiltinPassResourceBindingPlan plan = {};
String error;
EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr();
ASSERT_EQ(plan.bindings.Size(), 1u);
EXPECT_TRUE(plan.perObject.IsValid());
EXPECT_FALSE(plan.material.IsValid());
EXPECT_FALSE(plan.baseColorTexture.IsValid());
EXPECT_FALSE(plan.linearClampSampler.IsValid());
EXPECT_EQ(plan.firstDescriptorSet, 0u);
EXPECT_EQ(plan.descriptorSetCount, 1u);
EXPECT_TRUE(plan.usesConstantBuffers);
EXPECT_FALSE(plan.usesTextures);
EXPECT_FALSE(plan.usesSamplers);
delete shader;
}
TEST(BuiltinObjectIdPass_Test, BuildsBuiltinPassResourceBindingPlanFromExplicitObjectIdContract) {
ShaderPass pass = {};
pass.name = "ObjectId";
ShaderPassTagEntry tag = {};
tag.name = "LightMode";
tag.value = "ObjectId";
pass.tags.PushBack(tag);
AppendDefaultBuiltinPassResources(pass);
BuiltinPassResourceBindingPlan plan = {};
String error;
EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(pass, plan, &error)) << error.CStr();
ASSERT_EQ(plan.bindings.Size(), 1u);
EXPECT_TRUE(plan.perObject.IsValid());
EXPECT_FALSE(plan.material.IsValid());
EXPECT_EQ(plan.firstDescriptorSet, 0u);
EXPECT_EQ(plan.descriptorSetCount, 1u);
}
TEST(BuiltinDepthStylePass_Test, BuildsBuiltinPassResourceBindingPlanFromExplicitDepthOnlyContract) {
ShaderPass pass = {};
pass.name = "DepthOnly";
ShaderPassTagEntry tag = {};
tag.name = "LightMode";
tag.value = "DepthOnly";
pass.tags.PushBack(tag);
AppendDefaultBuiltinPassResources(pass);
BuiltinPassResourceBindingPlan plan = {};
String error;
EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(pass, plan, &error)) << error.CStr();
EXPECT_TRUE(plan.perObject.IsValid());
EXPECT_TRUE(plan.material.IsValid());
EXPECT_TRUE(plan.baseColorTexture.IsValid());
EXPECT_TRUE(plan.linearClampSampler.IsValid());
EXPECT_EQ(plan.firstDescriptorSet, 0u);
EXPECT_EQ(plan.descriptorSetCount, 4u);
}
TEST(BuiltinObjectIdPass_Test, UsesFloat3PositionInputLayoutForStaticMeshVertices) {
const InputLayoutDesc inputLayout = BuiltinObjectIdPass::BuildInputLayout();
ASSERT_EQ(inputLayout.elements.size(), 3u);
const InputElementDesc& position = inputLayout.elements[0];
EXPECT_EQ(position.semanticName, "POSITION");
EXPECT_EQ(position.semanticIndex, 0u);
EXPECT_EQ(position.format, static_cast<uint32_t>(Format::R32G32B32_Float));
EXPECT_EQ(position.inputSlot, 0u);
EXPECT_EQ(position.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, position)));
const InputElementDesc& normal = inputLayout.elements[1];
EXPECT_EQ(normal.semanticName, "NORMAL");
EXPECT_EQ(normal.semanticIndex, 0u);
EXPECT_EQ(normal.format, static_cast<uint32_t>(Format::R32G32B32_Float));
EXPECT_EQ(normal.inputSlot, 0u);
EXPECT_EQ(normal.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, normal)));
const InputElementDesc& texcoord = inputLayout.elements[2];
EXPECT_EQ(texcoord.semanticName, "TEXCOORD");
EXPECT_EQ(texcoord.semanticIndex, 0u);
EXPECT_EQ(texcoord.format, static_cast<uint32_t>(Format::R32G32_Float));
EXPECT_EQ(texcoord.inputSlot, 0u);
EXPECT_EQ(texcoord.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, uv0)));
}
TEST(BuiltinPassLayout_Test, BuildsSharedSetLayoutsFromExplicitObjectIdResources) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinObjectIdShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ObjectId");
ASSERT_NE(pass, nullptr);
BuiltinPassResourceBindingPlan plan = {};
String error;
ASSERT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr();
std::vector<BuiltinPassSetLayoutMetadata> setLayouts;
ASSERT_TRUE(TryBuildBuiltinPassSetLayouts(plan, setLayouts, &error)) << error.CStr();
ASSERT_EQ(setLayouts.size(), 1u);
EXPECT_EQ(setLayouts[0].layout.bindingCount, 1u);
EXPECT_TRUE(setLayouts[0].usesPerObject);
EXPECT_FALSE(setLayouts[0].usesMaterial);
EXPECT_FALSE(setLayouts[0].usesTexture);
EXPECT_FALSE(setLayouts[0].usesSampler);
EXPECT_FALSE(setLayouts[0].shaderVisible);
EXPECT_EQ(setLayouts[0].heapType, DescriptorHeapType::CBV_SRV_UAV);
delete shader;
}
TEST(BuiltinPassLayout_Test, BuildsSharedSetLayoutsFromBuiltinDepthOnlyShaderContract) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinDepthOnlyShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("DepthOnly");
ASSERT_NE(pass, nullptr);
BuiltinPassResourceBindingPlan plan = {};
String error;
ASSERT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr();
EXPECT_TRUE(plan.perObject.IsValid());
EXPECT_TRUE(plan.material.IsValid());
EXPECT_TRUE(plan.baseColorTexture.IsValid());
EXPECT_TRUE(plan.linearClampSampler.IsValid());
EXPECT_EQ(plan.firstDescriptorSet, 0u);
EXPECT_EQ(plan.descriptorSetCount, 4u);
EXPECT_TRUE(plan.usesConstantBuffers);
EXPECT_TRUE(plan.usesTextures);
EXPECT_TRUE(plan.usesSamplers);
std::vector<BuiltinPassSetLayoutMetadata> setLayouts;
ASSERT_TRUE(TryBuildBuiltinPassSetLayouts(plan, setLayouts, &error)) << error.CStr();
ASSERT_EQ(setLayouts.size(), 4u);
EXPECT_EQ(setLayouts[0].layout.bindingCount, 1u);
EXPECT_TRUE(setLayouts[0].usesPerObject);
EXPECT_FALSE(setLayouts[0].usesMaterial);
EXPECT_FALSE(setLayouts[0].usesTexture);
EXPECT_FALSE(setLayouts[0].usesSampler);
EXPECT_EQ(setLayouts[1].layout.bindingCount, 1u);
EXPECT_FALSE(setLayouts[1].usesPerObject);
EXPECT_TRUE(setLayouts[1].usesMaterial);
EXPECT_FALSE(setLayouts[1].usesTexture);
EXPECT_FALSE(setLayouts[1].usesSampler);
EXPECT_EQ(setLayouts[2].layout.bindingCount, 1u);
EXPECT_FALSE(setLayouts[2].usesPerObject);
EXPECT_FALSE(setLayouts[2].usesMaterial);
EXPECT_TRUE(setLayouts[2].usesTexture);
EXPECT_FALSE(setLayouts[2].usesSampler);
EXPECT_TRUE(setLayouts[2].shaderVisible);
EXPECT_EQ(setLayouts[3].layout.bindingCount, 1u);
EXPECT_FALSE(setLayouts[3].usesPerObject);
EXPECT_FALSE(setLayouts[3].usesMaterial);
EXPECT_FALSE(setLayouts[3].usesTexture);
EXPECT_TRUE(setLayouts[3].usesSampler);
EXPECT_TRUE(setLayouts[3].shaderVisible);
EXPECT_EQ(setLayouts[3].heapType, DescriptorHeapType::Sampler);
delete shader;
}
TEST(BuiltinPassLayout_Test, BuildsSharedSetLayoutsFromBuiltinShadowCasterShaderContract) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinShadowCasterShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("ShadowCaster");
ASSERT_NE(pass, nullptr);
BuiltinPassResourceBindingPlan plan = {};
String error;
ASSERT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr();
EXPECT_TRUE(plan.perObject.IsValid());
EXPECT_TRUE(plan.material.IsValid());
EXPECT_TRUE(plan.baseColorTexture.IsValid());
EXPECT_TRUE(plan.linearClampSampler.IsValid());
EXPECT_EQ(plan.firstDescriptorSet, 0u);
EXPECT_EQ(plan.descriptorSetCount, 4u);
EXPECT_TRUE(plan.usesConstantBuffers);
EXPECT_TRUE(plan.usesTextures);
EXPECT_TRUE(plan.usesSamplers);
std::vector<BuiltinPassSetLayoutMetadata> setLayouts;
ASSERT_TRUE(TryBuildBuiltinPassSetLayouts(plan, setLayouts, &error)) << error.CStr();
ASSERT_EQ(setLayouts.size(), 4u);
EXPECT_EQ(setLayouts[0].layout.bindingCount, 1u);
EXPECT_TRUE(setLayouts[0].usesPerObject);
EXPECT_FALSE(setLayouts[0].usesMaterial);
EXPECT_FALSE(setLayouts[0].usesTexture);
EXPECT_FALSE(setLayouts[0].usesSampler);
EXPECT_EQ(setLayouts[1].layout.bindingCount, 1u);
EXPECT_FALSE(setLayouts[1].usesPerObject);
EXPECT_TRUE(setLayouts[1].usesMaterial);
EXPECT_FALSE(setLayouts[1].usesTexture);
EXPECT_FALSE(setLayouts[1].usesSampler);
EXPECT_EQ(setLayouts[2].layout.bindingCount, 1u);
EXPECT_FALSE(setLayouts[2].usesPerObject);
EXPECT_FALSE(setLayouts[2].usesMaterial);
EXPECT_TRUE(setLayouts[2].usesTexture);
EXPECT_FALSE(setLayouts[2].usesSampler);
EXPECT_TRUE(setLayouts[2].shaderVisible);
EXPECT_EQ(setLayouts[3].layout.bindingCount, 1u);
EXPECT_FALSE(setLayouts[3].usesPerObject);
EXPECT_FALSE(setLayouts[3].usesMaterial);
EXPECT_FALSE(setLayouts[3].usesTexture);
EXPECT_TRUE(setLayouts[3].usesSampler);
EXPECT_TRUE(setLayouts[3].shaderVisible);
EXPECT_EQ(setLayouts[3].heapType, DescriptorHeapType::Sampler);
delete shader;
}
TEST(BuiltinPassLayout_Test, RejectsMixedSamplerAndNonSamplerBindingsInOneSet) {
Array<ShaderResourceBindingDesc> bindings;
bindings.Resize(2);
bindings[0].name = "BaseColorTexture";
bindings[0].type = ShaderResourceType::Texture2D;
bindings[0].set = 0;
bindings[0].binding = 0;
bindings[0].semantic = "BaseColorTexture";
bindings[1].name = "LinearClampSampler";
bindings[1].type = ShaderResourceType::Sampler;
bindings[1].set = 0;
bindings[1].binding = 1;
bindings[1].semantic = "LinearClampSampler";
BuiltinPassResourceBindingPlan plan = {};
String error;
ASSERT_TRUE(TryBuildBuiltinPassResourceBindingPlan(bindings, plan, &error)) << error.CStr();
std::vector<BuiltinPassSetLayoutMetadata> setLayouts;
EXPECT_FALSE(TryBuildBuiltinPassSetLayouts(plan, setLayouts, &error));
EXPECT_EQ(error, "Builtin pass does not support mixing sampler and non-sampler bindings in one set");
}
TEST(BuiltinPassLayout_Test, RejectsDuplicateBindingsInOneSet) {
BuiltinPassResourceBindingPlan plan = {};
BuiltinPassResourceBindingDesc perObjectBinding = {};
perObjectBinding.semantic = BuiltinPassResourceSemantic::PerObject;
perObjectBinding.resourceType = ShaderResourceType::ConstantBuffer;
perObjectBinding.location = { 0, 0 };
plan.bindings.PushBack(perObjectBinding);
BuiltinPassResourceBindingDesc materialBinding = {};
materialBinding.semantic = BuiltinPassResourceSemantic::Material;
materialBinding.resourceType = ShaderResourceType::ConstantBuffer;
materialBinding.location = { 0, 0 };
plan.bindings.PushBack(materialBinding);
plan.maxSetIndex = 0;
plan.firstDescriptorSet = 0;
plan.descriptorSetCount = 1;
plan.usesConstantBuffers = true;
plan.perObject = { 0, 0 };
plan.material = { 0, 0 };
std::vector<BuiltinPassSetLayoutMetadata> setLayouts;
String error;
EXPECT_FALSE(TryBuildBuiltinPassSetLayouts(plan, setLayouts, &error));
EXPECT_EQ(error, "Builtin pass encountered duplicate bindings inside one descriptor set");
}
2026-04-08 19:18:07 +08:00
TEST(BuiltinPassLayout_Test, AcceptsRuntimeMaterialBufferBindingsWithoutBuiltinSemanticMetadata) {
Array<ShaderResourceBindingDesc> bindings;
ShaderResourceBindingDesc perObjectBinding = {};
perObjectBinding.name = "PerObjectConstants";
perObjectBinding.type = ShaderResourceType::ConstantBuffer;
perObjectBinding.set = 0u;
perObjectBinding.binding = 0u;
perObjectBinding.semantic = "PerObject";
bindings.PushBack(perObjectBinding);
ShaderResourceBindingDesc materialBinding = {};
materialBinding.name = "MaterialConstants";
materialBinding.type = ShaderResourceType::ConstantBuffer;
materialBinding.set = 1u;
materialBinding.binding = 0u;
materialBinding.semantic = "Material";
bindings.PushBack(materialBinding);
ShaderResourceBindingDesc bufferBinding = {};
bufferBinding.name = "VolumeNodes";
bufferBinding.type = ShaderResourceType::StructuredBuffer;
bufferBinding.set = 2u;
bufferBinding.binding = 0u;
bindings.PushBack(bufferBinding);
BuiltinPassResourceBindingPlan plan = {};
String error;
ASSERT_TRUE(TryBuildBuiltinPassResourceBindingPlan(bindings, plan, &error)) << error.CStr();
EXPECT_TRUE(plan.usesMaterialBuffers);
ASSERT_EQ(plan.materialBufferBindings.Size(), 1u);
EXPECT_EQ(plan.materialBufferBindings[0].name, "VolumeNodes");
EXPECT_EQ(plan.materialBufferBindings[0].semantic, BuiltinPassResourceSemantic::MaterialBuffer);
EXPECT_EQ(plan.materialBufferBindings[0].resourceType, ShaderResourceType::StructuredBuffer);
EXPECT_EQ(plan.materialBufferBindings[0].location.set, 2u);
EXPECT_EQ(plan.materialBufferBindings[0].location.binding, 0u);
std::vector<BuiltinPassSetLayoutMetadata> setLayouts;
ASSERT_TRUE(TryBuildBuiltinPassSetLayouts(plan, setLayouts, &error)) << error.CStr();
ASSERT_EQ(setLayouts.size(), 3u);
EXPECT_TRUE(setLayouts[2].usesMaterialBuffers);
ASSERT_EQ(setLayouts[2].materialBufferBindings.size(), 1u);
EXPECT_EQ(setLayouts[2].materialBufferBindings[0].name, "VolumeNodes");
ASSERT_EQ(setLayouts[2].bindings.size(), 1u);
EXPECT_EQ(
static_cast<DescriptorType>(setLayouts[2].bindings[0].type),
DescriptorType::SRV);
EXPECT_EQ(setLayouts[2].bindings[0].resourceDimension, ResourceViewDimension::StructuredBuffer);
}
TEST(BuiltinPassLayout_Test, AcceptsRuntimeMaterialRawUavBufferBindings) {
Array<ShaderResourceBindingDesc> bindings;
ShaderResourceBindingDesc bufferBinding = {};
bufferBinding.name = "VolumeCounters";
bufferBinding.type = ShaderResourceType::RWRawBuffer;
bufferBinding.set = 4u;
bufferBinding.binding = 3u;
bindings.PushBack(bufferBinding);
BuiltinPassResourceBindingPlan plan = {};
String error;
ASSERT_TRUE(TryBuildBuiltinPassResourceBindingPlan(bindings, plan, &error)) << error.CStr();
ASSERT_EQ(plan.materialBufferBindings.Size(), 1u);
EXPECT_EQ(plan.materialBufferBindings[0].semantic, BuiltinPassResourceSemantic::MaterialBuffer);
std::vector<BuiltinPassSetLayoutMetadata> setLayouts;
ASSERT_TRUE(TryBuildBuiltinPassSetLayouts(plan, setLayouts, &error)) << error.CStr();
ASSERT_EQ(setLayouts.size(), 5u);
ASSERT_EQ(setLayouts[4].bindings.size(), 1u);
EXPECT_EQ(
static_cast<DescriptorType>(setLayouts[4].bindings[0].type),
DescriptorType::UAV);
EXPECT_EQ(setLayouts[4].bindings[0].resourceDimension, ResourceViewDimension::RawBuffer);
EXPECT_TRUE(setLayouts[4].usesMaterialBuffers);
}
TEST(BuiltinDepthOnlyPass_Test, UsesFloat3PositionInputLayoutForStaticMeshVertices) {
const InputLayoutDesc inputLayout = BuiltinDepthOnlyPass::BuildInputLayout();
ASSERT_EQ(inputLayout.elements.size(), 3u);
EXPECT_EQ(inputLayout.elements[0].semanticName, "POSITION");
EXPECT_EQ(inputLayout.elements[1].semanticName, "NORMAL");
EXPECT_EQ(inputLayout.elements[2].semanticName, "TEXCOORD");
EXPECT_EQ(inputLayout.elements[0].alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, position)));
EXPECT_EQ(inputLayout.elements[1].alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, normal)));
EXPECT_EQ(inputLayout.elements[2].alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, uv0)));
}
TEST(BuiltinShadowCasterPass_Test, UsesFloat3PositionInputLayoutForStaticMeshVertices) {
const InputLayoutDesc inputLayout = BuiltinShadowCasterPass::BuildInputLayout();
ASSERT_EQ(inputLayout.elements.size(), 3u);
EXPECT_EQ(inputLayout.elements[0].semanticName, "POSITION");
EXPECT_EQ(inputLayout.elements[1].semanticName, "NORMAL");
EXPECT_EQ(inputLayout.elements[2].semanticName, "TEXCOORD");
EXPECT_EQ(inputLayout.elements[0].alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, position)));
EXPECT_EQ(inputLayout.elements[1].alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, normal)));
EXPECT_EQ(inputLayout.elements[2].alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, uv0)));
}