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

@@ -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("");