From 6e6a98a022f90817501d3ade2976052f01e35225 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Wed, 8 Apr 2026 01:10:51 +0800 Subject: [PATCH] Tighten material loader schema parsing --- .../src/Resources/Material/MaterialLoader.cpp | 321 ++++++++++-------- .../Material/test_material_loader.cpp | 38 +-- 2 files changed, 182 insertions(+), 177 deletions(-) diff --git a/engine/src/Resources/Material/MaterialLoader.cpp b/engine/src/Resources/Material/MaterialLoader.cpp index 232d733f..423ab1f0 100644 --- a/engine/src/Resources/Material/MaterialLoader.cpp +++ b/engine/src/Resources/Material/MaterialLoader.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -572,6 +571,21 @@ bool TryParseFloatText(const std::string& text, float& outValue) { return true; } +bool TryParseFloatValue(const std::string& json, const char* key, float& outValue) { + size_t valuePos = 0; + if (!FindValueStart(json, key, valuePos)) { + return false; + } + + std::string rawValue; + JsonRawValueType rawType = JsonRawValueType::Invalid; + if (!TryExtractRawValue(json, valuePos, rawValue, rawType) || rawType != JsonRawValueType::Number) { + return false; + } + + return TryParseFloatText(rawValue, outValue); +} + bool TryParseIntText(const std::string& text, Core::int32& outValue) { const std::string trimmed = TrimCopy(text); if (trimmed.empty()) { @@ -641,91 +655,14 @@ bool TryParseFloatListText(const std::string& text, return outCount > 0; } -Containers::String NormalizeMaterialLookupToken(const Containers::String& value) { - std::string normalized; - normalized.reserve(value.Length()); - for (size_t index = 0; index < value.Length(); ++index) { - const unsigned char ch = static_cast(value[index]); - if (std::isalnum(ch) != 0) { - normalized.push_back(static_cast(std::tolower(ch))); - } - } - - return Containers::String(normalized.c_str()); -} - -const ShaderPropertyDesc* FindShaderPropertyBySemantic( - const Shader* shader, - const Containers::String& semantic) { - if (shader == nullptr || semantic.Empty()) { - return nullptr; - } - - const Containers::String normalizedSemantic = NormalizeMaterialLookupToken(semantic); - for (const ShaderPropertyDesc& property : shader->GetProperties()) { - if (NormalizeMaterialLookupToken(property.semantic) == normalizedSemantic) { - return &property; - } - } - - return nullptr; -} - -Containers::String ResolveLegacyMaterialSemanticAlias( - const Containers::String& propertyName, - JsonRawValueType rawType) { - const Containers::String normalizedName = NormalizeMaterialLookupToken(propertyName); - - if (rawType == JsonRawValueType::Array || - rawType == JsonRawValueType::Number) { - if (normalizedName == "basecolor" || - normalizedName == "color") { - return Containers::String("BaseColor"); - } - } - - if (rawType == JsonRawValueType::String) { - if (normalizedName == "basecolortexture" || - normalizedName == "maintex" || - normalizedName == "maintexture" || - normalizedName == "albedotexture" || - normalizedName == "texture") { - return Containers::String("BaseColorTexture"); - } - } - - return Containers::String(); -} - const ShaderPropertyDesc* ResolveShaderPropertyForMaterialKey( const Shader* shader, - const Containers::String& propertyName, - JsonRawValueType rawType) { + const Containers::String& propertyName) { if (shader == nullptr || propertyName.Empty()) { return nullptr; } - if (const ShaderPropertyDesc* property = shader->FindProperty(propertyName)) { - return property; - } - - const Containers::String normalizedName = NormalizeMaterialLookupToken(propertyName); - for (const ShaderPropertyDesc& property : shader->GetProperties()) { - if (NormalizeMaterialLookupToken(property.name) == normalizedName) { - return &property; - } - } - - if (const ShaderPropertyDesc* property = FindShaderPropertyBySemantic(shader, propertyName)) { - return property; - } - - const Containers::String semanticAlias = ResolveLegacyMaterialSemanticAlias(propertyName, rawType); - if (!semanticAlias.Empty()) { - return FindShaderPropertyBySemantic(shader, semanticAlias); - } - - return nullptr; + return shader->FindProperty(propertyName); } bool TryApplySchemaMaterialProperty(Material* material, @@ -875,7 +812,7 @@ bool TryParseMaterialPropertiesObject(const std::string& objectText, Material* m const Shader* shader = material->GetShader(); const ShaderPropertyDesc* shaderProperty = - ResolveShaderPropertyForMaterialKey(shader, propertyName, rawType); + ResolveShaderPropertyForMaterialKey(shader, propertyName); if (shader != nullptr && shaderProperty == nullptr) { return false; } @@ -1105,7 +1042,7 @@ bool TryApplyTexturePath(Material* material, const Shader* shader = material->GetShader(); const ShaderPropertyDesc* shaderProperty = - ResolveShaderPropertyForMaterialKey(shader, textureName, JsonRawValueType::String); + ResolveShaderPropertyForMaterialKey(shader, textureName); if (shader != nullptr && shaderProperty == nullptr) { return false; } @@ -1332,6 +1269,123 @@ bool TryParseBlendOp(const Containers::String& value, MaterialBlendOp& outOp) { return false; } +bool TryParseStencilOp(const Containers::String& value, MaterialStencilOp& outOp) { + const Containers::String normalized = value.Trim().ToLower(); + if (normalized == "keep") { + outOp = MaterialStencilOp::Keep; + return true; + } + if (normalized == "zero") { + outOp = MaterialStencilOp::Zero; + return true; + } + if (normalized == "replace") { + outOp = MaterialStencilOp::Replace; + return true; + } + if (normalized == "incrsat" || normalized == "incrementclamp" || normalized == "increment_clamp") { + outOp = MaterialStencilOp::IncrSat; + return true; + } + if (normalized == "decrsat" || normalized == "decrementclamp" || normalized == "decrement_clamp") { + outOp = MaterialStencilOp::DecrSat; + return true; + } + if (normalized == "invert") { + outOp = MaterialStencilOp::Invert; + return true; + } + if (normalized == "incrwrap" || normalized == "incr" || normalized == "incrementwrap" || normalized == "increment_wrap") { + outOp = MaterialStencilOp::IncrWrap; + return true; + } + if (normalized == "decrwrap" || normalized == "decr" || normalized == "decrementwrap" || normalized == "decrement_wrap") { + outOp = MaterialStencilOp::DecrWrap; + return true; + } + + return false; +} + +bool TryParseStencilStateObject(const std::string& objectText, MaterialStencilState& outState) { + if (objectText.empty() || objectText.front() != '{' || objectText.back() != '}') { + return false; + } + + MaterialStencilState stencilState = outState; + stencilState.enabled = true; + + bool boolValue = false; + if (HasKey(objectText, "enabled")) { + if (!TryParseBoolValue(objectText, "enabled", boolValue)) { + return false; + } + stencilState.enabled = boolValue; + } + + Core::int32 intValue = 0; + auto parseMaskValue = [&](const char* key, Core::uint8& outValue) -> bool { + if (!HasKey(objectText, key)) { + return true; + } + + if (!TryParseIntValue(objectText, key, intValue) || intValue < 0 || intValue > 0xFF) { + return false; + } + + outValue = static_cast(intValue); + return true; + }; + + if (!parseMaskValue("readMask", stencilState.readMask) || + !parseMaskValue("writeMask", stencilState.writeMask) || + !parseMaskValue("ref", stencilState.reference) || + !parseMaskValue("reference", stencilState.reference)) { + return false; + } + + Containers::String enumValue; + auto parseFaceCompare = [&](const char* key, MaterialComparisonFunc& outFunc) -> bool { + if (!HasKey(objectText, key)) { + return true; + } + + return TryParseStringValue(objectText, key, enumValue) && + TryParseComparisonFunc(enumValue, outFunc); + }; + + auto parseFaceOp = [&](const char* key, MaterialStencilOp& outOp) -> bool { + if (!HasKey(objectText, key)) { + return true; + } + + return TryParseStringValue(objectText, key, enumValue) && + TryParseStencilOp(enumValue, outOp); + }; + + if (!parseFaceCompare("comp", stencilState.front.func) || + !parseFaceCompare("comp", stencilState.back.func) || + !parseFaceOp("pass", stencilState.front.passOp) || + !parseFaceOp("pass", stencilState.back.passOp) || + !parseFaceOp("fail", stencilState.front.failOp) || + !parseFaceOp("fail", stencilState.back.failOp) || + !parseFaceOp("zFail", stencilState.front.depthFailOp) || + !parseFaceOp("zFail", stencilState.back.depthFailOp) || + !parseFaceCompare("compFront", stencilState.front.func) || + !parseFaceOp("passFront", stencilState.front.passOp) || + !parseFaceOp("failFront", stencilState.front.failOp) || + !parseFaceOp("zFailFront", stencilState.front.depthFailOp) || + !parseFaceCompare("compBack", stencilState.back.func) || + !parseFaceOp("passBack", stencilState.back.passOp) || + !parseFaceOp("failBack", stencilState.back.failOp) || + !parseFaceOp("zFailBack", stencilState.back.depthFailOp)) { + return false; + } + + outState = stencilState; + return true; +} + bool TryParseRenderStateObject(const std::string& objectText, Material* material) { if (material == nullptr || objectText.empty() || objectText.front() != '{' || objectText.back() != '}') { return false; @@ -1435,6 +1489,42 @@ bool TryParseRenderStateObject(const std::string& objectText, Material* material return false; } } + if (HasKey(objectText, "offset")) { + std::string offsetArray; + if (!TryExtractArray(objectText, "offset", offsetArray)) { + return false; + } + + float offsetValues[2] = {}; + size_t offsetCount = 0; + if (!TryParseFloatListText(offsetArray, offsetValues, 2, offsetCount) || offsetCount != 2u) { + return false; + } + + renderState.depthBiasFactor = offsetValues[0]; + renderState.depthBiasUnits = static_cast(offsetValues[1]); + } + if (HasKey(objectText, "offsetFactor")) { + float floatValue = 0.0f; + if (!TryParseFloatValue(objectText, "offsetFactor", floatValue)) { + return false; + } + renderState.depthBiasFactor = floatValue; + } + if (HasKey(objectText, "offsetUnits")) { + Core::int32 offsetUnits = 0; + if (!TryParseIntValue(objectText, "offsetUnits", offsetUnits)) { + return false; + } + renderState.depthBiasUnits = offsetUnits; + } + if (HasKey(objectText, "stencil")) { + std::string stencilObject; + if (!TryExtractObject(objectText, "stencil", stencilObject) || + !TryParseStencilStateObject(stencilObject, renderState.stencil)) { + return false; + } + } material->SetRenderState(renderState); return true; @@ -1473,18 +1563,6 @@ bool MaterialFileExists(const Containers::String& path) { return std::filesystem::exists(std::filesystem::path(resourceRoot.CStr()) / inputPath); } -void ApplyLegacyMaterialShaderPassHint(Material* material, const Containers::String& shaderPass) { - if (material == nullptr || shaderPass.Empty()) { - return; - } - - if (Rendering::IsRedundantLegacyMaterialShaderPassHint(material->GetShader(), shaderPass)) { - return; - } - - material->SetLegacyShaderPassHint(shaderPass); -} - ResourceHandle LoadShaderHandle(const Containers::String& shaderPath); template @@ -1572,11 +1650,9 @@ LoadResult LoadMaterialArtifact(const Containers::String& path) { } const std::string magic(fileHeader.magic, fileHeader.magic + 7); - const bool isLegacySchema = magic == "XCMAT02" && fileHeader.schemaVersion == 2u; - const bool isSchemaV3 = magic == "XCMAT03" && fileHeader.schemaVersion == 3u; const bool isCurrentSchema = - magic == "XCMAT04" && fileHeader.schemaVersion == kMaterialArtifactSchemaVersion; - if (!isLegacySchema && !isSchemaV3 && !isCurrentSchema) { + magic == "XCMAT06" && fileHeader.schemaVersion == kMaterialArtifactSchemaVersion; + if (!isCurrentSchema) { return LoadResult("Invalid material artifact magic: " + path); } @@ -1588,11 +1664,9 @@ LoadResult LoadMaterialArtifact(const Containers::String& path) { Containers::String materialName; Containers::String materialSourcePath; Containers::String shaderPath; - Containers::String shaderPass; if (!ReadMaterialArtifactString(data, offset, materialName) || !ReadMaterialArtifactString(data, offset, materialSourcePath) || - !ReadMaterialArtifactString(data, offset, shaderPath) || - !ReadMaterialArtifactString(data, offset, shaderPass)) { + !ReadMaterialArtifactString(data, offset, shaderPath)) { return LoadResult("Failed to parse material artifact strings: " + path); } @@ -1608,38 +1682,10 @@ LoadResult LoadMaterialArtifact(const Containers::String& path) { material->SetShader(shaderHandle); } } - ApplyLegacyMaterialShaderPassHint(material.get(), shaderPass); MaterialArtifactHeader header = {}; - if (isLegacySchema) { - MaterialArtifactHeaderV2 legacyHeader = {}; - if (!ReadMaterialArtifactValue(data, offset, legacyHeader)) { - return LoadResult("Failed to parse material artifact body: " + path); - } - - header.renderQueue = legacyHeader.renderQueue; - header.renderState = legacyHeader.renderState; - header.tagCount = legacyHeader.tagCount; - header.hasRenderStateOverride = 1u; - header.propertyCount = legacyHeader.propertyCount; - header.textureBindingCount = legacyHeader.textureBindingCount; - } else if (isSchemaV3) { - MaterialArtifactHeaderV3 legacyHeader = {}; - if (!ReadMaterialArtifactValue(data, offset, legacyHeader)) { - return LoadResult("Failed to parse material artifact body: " + path); - } - - header.renderQueue = legacyHeader.renderQueue; - header.renderState = legacyHeader.renderState; - header.tagCount = legacyHeader.tagCount; - header.hasRenderStateOverride = 1u; - header.keywordCount = legacyHeader.keywordCount; - header.propertyCount = legacyHeader.propertyCount; - header.textureBindingCount = legacyHeader.textureBindingCount; - } else { - if (!ReadMaterialArtifactValue(data, offset, header)) { - return LoadResult("Failed to parse material artifact body: " + path); - } + if (!ReadMaterialArtifactValue(data, offset, header)) { + return LoadResult("Failed to parse material artifact body: " + path); } material->SetRenderQueue(header.renderQueue); @@ -1794,19 +1840,6 @@ bool MaterialLoader::ParseMaterialData(const Containers::Array& dat } } - Containers::String shaderPass; - if (HasKey(jsonText, "shaderPass")) { - if (!TryParseStringValue(jsonText, "shaderPass", shaderPass)) { - return false; - } - ApplyLegacyMaterialShaderPassHint(material, shaderPass); - } else if (HasKey(jsonText, "pass")) { - if (!TryParseStringValue(jsonText, "pass", shaderPass)) { - return false; - } - ApplyLegacyMaterialShaderPassHint(material, shaderPass); - } - if (HasKey(jsonText, "renderQueue")) { Core::int32 renderQueue = 0; if (TryParseIntValue(jsonText, "renderQueue", renderQueue)) { diff --git a/tests/Resources/Material/test_material_loader.cpp b/tests/Resources/Material/test_material_loader.cpp index 8fbe0f94..7b2951d3 100644 --- a/tests/Resources/Material/test_material_loader.cpp +++ b/tests/Resources/Material/test_material_loader.cpp @@ -217,7 +217,6 @@ TEST(MaterialLoader, LoadValidMaterialParsesRenderMetadata) { materialFile << "{\n"; materialFile << " \"shader\": \"" << shaderPath.generic_string() << "\",\n"; materialFile << " \"renderQueue\": \"Transparent\",\n"; - materialFile << " \"shaderPass\": \"ForwardLit\",\n"; materialFile << " \"tags\": {\n"; materialFile << " \"LightMode\": \"ForwardBase\",\n"; materialFile << " \"RenderType\": \"Transparent\"\n"; @@ -243,7 +242,6 @@ TEST(MaterialLoader, LoadValidMaterialParsesRenderMetadata) { EXPECT_TRUE(material->IsValid()); EXPECT_NE(material->GetShader(), nullptr); EXPECT_EQ(material->GetRenderQueue(), static_cast(MaterialRenderQueue::Transparent)); - EXPECT_TRUE(material->GetLegacyShaderPassHint().Empty()); EXPECT_EQ(material->GetTag("LightMode"), "ForwardBase"); EXPECT_EQ(material->GetTag("RenderType"), "Transparent"); EXPECT_EQ(material->GetRenderState().cullMode, MaterialCullMode::Back); @@ -380,7 +378,6 @@ TEST(MaterialLoader, LoadMaterialWithAuthoringShaderResolvesShaderPass) { ASSERT_TRUE(materialFile.is_open()); materialFile << "{\n"; materialFile << " \"shader\": \"" << shaderPath.generic_string() << "\",\n"; - materialFile << " \"shaderPass\": \"ForwardLit\",\n"; materialFile << " \"renderQueue\": \"Geometry\"\n"; materialFile << "}\n"; } @@ -393,7 +390,6 @@ TEST(MaterialLoader, LoadMaterialWithAuthoringShaderResolvesShaderPass) { Material* material = static_cast(result.resource); ASSERT_NE(material, nullptr); ASSERT_NE(material->GetShader(), nullptr); - EXPECT_TRUE(material->GetLegacyShaderPassHint().Empty()); ASSERT_NE(material->GetShader()->FindPass("ForwardLit"), nullptr); const ShaderStageVariant* vertexVariant = material->GetShader()->FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::OpenGL); @@ -419,7 +415,6 @@ TEST(MaterialLoader, LoadMaterialWithPropertiesObjectAppliesTypedOverrides) { 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" @@ -435,7 +430,6 @@ TEST(MaterialLoader, LoadMaterialWithPropertiesObjectAppliesTypedOverrides) { auto* material = static_cast(result.resource); ASSERT_NE(material, nullptr); ASSERT_NE(material->GetShader(), nullptr); - EXPECT_TRUE(material->GetLegacyShaderPassHint().Empty()); 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); @@ -446,7 +440,7 @@ TEST(MaterialLoader, LoadMaterialWithPropertiesObjectAppliesTypedOverrides) { fs::remove_all(rootPath); } -TEST(MaterialLoader, LoadBuiltinShaderMaterialDropsRedundantBuiltinShaderPassHint) { +TEST(MaterialLoader, LoadBuiltinShaderMaterialUsesShaderMetadataWithoutMaterialPassHint) { namespace fs = std::filesystem; ResourceManager& manager = ResourceManager::Get(); @@ -460,8 +454,7 @@ TEST(MaterialLoader, LoadBuiltinShaderMaterialDropsRedundantBuiltinShaderPassHin WriteTextFile( materialPath, "{\n" - " \"shader\": \"" + std::string(GetBuiltinUnlitShaderPath().CStr()) + "\",\n" - " \"shaderPass\": \"Unlit\"\n" + " \"shader\": \"" + std::string(GetBuiltinUnlitShaderPath().CStr()) + "\"\n" "}\n"); MaterialLoader loader; @@ -472,7 +465,6 @@ TEST(MaterialLoader, LoadBuiltinShaderMaterialDropsRedundantBuiltinShaderPassHin auto* material = static_cast(result.resource); ASSERT_NE(material, nullptr); ASSERT_NE(material->GetShader(), nullptr); - EXPECT_TRUE(material->GetLegacyShaderPassHint().Empty()); EXPECT_NE(material->GetShader()->FindPass("Unlit"), nullptr); delete material; @@ -568,7 +560,7 @@ TEST(MaterialLoader, LoadMaterialWithPropertiesObjectPreservesShaderDefaultsForO fs::remove_all(rootPath); } -TEST(MaterialLoader, LoadMaterialMapsLegacySemanticKeysIntoShaderSchemaProperties) { +TEST(MaterialLoader, RejectsSemanticKeysWhenShaderSchemaRequiresExactPropertyNames) { namespace fs = std::filesystem; const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_semantic_alias_test"; @@ -593,21 +585,7 @@ TEST(MaterialLoader, LoadMaterialMapsLegacySemanticKeysIntoShaderSchemaPropertie MaterialLoader loader; LoadResult result = loader.Load(materialPath.generic_string().c_str()); - ASSERT_TRUE(result); - ASSERT_NE(result.resource, nullptr); - - auto* material = static_cast(result.resource); - ASSERT_NE(material, nullptr); - ASSERT_NE(material->GetShader(), nullptr); - EXPECT_FALSE(material->HasProperty("baseColor")); - EXPECT_EQ(material->GetFloat4("_BaseColor"), XCEngine::Math::Vector4(0.2f, 0.4f, 0.6f, 0.8f)); - ASSERT_EQ(material->GetTextureBindingCount(), 1u); - EXPECT_EQ(material->GetTextureBindingName(0), "_MainTex"); - EXPECT_EQ( - fs::path(material->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(), - (rootPath / "checker.bmp").lexically_normal().generic_string()); - - delete material; + EXPECT_FALSE(result); fs::remove_all(rootPath); } @@ -624,7 +602,6 @@ TEST(MaterialLoader, LoadMaterialParsesKeywordArrayAgainstShaderSchema) { materialPath, "{\n" " \"shader\": \"" + shaderPath.generic_string() + "\",\n" - " \"shaderPass\": \"ForwardLit\",\n" " \"keywords\": [\"XC_MAIN_LIGHT_SHADOWS\", \"XC_ALPHA_TEST\", \"XC_ALPHA_TEST\", \"_\"]\n" "}\n"); @@ -839,7 +816,6 @@ TEST(MaterialLoader, AssetDatabaseMaterialArtifactRoundTripsKeywords) { materialPath, "{\n" " \"shader\": \"Assets/Shaders/keyword.shader\",\n" - " \"shaderPass\": \"ForwardLit\",\n" " \"keywords\": [\"XC_MAIN_LIGHT_SHADOWS\", \"XC_ALPHA_TEST\"]\n" "}\n"); @@ -873,7 +849,7 @@ TEST(MaterialLoader, AssetDatabaseMaterialArtifactRoundTripsKeywords) { fs::remove_all(projectRoot); } -TEST(MaterialLoader, AssetDatabaseMaterialArtifactStripsRedundantBuiltinShaderPassHint) { +TEST(MaterialLoader, AssetDatabaseMaterialArtifactRoundTripsBuiltinShaderWithoutMaterialPassHint) { namespace fs = std::filesystem; ResourceManager& manager = ResourceManager::Get(); @@ -890,7 +866,6 @@ TEST(MaterialLoader, AssetDatabaseMaterialArtifactStripsRedundantBuiltinShaderPa materialPath, "{\n" " \"shader\": \"" + std::string(GetBuiltinUnlitShaderPath().CStr()) + "\",\n" - " \"shaderPass\": \"Unlit\",\n" " \"properties\": {\n" " \"_BaseColor\": [1.0, 1.0, 1.0, 1.0]\n" " }\n" @@ -914,7 +889,6 @@ TEST(MaterialLoader, AssetDatabaseMaterialArtifactStripsRedundantBuiltinShaderPa auto* material = static_cast(result.resource); ASSERT_NE(material, nullptr); ASSERT_NE(material->GetShader(), nullptr); - EXPECT_TRUE(material->GetLegacyShaderPassHint().Empty()); EXPECT_EQ(material->GetFloat4("_BaseColor"), XCEngine::Math::Vector4(1.0f, 1.0f, 1.0f, 1.0f)); delete material; @@ -1065,7 +1039,6 @@ TEST(MaterialLoader, AssetDatabaseReimportsMaterialWhenShaderDependencyChanges) ASSERT_TRUE(materialFile.is_open()); materialFile << "{\n"; materialFile << " \"shader\": \"Assets/Shaders/lit.shader\",\n"; - materialFile << " \"shaderPass\": \"ForwardLit\",\n"; materialFile << " \"renderQueue\": \"geometry\"\n"; materialFile << "}\n"; } @@ -1133,7 +1106,6 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) { WriteArtifactString(output, "LazyMaterial"); WriteArtifactString(output, "Assets/lazy.material"); WriteArtifactString(output, ""); - WriteArtifactString(output, ""); MaterialArtifactHeader header; header.renderQueue = static_cast(MaterialRenderQueue::Geometry);