LIBOMV-734 Thanks to Douglas R. Miles for converting the events in EstateManager over to new patterns LIBOMV-735 Removes redundant LoggedIn event from NetworkManager, when LoginProgress Status == Success you can reliably send packets to a simulator. If you send before this event is raised, an exception will be thrown and your application will crash, previously your request would just get sent to the bitbucket without any notice, Thanks lkalif for the help getting this bug fixed correctly git-svn-id: http://libopenmetaverse.googlecode.com/svn/libopenmetaverse/trunk@3187 52acb1d6-8a22-11de-b505-999d5b087335
825 lines
39 KiB
C#
825 lines
39 KiB
C#
/*
|
|
* 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
|
|
{
|
|
/// <summary>
|
|
/// The current status of a texture request as it moves through the pipeline or final result of a texture request.
|
|
/// </summary>
|
|
public enum TextureRequestState
|
|
{
|
|
/// <summary>The initial state given to a request. Requests in this state
|
|
/// are waiting for an available slot in the pipeline</summary>
|
|
Pending,
|
|
/// <summary>A request that has been added to the pipeline and the request packet
|
|
/// has been sent to the simulator</summary>
|
|
Started,
|
|
/// <summary>A request that has received one or more packets back from the simulator</summary>
|
|
Progress,
|
|
/// <summary>A request that has received all packets back from the simulator</summary>
|
|
Finished,
|
|
/// <summary>A request that has taken longer than <seealso cref="Settings.PIPELINE_REQUEST_TIMEOUT"/>
|
|
/// to download OR the initial packet containing the packet information was never received</summary>
|
|
Timeout,
|
|
/// <summary>The texture request was aborted by request of the agent</summary>
|
|
Aborted,
|
|
/// <summary>The simulator replied to the request that it was not able to find the requested texture</summary>
|
|
NotFound
|
|
}
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="state">The <see cref="TextureRequestState"/> indicating either Progress for textures not fully downloaded,
|
|
/// or the final result of the request after it has been processed through the TexturePipeline</param>
|
|
/// <param name="assetTexture">The <see cref="AssetTexture"/> object containing the Assets ID, raw data
|
|
/// and other information. For progressive rendering the <see cref="Asset.AssetData"/> will contain
|
|
/// the data from the beginning of the file. For failed, aborted and timed out requests it will contain
|
|
/// an empty byte array.</param>
|
|
public delegate void TextureDownloadCallback(TextureRequestState state, AssetTexture assetTexture);
|
|
|
|
/// <summary>
|
|
/// Texture request download handler, allows a configurable number of download slots which manage multiple
|
|
/// concurrent texture downloads from the <seealso cref="Simulator"/>
|
|
/// </summary>
|
|
/// <remarks>This class makes full use of the internal <seealso cref="TextureCache"/>
|
|
/// system for full texture downloads.</remarks>
|
|
public class TexturePipeline
|
|
{
|
|
#if DEBUG_TIMING // Timing globals
|
|
/// <summary>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</summary>
|
|
public static TimeSpan TotalTime;
|
|
/// <summary>The amount of time the request spent in the <see cref="TextureRequestState.Progress"/> state</summary>
|
|
public static TimeSpan NetworkTime;
|
|
/// <summary>The total number of bytes transferred since the TexturePipeline was started</summary>
|
|
public static int TotalBytes;
|
|
#endif
|
|
/// <summary>
|
|
/// A request task containing information and status of a request as it is processed through the <see cref="TexturePipeline"/>
|
|
/// </summary>
|
|
private class TaskInfo
|
|
{
|
|
/// <summary>The current <seealso cref="TextureRequestState"/> which identifies the current status of the request</summary>
|
|
public TextureRequestState State;
|
|
/// <summary>The Unique Request ID, This is also the Asset ID of the texture being requested</summary>
|
|
public UUID RequestID;
|
|
/// <summary>The slot this request is occupying in the threadpoolSlots array</summary>
|
|
public int RequestSlot;
|
|
/// <summary>The ImageType of the request.</summary>
|
|
public ImageType Type;
|
|
|
|
/// <summary>The callback to fire when the request is complete, will include
|
|
/// the <seealso cref="TextureRequestState"/> and the <see cref="AssetTexture"/>
|
|
/// object containing the result data</summary>
|
|
public List<TextureDownloadCallback> Callbacks;
|
|
/// <summary>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.</summary>
|
|
public bool ReportProgress;
|
|
#if DEBUG_TIMING
|
|
/// <summary>The time the request was added to the the PipeLine</summary>
|
|
public DateTime StartTime;
|
|
/// <summary>The time the request was sent to the simulator</summary>
|
|
public DateTime NetworkTime;
|
|
#endif
|
|
/// <summary>An object that maintains the data of an request thats in-process.</summary>
|
|
public ImageDownload Transfer;
|
|
}
|
|
|
|
/// <summary>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</summary>
|
|
private readonly Dictionary<UUID, TaskInfo> _Transfers;
|
|
/// <summary>Holds the reference to the <see cref="GridClient"/> client object</summary>
|
|
private readonly GridClient _Client;
|
|
/// <summary>Maximum concurrent texture requests allowed at a time</summary>
|
|
private readonly int maxTextureRequests;
|
|
/// <summary>An array of <see cref="AutoResetEvent"/> objects used to manage worker request threads</summary>
|
|
private readonly AutoResetEvent[] resetEvents;
|
|
/// <summary>An array of worker slots which shows the availablity status of the slot</summary>
|
|
private readonly int[] threadpoolSlots;
|
|
/// <summary>The primary thread which manages the requests.</summary>
|
|
private Thread downloadMaster;
|
|
/// <summary>true if the TexturePipeline is currently running</summary>
|
|
bool _Running;
|
|
/// <summary>A synchronization object used by the primary thread</summary>
|
|
private object lockerObject = new object();
|
|
/// <summary>A refresh timer used to increase the priority of stalled requests</summary>
|
|
private System.Timers.Timer RefreshDownloadsTimer;
|
|
|
|
/// <summary>Current number of pending and in-process transfers</summary>
|
|
public int TransferCount { get { return _Transfers.Count; } }
|
|
|
|
/// <summary>
|
|
/// Default constructor, Instantiates a new copy of the TexturePipeline class
|
|
/// </summary>
|
|
/// <param name="client">Reference to the instantiated <see cref="GridClient"/> object</param>
|
|
public TexturePipeline(GridClient client)
|
|
{
|
|
_Client = client;
|
|
|
|
maxTextureRequests = client.Settings.MAX_CONCURRENT_TEXTURE_DOWNLOADS;
|
|
|
|
resetEvents = new AutoResetEvent[maxTextureRequests];
|
|
threadpoolSlots = new int[maxTextureRequests];
|
|
|
|
_Transfers = new Dictionary<UUID, TaskInfo>();
|
|
|
|
// 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(); };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize callbacks required for the TexturePipeline to operate
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shutdown the TexturePipeline and cleanup any callbacks or transfers
|
|
/// </summary>
|
|
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
|
|
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;
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request a texture asset from the simulator using the <see cref="TexturePipeline"/> system to
|
|
/// manage the requests and re-assemble the image from the packets received from the simulator
|
|
/// </summary>
|
|
/// <param name="textureID">The <see cref="UUID"/> of the texture asset to download</param>
|
|
/// <param name="imageType">The <see cref="ImageType"/> of the texture asset.
|
|
/// Use <see cref="ImageType.Normal"/> for most textures, or <see cref="ImageType.Baked"/> for baked layer texture assets</param>
|
|
/// <param name="priority">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 <see cref="TexturePipeline"/> can have
|
|
/// its priority changed by resending the request with the new priority value</param>
|
|
/// <param name="discardLevel">Number of quality layers to discard.
|
|
/// This controls the end marker of the data sent</param>
|
|
/// <param name="packetStart">The packet number to begin the request at. A value of 0 begins the request
|
|
/// from the start of the asset texture</param>
|
|
/// <param name="callback">The <see cref="TextureDownloadCallback"/> callback to fire when the image is retrieved. The callback
|
|
/// will contain the result of the request and the texture asset data</param>
|
|
/// <param name="progressive">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</param>
|
|
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<TextureDownloadCallback>();
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends the actual request packet to the simulator
|
|
/// </summary>
|
|
/// <param name="imageID">The image to download</param>
|
|
/// <param name="type">Type of the image to download, either a baked
|
|
/// avatar texture or a normal texture</param>
|
|
/// <param name="priority">Priority level of the download. Default is
|
|
/// <c>1,013,000.0f</c></param>
|
|
/// <param name="discardLevel">Number of quality layers to discard.
|
|
/// This controls the end marker of the data sent</param>
|
|
/// <param name="packetNum">Packet number to start the download at.
|
|
/// This controls the start marker of the data sent</param>
|
|
/// <remarks>Sending a priority of 0 and a discardlevel of -1 aborts
|
|
/// download</remarks>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancel a pending or in process texture request
|
|
/// </summary>
|
|
/// <param name="textureID">The texture assets unique ID</param>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Master Download Thread, Queues up downloads in the threadpool
|
|
/// </summary>
|
|
private void DownloadThread()
|
|
{
|
|
int slot;
|
|
|
|
while (_Running)
|
|
{
|
|
// find free slots
|
|
int pending = 0;
|
|
int active = 0;
|
|
|
|
TaskInfo nextTask = null;
|
|
|
|
lock (_Transfers)
|
|
{
|
|
foreach (KeyValuePair<UUID, TaskInfo> 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));
|
|
ThreadPool.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);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// The worker thread that sends the request and handles timeouts
|
|
/// </summary>
|
|
/// <param name="threadContext">A <see cref="TaskInfo"/> object containing the request details</param>
|
|
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;
|
|
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<ushort, ushort> packetsSeen)
|
|
{
|
|
ushort packet = 0;
|
|
|
|
lock (packetsSeen)
|
|
{
|
|
bool first = true;
|
|
foreach (KeyValuePair<ushort, ushort> 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
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
/// <param name="sender">The sender</param>
|
|
/// <param name="e">The EventArgs object containing the packet data</param>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the remaining Image data that did not fit in the initial ImageData packet
|
|
/// </summary>
|
|
/// <param name="sender">The sender</param>
|
|
/// <param name="e">The EventArgs object containing the packet data</param>
|
|
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.PacketsSeen)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle the initial ImageDataPacket sent from the simulator
|
|
/// </summary>
|
|
/// <param name="sender">The sender</param>
|
|
/// <param name="e">The EventArgs object containing the packet data</param>
|
|
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;
|
|
|
|
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<ushort, ushort>();
|
|
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);
|
|
}
|
|
}
|
|
}
|