From 1fa1eaad158fae98e0339411e3fead04a6b120c9 Mon Sep 17 00:00:00 2001 From: santorac <55155825+santorac@users.noreply.github.com> Date: Tue, 28 Dec 2021 17:51:43 -0800 Subject: [PATCH] Added unit tests for the new functionality. I found a mistake where MaterialAssetCreator needs to clear the raw data when configured to finalize the material asset. Since MaterialSourceData no longer relies on the material type source file at all, I was able to change MaterialSourceDataTest to avoid saving the source data to disk. Signed-off-by: santorac <55155825+santorac@users.noreply.github.com> --- .../Atom/RPI.Edit/Material/MaterialUtils.h | 1 - .../Atom/RPI.Reflect/Material/MaterialAsset.h | 1 - .../RPI.Edit/Material/MaterialUtils.cpp | 1 - .../Material/MaterialAssetCreator.cpp | 5 + .../Material/MaterialSourceDataTests.cpp | 248 ++++++++++++++++-- 5 files changed, 230 insertions(+), 26 deletions(-) diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialUtils.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialUtils.h index 2a992159b3..2fb55e5f40 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialUtils.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialUtils.h @@ -69,7 +69,6 @@ namespace AZ //! Finalizing during asset processing reduces load times and obfuscates the material data. //! Waiting to finalize at load time reduces dependencies on the material type data, resulting in fewer asset rebuilds and less time spent processing assets. bool BuildersShouldFinalizeMaterialAssets(); - } } } diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialAsset.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialAsset.h index 736d7c5b53..4cc6608225 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialAsset.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialAsset.h @@ -15,7 +15,6 @@ #include #include #include -#include #include #include diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialUtils.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialUtils.cpp index 2fe30f632f..475c6ca219 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialUtils.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialUtils.cpp @@ -150,7 +150,6 @@ namespace AZ return shouldFinalize; } - } } } diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAssetCreator.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAssetCreator.cpp index f05fb8d848..7d4ecf0b18 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAssetCreator.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAssetCreator.cpp @@ -48,6 +48,11 @@ namespace AZ m_asset->Finalize( [this](const char* message) { ReportWarning("%s", message); }, [this](const char* message) { ReportError("%s", message); }); + + // Finalize() doesn't clear the raw property data because that's the same function used at runtime, which does need to maintain the raw data + // to support hot reload. But here we are pre-baking with the assumption that AP build dependencies will keep the material type + // and material asset in sync, so we can discard the raw property data and just rely on the data in the material type asset. + m_asset->m_rawPropertyValues.clear(); } return EndCommon(result); diff --git a/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp b/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp index 73aeacf818..e4d3cc9768 100644 --- a/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -65,9 +66,29 @@ namespace UnitTest m_testShaderAsset = CreateTestShaderAsset(Uuid::CreateRandom(), m_testMaterialSrgLayout); m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.shader", m_testShaderAsset.GetId()); - // The MaterialSourceData relies on both MaterialTypeSourceData and MaterialTypeAsset. We have to make sure the - // .materialtype file is present on disk, and that the MaterialTypeAsset is available through the asset database stub... + m_testMaterialTypeAsset = CreateTestMaterialTypeAsset(Uuid::CreateRandom()); + // Since this test doesn't actually instantiate a Material, it won't need to instantiate this ImageAsset, so all we + // need is an asset reference with a valid ID. + m_testImageAsset = Data::Asset{ Data::AssetId{Uuid::CreateRandom(), StreamingImageAsset::GetImageAssetSubId()}, azrtti_typeid() }; + + // Register the test assets with the AssetSystemStub so CreateMaterialAsset() can use AssetUtils. + m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.materialtype", m_testMaterialTypeAsset.GetId()); + m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.streamingimage", m_testImageAsset.GetId()); + } + + void TearDown() override + { + m_testMaterialTypeAsset.Reset(); + m_testMaterialSrgLayout = nullptr; + m_testShaderAsset.Reset(); + m_testImageAsset.Reset(); + + RPITestFixture::TearDown(); + } + + Data::Asset CreateTestMaterialTypeAsset(Data::AssetId assetId) + { const char* materialTypeJson = R"( { "version": 10, @@ -122,29 +143,10 @@ namespace UnitTest } )"; - AZ::Utils::WriteFile(materialTypeJson, "@exefolder@/Temp/test.materialtype"); MaterialTypeSourceData materialTypeSourceData; LoadTestDataFromJson(materialTypeSourceData, materialTypeJson); - m_testMaterialTypeAsset = materialTypeSourceData.CreateMaterialTypeAsset(Uuid::CreateRandom()).TakeValue(); - - // Since this test doesn't actually instantiate a Material, it won't need to instantiate this ImageAsset, so all we - // need is an asset reference with a valid ID. - m_testImageAsset = Data::Asset{ Data::AssetId{Uuid::CreateRandom(), StreamingImageAsset::GetImageAssetSubId()}, azrtti_typeid() }; - - // Register the test assets with the AssetSystemStub so CreateMaterialAsset() can use AssetUtils. - m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.materialtype", m_testMaterialTypeAsset.GetId()); - m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.streamingimage", m_testImageAsset.GetId()); - } - - void TearDown() override - { - m_testMaterialTypeAsset.Reset(); - m_testMaterialSrgLayout = nullptr; - m_testShaderAsset.Reset(); - m_testImageAsset.Reset(); - - RPITestFixture::TearDown(); + return materialTypeSourceData.CreateMaterialTypeAsset(assetId).TakeValue(); } }; @@ -180,7 +182,10 @@ namespace UnitTest Data::Asset materialAsset = materialAssetOutcome.GetValue(); - // The order here is based on the order in the MaterialTypeSourceData, as added to the the MaterialTypeAssetCreator. + EXPECT_TRUE(materialAsset->IsFinalized()); + EXPECT_EQ(0, materialAsset->GetRawPropertyValues().size()); // A pre-baked material has no need for the original raw property names and values + + // The order here is based on the order in the MaterialTypeSourceData, as added to the MaterialTypeAssetCreator. EXPECT_EQ(materialAsset->GetPropertyValues()[0].GetValue(), true); EXPECT_EQ(materialAsset->GetPropertyValues()[1].GetValue(), -10); EXPECT_EQ(materialAsset->GetPropertyValues()[2].GetValue(), 25u); @@ -192,6 +197,105 @@ namespace UnitTest EXPECT_EQ(materialAsset->GetPropertyValues()[8].GetValue>(), m_testImageAsset); EXPECT_EQ(materialAsset->GetPropertyValues()[9].GetValue(), 1u); } + + TEST_F(MaterialSourceDataTests, CreateMaterialAsset_DeferredBake) + { + // This test is similar to CreateMaterialAsset_BasicProperties but uses MaterialAssetProcessingMode::DeferredBake instead of PreBake. + + Data::AssetId materialTypeAssetId = Uuid::CreateRandom(); + + // This material type asset will be known by the asset system (stub) but doesn't exist in the AssetManager. + // This demonstrates that the CreateMaterialAsset does not attempt to access the MaterialTypeAsset data in MaterialAssetProcessingMode::DeferredBake. + m_assetSystemStub.RegisterSourceInfo("testDeferredBake.materialtype", materialTypeAssetId); + + MaterialSourceData sourceData; + + sourceData.m_materialType = "testDeferredBake.materialtype"; + AddPropertyGroup(sourceData, "general"); + AddProperty(sourceData, "general", "MyBool" , true); + AddProperty(sourceData, "general", "MyInt" , -10); + AddProperty(sourceData, "general", "MyUInt" , 25u); + AddProperty(sourceData, "general", "MyFloat" , 1.5f); + AddProperty(sourceData, "general", "MyColor" , AZ::Color{0.1f, 0.2f, 0.3f, 0.4f}); + AddProperty(sourceData, "general", "MyFloat2", AZ::Vector2(2.1f, 2.2f)); + AddProperty(sourceData, "general", "MyFloat3", AZ::Vector3(3.1f, 3.2f, 3.3f)); + AddProperty(sourceData, "general", "MyFloat4", AZ::Vector4(4.1f, 4.2f, 4.3f, 4.4f)); + AddProperty(sourceData, "general", "MyImage" , AZStd::string("@exefolder@/Temp/test.streamingimage")); + AddProperty(sourceData, "general", "MyEnum" , AZStd::string("Enum1")); + + auto materialAssetOutcome = sourceData.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::DeferredBake, true); + EXPECT_TRUE(materialAssetOutcome.IsSuccess()); + + Data::Asset materialAsset = materialAssetOutcome.GetValue(); + + EXPECT_FALSE(materialAsset->IsFinalized()); + // Note we avoid calling GetPropertyValues() because that will auto-finalize the material. We want to check its raw property values first. + + auto findRawPropertyValue = [materialAsset](const char* propertyId) + { + auto iter = AZStd::find_if(materialAsset->GetRawPropertyValues().begin(), materialAsset->GetRawPropertyValues().end(), [propertyId](const AZStd::pair& pair) + { + return pair.first == AZ::Name{propertyId}; + }); + + if (iter == materialAsset->GetRawPropertyValues().end()) + { + return MaterialPropertyValue{}; + } + else + { + return iter->second; + } + }; + + auto checkRawPropertyValues = [findRawPropertyValue, this]() + { + EXPECT_EQ(findRawPropertyValue("general.MyBool" ).GetValue(), true); + EXPECT_EQ(findRawPropertyValue("general.MyInt" ).GetValue(), -10); + EXPECT_EQ(findRawPropertyValue("general.MyUInt" ).GetValue(), 25u); + EXPECT_EQ(findRawPropertyValue("general.MyFloat" ).GetValue(), 1.5f); + EXPECT_EQ(findRawPropertyValue("general.MyFloat2").GetValue(), Vector2(2.1f, 2.2f)); + EXPECT_EQ(findRawPropertyValue("general.MyFloat3").GetValue(), Vector3(3.1f, 3.2f, 3.3f)); + EXPECT_EQ(findRawPropertyValue("general.MyFloat4").GetValue(), Vector4(4.1f, 4.2f, 4.3f, 4.4f)); + EXPECT_EQ(findRawPropertyValue("general.MyColor" ).GetValue(), Color(0.1f, 0.2f, 0.3f, 0.4f)); + EXPECT_EQ(findRawPropertyValue("general.MyImage" ).GetValue>(), m_testImageAsset); + // The raw value for an enum is the original string, not the numerical value, because the material type holds the necessary metadata to match the name to the value. + EXPECT_EQ(findRawPropertyValue("general.MyEnum" ).GetValue(), AZStd::string("Enum1")); + }; + + // We check the raw property values before the material type asset is even available + checkRawPropertyValues(); + + // Now we'll create the material type asset in memory so the material will have what it needs to finalize itself. + Data::Asset testMaterialTypeAsset = CreateTestMaterialTypeAsset(materialTypeAssetId); + + // The MaterialAsset is still holding an reference to an unloaded asset, so we run it through the serializer which causes the loaded MaterialAsset + // to have access to the testMaterialTypeAsset. This is similar to how the AP would save the MaterialAsset to the cache and the runtime would load it. + SerializeTester tester(GetSerializeContext()); + tester.SerializeOut(materialAsset.Get()); + materialAsset = tester.SerializeIn(Uuid::CreateRandom(), ObjectStream::FilterDescriptor{AZ::Data::AssetFilterNoAssetLoading}); + + // We check the raw property values again on the loaded data, showing that the same data is available in the original un-finalized state. + checkRawPropertyValues(); + + // The material will automatically finalize itself when the properties are accessed. + EXPECT_FALSE(materialAsset->IsFinalized()); + materialAsset->GetPropertyValues(); + EXPECT_TRUE(materialAsset->IsFinalized()); + + // Now all the property values should be available through the main GetPropertyValues() API. + EXPECT_EQ(materialAsset->GetPropertyValues()[0].GetValue(), true); + EXPECT_EQ(materialAsset->GetPropertyValues()[1].GetValue(), -10); + EXPECT_EQ(materialAsset->GetPropertyValues()[2].GetValue(), 25u); + EXPECT_EQ(materialAsset->GetPropertyValues()[3].GetValue(), 1.5f); + EXPECT_EQ(materialAsset->GetPropertyValues()[4].GetValue(), Vector2(2.1f, 2.2f)); + EXPECT_EQ(materialAsset->GetPropertyValues()[5].GetValue(), Vector3(3.1f, 3.2f, 3.3f)); + EXPECT_EQ(materialAsset->GetPropertyValues()[6].GetValue(), Vector4(4.1f, 4.2f, 4.3f, 4.4f)); + EXPECT_EQ(materialAsset->GetPropertyValues()[7].GetValue(), Color(0.1f, 0.2f, 0.3f, 0.4f)); + EXPECT_EQ(materialAsset->GetPropertyValues()[8].GetValue>(), m_testImageAsset); + EXPECT_EQ(materialAsset->GetPropertyValues()[9].GetValue(), 1u); + + } void CheckEqual(MaterialSourceData& a, MaterialSourceData& b) { @@ -546,16 +650,19 @@ namespace UnitTest auto materialAssetLevel1 = sourceDataLevel1.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::PreBake, true); EXPECT_TRUE(materialAssetLevel1.IsSuccess()); + EXPECT_TRUE(materialAssetLevel1.GetValue()->IsFinalized()); m_assetSystemStub.RegisterSourceInfo("level1.material", materialAssetLevel1.GetValue().GetId()); auto materialAssetLevel2 = sourceDataLevel2.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::PreBake, true); EXPECT_TRUE(materialAssetLevel2.IsSuccess()); + EXPECT_TRUE(materialAssetLevel2.GetValue()->IsFinalized()); m_assetSystemStub.RegisterSourceInfo("level2.material", materialAssetLevel2.GetValue().GetId()); auto materialAssetLevel3 = sourceDataLevel3.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::PreBake, true); EXPECT_TRUE(materialAssetLevel3.IsSuccess()); + EXPECT_TRUE(materialAssetLevel3.GetValue()->IsFinalized()); auto layout = m_testMaterialTypeAsset->GetMaterialPropertiesLayout(); MaterialPropertyIndex myFloat = layout->FindPropertyIndex(Name("general.MyFloat")); @@ -582,6 +689,101 @@ namespace UnitTest EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue(), Vector2(4.1f, 4.2f)); EXPECT_EQ(properties[myColor.GetIndex()].GetValue(), Color(0.15f, 0.25f, 0.35f, 0.45f)); } + + TEST_F(MaterialSourceDataTests, CreateMaterialAsset_MultiLevelDataInheritance_DeferredBake) + { + // This test is similar to CreateMaterialAsset_MultiLevelDataInheritance but uses MaterialAssetProcessingMode::DeferredBake instead of PreBake. + + Data::AssetId materialTypeAssetId = Uuid::CreateRandom(); + + // This material type asset will be known by the asset system (stub) but doesn't exist in the AssetManager. + // This demonstrates that the CreateMaterialAsset does not attempt to access the MaterialTypeAsset data in MaterialAssetProcessingMode::DeferredBake. + m_assetSystemStub.RegisterSourceInfo("testDeferredBake.materialtype", materialTypeAssetId); + + MaterialSourceData sourceDataLevel1; + sourceDataLevel1.m_materialType = "testDeferredBake.materialtype"; + AddPropertyGroup(sourceDataLevel1, "general"); + AddProperty(sourceDataLevel1, "general", "MyFloat", 1.5f); + AddProperty(sourceDataLevel1, "general", "MyColor", AZ::Color{0.1f, 0.2f, 0.3f, 0.4f}); + + MaterialSourceData sourceDataLevel2; + sourceDataLevel2.m_materialType = "testDeferredBake.materialtype"; + sourceDataLevel2.m_parentMaterial = "level1.material"; + AddPropertyGroup(sourceDataLevel2, "general"); + AddProperty(sourceDataLevel2, "general", "MyColor", AZ::Color{0.15f, 0.25f, 0.35f, 0.45f}); + AddProperty(sourceDataLevel2, "general", "MyFloat2", AZ::Vector2{4.1f, 4.2f}); + + MaterialSourceData sourceDataLevel3; + sourceDataLevel3.m_materialType = "testDeferredBake.materialtype"; + sourceDataLevel3.m_parentMaterial = "level2.material"; + AddPropertyGroup(sourceDataLevel3, "general"); + AddProperty(sourceDataLevel3, "general", "MyFloat", 3.5f); + + auto materialAssetLevel1Result = sourceDataLevel1.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::DeferredBake, true); + EXPECT_TRUE(materialAssetLevel1Result.IsSuccess()); + Data::Asset materialAssetLevel1 = materialAssetLevel1Result.TakeValue(); + EXPECT_FALSE(materialAssetLevel1->IsFinalized()); + + m_assetSystemStub.RegisterSourceInfo("level1.material", materialAssetLevel1.GetId()); + + auto materialAssetLevel2Result = sourceDataLevel2.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::DeferredBake, true); + EXPECT_TRUE(materialAssetLevel2Result.IsSuccess()); + Data::Asset materialAssetLevel2 = materialAssetLevel2Result.TakeValue(); + EXPECT_FALSE(materialAssetLevel2->IsFinalized()); + + m_assetSystemStub.RegisterSourceInfo("level2.material", materialAssetLevel2.GetId()); + + auto materialAssetLevel3Result = sourceDataLevel3.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::DeferredBake, true); + EXPECT_TRUE(materialAssetLevel3Result.IsSuccess()); + Data::Asset materialAssetLevel3 = materialAssetLevel3Result.TakeValue(); + EXPECT_FALSE(materialAssetLevel3->IsFinalized()); + + // Now we'll create the material type asset in memory so the materials will have what they need to finalize. + Data::Asset testMaterialTypeAsset = CreateTestMaterialTypeAsset(materialTypeAssetId); + + auto layout = testMaterialTypeAsset->GetMaterialPropertiesLayout(); + MaterialPropertyIndex myFloat = layout->FindPropertyIndex(Name("general.MyFloat")); + MaterialPropertyIndex myFloat2 = layout->FindPropertyIndex(Name("general.MyFloat2")); + MaterialPropertyIndex myColor = layout->FindPropertyIndex(Name("general.MyColor")); + + + // The MaterialAsset is still holding an reference to an unloaded asset, so we run it through the serializer which causes the loaded MaterialAsset + // to have access to the testMaterialTypeAsset. This is similar to how the AP would save the MaterialAsset to the cache and the runtime would load it. + SerializeTester tester(GetSerializeContext()); + tester.SerializeOut(materialAssetLevel1.Get()); + materialAssetLevel1 = tester.SerializeIn(Uuid::CreateRandom(), ObjectStream::FilterDescriptor{AZ::Data::AssetFilterNoAssetLoading}); + tester.SerializeOut(materialAssetLevel2.Get()); + materialAssetLevel2 = tester.SerializeIn(Uuid::CreateRandom(), ObjectStream::FilterDescriptor{AZ::Data::AssetFilterNoAssetLoading}); + tester.SerializeOut(materialAssetLevel3.Get()); + materialAssetLevel3 = tester.SerializeIn(Uuid::CreateRandom(), ObjectStream::FilterDescriptor{AZ::Data::AssetFilterNoAssetLoading}); + + + // The properties will finalize automatically when we call GetPropertyValues()... + + AZStd::array_view properties; + + // Check level 1 properties + properties = materialAssetLevel1->GetPropertyValues(); + EXPECT_EQ(properties[myFloat.GetIndex()].GetValue(), 1.5f); + EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue(), Vector2(0.0f, 0.0f)); + EXPECT_EQ(properties[myColor.GetIndex()].GetValue(), Color(0.1f, 0.2f, 0.3f, 0.4f)); + + // Check level 2 properties + properties = materialAssetLevel2->GetPropertyValues(); + EXPECT_EQ(properties[myFloat.GetIndex()].GetValue(), 1.5f); + EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue(), Vector2(4.1f, 4.2f)); + EXPECT_EQ(properties[myColor.GetIndex()].GetValue(), Color(0.15f, 0.25f, 0.35f, 0.45f)); + + // Check level 3 properties + properties = materialAssetLevel3->GetPropertyValues(); + EXPECT_EQ(properties[myFloat.GetIndex()].GetValue(), 3.5f); + EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue(), Vector2(4.1f, 4.2f)); + EXPECT_EQ(properties[myColor.GetIndex()].GetValue(), Color(0.15f, 0.25f, 0.35f, 0.45f)); + + EXPECT_TRUE(materialAssetLevel1->IsFinalized()); + EXPECT_TRUE(materialAssetLevel2->IsFinalized()); + EXPECT_TRUE(materialAssetLevel3->IsFinalized()); + } TEST_F(MaterialSourceDataTests, CreateMaterialAsset_MultiLevelDataInheritance_Error_MaterialTypesDontMatch) {