/* * Copyright (c) 2009, openmetaverse.org * All rights reserved. * * - Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Neither the name of the openmetaverse.org nor the names * of its contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ //#define DEBUG_TIMING using System; using System.Collections.Generic; using System.Threading; using OpenMetaverse.Packets; using OpenMetaverse.Assets; namespace OpenMetaverse { /// /// The current status of a texture request as it moves through the pipeline or final result of a texture request. /// public enum TextureRequestState { /// The initial state given to a request. Requests in this state /// are waiting for an available slot in the pipeline Pending, /// A request that has been added to the pipeline and the request packet /// has been sent to the simulator Started, /// A request that has received one or more packets back from the simulator Progress, /// A request that has received all packets back from the simulator Finished, /// A request that has taken longer than /// to download OR the initial packet containing the packet information was never received Timeout, /// The texture request was aborted by request of the agent Aborted, /// The simulator replied to the request that it was not able to find the requested texture NotFound } /// /// A callback fired to indicate the status or final state of the requested texture. For progressive /// downloads this will fire each time new asset data is returned from the simulator. /// /// The indicating either Progress for textures not fully downloaded, /// or the final result of the request after it has been processed through the TexturePipeline /// The object containing the Assets ID, raw data /// and other information. For progressive rendering the will contain /// the data from the beginning of the file. For failed, aborted and timed out requests it will contain /// an empty byte array. public delegate void TextureDownloadCallback(TextureRequestState state, AssetTexture assetTexture); /// /// Texture request download handler, allows a configurable number of download slots which manage multiple /// concurrent texture downloads from the /// /// This class makes full use of the internal /// system for full texture downloads. public class TexturePipeline { #if DEBUG_TIMING // Timing globals /// The combined time it has taken for all textures requested sofar. This includes the amount of time the /// texture spent waiting for a download slot, and the time spent retrieving the actual texture from the Grid public static TimeSpan TotalTime; /// The amount of time the request spent in the state public static TimeSpan NetworkTime; /// The total number of bytes transferred since the TexturePipeline was started public static int TotalBytes; #endif /// /// A request task containing information and status of a request as it is processed through the /// private class TaskInfo { /// The current which identifies the current status of the request public TextureRequestState State; /// The Unique Request ID, This is also the Asset ID of the texture being requested public UUID RequestID; /// The slot this request is occupying in the threadpoolSlots array public int RequestSlot; /// The ImageType of the request. public ImageType Type; /// The callback to fire when the request is complete, will include /// the and the /// object containing the result data public List Callbacks; /// If true, indicates the callback will be fired whenever new data is returned from the simulator. /// This is used to progressively render textures as portions of the texture are received. public bool ReportProgress; #if DEBUG_TIMING /// The time the request was added to the the PipeLine public DateTime StartTime; /// 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. 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; /// Holds the reference to the client object private readonly GridClient _Client; /// Maximum concurrent texture requests allowed at a time private readonly int maxTextureRequests; /// An array of objects used to manage worker request threads private readonly AutoResetEvent[] resetEvents; /// An array of worker slots which shows the availablity status of the slot private readonly int[] threadpoolSlots; /// The primary thread which manages the requests. private Thread downloadMaster; /// true if the TexturePipeline is currently running bool _Running; /// A synchronization object used by the primary thread private object lockerObject = new object(); /// 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 { return _Transfers.Count; } } /// /// Default constructor, Instantiates a new copy of the TexturePipeline class /// /// Reference to the instantiated object public TexturePipeline(GridClient client) { _Client = client; maxTextureRequests = client.Settings.MAX_CONCURRENT_TEXTURE_DOWNLOADS; resetEvents = new AutoResetEvent[maxTextureRequests]; threadpoolSlots = new int[maxTextureRequests]; _Transfers = new Dictionary(); // Pre-configure autoreset events and threadpool slots for (int i = 0; i < maxTextureRequests; i++) { resetEvents[i] = new AutoResetEvent(true); threadpoolSlots[i] = -1; } // Handle client connected and disconnected events client.Network.LoginProgress += delegate(object sender, LoginProgressEventArgs e) { if (e.Status == LoginStatus.Success) { Startup(); } }; client.Network.Disconnected += delegate { Shutdown(); }; } /// /// Initialize callbacks required for the TexturePipeline to operate /// public void Startup() { if (_Running) return; if (downloadMaster == null) { // Instantiate master thread that manages the request pool downloadMaster = new Thread(DownloadThread); downloadMaster.Name = "TexturePipeline"; downloadMaster.IsBackground = true; } _Running = true; _Client.Network.RegisterCallback(PacketType.ImageData, ImageDataHandler); _Client.Network.RegisterCallback(PacketType.ImagePacket, ImagePacketHandler); _Client.Network.RegisterCallback(PacketType.ImageNotInDatabase, ImageNotInDatabaseHandler); downloadMaster.Start(); if (RefreshDownloadsTimer == null) { RefreshDownloadsTimer = new System.Timers.Timer(Settings.PIPELINE_REFRESH_INTERVAL); RefreshDownloadsTimer.Elapsed += RefreshDownloadsTimer_Elapsed; RefreshDownloadsTimer.Start(); } } /// /// Shutdown the TexturePipeline and cleanup any callbacks or transfers /// public void Shutdown() { if (!_Running) return; #if DEBUG_TIMING Logger.Log(String.Format("Combined Execution Time: {0}, Network Execution Time {1}, Network {2}K/sec, Image Size {3}", TotalTime, NetworkTime, Math.Round(TotalBytes / NetworkTime.TotalSeconds / 60, 2), TotalBytes), Helpers.LogLevel.Debug); #endif if(null != RefreshDownloadsTimer) RefreshDownloadsTimer.Dispose(); RefreshDownloadsTimer = null; if (downloadMaster != null && downloadMaster.IsAlive) { downloadMaster.Abort(); } downloadMaster = null; _Client.Network.UnregisterCallback(PacketType.ImageNotInDatabase, ImageNotInDatabaseHandler); _Client.Network.UnregisterCallback(PacketType.ImageData, ImageDataHandler); _Client.Network.UnregisterCallback(PacketType.ImagePacket, ImagePacketHandler); lock (_Transfers) _Transfers.Clear(); for (int i = 0; i < resetEvents.Length; i++) if (resetEvents[i] != null) resetEvents[i].Set(); _Running = false; } private void RefreshDownloadsTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { lock (_Transfers) { foreach (TaskInfo transfer in _Transfers.Values) { if (transfer.State == TextureRequestState.Progress) { ImageDownload download = transfer.Transfer; // 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; download.TimeSinceLastPacket = 0; RequestImage(download.ID, download.ImageType, download.Priority, download.DiscardLevel, packet); } if (download.TimeSinceLastPacket > _Client.Settings.PIPELINE_REQUEST_TIMEOUT) { resetEvents[transfer.RequestSlot].Set(); } } } } } /// /// Request a texture asset from the simulator using the system to /// manage the requests and re-assemble the image from the packets received from the simulator /// /// The of the texture asset to download /// The of the texture asset. /// Use for most textures, or for baked layer texture assets /// A float indicating the requested priority for the transfer. Higher priority values tell the simulator /// to prioritize the request before lower valued requests. An image already being transferred using the can have /// its priority changed by resending the request with the new priority value /// Number of quality layers to discard. /// This controls the end marker of the data sent /// The packet number to begin the request at. A value of 0 begins the request /// from the start of the asset texture /// The callback to fire when the image is retrieved. The callback /// will contain the result of the request and the texture asset data /// If true, the callback will be fired for each chunk of the downloaded image. /// The callback asset parameter will contain all previously received chunks of the texture asset starting /// from the beginning of the request public void RequestTexture(UUID textureID, ImageType imageType, float priority, int discardLevel, uint packetStart, TextureDownloadCallback callback, bool progressive) { if (textureID == UUID.Zero) return; if (callback != null) { if (_Client.Assets.Cache.HasAsset(textureID)) { ImageDownload image = new ImageDownload(); image.ID = textureID; image.AssetData = _Client.Assets.Cache.GetCachedAssetBytes(textureID); image.Size = image.AssetData.Length; image.Transferred = image.AssetData.Length; image.ImageType = imageType; image.AssetType = AssetType.Texture; image.Success = true; callback(TextureRequestState.Finished, new AssetTexture(image.ID, image.AssetData)); _Client.Assets.FireImageProgressEvent(image.ID, image.Transferred, image.Size); } else { lock (_Transfers) { TaskInfo request; if (_Transfers.TryGetValue(textureID, out request)) { request.Callbacks.Add(callback); } else { request = new TaskInfo(); request.State = TextureRequestState.Pending; request.RequestID = textureID; request.ReportProgress = progressive; request.RequestSlot = -1; request.Type = imageType; request.Callbacks = new List(); request.Callbacks.Add(callback); ImageDownload downloadParams = new ImageDownload(); downloadParams.ID = textureID; downloadParams.Priority = priority; downloadParams.ImageType = imageType; downloadParams.DiscardLevel = discardLevel; request.Transfer = downloadParams; #if DEBUG_TIMING request.StartTime = DateTime.UtcNow; #endif _Transfers.Add(textureID, request); } } } } } /// /// Sends the actual request packet to the simulator /// /// The image to download /// Type of the image to download, either a baked /// avatar texture or a normal texture /// Priority level of the download. Default is /// 1,013,000.0f /// Number of quality layers to discard. /// This controls the end marker of the data sent /// Packet number to start the download at. /// This controls the start marker of the data sent /// Sending a priority of 0 and a discardlevel of -1 aborts /// download private void RequestImage(UUID imageID, ImageType type, float priority, int discardLevel, uint packetNum) { // Priority == 0 && DiscardLevel == -1 means cancel the transfer if (priority.Equals(0) && discardLevel.Equals(-1)) { AbortTextureRequest(imageID); } else { TaskInfo task; if (TryGetTransferValue(imageID, out task)) { if (task.Transfer.Simulator != null) { // Already downloading, just updating the priority float percentComplete = ((float)task.Transfer.Transferred / (float)task.Transfer.Size) * 100f; if (Single.IsNaN(percentComplete)) percentComplete = 0f; 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))); } else { ImageDownload transfer = task.Transfer; transfer.Simulator = _Client.Network.CurrentSim; } // Build and send the request packet RequestImagePacket request = new RequestImagePacket(); request.AgentData.AgentID = _Client.Self.AgentID; request.AgentData.SessionID = _Client.Self.SessionID; request.RequestImage = new RequestImagePacket.RequestImageBlock[1]; request.RequestImage[0] = new RequestImagePacket.RequestImageBlock(); request.RequestImage[0].DiscardLevel = (sbyte)discardLevel; request.RequestImage[0].DownloadPriority = priority; request.RequestImage[0].Packet = packetNum; request.RequestImage[0].Image = imageID; request.RequestImage[0].Type = (byte)type; _Client.Network.SendPacket(request, _Client.Network.CurrentSim); } else { Logger.Log("Received texture download request for a texture that isn't in the download queue: " + imageID, Helpers.LogLevel.Warning); } } } /// /// Cancel a pending or in process texture request /// /// The texture assets unique ID public void AbortTextureRequest(UUID textureID) { TaskInfo task; if (TryGetTransferValue(textureID, out task)) { // this means we've actually got the request assigned to the threadpool if (task.State == TextureRequestState.Progress) { RequestImagePacket request = new RequestImagePacket(); request.AgentData.AgentID = _Client.Self.AgentID; request.AgentData.SessionID = _Client.Self.SessionID; request.RequestImage = new RequestImagePacket.RequestImageBlock[1]; request.RequestImage[0] = new RequestImagePacket.RequestImageBlock(); request.RequestImage[0].DiscardLevel = -1; request.RequestImage[0].DownloadPriority = 0; request.RequestImage[0].Packet = 0; request.RequestImage[0].Image = textureID; request.RequestImage[0].Type = (byte)task.Type; _Client.Network.SendPacket(request); foreach (TextureDownloadCallback callback in task.Callbacks) callback(TextureRequestState.Aborted, new AssetTexture(textureID, Utils.EmptyBytes)); _Client.Assets.FireImageProgressEvent(task.RequestID, task.Transfer.Transferred, task.Transfer.Size); resetEvents[task.RequestSlot].Set(); RemoveTransfer(textureID); } else { RemoveTransfer(textureID); foreach (TextureDownloadCallback callback in task.Callbacks) callback(TextureRequestState.Aborted, new AssetTexture(textureID, Utils.EmptyBytes)); _Client.Assets.FireImageProgressEvent(task.RequestID, task.Transfer.Transferred, task.Transfer.Size); } } } /// /// Master Download Thread, Queues up downloads in the threadpool /// private void DownloadThread() { int slot; while (_Running) { // find free slots int pending = 0; int active = 0; TaskInfo nextTask = null; lock (_Transfers) { foreach (KeyValuePair request in _Transfers) { if (request.Value.State == TextureRequestState.Pending) { nextTask = request.Value; ++pending; } else if (request.Value.State == TextureRequestState.Progress) { ++active; } } } if (pending > 0 && active <= maxTextureRequests) { slot = -1; // find available slot for reset event lock (lockerObject) { for (int i = 0; i < threadpoolSlots.Length; i++) { if (threadpoolSlots[i] == -1) { // found a free slot threadpoolSlots[i] = 1; slot = i; break; } } } // -1 = slot not available if (slot != -1 && nextTask != null) { nextTask.State = TextureRequestState.Started; nextTask.RequestSlot = slot; //Logger.DebugLog(String.Format("Sending Worker thread new download request {0}", slot)); WorkPool.QueueUserWorkItem(TextureRequestDoWork, nextTask); continue; } } // Queue was empty or all download slots are inuse, let's give up some CPU time Thread.Sleep(500); } Logger.Log("Texture pipeline shutting down", Helpers.LogLevel.Info); } /// /// The worker thread that sends the request and handles timeouts /// /// A object containing the request details private void TextureRequestDoWork(Object threadContext) { TaskInfo task = (TaskInfo)threadContext; task.State = TextureRequestState.Progress; #if DEBUG_TIMING task.NetworkTime = DateTime.UtcNow; #endif // Find the first missing packet in the download ushort packet = 0; lock (task.Transfer) if (task.Transfer.PacketsSeen != null && task.Transfer.PacketsSeen.Count > 0) packet = GetFirstMissingPacket(task.Transfer.PacketsSeen); // Request the texture RequestImage(task.RequestID, task.Type, task.Transfer.Priority, task.Transfer.DiscardLevel, packet); // Set starting time task.Transfer.TimeSinceLastPacket = 0; // Don't release this worker slot until texture is downloaded or timeout occurs if (!resetEvents[task.RequestSlot].WaitOne()) { // Timed out Logger.Log("Worker " + task.RequestSlot + " 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) callback(TextureRequestState.Timeout, texture); _Client.Assets.FireImageProgressEvent(task.RequestID, task.Transfer.Transferred, task.Transfer.Size); RemoveTransfer(task.RequestID); } // Free up this download slot lock (lockerObject) threadpoolSlots[task.RequestSlot] = -1; } private ushort GetFirstMissingPacket(SortedList packetsSeen) { ushort packet = 0; lock (packetsSeen) { bool first = true; foreach (KeyValuePair packetSeen in packetsSeen) { if (first) { // Initially set this to the earliest packet received in the transfer packet = packetSeen.Value; first = false; } else { ++packet; // If there is a missing packet in the list, break and request the download // resume here if (packetSeen.Value != packet) { --packet; break; } } } ++packet; } return packet; } #region Raw Packet Handlers /// /// Handle responses from the simulator that tell us a texture we have requested is unable to be located /// or no longer exists. This will remove the request from the pipeline and free up a slot if one is in use /// /// The sender /// The EventArgs object containing the packet data protected void ImageNotInDatabaseHandler(object sender, PacketReceivedEventArgs e) { ImageNotInDatabasePacket imageNotFoundData = (ImageNotInDatabasePacket)e.Packet; TaskInfo task; if (TryGetTransferValue(imageNotFoundData.ImageID.ID, out task)) { // cancel acive request and free up the threadpool slot if (task.State == TextureRequestState.Progress) resetEvents[task.RequestSlot].Set(); // fire callback to inform the caller foreach (TextureDownloadCallback callback in task.Callbacks) callback(TextureRequestState.NotFound, new AssetTexture(imageNotFoundData.ImageID.ID, Utils.EmptyBytes)); resetEvents[task.RequestSlot].Set(); RemoveTransfer(imageNotFoundData.ImageID.ID); } else { Logger.Log("Received an ImageNotFound packet for an image we did not request: " + imageNotFoundData.ImageID.ID, Helpers.LogLevel.Warning); } } /// /// Handles the remaining Image data that did not fit in the initial ImageData packet /// /// The sender /// The EventArgs object containing the packet data protected void ImagePacketHandler(object sender, PacketReceivedEventArgs e) { ImagePacketPacket image = (ImagePacketPacket)e.Packet; TaskInfo task; if (TryGetTransferValue(image.ImageID.ID, out task)) { if (task.Transfer.Size == 0) { // We haven't received the header yet, block until it's received or times out task.Transfer.HeaderReceivedEvent.WaitOne(1000 * 5, false); 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); RemoveTransfer(task.Transfer.ID); resetEvents[task.RequestSlot].Set(); // free up request slot foreach (TextureDownloadCallback callback in task.Callbacks) callback(TextureRequestState.Timeout, new AssetTexture(task.RequestID, task.Transfer.AssetData)); return; } } // The header is downloaded, we can insert this data in to the proper position // Only insert if we haven't seen this packet before lock (task.Transfer) { if (!task.Transfer.PacketsSeen.ContainsKey(image.ImageID.Packet)) { task.Transfer.PacketsSeen[image.ImageID.Packet] = image.ImageID.Packet; Buffer.BlockCopy(image.ImageData.Data, 0, task.Transfer.AssetData, task.Transfer.InitialDataSize + (1000 * (image.ImageID.Packet - 1)), image.ImageData.Data.Length); task.Transfer.Transferred += image.ImageData.Data.Length; } } task.Transfer.TimeSinceLastPacket = 0; if (task.Transfer.Transferred >= task.Transfer.Size) { #if DEBUG_TIMING DateTime stopTime = DateTime.UtcNow; TimeSpan requestDuration = stopTime - task.StartTime; TimeSpan networkDuration = stopTime - task.NetworkTime; TotalTime += requestDuration; NetworkTime += networkDuration; TotalBytes += task.Transfer.Size; Logger.Log( String.Format( "Transfer Complete {0} [{1}] Total Request Time: {2}, Download Time {3}, Network {4}Kb/sec, Image Size {5} bytes", task.RequestID, task.RequestSlot, requestDuration, networkDuration, Math.Round(task.Transfer.Size/networkDuration.TotalSeconds/60, 2), task.Transfer.Size), Helpers.LogLevel.Debug); #endif task.Transfer.Success = true; RemoveTransfer(task.Transfer.ID); resetEvents[task.RequestSlot].Set(); // free up request slot _Client.Assets.Cache.SaveAssetToCache(task.RequestID, task.Transfer.AssetData); foreach (TextureDownloadCallback callback in task.Callbacks) callback(TextureRequestState.Finished, new AssetTexture(task.RequestID, task.Transfer.AssetData)); _Client.Assets.FireImageProgressEvent(task.RequestID, task.Transfer.Transferred, task.Transfer.Size); } else { if (task.ReportProgress) { foreach (TextureDownloadCallback callback in task.Callbacks) callback(TextureRequestState.Progress, new AssetTexture(task.RequestID, task.Transfer.AssetData)); } _Client.Assets.FireImageProgressEvent(task.RequestID, task.Transfer.Transferred, task.Transfer.Size); } } } /// /// Handle the initial ImageDataPacket sent from the simulator /// /// The sender /// The EventArgs object containing the packet data protected void ImageDataHandler(object sender, PacketReceivedEventArgs e) { ImageDataPacket data = (ImageDataPacket)e.Packet; TaskInfo task; if (TryGetTransferValue(data.ImageID.ID, out task)) { // reset the timeout interval since we got data task.Transfer.TimeSinceLastPacket = 0; lock (task.Transfer) if (task.Transfer.Size == 0) { task.Transfer.Codec = (ImageCodec)data.ImageID.Codec; task.Transfer.PacketCount = data.ImageID.Packets; task.Transfer.Size = (int)data.ImageID.Size; task.Transfer.AssetData = new byte[task.Transfer.Size]; task.Transfer.AssetType = AssetType.Texture; task.Transfer.PacketsSeen = new SortedList(); Buffer.BlockCopy(data.ImageData.Data, 0, task.Transfer.AssetData, 0, data.ImageData.Data.Length); task.Transfer.InitialDataSize = data.ImageData.Data.Length; task.Transfer.Transferred += data.ImageData.Data.Length; } task.Transfer.HeaderReceivedEvent.Set(); if (task.Transfer.Transferred >= task.Transfer.Size) { #if DEBUG_TIMING DateTime stopTime = DateTime.UtcNow; TimeSpan requestDuration = stopTime - task.StartTime; TimeSpan networkDuration = stopTime - task.NetworkTime; TotalTime += requestDuration; NetworkTime += networkDuration; TotalBytes += task.Transfer.Size; Logger.Log( String.Format( "Transfer Complete {0} [{1}] Total Request Time: {2}, Download Time {3}, Network {4}Kb/sec, Image Size {5} bytes", task.RequestID, task.RequestSlot, requestDuration, networkDuration, Math.Round(task.Transfer.Size/networkDuration.TotalSeconds/60, 2), task.Transfer.Size), Helpers.LogLevel.Debug); #endif task.Transfer.Success = true; RemoveTransfer(task.RequestID); resetEvents[task.RequestSlot].Set(); _Client.Assets.Cache.SaveAssetToCache(task.RequestID, task.Transfer.AssetData); foreach (TextureDownloadCallback callback in task.Callbacks) callback(TextureRequestState.Finished, new AssetTexture(task.RequestID, task.Transfer.AssetData)); _Client.Assets.FireImageProgressEvent(task.RequestID, task.Transfer.Transferred, task.Transfer.Size); } else { if (task.ReportProgress) { foreach (TextureDownloadCallback callback in task.Callbacks) callback(TextureRequestState.Progress, new AssetTexture(task.RequestID, task.Transfer.AssetData)); } _Client.Assets.FireImageProgressEvent(task.RequestID, task.Transfer.Transferred, task.Transfer.Size); } } } #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); } } }