diff --git a/LibreMetaverse/TexturePipeline.cs b/LibreMetaverse/TexturePipeline.cs
index cd042bde..a7082e30 100644
--- a/LibreMetaverse/TexturePipeline.cs
+++ b/LibreMetaverse/TexturePipeline.cs
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2006-2016, openmetaverse.co
+ * Copyright (c) 2025, Sjofn LLC.
* All rights reserved.
*
* - Redistribution and use in source and binary forms, with or without
@@ -27,6 +28,8 @@
//#define DEBUG_TIMING
using System;
+using System.Collections.Concurrent;
+using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -114,14 +117,14 @@ namespace OpenMetaverse
/// The time the request was sent to the simulator
public DateTime NetworkTime;
#endif
- /// An object that maintains the data of an request thats in-process.
+ /// An object that maintains the data of a request that is in-process.
public ImageDownload Transfer;
}
/// A dictionary containing all pending and in-process transfer requests where the Key is both the RequestID
/// and also the Asset Texture ID, and the value is an object containing the current state of the request and also
/// the asset data as it is being re-assembled
- private readonly Dictionary _Transfers;
+ private readonly ConcurrentDictionary _Transfers;
/// Holds the reference to the client object
private readonly GridClient _Client;
/// Maximum concurrent texture requests allowed at a time
@@ -129,17 +132,14 @@ namespace OpenMetaverse
/// The primary thread which manages the requests.
private Thread downloadMaster;
/// The cancellation token for the TexturePipeline and all child tasks.
- private CancellationTokenSource downloadTokenSource;
+ private readonly CancellationTokenSource downloadTokenSource;
/// true if the TexturePipeline is currently running
- bool _Running;
+ private bool _Running;
/// A refresh timer used to increase the priority of stalled requests
private System.Timers.Timer RefreshDownloadsTimer;
/// Current number of pending and in-process transfers
- public int TransferCount
- {
- get { lock (_Transfers) return _Transfers.Count; }
- }
+ public int TransferCount => _Transfers.Count;
///
/// Default constructor, Instantiates a new copy of the TexturePipeline class
@@ -153,7 +153,7 @@ namespace OpenMetaverse
downloadTokenSource = new CancellationTokenSource();
- _Transfers = new Dictionary();
+ _Transfers = new ConcurrentDictionary();
// Handle client connected and disconnected events
client.Network.LoginProgress += delegate(object sender, LoginProgressEventArgs e) {
@@ -224,42 +224,40 @@ namespace OpenMetaverse
_Client.Network.UnregisterCallback(PacketType.ImageData, ImageDataHandler);
_Client.Network.UnregisterCallback(PacketType.ImagePacket, ImagePacketHandler);
- lock (_Transfers)
- _Transfers.Clear();
+ _Transfers.Clear();
_Running = false;
}
private void RefreshDownloadsTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
- lock (_Transfers)
+ var transfers = _Transfers.ToFrozenDictionary();
+
+ foreach (TaskInfo transfer in transfers.Values)
{
- foreach (TaskInfo transfer in _Transfers.Values)
+ if (transfer.State != TextureRequestState.Progress) continue;
+ ImageDownload download = transfer.Transfer;
+
+ // Find the first missing packet in the download
+ ushort packet = 0;
+ lock (download)
{
- if (transfer.State != TextureRequestState.Progress) continue;
- ImageDownload download = transfer.Transfer;
+ if (download.PacketsSeen != null && download.PacketsSeen.Count > 0)
+ packet = GetFirstMissingPacket(download.PacketsSeen);
+ }
- // Find the first missing packet in the download
- ushort packet = 0;
- lock (download)
- {
- if (download.PacketsSeen != null && download.PacketsSeen.Count > 0)
- packet = GetFirstMissingPacket(download.PacketsSeen);
- }
+ if (download.TimeSinceLastPacket > 5000)
+ {
+ // We're not receiving data for this texture fast enough, bump up the priority by 5%
+ download.Priority *= 1.05f;
- if (download.TimeSinceLastPacket > 5000)
- {
- // We're not receiving data for this texture fast enough, bump up the priority by 5%
- download.Priority *= 1.05f;
+ download.TimeSinceLastPacket = 0;
+ RequestImage(download.ID, download.ImageType, download.Priority, download.DiscardLevel, packet);
+ }
- download.TimeSinceLastPacket = 0;
- RequestImage(download.ID, download.ImageType, download.Priority, download.DiscardLevel, packet);
- }
-
- if (download.TimeSinceLastPacket > _Client.Settings.PIPELINE_REQUEST_TIMEOUT)
- {
- transfer.TokenSource.Cancel();
- }
+ if (download.TimeSinceLastPacket > _Client.Settings.PIPELINE_REQUEST_TIMEOUT)
+ {
+ transfer.TokenSource.Cancel();
}
}
}
@@ -307,42 +305,30 @@ namespace OpenMetaverse
}
else
{
- lock (_Transfers)
- {
- TaskInfo request;
-
- if (_Transfers.TryGetValue(textureID, out request))
+ _Transfers.AddOrUpdate(textureID,
+ new TaskInfo
{
- request.Callbacks.Add(callback);
- }
- else
- {
- request = new TaskInfo
- {
- State = TextureRequestState.Pending,
- RequestID = textureID,
- ReportProgress = progressive,
- TokenSource = CancellationTokenSource.CreateLinkedTokenSource(downloadTokenSource.Token),
- Type = imageType,
- Callbacks = new List {callback}
- };
-
-
- ImageDownload downloadParams = new ImageDownload
+ State = TextureRequestState.Pending,
+ RequestID = textureID,
+ ReportProgress = progressive,
+ TokenSource = CancellationTokenSource.CreateLinkedTokenSource(downloadTokenSource.Token),
+ Type = imageType,
+ Callbacks = new List { callback },
+ Transfer = new ImageDownload
{
ID = textureID,
Priority = priority,
ImageType = imageType,
DiscardLevel = discardLevel
- };
-
- request.Transfer = downloadParams;
+ },
#if DEBUG_TIMING
- request.StartTime = DateTime.UtcNow;
+ StartTime = DateTime.UtcNow
#endif
- _Transfers.Add(textureID, request);
- }
- }
+ }, (uuid, info) =>
+ {
+ info.Callbacks.Add(callback);
+ return info;
+ });
}
}
@@ -369,8 +355,7 @@ namespace OpenMetaverse
}
else
{
- TaskInfo task;
- if (TryGetTransferValue(imageID, out task))
+ if (_Transfers.TryGetValue(imageID, out var task))
{
if (task.Transfer.Simulator != null)
{
@@ -381,8 +366,8 @@ namespace OpenMetaverse
if (percentComplete > 0f)
{
- Logger.DebugLog(string.Format("Updating priority on image transfer {0} to {1}, {2}% complete",
- imageID, task.Transfer.Priority, Math.Round(percentComplete, 2)));
+ Logger.DebugLog(
+ $"Updating priority on image transfer {imageID} to {task.Transfer.Priority}, {Math.Round(percentComplete, 2)}% complete");
}
}
else
@@ -414,7 +399,8 @@ namespace OpenMetaverse
}
else
{
- Logger.Log("Received texture download request for a texture that isn't in the download queue: " + imageID, Helpers.LogLevel.Warning);
+ Logger.Log($"Received texture download request for a texture that isn't in the download queue: {imageID}",
+ Helpers.LogLevel.Warning);
}
}
}
@@ -425,8 +411,7 @@ namespace OpenMetaverse
/// The texture assets unique ID
public void AbortTextureRequest(UUID textureID)
{
- TaskInfo task;
- if (!TryGetTransferValue(textureID, out task)) return;
+ if (!_Transfers.TryGetValue(textureID, out var task)) { return; }
// this means we've actually got the request assigned to the threadpool
if (task.State == TextureRequestState.Progress)
@@ -457,11 +442,11 @@ namespace OpenMetaverse
task.TokenSource.Cancel();
- RemoveTransfer(textureID);
+ _Transfers.TryRemove(textureID, out _);
}
else
{
- RemoveTransfer(textureID);
+ _Transfers.TryRemove(textureID, out _);
foreach (var callback in task.Callbacks)
callback(TextureRequestState.Aborted, new AssetTexture(textureID, Utils.EmptyBytes));
@@ -482,20 +467,17 @@ namespace OpenMetaverse
var pendingTasks = new Queue();
- lock (_Transfers)
+ foreach (var request in _Transfers)
{
- foreach (var request in _Transfers)
+ switch (request.Value.State)
{
- switch (request.Value.State)
- {
- case TextureRequestState.Pending:
- pendingTasks.Enqueue(request.Value);
- break;
- case TextureRequestState.Started:
- case TextureRequestState.Progress:
- ++active;
- break;
- }
+ case TextureRequestState.Pending:
+ pendingTasks.Enqueue(request.Value);
+ break;
+ case TextureRequestState.Started:
+ case TextureRequestState.Progress:
+ ++active;
+ break;
}
}
@@ -510,7 +492,7 @@ namespace OpenMetaverse
++active;
}
- // Queue was empty or all download slots are inuse, let's give up some CPU time
+ // Queue was empty or all download slots are in use, let's give up some CPU time
Thread.Sleep(500);
}
@@ -549,8 +531,8 @@ namespace OpenMetaverse
if (task.TokenSource.Token.WaitHandle.WaitOne())
{
// Timed out
- Logger.Log("Worker timeout waiting for texture " + task.RequestID + " to download got " +
- task.Transfer.Transferred + " of " + task.Transfer.Size, Helpers.LogLevel.Warning);
+ Logger.Log($"Worker timeout waiting for texture {task.RequestID} to download got " +
+ $"{task.Transfer.Transferred} of {task.Transfer.Size}", Helpers.LogLevel.Warning);
AssetTexture texture = new AssetTexture(task.RequestID, task.Transfer.AssetData);
foreach (TextureDownloadCallback callback in task.Callbacks)
@@ -558,7 +540,7 @@ namespace OpenMetaverse
_Client.Assets.FireImageProgressEvent(task.RequestID, task.Transfer.Transferred, task.Transfer.Size);
- RemoveTransfer(task.RequestID);
+ _Transfers.TryRemove(task.RequestID, out _);
}
}
@@ -569,7 +551,7 @@ namespace OpenMetaverse
lock (packetsSeen)
{
bool first = true;
- foreach (KeyValuePair packetSeen in packetsSeen)
+ foreach (var packetSeen in packetsSeen)
{
if (first)
{
@@ -608,9 +590,8 @@ namespace OpenMetaverse
protected void ImageNotInDatabaseHandler(object sender, PacketReceivedEventArgs e)
{
ImageNotInDatabasePacket imageNotFoundData = (ImageNotInDatabasePacket)e.Packet;
- TaskInfo task;
- if (TryGetTransferValue(imageNotFoundData.ImageID.ID, out task))
+ if (_Transfers.TryGetValue(imageNotFoundData.ImageID.ID, out var task))
{
// cancel active request and free up the threadpool slot
task.TokenSource.Cancel();
@@ -619,11 +600,12 @@ namespace OpenMetaverse
foreach (TextureDownloadCallback callback in task.Callbacks)
callback(TextureRequestState.NotFound, new AssetTexture(imageNotFoundData.ImageID.ID, Utils.EmptyBytes));
- RemoveTransfer(imageNotFoundData.ImageID.ID);
+ _Transfers.TryRemove(imageNotFoundData.ImageID.ID, out _);
}
else
{
- Logger.Log("Received an ImageNotFound packet for an image we did not request: " + imageNotFoundData.ImageID.ID, Helpers.LogLevel.Warning);
+ Logger.Log($"Received an ImageNotFound packet for an image we did not request: {imageNotFoundData.ImageID.ID}",
+ Helpers.LogLevel.Warning);
}
}
@@ -635,9 +617,8 @@ namespace OpenMetaverse
protected void ImagePacketHandler(object sender, PacketReceivedEventArgs e)
{
ImagePacketPacket image = (ImagePacketPacket)e.Packet;
- TaskInfo task;
- if (TryGetTransferValue(image.ImageID.ID, out task))
+ if (_Transfers.TryGetValue(image.ImageID.ID, out var task))
{
if (task.Transfer.Size == 0)
{
@@ -646,12 +627,12 @@ namespace OpenMetaverse
if (task.Transfer.Size == 0)
{
- Logger.Log("Timed out while waiting for the image header to download for " +
- task.Transfer.ID, Helpers.LogLevel.Warning, _Client);
+ Logger.Log($"Timed out while waiting for the image header to download for {task.Transfer.ID}",
+ Helpers.LogLevel.Warning, _Client);
task.TokenSource.Cancel();
- RemoveTransfer(task.Transfer.ID);
+ _Transfers.TryRemove(task.Transfer.ID, out _);
foreach (TextureDownloadCallback callback in task.Callbacks)
callback(TextureRequestState.Timeout, new AssetTexture(task.RequestID, task.Transfer.AssetData));
@@ -698,7 +679,7 @@ namespace OpenMetaverse
task.Transfer.Success = true;
task.TokenSource.Cancel();
- RemoveTransfer(task.Transfer.ID);
+ _Transfers.TryRemove(task.Transfer.ID, out _);
_Client.Assets.Cache.SaveAssetToCache(task.RequestID, task.Transfer.AssetData);
foreach (var callback in task.Callbacks)
callback(TextureRequestState.Finished, new AssetTexture(task.RequestID, task.Transfer.AssetData));
@@ -729,9 +710,8 @@ namespace OpenMetaverse
protected void ImageDataHandler(object sender, PacketReceivedEventArgs e)
{
ImageDataPacket data = (ImageDataPacket)e.Packet;
- TaskInfo task;
- if (TryGetTransferValue(data.ImageID.ID, out task))
+ if (_Transfers.TryGetValue(data.ImageID.ID, out var task))
{
// reset the timeout interval since we got data
task.Transfer.TimeSinceLastPacket = 0;
@@ -775,7 +755,7 @@ namespace OpenMetaverse
#endif
task.Transfer.Success = true;
task.TokenSource.Cancel();
- RemoveTransfer(task.RequestID);
+ _Transfers.TryRemove(task.RequestID, out _);
_Client.Assets.Cache.SaveAssetToCache(task.RequestID, task.Transfer.AssetData);
@@ -802,17 +782,5 @@ namespace OpenMetaverse
}
#endregion
-
- private bool TryGetTransferValue(UUID textureID, out TaskInfo task)
- {
- lock (_Transfers)
- return _Transfers.TryGetValue(textureID, out task);
- }
-
- private bool RemoveTransfer(UUID textureID)
- {
- lock (_Transfers)
- return _Transfers.Remove(textureID);
- }
}
}