StaticMesh optional index buffer serialization / AvailabilityInfo reconstruction に起因する Cook determinism 問題について

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

以下の環境で StaticMesh アセットの Cook determinism を調査しています。

検証環境:

- UE version: 5.7.4-based custom branch

- TargetPlatform: Windows

同一の checkout / content revision / Cook 設定から Pass1 / Pass2 形式で Cook し、Pass2 では `-diffonly` により Pass1 の cooked package と Pass2 の in-memory cook result を比較しました。

その結果、ソースアセットに変更がないにもかかわらず、多数の StaticMesh cooked package でバイナリ差分が発生しました。ログ全文や具体的なアセットパスは社内情報を含むため省略します。

調査の結果、StaticMesh の optional index buffer availability と optional index buffer payload の解釈が、load / save / cook 経路で一致していないことが原因ではないかと考えています。

現在確認している挙動では、AvailabilityInfo の packed bits 自体が単独で非決定的に変動しているというより、load 時に CVar や初期 `bHas*` state から計算される `bEnableDepthOnlyIndexBuffer` / `bEnableReversedIndexBuffer` が、既に serialized されている optional index buffer payload を dummy / discard / `ClearMetaData()` 側へ流す gate として作用している点が問題の中心ではないかと考えています。

その結果、後続の save / cook 経路で availability state と実際の optional buffer payload の整合が崩れ、DiffOnly 差分として表面化しているように見えます。

関係している情報源は、Serialized AvailabilityInfo の packed bits、ロード済み optional index buffer data の有無、`bHasDepthOnlyIndices` / `bHasReversedIndices` / `bHasReversedDepthOnlyIndices`、`r.SupportDepthOnlyIndexBuffers` / `r.SupportReversedIndexBuffers`、および depth-only / reversed index buffer に対する serialize / discard / `ClearMetaData()` の gate です。

Diff stack 上は、主に `FRawStaticIndexBuffer::Serialize()`、`FStaticMeshLODResources::SerializeBuffers()`、`FStaticMeshLODResources::Serialize()`、`FStaticMeshRenderData::Serialize()`、`UStaticMesh::Serialize()` 経由の差分として確認しました。対象 payload は主に `ReversedIndexBuffer`、`DepthOnlyIndexBuffer`、`ReversedDepthOnlyIndexBuffer` です。

なお、Pass2 から `-iterate` を外した control run でも StaticMesh 差分は再現しました。そのため、`-iterate` 自体が主因ではなく、`-diffonly` 単体でも入る previous cooked package on disk と current in-memory cook buffer の比較経路で検出されているものと考えています。

DDC が有効に効いている状態では再生成や serialize 経路が回避される可能性があるため、検証時には DDC 再利用を避ける設定にしたうえで、Cook 前に `ResavePackages` を実行し、StaticMesh の load / save / serialize 経路を明示的に通しました。

この挙動を安定化するため、ローカルで Engine patch を適用しました。修正の主旨は以下です。

1. `SerializeBuffers()` の load 時には、既に serialized されている optional index buffer payload を CVar gate や初期 `bHas*` 値だけで dummy / discard 側に流さず、実体として保持する。

2. `SerializeAvailabilityInfo()` の load 時には、serialized packed bits だけでなく、実際にロード済みの optional buffer data の有無も考慮して effective な `bHas*` 値を決定する。

3. Save 時には、実際に serialize される buffer data と保存される availability state が一致するようにする。

この修正では、CVar は「新規に depth-only / reversed index buffer support を要求するか」の条件としては使用しますが、既に serialized されている optional index buffer payload / availability を load 時に消す条件としては扱わないようにしています。

修正前:

DiffOnly 上で StaticMesh として報告された差分エントリ: 578

修正後:

DiffOnly 上で StaticMesh として報告された差分エントリ: 0

最終確認では、StaticMesh / `FRawStaticIndexBuffer::Serialize()` / optional index buffer payload 由来の差分は 0 件になり、一方で StaticMesh 以外の差分は引き続き検出されていました。そのため、DiffOnly の検出自体が無効になったわけではないと考えています。

確認したい点は以下です。

1. StaticMesh optional index buffer serialization / AvailabilityInfo reconstruction の経路は、同一条件の繰り返し Cook で決定的になることを意図されていますでしょうか。

2. Load 時には、`bEnableDepthOnlyIndexBuffer` / `bEnableReversedIndexBuffer` が false の場合でも、既に serialized されている optional index buffer payload を discard / `ClearMetaData()` せず保持すべきでしょうか。

3. AvailabilityInfo 再構築時に、serialized packed bits だけでなく、実際にロードされた optional buffer data の有無も考慮して effective な `bHas*` 値を決めることは妥当でしょうか。

4. CVar は、新規生成や platform support の可否には影響する一方で、既存 package からロードされた serialized optional index buffer payload / availability を消す条件としては扱わない、という方針は妥当でしょうか。

5. この方針により、runtime memory usage、platform-specific stripping、DDC compatibility、cooked package compatibility などに問題が発生する可能性はありますでしょうか。

6. Epic 側で Engine 修正として取り込んでいただくことは可能でしょうか。または、既存の推奨修正や workaround はありますでしょうか。

この問題は StaticMesh serialization path における Engine 側の Cook determinism 問題ではないかと考えています。ローカル patch で環境の StaticMesh Cook 出力は安定化できましたが、根治としては Epic 側の意図に沿った Engine 修正に揃えたいと考えています。

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

[Attachment Removed]

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

1. StaticMesh optional index buffer serialization / AvailabilityInfo reconstruction の経路は、同一条件の繰り返し Cook で決定的になることを意図されていますでしょうか。

意図されていない挙動で、StaticMeshのCookが決定的でないことは UE-366564 で非公開のバグとして登録されていました。同一cook条件でDDCミス時とDDCヒット時にoutput sizeが変動することが検証で確認されており、それが根本的な non-determinism の原因となっていました。この修正は CL#51083751 で修正されており、UE5.8にてリリースされる予定です。早期にチェックが必要な場合は、こちらのチェンジリストを適用していただけますと幸いです。

2. Load 時には、bEnableDepthOnlyIndexBuffer/bEnableReversedIndexBuffer が false の場合でも、既に serialized されている optional index buffer payload を discard/ClearMetaData() せず保持すべきでしょうか。

上記の修正は「Cook時に設定がdisabledであればパッケージに optional index bufferをserializeしない(dead-stripする)」という方向で、「load 時に保持する」というアプローチは load/save 間の整合性を保つ点では合理的ですが、「そもそもdisabled featuresのバッファはcooked packageに含めない」ことで修正する方針です。

3. AvailabilityInfo 再構築時に、serialized packed bits だけでなく、実際にロードされた optional buffer data の有無も考慮して effective な `bHas*` 値を決めることは妥当でしょうか。

技術的に妥当ではありますが、修正では AvailabilityInfo をロード済みデータから再構築するのではなく、Cook 時に serialize するデータ自体を機能フラグと一致させる方向で修正しています。実際のバッファが存在する場合に AvailabilityInfo をそれに合わせることは一時的な整合性確保として有効ですが、根本的な修正としては Cook 側の制御が適切です。

4. CVar は、新規生成や platform support の可否には影響する一方で、既存 package からロードされた serialized optional index buffer payload/availability を消す条件としては扱わない、という方針は妥当でしょうか。

CVarをload 時の消去条件として使わないというのは理にかなっており、内部での議論でも「既にシリアライズされているデータをロード時に消す」というロジックに問題があると認識していますが、Cook 時に明示的に strip することが正しい対処としています。

5. この方針により、runtime memory usage、platform-specific stripping、DDC compatibility、cooked package compatibility などに問題が発生する可能性はありますでしょうか。

disabled features のバッファを Cook 時に stripすることでの影響は以下の認識です。

  • Runtime memory: features が disabled なプラットフォームではバッファが保持されなくなるため memory 削減につながる
  • Package size: depth-only index buffer を dead-strip した結果として実際にパッケージサイズが削減される

6. Epic 側で Engine 修正として取り込んでいただくことは可能でしょうか。または、既存の推奨修正や workaround はありますでしょうか。

CL#51083751 にて修正が正式にリリースされております。

よろしくお願いします。

[Attachment Removed]

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

ご確認ありがとうございます。

UE-366564 / CL#51083751 の情報ありがとうございます。

こちらの調査で確認していた現象も、DDC miss / hit 間で optional index buffer payload / availability state が揺れることによる StaticMesh Cook nondeterminism と理解しました。

弊社ローカルpatchでは load 時に既存 serialized optional buffer を保持する方向で安定化していましたが、Epic側の修正方針が Cook 時に disabled feature の optional index buffer を dead-strip し、serialize data と feature flag を一致させるものである点、理解しました。

まずは CL#51083751 を弊社 branch に適用し、同じ Pass1 / Pass2 `-diffonly` 検証で StaticMesh 差分が解消されるか確認します。

確認後、必要があれば追加でご連絡いたします。

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

[Attachment Removed]