@@ -268,6 +268,7 @@ namespace
268268
269269 RwTexDictionary* g_pCachedVehicleTxd = nullptr ;
270270 RwListEntry* g_pCachedVehicleTxdListHead = nullptr ; // textures.root.next at cache time; detects texture rebuild at same TXD address
271+ RwListEntry* g_pCachedVehicleTxdListTail = nullptr ; // textures.root.prev at cache time; detects changes at list tail
271272 TxdTextureMap g_CachedVehicleTxdMap;
272273 unsigned short g_usVehicleTxdSlotId = 0xFFFF ;
273274
@@ -370,7 +371,7 @@ namespace
370371 return nullptr ;
371372 }
372373
373- void BuildTxdTextureMapFast (RwTexDictionary* pTxd, TxdTextureMap& outMap);
374+ bool BuildTxdTextureMapFast (RwTexDictionary* pTxd, TxdTextureMap& outMap);
374375 static void AddVehicleTxdFallback (TxdTextureMap& outMap);
375376 static bool ShouldUseVehicleTxdFallback (unsigned short usModelId);
376377 static bool IsReadableTexture (RwTexture* pTexture);
@@ -417,17 +418,21 @@ namespace
417418 constexpr std::size_t MAX_PENDING_ISOLATED_PER_TICK = 64 ;
418419
419420 // Per-TXD texture map cache to avoid repeated linked list iteration.
420- // Keyed by TXD slot ID; validated by TXD pointer and texture list head
421- // (the list head catches TXD rebuilds at the same pool address ).
421+ // Keyed by TXD slot ID; validated by TXD pointer, list head, and list tail
422+ // (head/tail catch TXD rebuilds and texture adds/removes at either end ).
422423 struct SCachedTxdTextureMap
423424 {
424425 RwTexDictionary* pTxd = nullptr ; // TXD pointer at cache time (detects reload/slot reuse)
425- RwListEntry* pListHead = nullptr ; // textures.root.next at cache time (detects texture rebuild at same TXD address)
426+ RwListEntry* pListHead = nullptr ; // textures.root.next at cache time (detects head change)
427+ RwListEntry* pListTail = nullptr ; // textures.root.prev at cache time (detects tail change)
426428 TxdTextureMap textureMap; // Cached texture name > texture map
427429
428430 bool IsValid (RwTexDictionary* pCurrentTxd) const noexcept
429431 {
430- if (pTxd == nullptr || pTxd != pCurrentTxd || pListHead != pCurrentTxd->textures .root .next )
432+ if (pTxd == nullptr || pTxd != pCurrentTxd)
433+ return false ;
434+
435+ if (pListHead != pCurrentTxd->textures .root .next || pListTail != pCurrentTxd->textures .root .prev )
431436 return false ;
432437
433438 if (pListHead && pListHead != &pCurrentTxd->textures .root )
@@ -458,7 +463,7 @@ namespace
458463 g_TxdTextureMapCache.swap (temp);
459464 }
460465
461- // Build or retrieve cached texture map and merge into output map
466+ // Build or retrieve cached texture map and merge into output map.
462467 // Use when building combined maps (parent + child + vehicle fallback)
463468 static void MergeCachedTxdTextureMap (unsigned short usTxdId, RwTexDictionary* pTxd, TxdTextureMap& outMap)
464469 {
@@ -474,16 +479,25 @@ namespace
474479 return ;
475480 }
476481
482+ // Rebuild: reset cached metadata before populating
477483 entry.pTxd = pTxd;
478484 entry.pListHead = pTxd->textures .root .next ;
485+ entry.pListTail = pTxd->textures .root .prev ;
479486 // Use swap idiom to ensure complete memory release
480487 // (avoids reconnect leaks; safer than .clear() on potentially corrupted maps)
481488 TxdTextureMap temp;
482489 entry.textureMap .swap (temp);
483- BuildTxdTextureMapFast (pTxd, entry.textureMap );
490+ bool bComplete = BuildTxdTextureMapFast (pTxd, entry.textureMap );
484491
485492 for (const auto & kv : entry.textureMap )
486493 outMap[kv.first ] = kv.second ;
494+
495+ // Don't keep a partial map in the cache.. next call will rebuild from scratch
496+ if (!bComplete)
497+ {
498+ g_TxdTextureMapCache.erase (usTxdId);
499+ AddReportLog (9401 , SString (" BuildTxdTextureMapFast: partial walk for TXD %u, cache entry discarded" , usTxdId));
500+ }
487501 }
488502
489503 // Remove pReplacementTextures from all usedByReplacements vectors in the map.
@@ -1235,7 +1249,10 @@ namespace
12351249
12361250 const bool bProcessedAll = (pNode == pRoot);
12371251 if (!bProcessedAll)
1252+ {
1253+ AddReportLog (9401 , SString (" OrphanTxdTexturesBounded: bailed after %u entries, TXD list too large or corrupt" , (unsigned int )uiCount));
12381254 return false ;
1255+ }
12391256
12401257 for (RwTexture* pTexture : vecTexturesToOrphan)
12411258 {
@@ -2468,17 +2485,19 @@ namespace
24682485 return true ;
24692486 }
24702487
2471- // Fast name->texture map builder
2472- void BuildTxdTextureMapFast (RwTexDictionary* pTxd, TxdTextureMap& outMap)
2488+ // Fast name->texture map builder. Returns true if the full list was walked,
2489+ // false if the walk bailed early (corrupt list, limit hit, null node).
2490+ // Callers that cache the result must not cache incomplete maps.
2491+ bool BuildTxdTextureMapFast (RwTexDictionary* pTxd, TxdTextureMap& outMap)
24732492 {
24742493 if (!pTxd)
2475- return ;
2494+ return false ;
24762495
24772496 RwListEntry* const pRoot = &pTxd->textures .root ;
24782497 RwListEntry* pNode = pRoot->next ;
24792498
24802499 if (pNode == nullptr || pNode == pRoot)
2481- return ;
2500+ return true ;
24822501
24832502 if (outMap.empty ())
24842503 outMap.reserve (32 );
@@ -2489,19 +2508,21 @@ namespace
24892508 while (pNode != pRoot)
24902509 {
24912510 if (++count > kMaxTextures )
2492- return ;
2511+ return false ;
24932512
24942513 RwTexture* pTex = reinterpret_cast <RwTexture*>(reinterpret_cast <char *>(pNode) - offsetof (RwTexture, TXDList));
24952514 if (!pTex)
2496- return ;
2515+ return false ;
24972516
24982517 if (strnlen (pTex->name , RW_TEXTURE_NAME_LENGTH) < RW_TEXTURE_NAME_LENGTH)
24992518 outMap[pTex->name ] = pTex;
25002519
25012520 pNode = pNode->next ;
25022521 if (!pNode)
2503- return ;
2522+ return false ;
25042523 }
2524+
2525+ return true ;
25052526 }
25062527
25072528 RwTexDictionary* GetVehicleTxd ()
@@ -2519,15 +2540,16 @@ namespace
25192540 if (!pVehicleTxd)
25202541 return ;
25212542
2522- // Check both TXD pointer and its texture list head.
2543+ // Check TXD pointer, texture list head and tail .
25232544 // Streaming can destroy and reload a TXD at the same address (pool/allocator reuse),
25242545 // which leaves the cache with dangling texture name pointers.
25252546 RwListEntry* pListHead = pVehicleTxd->textures .root .next ;
2547+ RwListEntry* pListTail = pVehicleTxd->textures .root .prev ;
25262548 RwTexture* pFirstTex = nullptr ;
25272549 if (pListHead && pListHead != &pVehicleTxd->textures .root )
25282550 pFirstTex = (RwTexture*)((char *)pListHead - offsetof (RwTexture, TXDList));
25292551
2530- bool bInvalidate = (pVehicleTxd != g_pCachedVehicleTxd || pListHead != g_pCachedVehicleTxdListHead);
2552+ bool bInvalidate = (pVehicleTxd != g_pCachedVehicleTxd || pListHead != g_pCachedVehicleTxdListHead || pListTail != g_pCachedVehicleTxdListTail );
25312553
25322554 if (!bInvalidate && pFirstTex && !g_CachedVehicleTxdMap.empty ())
25332555 {
@@ -2543,8 +2565,14 @@ namespace
25432565 g_CachedVehicleTxdMap.clear ();
25442566 g_pCachedVehicleTxd = pVehicleTxd;
25452567 g_pCachedVehicleTxdListHead = pListHead;
2568+ g_pCachedVehicleTxdListTail = pListTail;
25462569 g_usVehicleTxdSlotId = 0xFFFF ;
2547- BuildTxdTextureMapFast (pVehicleTxd, g_CachedVehicleTxdMap);
2570+ if (!BuildTxdTextureMapFast (pVehicleTxd, g_CachedVehicleTxdMap))
2571+ {
2572+ // Incomplete build - use what we have but don't keep caching it
2573+ g_pCachedVehicleTxd = nullptr ;
2574+ AddReportLog (9401 , " BuildTxdTextureMapFast: partial walk for vehicle TXD, cache disabled" );
2575+ }
25482576 }
25492577
25502578 for (const auto & entry : g_CachedVehicleTxdMap)
@@ -5058,6 +5086,7 @@ CModelTexturesInfo* CRenderWareSA::GetModelTexturesInfo(unsigned short usModelId
50585086 g_bInTxdReapply = true ;
50595087
50605088 RwTexDictionary* txdAtStart = pCurrentTxd;
5089+ bool bTxdChangedMidReapply = false ;
50615090
50625091 for (auto & reapplyEntry : replacementsToReapply)
50635092 {
@@ -5069,13 +5098,19 @@ CModelTexturesInfo* CRenderWareSA::GetModelTexturesInfo(unsigned short usModelId
50695098 if (!modelIds.empty ())
50705099 {
50715100 if (CTxdStore_GetTxd (usTxdId) != txdAtStart)
5101+ {
5102+ bTxdChangedMidReapply = true ;
50725103 break ;
5104+ }
50735105
50745106 size_t uiAppliedIndex = modelIds.size ();
50755107 for (size_t i = 0 ; i < modelIds.size (); ++i)
50765108 {
50775109 if (CTxdStore_GetTxd (usTxdId) != txdAtStart)
5110+ {
5111+ bTxdChangedMidReapply = true ;
50785112 break ;
5113+ }
50795114
50805115 unsigned short usTestModelId = modelIds[i];
50815116 const uint32_t uiStartSerial = g_uiIsolationDeniedSerial;
@@ -5091,10 +5126,16 @@ CModelTexturesInfo* CRenderWareSA::GetModelTexturesInfo(unsigned short usModelId
50915126 break ;
50925127 }
50935128
5129+ if (bTxdChangedMidReapply)
5130+ break ;
5131+
50945132 if (bAppliedToFirstModel)
50955133 {
50965134 if (CTxdStore_GetTxd (usTxdId) != txdAtStart)
5135+ {
5136+ bTxdChangedMidReapply = true ;
50975137 break ;
5138+ }
50985139
50995140 for (size_t i = 0 ; i < modelIds.size (); ++i)
51005141 {
@@ -5111,6 +5152,19 @@ CModelTexturesInfo* CRenderWareSA::GetModelTexturesInfo(unsigned short usModelId
51115152 }
51125153 }
51135154
5155+ // Queue un-applied replacements when a TXD change forced erly exit
5156+ if (bTxdChangedMidReapply)
5157+ {
5158+ for (auto & remaining : replacementsToReapply)
5159+ {
5160+ if (!remaining.first )
5161+ continue ;
5162+ for (unsigned short usModelId : remaining.second )
5163+ QueuePendingReplacement (usModelId, remaining.first , 0 , 0 );
5164+ }
5165+ AddReportLog (9401 , SString (" GetModelTexturesInfo: TXD %u changed mid-reapply, queued remaining replacements" , usTxdId));
5166+ }
5167+
51145168 g_bInTxdReapply = bPrevInReapply;
51155169 }
51165170 info.bReapplyingTextures = false ;
@@ -7971,6 +8025,7 @@ void CRenderWareSA::StaticResetModelTextureReplacing()
79718025
79728026 g_pCachedVehicleTxd = nullptr ;
79738027 g_pCachedVehicleTxdListHead = nullptr ;
8028+ g_pCachedVehicleTxdListTail = nullptr ;
79748029 g_CachedVehicleTxdMap = TxdTextureMap{};
79758030 g_usVehicleTxdSlotId = 0xFFFF ;
79768031 ClearTxdTextureMapCache ();
0 commit comments