@@ -201,6 +201,72 @@ public void RecordFailedAssetWithoutTouchingCache()
201201 Assert . That ( cache . Outstanding ( "MISSING_HASH" ) , Is . EqualTo ( 0 ) ) ;
202202 }
203203
204+ [ Test ]
205+ public void SuppressRenderingButKeepObjectsActiveUntilRevealed ( )
206+ {
207+ // Guards the LOD_1 -> LOD_0 atomic-swap fix: while descriptor assets stream in, their rendering
208+ // must be suppressed so the half-assembled LOD_0 never draws on top of the still-visible LOD_1.
209+ // Crucially the GameObjects stay active so colliders remain registered; the reveal (owned by
210+ // InstantiateSceneLODInfoSystem at the swap) restores rendering, not GameObject activation.
211+ const string HASH_A = "ITEM_A" ;
212+ const string HASH_B = "ITEM_B" ;
213+
214+ GltfContainerAsset assetA = MakeFakeGltfWithRenderer ( HASH_A , out Renderer rendererA ) ;
215+ GltfContainerAsset assetB = MakeFakeGltfWithRenderer ( HASH_B , out Renderer rendererB ) ;
216+ cache . Stash ( HASH_A , assetA ) ;
217+ cache . Stash ( HASH_B , assetB ) ;
218+
219+ var descriptor = ISSDescriptor . CreateUninitialized ( ) ;
220+
221+ descriptor . MarkResolved ( new [ ]
222+ {
223+ NewDescriptorEntry ( HASH_A ) ,
224+ NewDescriptorEntry ( HASH_B ) ,
225+ } ) ;
226+
227+ InitialSceneStateLOD lod = CreateLODEntity ( descriptor ) ;
228+
229+ system . Update ( 0 ) ;
230+
231+ Assert . That ( lod . AllAssetsInstantiated ( ) , Is . True ) ;
232+
233+ Assert . That ( rendererA . forceRenderingOff , Is . True , "Rendering must be suppressed while LOD_0 assembles" ) ;
234+ Assert . That ( rendererB . forceRenderingOff , Is . True , "Rendering must be suppressed while LOD_0 assembles" ) ;
235+ Assert . That ( assetA . Root . activeInHierarchy , Is . True , "GameObject must stay active so colliders survive" ) ;
236+ Assert . That ( assetB . Root . activeInHierarchy , Is . True , "GameObject must stay active so colliders survive" ) ;
237+
238+ lod . RevealAssembledAssets ( ) ;
239+
240+ Assert . That ( rendererA . forceRenderingOff , Is . False , "Reveal must hand rendering back so the LODGroup can cull by distance" ) ;
241+ Assert . That ( rendererB . forceRenderingOff , Is . False , "Reveal must hand rendering back so the LODGroup can cull by distance" ) ;
242+
243+ lod . Dispose ( world ) ;
244+ }
245+
246+ [ Test ]
247+ public void RestoreRenderingWhenAbortedBeforeReveal ( )
248+ {
249+ // An aborted run (ForgetLoading while PROCESSING) dereferences positioned assets back to the
250+ // cache. They must not return with forceRenderingOff stuck on, or they reappear invisible on reuse.
251+ const string HASH = "ITEM_A" ;
252+
253+ GltfContainerAsset asset = MakeFakeGltfWithRenderer ( HASH , out Renderer renderer ) ;
254+ cache . Stash ( HASH , asset ) ;
255+
256+ var descriptor = ISSDescriptor . CreateUninitialized ( ) ;
257+ descriptor . MarkResolved ( new [ ] { NewDescriptorEntry ( HASH ) } ) ;
258+
259+ InitialSceneStateLOD lod = CreateLODEntity ( descriptor ) ;
260+
261+ system . Update ( 0 ) ;
262+ Assert . That ( renderer . forceRenderingOff , Is . True ) ;
263+
264+ lod . Dispose ( world ) ;
265+
266+ Assert . That ( renderer . forceRenderingOff , Is . False ,
267+ "Clear must restore rendering before the asset is handed back to the cache" ) ;
268+ }
269+
204270 [ Test ]
205271 public void ReleaseBridgeSlotOnCacheHit ( )
206272 {
@@ -263,6 +329,15 @@ private static ISSDescriptorAsset NewDescriptorEntry(string hash) =>
263329 private static GltfContainerAsset MakeFakeGltf ( string label ) =>
264330 GltfContainerAsset . Create ( new GameObject ( $ "fake_{ label } ") , IStreamableRefCountData . Null . INSTANCE ) ;
265331
332+ private static GltfContainerAsset MakeFakeGltfWithRenderer ( string label , out Renderer renderer )
333+ {
334+ var go = new GameObject ( $ "fake_{ label } ") ;
335+ renderer = go . AddComponent < MeshRenderer > ( ) ;
336+ GltfContainerAsset asset = GltfContainerAsset . Create ( go , IStreamableRefCountData . Null . INSTANCE ) ;
337+ asset . Renderers . Add ( renderer ) ;
338+ return asset ;
339+ }
340+
266341 /// <summary>
267342 /// Pool-style stub mirroring <see cref="GltfContainerAssetsCache" />: <c>TryGet</c> removes,
268343 /// <c>Dereference</c> returns. Per-key counters are what the tests assert against.
@@ -309,6 +384,14 @@ public void Dereference(in string key, GltfContainerAsset asset, bool putInBridg
309384 stash [ key ] = entries = new Stack < GltfContainerAsset > ( ) ;
310385
311386 entries . Push ( asset ) ;
387+
388+ // Mirror GltfContainerAssetsCache.DereferenceFinalOperation: the real cache reparents the
389+ // asset's Root out of the LOD container when it is returned to the pool. Without this the
390+ // pooled asset stays a child of InitialSceneStateLOD.ParentContainer, so Dispose's
391+ // SafeDestroy(ParentContainer) cascades into it and destroys its renderers — the asset (and
392+ // its renderers) must survive intact for reuse.
393+ if ( asset . Root != null )
394+ asset . Root . transform . SetParent ( null , true ) ;
312395 }
313396
314397 public void Unload ( IPerformanceBudget frameTimeBudget , int maxUnloadAmount ) { }
0 commit comments