Formalize material schema and constant layout contract

This commit is contained in:
2026-04-03 16:49:30 +08:00
parent 052ac28aa3
commit 03bd755e0a
10 changed files with 1841 additions and 87 deletions

View File

@@ -479,6 +479,55 @@ TEST(RenderMaterialUtility_Test, ResolvesBuiltinForwardMaterialContractFromShade
EXPECT_EQ(ResolveBuiltinBaseColorTexture(&material), texture);
}
TEST(RenderMaterialUtility_Test, ResolvesBuiltinForwardMaterialContractFromShaderSemanticDefaults) {
auto* shader = new Shader();
ShaderPropertyDesc colorProperty = {};
colorProperty.name = "TintColor";
colorProperty.displayName = "Tint";
colorProperty.type = ShaderPropertyType::Color;
colorProperty.defaultValue = "(0.11,0.22,0.33,0.44)";
colorProperty.semantic = "BaseColor";
shader->AddProperty(colorProperty);
Material material;
material.SetShader(ResourceHandle<Shader>(shader));
EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(0.11f, 0.22f, 0.33f, 0.44f));
EXPECT_EQ(ResolveBuiltinBaseColorTexture(&material), nullptr);
}
TEST(RenderMaterialUtility_Test, ExposesSchemaDrivenMaterialConstantPayload) {
auto* shader = new Shader();
ShaderPropertyDesc colorProperty = {};
colorProperty.name = "_BaseColor";
colorProperty.type = ShaderPropertyType::Color;
colorProperty.defaultValue = "(0.25,0.5,0.75,1.0)";
colorProperty.semantic = "BaseColor";
shader->AddProperty(colorProperty);
Material material;
material.SetShader(ResourceHandle<Shader>(shader));
const MaterialConstantPayloadView payload = ResolveSchemaMaterialConstantPayload(&material);
ASSERT_TRUE(payload.IsValid());
ASSERT_EQ(payload.size, 16u);
ASSERT_TRUE(payload.layout.IsValid());
ASSERT_EQ(payload.layout.count, 1u);
EXPECT_EQ(payload.layout.size, 16u);
EXPECT_EQ(payload.layout.fields[0].name, "_BaseColor");
EXPECT_EQ(payload.layout.fields[0].offset, 0u);
EXPECT_EQ(payload.layout.fields[0].size, 16u);
EXPECT_EQ(payload.layout.fields[0].alignedSize, 16u);
const float* values = static_cast<const float*>(payload.data);
EXPECT_FLOAT_EQ(values[0], 0.25f);
EXPECT_FLOAT_EQ(values[1], 0.5f);
EXPECT_FLOAT_EQ(values[2], 0.75f);
EXPECT_FLOAT_EQ(values[3], 1.0f);
}
TEST(RenderMaterialUtility_Test, UsesOpacityOnlyWhenBaseColorFactorIsMissing) {
Material material;
material.SetFloat("opacity", 0.35f);

View File

@@ -12,6 +12,35 @@ using namespace XCEngine::Math;
namespace {
Shader* CreateMaterialSchemaShader() {
auto* shader = new Shader();
ShaderPropertyDesc baseColor = {};
baseColor.name = "_BaseColor";
baseColor.displayName = "Base Color";
baseColor.type = ShaderPropertyType::Color;
baseColor.defaultValue = "(1.0,0.5,0.25,1.0)";
baseColor.semantic = "BaseColor";
shader->AddProperty(baseColor);
ShaderPropertyDesc metallic = {};
metallic.name = "_Metallic";
metallic.displayName = "Metallic";
metallic.type = ShaderPropertyType::Float;
metallic.defaultValue = "0.7";
shader->AddProperty(metallic);
ShaderPropertyDesc baseMap = {};
baseMap.name = "_MainTex";
baseMap.displayName = "Base Map";
baseMap.type = ShaderPropertyType::Texture2D;
baseMap.defaultValue = "white";
baseMap.semantic = "BaseColorTexture";
shader->AddProperty(baseMap);
return shader;
}
TEST(Material, DefaultConstructor) {
Material material;
EXPECT_EQ(material.GetType(), ResourceType::Material);
@@ -225,6 +254,28 @@ TEST(Material, SetTextureReplacesExistingBinding) {
EXPECT_EQ(material.GetTexture("uDiffuse").Get(), secondTexture);
}
TEST(Material, SetTextureAssetRefStoresStableBindingMetadata) {
Material material;
AssetRef textureRef;
textureRef.assetGuid = AssetGUID(1, 2);
textureRef.localID = kMainAssetLocalID;
textureRef.resourceType = ResourceType::Texture;
material.SetTextureAssetRef("uDiffuse", textureRef, "Assets/diffuse.bmp");
ASSERT_EQ(material.GetTextureBindingCount(), 1u);
EXPECT_TRUE(material.HasProperty("uDiffuse"));
EXPECT_EQ(material.GetTextureBindingName(0), "uDiffuse");
EXPECT_EQ(material.GetTextureBindingPath(0), "Assets/diffuse.bmp");
const AssetRef storedRef = material.GetTextureBindingAssetRef(0);
EXPECT_EQ(storedRef.assetGuid, textureRef.assetGuid);
EXPECT_EQ(storedRef.localID, textureRef.localID);
EXPECT_EQ(storedRef.resourceType, textureRef.resourceType);
EXPECT_FALSE(material.GetTextureBindingLoadedTexture(0).IsValid());
}
TEST(Material, ChangeVersionIncrementsWhenMaterialMutates) {
Material material;
const XCEngine::Core::uint64 initialVersion = material.GetChangeVersion();
@@ -243,6 +294,24 @@ TEST(Material, UpdateConstantBufferPacksNumericPropertiesIntoStableSlots) {
material.SetFloat4("beta", Vector4(1.0f, 2.0f, 3.0f, 4.0f));
material.SetInt("gamma", 7);
const auto& constantLayout = material.GetConstantLayout();
ASSERT_EQ(constantLayout.Size(), 3u);
EXPECT_EQ(constantLayout[0].name, "alpha");
EXPECT_EQ(constantLayout[0].offset, 0u);
EXPECT_EQ(constantLayout[0].size, 4u);
EXPECT_EQ(constantLayout[0].alignedSize, 16u);
EXPECT_EQ(constantLayout[1].name, "beta");
EXPECT_EQ(constantLayout[1].offset, 16u);
EXPECT_EQ(constantLayout[1].size, 16u);
EXPECT_EQ(constantLayout[2].name, "gamma");
EXPECT_EQ(constantLayout[2].offset, 32u);
EXPECT_EQ(constantLayout[2].size, 4u);
const MaterialConstantFieldDesc* betaField = material.FindConstantField("beta");
ASSERT_NE(betaField, nullptr);
EXPECT_EQ(betaField->offset, 16u);
EXPECT_EQ(betaField->alignedSize, 16u);
const auto& constantBufferData = material.GetConstantBufferData();
ASSERT_EQ(constantBufferData.Size(), 48u);
@@ -331,4 +400,138 @@ TEST(Material, ClearAllProperties) {
EXPECT_FALSE(material.HasProperty("uIndex"));
}
TEST(Material, SetShaderSeedsDefaultsAndRemovePropertyRestoresShaderDefault) {
Material material;
Shader* shader = CreateMaterialSchemaShader();
material.SetShader(ResourceHandle<Shader>(shader));
EXPECT_TRUE(material.HasProperty("_BaseColor"));
EXPECT_TRUE(material.HasProperty("_Metallic"));
EXPECT_TRUE(material.HasProperty("_MainTex"));
EXPECT_EQ(material.GetFloat4("_BaseColor"), Vector4(1.0f, 0.5f, 0.25f, 1.0f));
EXPECT_FLOAT_EQ(material.GetFloat("_Metallic"), 0.7f);
EXPECT_EQ(material.GetTextureBindingCount(), 0u);
material.SetFloat4("_BaseColor", Vector4(0.2f, 0.3f, 0.4f, 0.5f));
EXPECT_EQ(material.GetFloat4("_BaseColor"), Vector4(0.2f, 0.3f, 0.4f, 0.5f));
material.RemoveProperty("_BaseColor");
EXPECT_EQ(material.GetFloat4("_BaseColor"), Vector4(1.0f, 0.5f, 0.25f, 1.0f));
}
TEST(Material, ClearAllPropertiesWithShaderRestoresSchemaDefaults) {
Material material;
Shader* shader = CreateMaterialSchemaShader();
material.SetShader(ResourceHandle<Shader>(shader));
material.SetFloat("_Metallic", 0.15f);
material.SetTexture("_MainTex", ResourceHandle<Texture>(new Texture()));
ASSERT_EQ(material.GetTextureBindingCount(), 1u);
material.ClearAllProperties();
EXPECT_TRUE(material.HasProperty("_BaseColor"));
EXPECT_TRUE(material.HasProperty("_Metallic"));
EXPECT_TRUE(material.HasProperty("_MainTex"));
EXPECT_EQ(material.GetFloat4("_BaseColor"), Vector4(1.0f, 0.5f, 0.25f, 1.0f));
EXPECT_FLOAT_EQ(material.GetFloat("_Metallic"), 0.7f);
EXPECT_EQ(material.GetTextureBindingCount(), 0u);
}
TEST(Material, ShaderSchemaRejectsUnknownAndTypeMismatchedAssignments) {
Material material;
Shader* shader = CreateMaterialSchemaShader();
material.SetShader(ResourceHandle<Shader>(shader));
const Vector4 defaultBaseColor = material.GetFloat4("_BaseColor");
const float defaultMetallic = material.GetFloat("_Metallic");
material.SetFloat("_BaseColor", 0.1f);
material.SetFloat4("_Metallic", Vector4(1.0f, 2.0f, 3.0f, 4.0f));
material.SetFloat("UnknownProperty", 5.0f);
EXPECT_EQ(material.GetFloat4("_BaseColor"), defaultBaseColor);
EXPECT_FLOAT_EQ(material.GetFloat("_Metallic"), defaultMetallic);
EXPECT_FALSE(material.HasProperty("UnknownProperty"));
}
TEST(Material, SwitchingShaderResyncsPropertiesAgainstNewSchema) {
Material material;
auto* shaderA = new Shader();
ShaderPropertyDesc sharedA = {};
sharedA.name = "Shared";
sharedA.type = ShaderPropertyType::Float;
sharedA.defaultValue = "1.0";
shaderA->AddProperty(sharedA);
ShaderPropertyDesc onlyA = {};
onlyA.name = "OnlyA";
onlyA.type = ShaderPropertyType::Float;
onlyA.defaultValue = "2.0";
shaderA->AddProperty(onlyA);
auto* shaderB = new Shader();
ShaderPropertyDesc sharedB = {};
sharedB.name = "Shared";
sharedB.type = ShaderPropertyType::Float;
sharedB.defaultValue = "4.0";
shaderB->AddProperty(sharedB);
ShaderPropertyDesc onlyB = {};
onlyB.name = "OnlyB";
onlyB.type = ShaderPropertyType::Color;
onlyB.defaultValue = "(0.1,0.2,0.3,0.4)";
shaderB->AddProperty(onlyB);
material.SetFloat("Legacy", 9.0f);
material.SetShader(ResourceHandle<Shader>(shaderA));
material.SetFloat("Shared", 5.0f);
material.SetFloat("OnlyA", 8.0f);
material.SetShader(ResourceHandle<Shader>(shaderB));
EXPECT_FALSE(material.HasProperty("Legacy"));
EXPECT_FALSE(material.HasProperty("OnlyA"));
EXPECT_TRUE(material.HasProperty("Shared"));
EXPECT_TRUE(material.HasProperty("OnlyB"));
EXPECT_FLOAT_EQ(material.GetFloat("Shared"), 5.0f);
EXPECT_EQ(material.GetFloat4("OnlyB"), Vector4(0.1f, 0.2f, 0.3f, 0.4f));
}
TEST(Material, UpdateConstantBufferFollowsShaderSchemaOrderInsteadOfAlphabeticalOrder) {
Material material;
auto* shader = new Shader();
ShaderPropertyDesc beta = {};
beta.name = "beta";
beta.type = ShaderPropertyType::Float;
beta.defaultValue = "0.0";
shader->AddProperty(beta);
ShaderPropertyDesc alpha = {};
alpha.name = "alpha";
alpha.type = ShaderPropertyType::Float;
alpha.defaultValue = "0.0";
shader->AddProperty(alpha);
material.SetShader(ResourceHandle<Shader>(shader));
material.SetFloat("alpha", 10.0f);
material.SetFloat("beta", 20.0f);
const auto& constantLayout = material.GetConstantLayout();
ASSERT_EQ(constantLayout.Size(), 2u);
EXPECT_EQ(constantLayout[0].name, "beta");
EXPECT_EQ(constantLayout[0].offset, 0u);
EXPECT_EQ(constantLayout[1].name, "alpha");
EXPECT_EQ(constantLayout[1].offset, 16u);
const auto& constantBufferData = material.GetConstantBufferData();
ASSERT_EQ(constantBufferData.Size(), 32u);
const float* firstSlot = reinterpret_cast<const float*>(constantBufferData.Data());
const float* secondSlot = reinterpret_cast<const float*>(constantBufferData.Data() + 16);
EXPECT_FLOAT_EQ(firstSlot[0], 20.0f);
EXPECT_FLOAT_EQ(secondSlot[0], 10.0f);
}
} // namespace

View File

@@ -66,6 +66,66 @@ void WriteTextFile(const std::filesystem::path& path, const std::string& content
ASSERT_TRUE(static_cast<bool>(output));
}
std::filesystem::path WriteSchemaMaterialShaderManifest(const std::filesystem::path& rootPath) {
namespace fs = std::filesystem;
const fs::path shaderDir = rootPath / "Shaders";
fs::create_directories(shaderDir);
WriteTextFile(shaderDir / "schema.vert.glsl", "#version 430\nvoid main() {}\n");
WriteTextFile(shaderDir / "schema.frag.glsl", "#version 430\nvoid main() {}\n");
const fs::path manifestPath = shaderDir / "schema.shader";
std::ofstream manifest(manifestPath, std::ios::binary | std::ios::trunc);
EXPECT_TRUE(manifest.is_open());
if (!manifest.is_open()) {
return {};
}
manifest << "{\n";
manifest << " \"name\": \"SchemaMaterialShader\",\n";
manifest << " \"properties\": [\n";
manifest << " {\n";
manifest << " \"name\": \"_BaseColor\",\n";
manifest << " \"displayName\": \"Base Color\",\n";
manifest << " \"type\": \"Color\",\n";
manifest << " \"defaultValue\": \"(1,0.5,0.25,1)\",\n";
manifest << " \"semantic\": \"BaseColor\"\n";
manifest << " },\n";
manifest << " {\n";
manifest << " \"name\": \"_Metallic\",\n";
manifest << " \"displayName\": \"Metallic\",\n";
manifest << " \"type\": \"Float\",\n";
manifest << " \"defaultValue\": \"0.7\"\n";
manifest << " },\n";
manifest << " {\n";
manifest << " \"name\": \"_Mode\",\n";
manifest << " \"displayName\": \"Mode\",\n";
manifest << " \"type\": \"Int\",\n";
manifest << " \"defaultValue\": \"2\"\n";
manifest << " },\n";
manifest << " {\n";
manifest << " \"name\": \"_MainTex\",\n";
manifest << " \"displayName\": \"Main Tex\",\n";
manifest << " \"type\": \"Texture2D\",\n";
manifest << " \"defaultValue\": \"white\",\n";
manifest << " \"semantic\": \"BaseColorTexture\"\n";
manifest << " }\n";
manifest << " ],\n";
manifest << " \"passes\": [\n";
manifest << " {\n";
manifest << " \"name\": \"ForwardLit\",\n";
manifest << " \"variants\": [\n";
manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"schema.vert.glsl\" },\n";
manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"schema.frag.glsl\" }\n";
manifest << " ]\n";
manifest << " }\n";
manifest << " ]\n";
manifest << "}\n";
EXPECT_TRUE(static_cast<bool>(manifest));
return manifestPath;
}
TEST(MaterialLoader, GetResourceType) {
MaterialLoader loader;
EXPECT_EQ(loader.GetResourceType(), ResourceType::Material);
@@ -235,6 +295,135 @@ TEST(MaterialLoader, LoadMaterialWithShaderManifestResolvesShaderPass) {
fs::remove_all(shaderRoot);
}
TEST(MaterialLoader, LoadMaterialWithPropertiesObjectAppliesTypedOverrides) {
namespace fs = std::filesystem;
const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_override_test";
fs::remove_all(rootPath);
const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath);
ASSERT_FALSE(shaderPath.empty());
const fs::path materialPath = rootPath / "override.material";
WriteTextFile(
materialPath,
"{\n"
" \"shader\": \"" + shaderPath.generic_string() + "\",\n"
" \"shaderPass\": \"ForwardLit\",\n"
" \"properties\": {\n"
" \"_BaseColor\": [0.2, 0.4, 0.6, 0.8],\n"
" \"_Metallic\": 0.15,\n"
" \"_Mode\": 5\n"
" }\n"
"}\n");
MaterialLoader loader;
LoadResult result = loader.Load(materialPath.generic_string().c_str());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
auto* material = static_cast<Material*>(result.resource);
ASSERT_NE(material, nullptr);
ASSERT_NE(material->GetShader(), nullptr);
EXPECT_EQ(material->GetShaderPass(), "ForwardLit");
EXPECT_EQ(material->GetFloat4("_BaseColor"), XCEngine::Math::Vector4(0.2f, 0.4f, 0.6f, 0.8f));
EXPECT_FLOAT_EQ(material->GetFloat("_Metallic"), 0.15f);
EXPECT_EQ(material->GetInt("_Mode"), 5);
EXPECT_TRUE(material->HasProperty("_MainTex"));
EXPECT_EQ(material->GetTextureBindingCount(), 0u);
delete material;
fs::remove_all(rootPath);
}
TEST(MaterialLoader, RejectsUnknownPropertyAgainstShaderSchema) {
namespace fs = std::filesystem;
const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_unknown_test";
fs::remove_all(rootPath);
const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath);
ASSERT_FALSE(shaderPath.empty());
const fs::path materialPath = rootPath / "unknown_property.material";
WriteTextFile(
materialPath,
"{\n"
" \"shader\": \"" + shaderPath.generic_string() + "\",\n"
" \"properties\": {\n"
" \"_Unknown\": 1.0\n"
" }\n"
"}\n");
MaterialLoader loader;
LoadResult result = loader.Load(materialPath.generic_string().c_str());
EXPECT_FALSE(result);
fs::remove_all(rootPath);
}
TEST(MaterialLoader, RejectsTypeMismatchAgainstShaderSchema) {
namespace fs = std::filesystem;
const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_mismatch_test";
fs::remove_all(rootPath);
const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath);
ASSERT_FALSE(shaderPath.empty());
const fs::path materialPath = rootPath / "type_mismatch.material";
WriteTextFile(
materialPath,
"{\n"
" \"shader\": \"" + shaderPath.generic_string() + "\",\n"
" \"properties\": {\n"
" \"_BaseColor\": 1.0\n"
" }\n"
"}\n");
MaterialLoader loader;
LoadResult result = loader.Load(materialPath.generic_string().c_str());
EXPECT_FALSE(result);
fs::remove_all(rootPath);
}
TEST(MaterialLoader, LoadMaterialWithPropertiesObjectPreservesShaderDefaultsForOmittedValues) {
namespace fs = std::filesystem;
const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_defaults_test";
fs::remove_all(rootPath);
const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath);
ASSERT_FALSE(shaderPath.empty());
const fs::path materialPath = rootPath / "defaults.material";
WriteTextFile(
materialPath,
"{\n"
" \"shader\": \"" + shaderPath.generic_string() + "\",\n"
" \"properties\": {\n"
" \"_Metallic\": 0.33\n"
" }\n"
"}\n");
MaterialLoader loader;
LoadResult result = loader.Load(materialPath.generic_string().c_str());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
auto* material = static_cast<Material*>(result.resource);
ASSERT_NE(material, nullptr);
EXPECT_EQ(material->GetFloat4("_BaseColor"), XCEngine::Math::Vector4(1.0f, 0.5f, 0.25f, 1.0f));
EXPECT_FLOAT_EQ(material->GetFloat("_Metallic"), 0.33f);
EXPECT_EQ(material->GetInt("_Mode"), 2);
EXPECT_TRUE(material->HasProperty("_MainTex"));
EXPECT_EQ(material->GetTextureBindingCount(), 0u);
delete material;
fs::remove_all(rootPath);
}
TEST(MaterialLoader, RejectsUnknownRenderQueueName) {
const std::filesystem::path materialPath =
std::filesystem::current_path() / "material_loader_invalid_queue.material";
@@ -380,6 +569,7 @@ TEST(MaterialLoader, ResourceManagerLoadsProjectMaterialTextureAsLazyDependency)
ASSERT_TRUE(materialHandle.IsValid());
ASSERT_EQ(materialHandle->GetTextureBindingCount(), 1u);
EXPECT_EQ(materialHandle->GetTextureBindingName(0), "baseColorTexture");
EXPECT_TRUE(materialHandle->GetTextureBindingAssetRef(0).IsValid());
EXPECT_EQ(
fs::path(materialHandle->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(),
(projectRoot / "Assets" / "checker.bmp").lexically_normal().generic_string());
@@ -542,6 +732,9 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) {
assetsDir / "checker.bmp",
fs::copy_options::overwrite_existing);
manager.SetResourceRoot(projectRoot.string().c_str());
manager.RefreshProjectAssets();
{
std::ofstream output(materialArtifactPath, std::ios::binary | std::ios::trunc);
ASSERT_TRUE(output.is_open());
@@ -558,11 +751,18 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) {
header.textureBindingCount = 1;
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
WriteArtifactString(output, "baseColorTexture");
WriteArtifactString(output, "Assets/checker.bmp");
}
AssetRef textureRef;
ASSERT_TRUE(manager.TryGetAssetRef("Assets/checker.bmp", ResourceType::Texture, textureRef));
ASSERT_TRUE(textureRef.IsValid());
const String encodedTextureRef =
textureRef.assetGuid.ToString() + "," +
String(std::to_string(textureRef.localID).c_str()) + "," +
String(std::to_string(static_cast<int>(textureRef.resourceType)).c_str());
manager.SetResourceRoot(projectRoot.string().c_str());
WriteArtifactString(output, "baseColorTexture");
WriteArtifactString(output, encodedTextureRef);
WriteArtifactString(output, "");
}
MaterialLoader loader;
LoadResult result = loader.Load("Library/Manual/test.xcmat");
@@ -572,9 +772,8 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) {
auto* material = static_cast<Material*>(result.resource);
ASSERT_NE(material, nullptr);
EXPECT_EQ(material->GetTextureBindingCount(), 1u);
EXPECT_EQ(
fs::path(material->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(),
(projectRoot / "Assets" / "checker.bmp").lexically_normal().generic_string());
EXPECT_TRUE(material->GetTextureBindingAssetRef(0).IsValid());
EXPECT_TRUE(material->GetTextureBindingPath(0).Empty());
const ResourceHandle<Texture> initialTexture = material->GetTexture("baseColorTexture");
EXPECT_FALSE(initialTexture.IsValid());
@@ -585,6 +784,9 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) {
ASSERT_TRUE(loadedTexture.IsValid());
EXPECT_EQ(loadedTexture->GetWidth(), 2u);
EXPECT_EQ(loadedTexture->GetHeight(), 2u);
EXPECT_EQ(
fs::path(material->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(),
fs::path("Assets/checker.bmp").lexically_normal().generic_string());
delete material;
manager.SetResourceRoot("");