@@ -738,17 +738,24 @@ namespace
738738 id = pSlot->usParentIndex ;
739739 }
740740
741+ bool bChainComplete = true ;
741742 for (std::size_t i = chainLen; i > 0 ; --i)
742743 {
743744 RwTexDictionary* pChainTxd = CTxdStore_GetTxd (chain[i - 1 ]);
744745 if (pChainTxd)
745746 MergeCachedTxdTextureMap (chain[i - 1 ], pChainTxd, targetTxdMap);
747+ else
748+ bChainComplete = false ;
746749 }
747750
748751 if (ShouldUseVehicleTxdFallback (usModelId))
749752 AddVehicleTxdFallback (targetTxdMap);
750753
751- if (!targetTxdMap.empty ())
754+ // Only accept a non-empty map when every ancestor in the chain
755+ // was loaded. A partial chain can produce a map that looks valid
756+ // but is missing textures from unloaded ancestors, which leads
757+ // to white surfaces after rebind.
758+ if (!targetTxdMap.empty () && bChainComplete)
752759 return true ;
753760
754761 TextureNameSet modelTextureNames;
@@ -3943,11 +3950,14 @@ namespace
39433950 }
39443951
39453952 TxdTextureMap txdTextureMap;
3953+ bool bChainComplete = true ;
39463954 for (std::size_t i = chainLen; i > 0 ; --i)
39473955 {
39483956 RwTexDictionary* pChainTxd = CTxdStore_GetTxd (chain[i - 1 ]);
39493957 if (pChainTxd)
39503958 MergeCachedTxdTextureMap (chain[i - 1 ], pChainTxd, txdTextureMap);
3959+ else
3960+ bChainComplete = false ;
39513961 }
39523962
39533963 bool bNeedVehicleFallback = (usFirstParentId != static_cast <unsigned short >(-1 )) && TxdChainContainsVehicleTxd (usFirstParentId);
@@ -3971,6 +3981,16 @@ namespace
39713981 if (bNeedVehicleFallback)
39723982 AddVehicleTxdFallback (txdTextureMap);
39733983
3984+ // Skip rebind when any ancestor in the chain was not loaded.
3985+ // A partial map would replace some textures but leave others
3986+ // as white until the missing ancestors stream in.
3987+ if (!bChainComplete)
3988+ {
3989+ AddReportLog (9402 , SString (" RebindLoadedModelToCurrentTxd: incomplete TXD chain for model %u TXD %u (chain length %u)" , pModelInfo->GetModel (),
3990+ usTxdId, static_cast <unsigned int >(chainLen)));
3991+ return ;
3992+ }
3993+
39743994 if (txdTextureMap.empty ())
39753995 {
39763996 AddReportLog (9402 , SString (" RebindLoadedModelToCurrentTxd: empty texture map for model %u TXD %u (chain length %u)" , pModelInfo->GetModel (),
@@ -4233,12 +4253,45 @@ void CRenderWareSA::ProcessPendingIsolatedModels(bool bBlockingParentLoad)
42334253 pModelInfo->SetTextureDictionaryID (childTxdId);
42344254 }
42354255
4236- RwTexDictionary* pChildTxd = CTxdStore_GetTxd (childTxdId);
4237- TxdTextureMap txdTextureMap;
4238- if (pParentTxd)
4239- MergeCachedTxdTextureMap (parentTxdId, pParentTxd, txdTextureMap);
4240- if (pChildTxd)
4241- MergeCachedTxdTextureMap (childTxdId, pChildTxd, txdTextureMap);
4256+ // Walk the full TXD parent chain from childTxdId so all loaded
4257+ // ancestors contribute textures, not just the immediate parent.
4258+ TxdTextureMap txdTextureMap;
4259+ {
4260+ constexpr std::size_t kMaxChainDepth = 16 ;
4261+ unsigned short chainIds[kMaxChainDepth ];
4262+ std::size_t chainLen = 0 ;
4263+
4264+ for (unsigned short id = childTxdId; chainLen < kMaxChainDepth ;)
4265+ {
4266+ bool bCycle = false ;
4267+ for (std::size_t j = 0 ; j < chainLen; ++j)
4268+ {
4269+ if (chainIds[j] == id)
4270+ {
4271+ bCycle = true ;
4272+ break ;
4273+ }
4274+ }
4275+ if (bCycle)
4276+ break ;
4277+
4278+ chainIds[chainLen++] = id;
4279+
4280+ CTextureDictonarySAInterface* pChainSlot = pTxdPoolSA->GetTextureDictonarySlot (id);
4281+ if (!pChainSlot || pChainSlot->usParentIndex == static_cast <unsigned short >(-1 ))
4282+ break ;
4283+
4284+ id = pChainSlot->usParentIndex ;
4285+ }
4286+
4287+ // Root-to-child order so child textures override ancestors on name conflict.
4288+ for (std::size_t i = chainLen; i > 0 ; --i)
4289+ {
4290+ RwTexDictionary* pChainTxd = CTxdStore_GetTxd (chainIds[i - 1 ]);
4291+ if (pChainTxd)
4292+ MergeCachedTxdTextureMap (chainIds[i - 1 ], pChainTxd, txdTextureMap);
4293+ }
4294+ }
42424295
42434296 bool bNeedVehicleFallback = TxdChainContainsVehicleTxd (parentTxdId);
42444297 if (!bNeedVehicleFallback && pModelInfo)
@@ -4420,12 +4473,45 @@ void CRenderWareSA::ProcessPendingIsolatedModels(bool bBlockingParentLoad)
44204473 pModelInfo->SetTextureDictionaryID (childTxdId);
44214474 }
44224475
4423- RwTexDictionary* pChildTxd = CTxdStore_GetTxd (childTxdId);
4424- TxdTextureMap txdTextureMap;
4425- if (pParentTxd)
4426- MergeCachedTxdTextureMap (parentTxdId, pParentTxd, txdTextureMap);
4427- if (pChildTxd)
4428- MergeCachedTxdTextureMap (childTxdId, pChildTxd, txdTextureMap);
4476+ // Walk the full TXD parent chain from childTxdId so all loaded
4477+ // ancestors contribute textures, not just the immediate parent.
4478+ TxdTextureMap txdTextureMap;
4479+ {
4480+ constexpr std::size_t kMaxChainDepth = 16 ;
4481+ unsigned short chainIds[kMaxChainDepth ];
4482+ std::size_t chainLen = 0 ;
4483+
4484+ for (unsigned short id = childTxdId; chainLen < kMaxChainDepth ;)
4485+ {
4486+ bool bCycle = false ;
4487+ for (std::size_t j = 0 ; j < chainLen; ++j)
4488+ {
4489+ if (chainIds[j] == id)
4490+ {
4491+ bCycle = true ;
4492+ break ;
4493+ }
4494+ }
4495+ if (bCycle)
4496+ break ;
4497+
4498+ chainIds[chainLen++] = id;
4499+
4500+ CTextureDictonarySAInterface* pChainSlot = pTxdPoolSA->GetTextureDictonarySlot (id);
4501+ if (!pChainSlot || pChainSlot->usParentIndex == static_cast <unsigned short >(-1 ))
4502+ break ;
4503+
4504+ id = pChainSlot->usParentIndex ;
4505+ }
4506+
4507+ // Root-to-child order so child textures override ancestors on name conflict.
4508+ for (std::size_t i = chainLen; i > 0 ; --i)
4509+ {
4510+ RwTexDictionary* pChainTxd = CTxdStore_GetTxd (chainIds[i - 1 ]);
4511+ if (pChainTxd)
4512+ MergeCachedTxdTextureMap (chainIds[i - 1 ], pChainTxd, txdTextureMap);
4513+ }
4514+ }
44294515
44304516 bool bNeedVehicleFallback = TxdChainContainsVehicleTxd (parentTxdId);
44314517 if (!bNeedVehicleFallback && pModelInfo)
@@ -6967,10 +7053,22 @@ void CRenderWareSA::ModelInfoTXDRemoveTextures(SReplacementTextures* pReplacemen
69677053 g_GlobalMtaRasterCache.insert (pMaster->raster );
69687054 }
69697055
7056+ // Re-resolve loaded models to their current TXD so geometry materiale
7057+ // no longer reference stale replacement textures from the old session.
7058+ for (const unsigned short usModelId : pReplacementTextures->usedInModelIds )
7059+ {
7060+ auto * pModelInfo = static_cast <CModelInfoSA*>(pGame->GetModelInfo (usModelId));
7061+ if (!pModelInfo)
7062+ continue ;
7063+
7064+ const unsigned short usTxdId = pModelInfo->GetTextureDictionaryID ();
7065+ RebindLoadedModelToCurrentTxd (pModelInfo, usTxdId);
7066+ }
7067+
69707068 pReplacementTextures->textures .clear ();
69717069 pReplacementTextures->perTxdList .clear ();
69727070 pReplacementTextures->usedInTxdIds .clear ();
6973- pReplacementTextures-> usedInModelIds . clear ();
7071+ // Keep usedInModelIds for the caller's restream pass, matching the normal cleanup path.
69747072 pReplacementTextures->bHasRequestedSpace = false ;
69757073 return ;
69767074 }
0 commit comments