From 94ec7178235be8054336bd5608d97af06336c643 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 01:19:12 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Cache=20VRCUrl.Get()=20call?= =?UTF-8?q?s=20in=20ImageLoader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 💡 What: Introduced a `_cachedUrls` string array to `ImageLoader.cs` that mirrors the string values of `predefinedUrls`. Updated `Start`, `ClearPredefinedUrls`, and relevant loops to populate and use this cache instead of calling `.Get()` repeatedly. 🎯 Why: In UdonSharp, calling `VRCUrl.Get()` involves an interop call from the Udon VM to the Unity host, which is expensive when performed inside loops (e.g., during URL matching). 📊 Impact: Reduces the overhead of URL lookups from O(N * Interop) to O(N * Array Access). For lists with many predefined URLs, this significantly reduces the CPU cost during `FindMatchingUrlIndex`. 🔬 Measurement: Verified via code review that `predefinedUrls` logic is preserved and that the cache is invalidated/updated whenever the source array might change (initialization and clearing). --- .jules/bolt.md | 3 ++ Scripts/ImageLoader.cs | 62 +++++++++++++++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..ad2c24d --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-22 - VRCUrl.Get() Interop Overhead +**Learning:** Calling `VRCUrl.Get()` inside loops in UdonSharp creates significant performance overhead due to interop calls between the Udon VM and the host engine. +**Action:** Cache `VRCUrl` string values into a native `string[]` array at startup (e.g., in `Start()`) and use the cached array for lookups and comparisons in loops. diff --git a/Scripts/ImageLoader.cs b/Scripts/ImageLoader.cs index 8bccf34..745dde8 100644 --- a/Scripts/ImageLoader.cs +++ b/Scripts/ImageLoader.cs @@ -54,6 +54,7 @@ public class ImageLoader : UdonSharpBehaviour private string[] _captions = new string[0]; private int _currentIndex = 0; private string _lastLoadedUrlList = ""; + private string[] _cachedUrls; // Current texture reference private Texture2D _currentTexture; @@ -64,6 +65,8 @@ void Start() // Initialize core components _imageDownloader = new VRCImageDownloader(); _udonEventReceiver = (IUdonEventReceiver)this; + + CacheUrls(); // Store reference to original material if (renderer != null) @@ -90,6 +93,28 @@ void Start() } } + private void CacheUrls() + { + if (predefinedUrls == null) + { + _cachedUrls = new string[0]; + return; + } + + _cachedUrls = new string[predefinedUrls.Length]; + for (int i = 0; i < predefinedUrls.Length; i++) + { + if (predefinedUrls[i] != null) + { + _cachedUrls[i] = predefinedUrls[i].Get(); + } + else + { + _cachedUrls[i] = null; + } + } + } + private void InitFromGeneratedUrls() { // Make sure predefinedUrls are properly initialized @@ -427,11 +452,16 @@ private int FindMatchingUrlIndex(string urlToFind, int additionalCount = 0) Debug.LogError("No predefined URLs available!"); return -1; } + + if (_cachedUrls == null || _cachedUrls.Length != predefinedUrls.Length) + { + CacheUrls(); + } // First try to find an exact match - for (int i = 0; i < predefinedUrls.Length; i++) + for (int i = 0; i < _cachedUrls.Length; i++) { - if (predefinedUrls[i] != null && predefinedUrls[i].Get() == urlToFind) + if (_cachedUrls[i] != null && _cachedUrls[i] == urlToFind) { return i; } @@ -454,11 +484,11 @@ private int FindMatchingUrlIndex(string urlToFind, int additionalCount = 0) string filename = ExtractFilenameFromUrl(urlToFind); if (!string.IsNullOrEmpty(filename)) { - for (int i = 0; i < predefinedUrls.Length; i++) + for (int i = 0; i < _cachedUrls.Length; i++) { - if (predefinedUrls[i] != null && - !string.IsNullOrEmpty(predefinedUrls[i].Get()) && - predefinedUrls[i].Get().Contains(filename)) + if (_cachedUrls[i] != null && + !string.IsNullOrEmpty(_cachedUrls[i]) && + _cachedUrls[i].Contains(filename)) { return i; } @@ -505,13 +535,18 @@ private int FindUrlSlotForFilename(string filename) Debug.LogError("No valid VRCUrl found in predefinedUrls array!"); return -1; } + + if (_cachedUrls == null || _cachedUrls.Length != predefinedUrls.Length) + { + CacheUrls(); + } // Find a predefined URL that matches this filename - for (int i = 0; i < predefinedUrls.Length; i++) + for (int i = 0; i < _cachedUrls.Length; i++) { - if (predefinedUrls[i] != null && !string.IsNullOrEmpty(predefinedUrls[i].Get())) + if (_cachedUrls[i] != null && !string.IsNullOrEmpty(_cachedUrls[i])) { - if (predefinedUrls[i].Get().Contains(filename)) + if (_cachedUrls[i].Contains(filename)) { return i; } @@ -598,13 +633,18 @@ public override void OnImageLoadSuccess(IVRCImageDownload result) string loadedUrl = result.Url.Get(); Debug.Log($"Image loaded successfully: {loadedUrl}"); + + if (_cachedUrls == null || _cachedUrls.Length != predefinedUrls.Length) + { + CacheUrls(); + } // Find which of our URLs this corresponds to for (int i = 0; i < _activeUrlIndices.Length; i++) { int urlIndex = _activeUrlIndices[i]; if (urlIndex < predefinedUrls.Length && predefinedUrls[urlIndex] != null && - predefinedUrls[urlIndex].Get() == loadedUrl) + _cachedUrls[urlIndex] == loadedUrl) { // Store the downloaded texture _downloadedTextures[i] = result.Result; @@ -700,6 +740,8 @@ public void ClearPredefinedUrls() } } + CacheUrls(); + // Reset URL count urlCount = 0;