diff --git a/engine/assets/builtin/shaders/forward-lit/forward-lit.shader b/engine/assets/builtin/shaders/forward-lit/forward-lit.shader index ab956556..e2460703 100644 --- a/engine/assets/builtin/shaders/forward-lit/forward-lit.shader +++ b/engine/assets/builtin/shaders/forward-lit/forward-lit.shader @@ -1,11 +1,57 @@ { "name": "Builtin Forward Lit", + "properties": [ + { + "name": "_BaseColor", + "displayName": "Base Color", + "type": "Color", + "defaultValue": "(1,1,1,1)", + "semantic": "BaseColor" + }, + { + "name": "_MainTex", + "displayName": "Base Map", + "type": "2D", + "defaultValue": "white", + "semantic": "BaseColorTexture" + } + ], "passes": [ { "name": "ForwardLit", "tags": { "LightMode": "ForwardBase" }, + "resources": [ + { + "name": "PerObjectConstants", + "type": "ConstantBuffer", + "set": 1, + "binding": 0, + "semantic": "PerObject" + }, + { + "name": "MaterialConstants", + "type": "ConstantBuffer", + "set": 2, + "binding": 0, + "semantic": "Material" + }, + { + "name": "BaseColorTexture", + "type": "Texture2D", + "set": 3, + "binding": 0, + "semantic": "BaseColorTexture" + }, + { + "name": "LinearClampSampler", + "type": "Sampler", + "set": 4, + "binding": 0, + "semantic": "LinearClampSampler" + } + ], "variants": [ { "stage": "Vertex", diff --git a/engine/include/XCEngine/Rendering/RenderMaterialUtility.h b/engine/include/XCEngine/Rendering/RenderMaterialUtility.h index 7cc2f249..2c2b1301 100644 --- a/engine/include/XCEngine/Rendering/RenderMaterialUtility.h +++ b/engine/include/XCEngine/Rendering/RenderMaterialUtility.h @@ -122,11 +122,36 @@ struct BuiltinForwardMaterialData { Math::Vector4 baseColorFactor = Math::Vector4::One(); }; +inline const Resources::ShaderPropertyDesc* FindShaderPropertyBySemantic( + const Resources::Material* material, + const Containers::String& semantic) { + if (material == nullptr || material->GetShader() == nullptr) { + return nullptr; + } + + const Containers::String normalizedSemantic = NormalizeBuiltinPassMetadataValue(semantic); + for (const Resources::ShaderPropertyDesc& property : material->GetShader()->GetProperties()) { + if (NormalizeBuiltinPassMetadataValue(property.semantic) == normalizedSemantic) { + return &property; + } + } + + return nullptr; +} + inline Math::Vector4 ResolveBuiltinBaseColorFactor(const Resources::Material* material) { if (material == nullptr) { return Math::Vector4::One(); } + if (const Resources::ShaderPropertyDesc* property = FindShaderPropertyBySemantic(material, "BaseColor")) { + if (material->HasProperty(property->name) && + (property->type == Resources::ShaderPropertyType::Color || + property->type == Resources::ShaderPropertyType::Vector)) { + return material->GetFloat4(property->name); + } + } + static const char* kBaseColorPropertyNames[] = { "baseColor", "_BaseColor", @@ -162,6 +187,13 @@ inline const Resources::Texture* ResolveBuiltinBaseColorTexture(const Resources: return nullptr; } + if (const Resources::ShaderPropertyDesc* property = FindShaderPropertyBySemantic(material, "BaseColorTexture")) { + const Resources::ResourceHandle textureHandle = material->GetTexture(property->name); + if (textureHandle.Get() != nullptr && textureHandle->IsValid()) { + return textureHandle.Get(); + } + } + static const char* kTextureNames[] = { "baseColorTexture", "_BaseColorTexture", diff --git a/engine/include/XCEngine/Resources/Shader/Shader.h b/engine/include/XCEngine/Resources/Shader/Shader.h index 9a2cd01f..6dfa69a4 100644 --- a/engine/include/XCEngine/Resources/Shader/Shader.h +++ b/engine/include/XCEngine/Resources/Shader/Shader.h @@ -29,6 +29,25 @@ enum class ShaderBackend : Core::uint8 { Vulkan }; +// Keep shader property kinds close to Unity's public shader syntax so the +// runtime contract can be reused when ShaderLab-compatible parsing is added. +enum class ShaderPropertyType : Core::uint8 { + Float = 0, + Range, + Int, + Vector, + Color, + Texture2D, + TextureCube +}; + +enum class ShaderResourceType : Core::uint8 { + ConstantBuffer = 0, + Texture2D, + TextureCube, + Sampler +}; + struct ShaderUniform { Containers::String name; Core::uint32 location; @@ -48,6 +67,22 @@ struct ShaderPassTagEntry { Containers::String value; }; +struct ShaderPropertyDesc { + Containers::String name; + Containers::String displayName; + ShaderPropertyType type = ShaderPropertyType::Float; + Containers::String defaultValue; + Containers::String semantic; +}; + +struct ShaderResourceBindingDesc { + Containers::String name; + ShaderResourceType type = ShaderResourceType::ConstantBuffer; + Core::uint32 set = 0; + Core::uint32 binding = 0; + Containers::String semantic; +}; + struct ShaderStageVariant { ShaderType stage = ShaderType::Fragment; ShaderLanguage language = ShaderLanguage::GLSL; @@ -61,6 +96,7 @@ struct ShaderStageVariant { struct ShaderPass { Containers::String name; Containers::Array tags; + Containers::Array resources; Containers::Array variants; }; @@ -95,6 +131,11 @@ public: void AddAttribute(const ShaderAttribute& attribute); const Containers::Array& GetAttributes() const { return m_attributes; } + void AddProperty(const ShaderPropertyDesc& property); + void ClearProperties(); + const Containers::Array& GetProperties() const { return m_properties; } + const ShaderPropertyDesc* FindProperty(const Containers::String& propertyName) const; + void AddPass(const ShaderPass& pass); void ClearPasses(); Core::uint32 GetPassCount() const { return static_cast(m_passes.Size()); } @@ -105,9 +146,15 @@ public: const Containers::String& passName, const Containers::String& tagName, const Containers::String& tagValue); + void AddPassResourceBinding( + const Containers::String& passName, + const ShaderResourceBindingDesc& binding); bool HasPass(const Containers::String& passName) const; const ShaderPass* FindPass(const Containers::String& passName) const; ShaderPass* FindPass(const Containers::String& passName); + const ShaderResourceBindingDesc* FindPassResourceBinding( + const Containers::String& passName, + const Containers::String& resourceName) const; const ShaderStageVariant* FindVariant( const Containers::String& passName, ShaderType stage, @@ -129,6 +176,7 @@ private: Containers::Array m_uniforms; Containers::Array m_attributes; + Containers::Array m_properties; Containers::Array m_passes; class IRHIShader* m_rhiResource = nullptr; diff --git a/engine/src/Resources/BuiltinResources.cpp b/engine/src/Resources/BuiltinResources.cpp index bd549855..d2f2d4c9 100644 --- a/engine/src/Resources/BuiltinResources.cpp +++ b/engine/src/Resources/BuiltinResources.cpp @@ -617,12 +617,22 @@ Mesh* BuildMeshResource( size_t CalculateBuiltinShaderMemorySize(const Shader& shader) { size_t memorySize = sizeof(Shader) + shader.GetName().Length() + shader.GetPath().Length(); + for (const ShaderPropertyDesc& property : shader.GetProperties()) { + memorySize += property.name.Length(); + memorySize += property.displayName.Length(); + memorySize += property.defaultValue.Length(); + memorySize += property.semantic.Length(); + } for (const ShaderPass& pass : shader.GetPasses()) { memorySize += pass.name.Length(); for (const ShaderPassTagEntry& tag : pass.tags) { memorySize += tag.name.Length(); memorySize += tag.value.Length(); } + for (const ShaderResourceBindingDesc& binding : pass.resources) { + memorySize += binding.name.Length(); + memorySize += binding.semantic.Length(); + } for (const ShaderStageVariant& variant : pass.variants) { memorySize += variant.entryPoint.Length(); memorySize += variant.profile.Length(); diff --git a/engine/src/Resources/Shader/Shader.cpp b/engine/src/Resources/Shader/Shader.cpp index d8249eaa..875cd1c2 100644 --- a/engine/src/Resources/Shader/Shader.cpp +++ b/engine/src/Resources/Shader/Shader.cpp @@ -20,6 +20,7 @@ void Shader::Release() { m_compiledBinary.Clear(); m_uniforms.Clear(); m_attributes.Clear(); + m_properties.Clear(); m_passes.Clear(); m_rhiResource = nullptr; m_isValid = false; @@ -53,6 +54,31 @@ void Shader::AddAttribute(const ShaderAttribute& attribute) { m_attributes.PushBack(attribute); } +void Shader::AddProperty(const ShaderPropertyDesc& property) { + for (ShaderPropertyDesc& existingProperty : m_properties) { + if (existingProperty.name == property.name) { + existingProperty = property; + return; + } + } + + m_properties.PushBack(property); +} + +void Shader::ClearProperties() { + m_properties.Clear(); +} + +const ShaderPropertyDesc* Shader::FindProperty(const Containers::String& propertyName) const { + for (const ShaderPropertyDesc& property : m_properties) { + if (property.name == propertyName) { + return &property; + } + } + + return nullptr; +} + void Shader::AddPass(const ShaderPass& pass) { m_passes.PushBack(pass); } @@ -85,6 +111,20 @@ void Shader::SetPassTag( tag.value = tagValue; } +void Shader::AddPassResourceBinding( + const Containers::String& passName, + const ShaderResourceBindingDesc& binding) { + ShaderPass& pass = GetOrCreatePass(passName); + for (ShaderResourceBindingDesc& existingBinding : pass.resources) { + if (existingBinding.name == binding.name) { + existingBinding = binding; + return; + } + } + + pass.resources.PushBack(binding); +} + bool Shader::HasPass(const Containers::String& passName) const { return FindPass(passName) != nullptr; } @@ -109,6 +149,23 @@ ShaderPass* Shader::FindPass(const Containers::String& passName) { return nullptr; } +const ShaderResourceBindingDesc* Shader::FindPassResourceBinding( + const Containers::String& passName, + const Containers::String& resourceName) const { + const ShaderPass* pass = FindPass(passName); + if (pass == nullptr) { + return nullptr; + } + + for (const ShaderResourceBindingDesc& binding : pass->resources) { + if (binding.name == resourceName) { + return &binding; + } + } + + return nullptr; +} + const ShaderStageVariant* Shader::FindVariant( const Containers::String& passName, ShaderType stage, diff --git a/engine/src/Resources/Shader/ShaderLoader.cpp b/engine/src/Resources/Shader/ShaderLoader.cpp index 2f82c5df..4d76ed43 100644 --- a/engine/src/Resources/Shader/ShaderLoader.cpp +++ b/engine/src/Resources/Shader/ShaderLoader.cpp @@ -431,6 +431,62 @@ bool TryParseShaderBackend(const Containers::String& value, ShaderBackend& outBa return false; } +bool TryParseShaderPropertyType(const Containers::String& value, ShaderPropertyType& outType) { + const Containers::String normalized = value.Trim().ToLower(); + if (normalized == "float") { + outType = ShaderPropertyType::Float; + return true; + } + if (normalized == "range") { + outType = ShaderPropertyType::Range; + return true; + } + if (normalized == "int" || normalized == "integer") { + outType = ShaderPropertyType::Int; + return true; + } + if (normalized == "vector" || normalized == "float4") { + outType = ShaderPropertyType::Vector; + return true; + } + if (normalized == "color") { + outType = ShaderPropertyType::Color; + return true; + } + if (normalized == "2d" || normalized == "texture2d" || normalized == "texture") { + outType = ShaderPropertyType::Texture2D; + return true; + } + if (normalized == "cube" || normalized == "cubemap" || normalized == "texturecube") { + outType = ShaderPropertyType::TextureCube; + return true; + } + + return false; +} + +bool TryParseShaderResourceType(const Containers::String& value, ShaderResourceType& outType) { + const Containers::String normalized = value.Trim().ToLower(); + if (normalized == "constantbuffer" || normalized == "cbuffer" || normalized == "cbv") { + outType = ShaderResourceType::ConstantBuffer; + return true; + } + if (normalized == "texture2d" || normalized == "texture" || normalized == "srvtexture2d") { + outType = ShaderResourceType::Texture2D; + return true; + } + if (normalized == "texturecube" || normalized == "cubemap") { + outType = ShaderResourceType::TextureCube; + return true; + } + if (normalized == "sampler" || normalized == "samplerstate") { + outType = ShaderResourceType::Sampler; + return true; + } + + return false; +} + Containers::String GetDefaultEntryPoint(ShaderLanguage language, ShaderType stage) { if (language != ShaderLanguage::HLSL) { return Containers::String("main"); @@ -520,14 +576,47 @@ bool ReadTextFile(const Containers::String& path, Containers::String& outText) { return true; } +bool TryParseUnsignedValue(const std::string& json, const char* key, Core::uint32& outValue) { + size_t valuePos = 0; + if (!FindValueStart(json, key, valuePos)) { + return false; + } + + size_t endPos = valuePos; + while (endPos < json.size() && std::isdigit(static_cast(json[endPos])) != 0) { + ++endPos; + } + + if (endPos == valuePos) { + return false; + } + + try { + outValue = static_cast(std::stoul(json.substr(valuePos, endPos - valuePos))); + return true; + } catch (...) { + return false; + } +} + size_t CalculateShaderMemorySize(const Shader& shader) { size_t memorySize = sizeof(Shader) + shader.GetName().Length() + shader.GetPath().Length(); + for (const ShaderPropertyDesc& property : shader.GetProperties()) { + memorySize += property.name.Length(); + memorySize += property.displayName.Length(); + memorySize += property.defaultValue.Length(); + memorySize += property.semantic.Length(); + } for (const ShaderPass& pass : shader.GetPasses()) { memorySize += pass.name.Length(); for (const ShaderPassTagEntry& tag : pass.tags) { memorySize += tag.name.Length(); memorySize += tag.value.Length(); } + for (const ShaderResourceBindingDesc& binding : pass.resources) { + memorySize += binding.name.Length(); + memorySize += binding.semantic.Length(); + } for (const ShaderStageVariant& variant : pass.variants) { memorySize += variant.entryPoint.Length(); memorySize += variant.profile.Length(); @@ -583,6 +672,38 @@ LoadResult LoadShaderManifest(const Containers::String& path, const std::string& shader->Initialize(params); + std::string propertiesArray; + if (TryExtractArray(jsonText, "properties", propertiesArray)) { + std::vector propertyObjects; + if (!SplitTopLevelArrayElements(propertiesArray, propertyObjects)) { + return LoadResult("Shader manifest properties array could not be parsed: " + path); + } + + for (const std::string& propertyObject : propertyObjects) { + ShaderPropertyDesc property = {}; + if (!TryParseStringValue(propertyObject, "name", property.name) || property.name.Empty()) { + return LoadResult("Shader manifest property is missing a valid name: " + path); + } + + Containers::String propertyTypeName; + if (!TryParseStringValue(propertyObject, "type", propertyTypeName) || + !TryParseShaderPropertyType(propertyTypeName, property.type)) { + return LoadResult("Shader manifest property has an invalid type: " + path); + } + + if (!TryParseStringValue(propertyObject, "displayName", property.displayName)) { + property.displayName = property.name; + } + + if (!TryParseStringValue(propertyObject, "defaultValue", property.defaultValue)) { + TryParseStringValue(propertyObject, "default", property.defaultValue); + } + + TryParseStringValue(propertyObject, "semantic", property.semantic); + shader->AddProperty(property); + } + } + for (const std::string& passObject : passObjects) { Containers::String passName; if (!TryParseStringValue(passObject, "name", passName) || passName.Empty()) { @@ -600,6 +721,38 @@ LoadResult LoadShaderManifest(const Containers::String& path, const std::string& } } + std::string resourcesArray; + if (TryExtractArray(passObject, "resources", resourcesArray)) { + std::vector resourceObjects; + if (!SplitTopLevelArrayElements(resourcesArray, resourceObjects)) { + return LoadResult("Shader manifest pass resources could not be parsed: " + path); + } + + for (const std::string& resourceObject : resourceObjects) { + ShaderResourceBindingDesc resourceBinding = {}; + if (!TryParseStringValue(resourceObject, "name", resourceBinding.name) || + resourceBinding.name.Empty()) { + return LoadResult("Shader manifest pass resource is missing a valid name: " + path); + } + + Containers::String resourceTypeName; + if (!TryParseStringValue(resourceObject, "type", resourceTypeName) || + !TryParseShaderResourceType(resourceTypeName, resourceBinding.type)) { + return LoadResult("Shader manifest pass resource has an invalid type: " + path); + } + + if (!TryParseUnsignedValue(resourceObject, "set", resourceBinding.set)) { + return LoadResult("Shader manifest pass resource is missing a valid set: " + path); + } + if (!TryParseUnsignedValue(resourceObject, "binding", resourceBinding.binding)) { + return LoadResult("Shader manifest pass resource is missing a valid binding: " + path); + } + + TryParseStringValue(resourceObject, "semantic", resourceBinding.semantic); + shader->AddPassResourceBinding(passName, resourceBinding); + } + } + std::string variantsArray; if (!TryExtractArray(passObject, "variants", variantsArray)) { return LoadResult("Shader manifest pass is missing variants: " + path); diff --git a/tests/Rendering/unit/test_render_scene_extractor.cpp b/tests/Rendering/unit/test_render_scene_extractor.cpp index 9dd32859..5916c029 100644 --- a/tests/Rendering/unit/test_render_scene_extractor.cpp +++ b/tests/Rendering/unit/test_render_scene_extractor.cpp @@ -446,6 +446,39 @@ TEST(RenderMaterialUtility_Test, ResolvesBuiltinForwardMaterialContractFromCanon EXPECT_EQ(ResolveBuiltinBaseColorTexture(&aliasMaterial), baseColorTexture); } +TEST(RenderMaterialUtility_Test, ResolvesBuiltinForwardMaterialContractFromShaderSemanticMetadata) { + auto* shader = new Shader(); + + ShaderPropertyDesc colorProperty = {}; + colorProperty.name = "TintColor"; + colorProperty.displayName = "Tint"; + colorProperty.type = ShaderPropertyType::Color; + colorProperty.semantic = "BaseColor"; + shader->AddProperty(colorProperty); + + ShaderPropertyDesc textureProperty = {}; + textureProperty.name = "AlbedoMap"; + textureProperty.displayName = "Albedo"; + textureProperty.type = ShaderPropertyType::Texture2D; + textureProperty.semantic = "BaseColorTexture"; + shader->AddProperty(textureProperty); + + Material material; + material.SetShader(ResourceHandle(shader)); + material.SetFloat4("TintColor", Vector4(0.3f, 0.5f, 0.7f, 0.9f)); + + Texture* texture = new Texture(); + IResource::ConstructParams textureParams = {}; + textureParams.name = "SemanticTexture"; + textureParams.path = "Textures/semantic_base_color.texture"; + textureParams.guid = ResourceGUID::Generate(textureParams.path); + texture->Initialize(textureParams); + material.SetTexture("AlbedoMap", ResourceHandle(texture)); + + EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(0.3f, 0.5f, 0.7f, 0.9f)); + EXPECT_EQ(ResolveBuiltinBaseColorTexture(&material), texture); +} + TEST(RenderMaterialUtility_Test, UsesOpacityOnlyWhenBaseColorFactorIsMissing) { Material material; material.SetFloat("opacity", 0.35f); diff --git a/tests/Resources/Shader/test_shader.cpp b/tests/Resources/Shader/test_shader.cpp index 7bd49fca..1c70fd95 100644 --- a/tests/Resources/Shader/test_shader.cpp +++ b/tests/Resources/Shader/test_shader.cpp @@ -158,9 +158,48 @@ TEST(Shader, StoresPerPassTags) { EXPECT_EQ(pass->tags[1].value, "Geometry"); } +TEST(Shader, StoresShaderPropertiesAndPassResources) { + Shader shader; + + ShaderPropertyDesc baseColor = {}; + baseColor.name = "_BaseColor"; + baseColor.displayName = "Base Color"; + baseColor.type = ShaderPropertyType::Color; + baseColor.defaultValue = "(1,1,1,1)"; + baseColor.semantic = "BaseColor"; + shader.AddProperty(baseColor); + + ShaderResourceBindingDesc perObject = {}; + perObject.name = "PerObjectConstants"; + perObject.type = ShaderResourceType::ConstantBuffer; + perObject.set = 1; + perObject.binding = 0; + perObject.semantic = "PerObject"; + shader.AddPassResourceBinding("ForwardLit", perObject); + + ASSERT_EQ(shader.GetProperties().Size(), 1u); + const ShaderPropertyDesc* storedProperty = shader.FindProperty("_BaseColor"); + ASSERT_NE(storedProperty, nullptr); + EXPECT_EQ(storedProperty->displayName, "Base Color"); + EXPECT_EQ(storedProperty->type, ShaderPropertyType::Color); + EXPECT_EQ(storedProperty->semantic, "BaseColor"); + + const ShaderResourceBindingDesc* storedBinding = + shader.FindPassResourceBinding("ForwardLit", "PerObjectConstants"); + ASSERT_NE(storedBinding, nullptr); + EXPECT_EQ(storedBinding->type, ShaderResourceType::ConstantBuffer); + EXPECT_EQ(storedBinding->set, 1u); + EXPECT_EQ(storedBinding->binding, 0u); + EXPECT_EQ(storedBinding->semantic, "PerObject"); +} + TEST(Shader, ReleaseClearsPassRuntimeData) { Shader shader; shader.SetSourceCode("void main() {}"); + ShaderPropertyDesc property = {}; + property.name = "_BaseColor"; + property.type = ShaderPropertyType::Color; + shader.AddProperty(property); ShaderStageVariant variant = {}; variant.stage = ShaderType::Fragment; variant.sourceCode = "fragment"; @@ -168,6 +207,7 @@ TEST(Shader, ReleaseClearsPassRuntimeData) { shader.Release(); + EXPECT_EQ(shader.GetProperties().Size(), 0u); EXPECT_EQ(shader.GetPassCount(), 0u); EXPECT_EQ(shader.GetSourceCode(), ""); EXPECT_EQ(shader.GetCompiledBinary().Size(), 0u); diff --git a/tests/Resources/Shader/test_shader_loader.cpp b/tests/Resources/Shader/test_shader_loader.cpp index e810f6ee..1d1169ae 100644 --- a/tests/Resources/Shader/test_shader_loader.cpp +++ b/tests/Resources/Shader/test_shader_loader.cpp @@ -107,6 +107,22 @@ TEST(ShaderLoader, LoadShaderManifestBuildsMultiPassBackendVariants) { ASSERT_TRUE(manifest.is_open()); manifest << "{\n"; manifest << " \"name\": \"TestLitShader\",\n"; + manifest << " \"properties\": [\n"; + manifest << " {\n"; + manifest << " \"name\": \"_BaseColor\",\n"; + manifest << " \"displayName\": \"Base Color\",\n"; + manifest << " \"type\": \"Color\",\n"; + manifest << " \"defaultValue\": \"(1,1,1,1)\",\n"; + manifest << " \"semantic\": \"BaseColor\"\n"; + manifest << " },\n"; + manifest << " {\n"; + manifest << " \"name\": \"_MainTex\",\n"; + manifest << " \"displayName\": \"Base Map\",\n"; + manifest << " \"type\": \"2D\",\n"; + manifest << " \"defaultValue\": \"white\",\n"; + manifest << " \"semantic\": \"BaseColorTexture\"\n"; + manifest << " }\n"; + manifest << " ],\n"; manifest << " \"passes\": [\n"; manifest << " {\n"; manifest << " \"name\": \"ForwardLit\",\n"; @@ -114,6 +130,12 @@ TEST(ShaderLoader, LoadShaderManifestBuildsMultiPassBackendVariants) { manifest << " \"LightMode\": \"ForwardBase\",\n"; manifest << " \"Queue\": \"Geometry\"\n"; manifest << " },\n"; + manifest << " \"resources\": [\n"; + manifest << " { \"name\": \"PerObjectConstants\", \"type\": \"ConstantBuffer\", \"set\": 1, \"binding\": 0, \"semantic\": \"PerObject\" },\n"; + manifest << " { \"name\": \"MaterialConstants\", \"type\": \"ConstantBuffer\", \"set\": 2, \"binding\": 0, \"semantic\": \"Material\" },\n"; + manifest << " { \"name\": \"BaseColorTexture\", \"type\": \"Texture2D\", \"set\": 3, \"binding\": 0, \"semantic\": \"BaseColorTexture\" },\n"; + manifest << " { \"name\": \"LinearClampSampler\", \"type\": \"Sampler\", \"set\": 4, \"binding\": 0, \"semantic\": \"LinearClampSampler\" }\n"; + manifest << " ],\n"; manifest << " \"variants\": [\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/forward_lit.vs.hlsl\", \"entryPoint\": \"MainVS\", \"profile\": \"vs_5_0\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/forward_lit.ps.hlsl\", \"entryPoint\": \"MainPS\", \"profile\": \"ps_5_0\" },\n"; @@ -146,16 +168,39 @@ TEST(ShaderLoader, LoadShaderManifestBuildsMultiPassBackendVariants) { ASSERT_NE(shader, nullptr); ASSERT_TRUE(shader->IsValid()); EXPECT_EQ(shader->GetName(), "TestLitShader"); + ASSERT_EQ(shader->GetProperties().Size(), 2u); ASSERT_EQ(shader->GetPassCount(), 2u); + const ShaderPropertyDesc* baseColorProperty = shader->FindProperty("_BaseColor"); + ASSERT_NE(baseColorProperty, nullptr); + EXPECT_EQ(baseColorProperty->displayName, "Base Color"); + EXPECT_EQ(baseColorProperty->type, ShaderPropertyType::Color); + EXPECT_EQ(baseColorProperty->defaultValue, "(1,1,1,1)"); + EXPECT_EQ(baseColorProperty->semantic, "BaseColor"); + + const ShaderPropertyDesc* baseMapProperty = shader->FindProperty("_MainTex"); + ASSERT_NE(baseMapProperty, nullptr); + EXPECT_EQ(baseMapProperty->type, ShaderPropertyType::Texture2D); + EXPECT_EQ(baseMapProperty->defaultValue, "white"); + EXPECT_EQ(baseMapProperty->semantic, "BaseColorTexture"); + const ShaderPass* forwardLitPass = shader->FindPass("ForwardLit"); ASSERT_NE(forwardLitPass, nullptr); ASSERT_EQ(forwardLitPass->tags.Size(), 2u); + ASSERT_EQ(forwardLitPass->resources.Size(), 4u); EXPECT_EQ(forwardLitPass->tags[0].name, "LightMode"); EXPECT_EQ(forwardLitPass->tags[0].value, "ForwardBase"); EXPECT_EQ(forwardLitPass->tags[1].name, "Queue"); EXPECT_EQ(forwardLitPass->tags[1].value, "Geometry"); + const ShaderResourceBindingDesc* baseTextureBinding = + shader->FindPassResourceBinding("ForwardLit", "BaseColorTexture"); + ASSERT_NE(baseTextureBinding, nullptr); + EXPECT_EQ(baseTextureBinding->type, ShaderResourceType::Texture2D); + EXPECT_EQ(baseTextureBinding->set, 3u); + EXPECT_EQ(baseTextureBinding->binding, 0u); + EXPECT_EQ(baseTextureBinding->semantic, "BaseColorTexture"); + const ShaderStageVariant* d3d12Vertex = shader->FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::D3D12); ASSERT_NE(d3d12Vertex, nullptr); EXPECT_EQ(d3d12Vertex->entryPoint, "MainVS"); @@ -252,11 +297,31 @@ TEST(ShaderLoader, LoadBuiltinForwardLitShaderBuildsBackendVariants) { const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); + ASSERT_EQ(shader->GetProperties().Size(), 2u); ASSERT_EQ(pass->variants.Size(), 6u); ASSERT_EQ(pass->tags.Size(), 1u); + ASSERT_EQ(pass->resources.Size(), 4u); EXPECT_EQ(pass->tags[0].name, "LightMode"); EXPECT_EQ(pass->tags[0].value, "ForwardBase"); + const ShaderPropertyDesc* baseColorProperty = shader->FindProperty("_BaseColor"); + ASSERT_NE(baseColorProperty, nullptr); + EXPECT_EQ(baseColorProperty->type, ShaderPropertyType::Color); + EXPECT_EQ(baseColorProperty->semantic, "BaseColor"); + + const ShaderPropertyDesc* baseMapProperty = shader->FindProperty("_MainTex"); + ASSERT_NE(baseMapProperty, nullptr); + EXPECT_EQ(baseMapProperty->type, ShaderPropertyType::Texture2D); + EXPECT_EQ(baseMapProperty->semantic, "BaseColorTexture"); + + const ShaderResourceBindingDesc* perObjectBinding = + shader->FindPassResourceBinding("ForwardLit", "PerObjectConstants"); + ASSERT_NE(perObjectBinding, nullptr); + EXPECT_EQ(perObjectBinding->type, ShaderResourceType::ConstantBuffer); + EXPECT_EQ(perObjectBinding->set, 1u); + EXPECT_EQ(perObjectBinding->binding, 0u); + EXPECT_EQ(perObjectBinding->semantic, "PerObject"); + EXPECT_NE(shader->FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::OpenGL), nullptr);