From eb6699f23fa9f8957dc04e938aece205507cd335 Mon Sep 17 00:00:00 2001 From: seton Date: Thu, 25 Jun 2026 20:14:52 -0400 Subject: [PATCH] Fix proxy box color, mesh leak, and stale comment The Mesh LOD proxy box rendered flat gray instead of the car's color. It was being given a shared material tinted via Material.color (the legacy _Color property), which URP ignores in favor of _BaseColor, and a single shared material cannot match each car anyway. Reuse the car's own material so the box matches its color again. Also destroy the proxy box mesh when tearing down or refreshing a car's LOD (the GameObject was destroyed but the Mesh leaked until scene unload), and prune dead weak-refs from the tracked-car list periodically so it cannot grow unbounded across a long session. Fix a stale comment in PhysicsTimer that still referenced PhysicsOverlayGUI after the overlay moved to the Profiler module. --- src/Modules/MeshLod/MeshLodInjector.cs | 41 ++++++++------------ src/Modules/PhysicsOptimizer/PhysicsTimer.cs | 2 +- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/src/Modules/MeshLod/MeshLodInjector.cs b/src/Modules/MeshLod/MeshLodInjector.cs index fbf994d..06a2d95 100644 --- a/src/Modules/MeshLod/MeshLodInjector.cs +++ b/src/Modules/MeshLod/MeshLodInjector.cs @@ -12,7 +12,6 @@ static class MeshLodInjector { static MeshLodSettings _settings = new(); static readonly List> _trackedCars = new(); - static Material? _bboxMat; // Debounced auto-refresh: set by MarkDirty, consumed by CheckAutoRefresh (called from Update). static float _pendingRefreshAt = -1f; @@ -33,7 +32,6 @@ static class MeshLodInjector { _trackedCars.Clear(); _pendingRefreshAt = -1f; - if (_bboxMat != null) { UnityEngine.Object.Destroy(_bboxMat); _bboxMat = null; } } public static (int total, int locos, int freight) GetTrackedCounts() @@ -129,6 +127,11 @@ static class MeshLodInjector try { ApplyLods(car, renderers); + // Opportunistically drop dead weak-refs so the list can't grow unbounded over a + // long session of cars loading/unloading (RefreshAll also compacts, but only runs + // when a setting changes). Cheap: the modulo check runs once per car load. + if (_trackedCars.Count > 0 && _trackedCars.Count % 128 == 0) + _trackedCars.RemoveAll(wr => !wr.TryGetTarget(out _)); _trackedCars.Add(new WeakReference(car)); } catch (Exception ex) { Log.Warn($"MeshLOD: apply failed for {car.DisplayName}: {ex.Message}"); } @@ -208,9 +211,12 @@ static class MeshLodInjector Renderer[] lod2Rend = sorted.Take(lod2Keep).Cast().ToArray(); // LOD3: 12-triangle proxy box derived from the largest renderer's mesh-local AABB. + // Reuse the largest renderer's own material so the box matches the car's colour + // (a shared/tinted material can't match per-car, and Material.color sets the legacy + // _Color, not URP's _BaseColor — both reasons the previous matte material rendered gray). Bounds localBounds = ComputeMeshLocalBounds(sorted[0], bodyRoot); MeshRenderer bboxMr = CreateBoundingBoxRenderer(bodyRoot, localBounds, - GetOrCreateBBoxMat(sorted[0].sharedMaterial), + sorted[0].sharedMaterial, sorted[0].gameObject.layer); Renderer[] lod3Rend = new[] { (Renderer)bboxMr }; @@ -261,7 +267,14 @@ static class MeshLodInjector } Transform? bbox = car.BodyTransform.Find("S3_BBox"); - if (bbox != null) UnityEngine.Object.Destroy(bbox.gameObject); + if (bbox != null) + { + // Destroy the procedural box mesh too: destroying the GameObject alone leaves + // the Mesh asset alive until scene unload (a small leak on every refresh). + MeshFilter? bf = bbox.GetComponent(); + if (bf != null && bf.sharedMesh != null) UnityEngine.Object.Destroy(bf.sharedMesh); + UnityEngine.Object.Destroy(bbox.gameObject); + } } // ── Bounds helpers ──────────────────────────────────────────────────────── @@ -316,26 +329,6 @@ static class MeshLodInjector // ── Proxy box mesh ──────────────────────────────────────────────────────── - // One matte material shared across all proxy boxes; tinted to the car body's - // albedo so it looks approximately correct under any lighting / post-processing. - // Tries URP first since Railroader uses the Universal Render Pipeline. - static Material GetOrCreateBBoxMat(Material? src) - { - if (_bboxMat != null) return _bboxMat; - var shader = Shader.Find("Universal Render Pipeline/Lit") - ?? Shader.Find("Universal Render Pipeline/Simple Lit") - ?? Shader.Find("Standard") - ?? Shader.Find("Legacy Shaders/Diffuse"); - _bboxMat = shader != null ? new Material(shader) : new Material(src); - _bboxMat.color = src != null && src.HasProperty("_Color") - ? src.color - : new Color(0.45f, 0.45f, 0.45f); - if (_bboxMat.HasProperty("_Metallic")) _bboxMat.SetFloat("_Metallic", 0f); - if (_bboxMat.HasProperty("_Glossiness")) _bboxMat.SetFloat("_Glossiness", 0.1f); // built-in - if (_bboxMat.HasProperty("_Smoothness")) _bboxMat.SetFloat("_Smoothness", 0.1f); // URP - return _bboxMat; - } - static MeshRenderer CreateBoundingBoxRenderer(Transform parent, Bounds localBounds, Material mat, int layer) { diff --git a/src/Modules/PhysicsOptimizer/PhysicsTimer.cs b/src/Modules/PhysicsOptimizer/PhysicsTimer.cs index e66f8e3..f051d91 100644 --- a/src/Modules/PhysicsOptimizer/PhysicsTimer.cs +++ b/src/Modules/PhysicsOptimizer/PhysicsTimer.cs @@ -102,7 +102,7 @@ public static class PhysicsTimer _frames = 0; } - // Called from PhysicsOverlayGUI.LateUpdate() to capture render frame time. + // Called from ProfilerOverlayGUI.LateUpdate() to capture render frame time. public static void RecordRenderFrame(float deltaMs) { _lastInstantRenderMs = deltaMs; // sampled into RingRender on the next RecordFrame call