Material Function出力の初期化順序によりPropertyConnectedMaskが非決定的になりCooked出力が変化する

お世話になっております。

UE 5.8.1で、同一コンテンツを複数回Cookした際、Material InstanceのCookedバイナリが非決定的に変化する問題を確認しました。

差分が発生する値は以下です。

FMaterialCachedExpressionData::PropertyConnectedMask

対象Materialは、Material Attributesを返すMaterial Function Callをネストし、Static Switchで複数のMaterial Attributes経路を選択する構成です。

CookによってPropertyConnectedMaskが本来の接続状態を表す値になる場合と、0x1BFFFFFFFになる場合があります。

後者はDoPostAnalyzeChecks()でFront Material以外の全Material Propertyが接続済みと判定された値と一致します。

コード上では、次の経路が疑われます。

1. UpdateForFunction()がMaterial Function内部を解析する。

2. ネストされたUMaterialExpressionMaterialFunctionCallのFunctionOutputs[].ExpressionOutputがnullptrのままIsResultMaterialAttributes()が呼ばれる場合がある。

3. IsResultMaterialAttributes()がfalseを返し、DoPostAnalyzeChecks()が多数のMaterial Propertyを接続済みとして設定する。

4. その結果がPropertyConnectedMaskとしてCooked dataに保存され、Cook間のバイナリ差分になる。

ExpressionOutputはtransientであり、UpdateFromFunctionResource()で再構築される値と認識しています。この初期化時期がロード順序やキャッシュ状態に依存しているように見えます。

UE 5.2の以下の投稿で、ExpressionInputおよびExpressionOutputが未初期化になる類似問題が報告されています。

Editor only crash UMaterialExpressionMaterialFunctionCall::Compile due to async loading [Content removed]

既報ではEditorのクラッシュとして発生し、未初期化時にUpdateFromFunctionResource()を呼ぶ回避策が提示されています。今回の症状はクラッシュではなく、Cooked dataの非決定的な変化です。

質問:

1. UE 5.2の類似投稿以降、関連する修正CLまたは既知問題はありますでしょうか。

2. UpdateForFunction()またはDoPostAnalyzeChecks()より前に、ネストされたMaterial Function CallのUpdateFromFunctionResource()を保証する方針は適切でしょうか。

3. transientなExpressionOutputの初期化状態によって、シリアライズ対象のPropertyConnectedMaskが変化することは想定された挙動でしょうか。

よろしくお願いいたします。

お世話になっております。

​詳細な内容をご報告頂きありがとうございます。以下、ご質問についての回答となります。

​

1. UE 5.2の類似投稿以降、関連する修正CLまたは既知問題はありますでしょうか。

ご指摘の問題について、非決定的なCookedDataとなる問題についてはデータベースを検索しましたが見つけることができませんでした。また、ご指摘の ExpressionOutput 未初期化を UpdateFromFunctionResource() で修正する回避策や対応は、5.8.1およびそれ以降の変更でも見つけることができませんでした。一方で UE5.9 では、0x1BFFFFFFF を書く部分である DoPostAnalyzeChecks() 側の処理自体が削除されており、この問題はUE5.8において発生しうるが、それ以降では発生しない可能性もあるといったことが推測されます。

以下の部分が該当箇所かと推測しております。​

// Legacy materials may have a static switch with MaterialAttributes on one branch
// and a non-MaterialAttributes value (e.g. scalar constant) on the other.
...
		if (bAIsMaterialAttributes != bBIsMaterialAttributes)
		{
			for (int32 PropertyIndex = 0; PropertyIndex < MP_MaterialAttributes; ++PropertyIndex)
			{
				if (PropertyIndex != MP_FrontMaterial || bAIsSubstrateAttributes != bBIsSubstrateAttributes)
				{
					SetPropertyConnected((EMaterialProperty)PropertyIndex);
				}
			}
		}

​

​

2. UpdateForFunction()またはDoPostAnalyzeChecks()より前に、ネストされたMaterial Function CallのUpdateFromFunctionResource()を保証する方針は適切でしょうか。

​解析前に UpdateFromFunctionResource() を保証する方針は適切かと思われます。UpdateFromFunctionResource() はパラメータ収集順を変えないため、次の方法は安全です。

​

(1) IsResultMaterialAttributes()/IsResultSubstrateMaterial() で ExpressionOutput == nullptr なら UpdateFromFunctionResource() してから判定

(2) AnalyzeMaterial() の冒頭で Material とその依存 Function 内の Function Call に UpdateFromFunctionResource() を先に実行

(3) DoPostAnalyzeChecks() では ExpressionOutput == nullptr を"不一致"として扱わないようにする

​

UE5.8で修正が必要な場合は、1. が安全かと思います。

​

3. transientなExpressionOutputの初期化状態によって、シリアライズ対象のPropertyConnectedMaskが変化することは想定された挙動でしょうか。

​

​想定されていない挙動です。もしこの問題の明確な再現手順がございましたらお知らせ頂けますと幸いです。これを最新のエンジンバージョンでも検証してエンジンの本流に反映させるように修正を行いたいと思います。

​

ご不便をおかけしまして恐れ入りますが、どうぞよろしくお願いします。

​

お世話になっております。

詳細なご回答ありがとうございます。

​

追加調査の結果、本現象は単にCookを複数回行うだけではなく、DDCキャッシュの有無やロード順序の違いが再現条件に関係している可能性が高いことを確認しています。

​

また、本現象はUE 5.8への移行後に確認されており、UE 5.8環境でデフォルト有効となっているZen StoreのCooked Output Storeや、Platform Oplogの状態についても再現性に影響している可能性があります。

DDCとZen Storeは別の仕組みであるため、現在はDDC、Zen Store、Platform Oplogの状態を分離し、以下の条件を切り替えて検証しています。

​

・DDCが空の状態/既存キャッシュがある状態

・Zen Storeが有効/無効の状態

・Platform Oplogを削除した状態/既存Oplogがある状態

​

あわせて、UE 5.8上で解析前にネストされたMaterial Function CallのUpdateFromFunctionResource()を保証する修正を適用し、修正前後のCooked出力を比較しています。

ただ、Cookおよび最終コンテナの比較には時間を要するため、再現手順と修正結果をご報告するまで、今しばらくお時間をいただけますでしょうか。

再現条件が確定しましたら、PropertyConnectedMaskの修正前後の値、DDC/Zen Store/Platform Oplogの各条件、および実行手順を整理してご連絡させていただければと思います。

よろしくお願いいたします。