Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## 2024-05-22 - Array Resizing Bottlenecks in UdonSharp

**Learning:** UdonSharp environments often encourage array usage over Lists due to historical or performance reasons (interop overhead). However, standard patterns like "resize array by +1 for each new item" (Shlemiel the Painter's algorithm) are catastrophic for bulk loading operations, turning $O(N)$ operations into $O(N^2)$.

**Action:** When handling bulk data (like loading a list of URLs from a string), always parse to a temporary buffer or count first, then allocate the final array once. Avoid `Array.Resize` (or manual resize) inside a loop.
131 changes: 88 additions & 43 deletions Scripts/ImageLoader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using UdonSharp;
using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDK3.Image;
Expand Down Expand Up @@ -99,17 +99,42 @@ private void InitFromGeneratedUrls()
return;
}

// For each available slot, populate with initial URLs
// Calculate the number of valid URLs first to allocate efficiently
int validCount = 0;
for (int i = 0; i < urlCount; i++)
{
// Check if URL is valid
if (predefinedUrls[i] != null)
{
// Initialize activeUrlIndices array with valid URLs
AddImageIndex(i, defaultCaption);
validCount++;
}
}

if (validCount > 0)
{
// Allocate arrays once
int[] newIndices = new int[validCount];
Texture2D[] newTextures = new Texture2D[validCount];
string[] newCaptions = new string[validCount];

int currentIndex = 0;
for (int i = 0; i < urlCount; i++)
{
if (predefinedUrls[i] != null)
{
newIndices[currentIndex] = i;
newCaptions[currentIndex] = defaultCaption;
currentIndex++;
}
}

// Assign to state
_activeUrlIndices = newIndices;
_downloadedTextures = newTextures;
_captions = newCaptions;

Debug.Log($"Initialized {validCount} editor-generated URLs (Bulk Optimized)");
}

// Start the slideshow if we have images
if (_activeUrlIndices.Length > 0)
{
Expand Down Expand Up @@ -248,11 +273,13 @@ public override void OnStringLoadSuccess(IVRCStringDownload result)
// Split the file into lines
string[] lines = urlList.Split('\n');

bool foundNewImages = false;
int newImagesProcessed = 0;

Debug.Log($"Processing URL list with {lines.Length} lines");

// Temporary arrays for batching (max size is number of lines)
int[] tempIndices = new int[lines.Length];
string[] tempCaptions = new string[lines.Length];
int addedCount = 0;

// Process each line (URL)
foreach (string line in lines)
{
Expand Down Expand Up @@ -285,6 +312,19 @@ public override void OnStringLoadSuccess(IVRCStringDownload result)
}
}

// ALSO check if it is in the current batch we are building!
if (!alreadyActive)
{
for (int i = 0; i < addedCount; i++)
{
if (tempIndices[i] == matchingUrlIndex)
{
alreadyActive = true;
break;
}
}
}

if (!alreadyActive)
{
// Extract caption if available
Expand All @@ -294,11 +334,12 @@ public override void OnStringLoadSuccess(IVRCStringDownload result)
caption = defaultCaption;
}

// Add this URL index to our active list
AddImageIndex(matchingUrlIndex, caption);
foundNewImages = true;
newImagesProcessed++;
Debug.Log($"Added new image: {urlStr} (index: {matchingUrlIndex})");
// Add to temp batch
tempIndices[addedCount] = matchingUrlIndex;
tempCaptions[addedCount] = caption;
addedCount++;

Debug.Log($"Found new image: {urlStr} (index: {matchingUrlIndex})");
}
}
else
Expand All @@ -307,10 +348,41 @@ public override void OnStringLoadSuccess(IVRCStringDownload result)
}
}

// If we found new images
if (foundNewImages)
// If we found new images, perform bulk update
if (addedCount > 0)
{
Debug.Log($"Found {newImagesProcessed} new images. Continuing current slideshow.");
Debug.Log($"Bulk adding {addedCount} new images. Allocating arrays once.");

int oldLength = _activeUrlIndices.Length;
int newTotal = oldLength + addedCount;

// Allocate new arrays
int[] finalIndices = new int[newTotal];
Texture2D[] finalTextures = new Texture2D[newTotal];
string[] finalCaptions = new string[newTotal];

// Copy old data
for(int i = 0; i < oldLength; i++)
{
finalIndices[i] = _activeUrlIndices[i];
finalTextures[i] = _downloadedTextures[i];
finalCaptions[i] = _captions[i];
}

// Copy new data
for(int i = 0; i < addedCount; i++)
{
finalIndices[oldLength + i] = tempIndices[i];
finalCaptions[oldLength + i] = tempCaptions[i];
// finalTextures is null for new slots, which is correct
}

// Update state
_activeUrlIndices = finalIndices;
_downloadedTextures = finalTextures;
_captions = finalCaptions;

Debug.Log($"Found {addedCount} new images. Total: {newTotal}. Continuing current slideshow.");

// If we're at the beginning, start the slideshow
if (_currentIndex == 0 && _activeUrlIndices.Length > 0)
Expand Down Expand Up @@ -497,33 +569,6 @@ private int FindUrlSlotForFilename(string filename)
return _activeUrlIndices.Length % predefinedUrls.Length;
}

private void AddImageIndex(int urlIndex, string caption)
{
// Extend arrays
int[] newIndices = new int[_activeUrlIndices.Length + 1];
Texture2D[] newTextures = new Texture2D[_downloadedTextures.Length + 1];
string[] newCaptions = new string[_captions.Length + 1];

// Copy existing data
for (int i = 0; i < _activeUrlIndices.Length; i++)
{
newIndices[i] = _activeUrlIndices[i];
newTextures[i] = _downloadedTextures[i];
newCaptions[i] = _captions[i];
}

// Add new data
newIndices[_activeUrlIndices.Length] = urlIndex;
newCaptions[_captions.Length] = caption;

// Update arrays
_activeUrlIndices = newIndices;
_downloadedTextures = newTextures;
_captions = newCaptions;

Debug.Log($"Added new image (index: {_activeUrlIndices.Length-1}, URL index: {urlIndex})");
}

private void TrimOldestImages(int countToRemove)
{
if (countToRemove <= 0 || countToRemove >= _activeUrlIndices.Length) return;
Expand Down
Loading