using System; using System.Collections.Generic; using System.Text; using System.Threading; using OpenMetaverse; /* * the onnewprim function will add missing texture uuids to the download queue, * and a separate thread will pull entries off that queue. * if they exist in the cache it will add that texture to a dictionary that the rendering loop accesses, * otherwise it will start the download. * the ondownloaded function will put the new texture in the same dictionary * * * Easy Start: * subscribe to OnImageRenderReady event * send request with RequestTexture() * * when OnImageRenderReady fires: * request image data with GetTextureToRender() using key returned in OnImageRenderReady event * (optionally) use RemoveFromPipeline() with key to cleanup dictionary */ namespace SimExport { class TaskInfo { public UUID RequestID; public int RequestNbr; public TaskInfo(UUID reqID, int reqNbr) { RequestID = reqID; RequestNbr = reqNbr; } } /// /// Texture request download handler, allows a configurable number of download slots /// public class TexturePipeline { private static GridClient Client; // queue for requested images private Queue RequestQueue; // list of current requests in process private Dictionary CurrentRequests; private static AutoResetEvent[] resetEvents; private static int[] threadpoolSlots; /// /// For keeping track of active threads available/downloading textures /// public static int[] ThreadpoolSlots { get { lock (threadpoolSlots) { return threadpoolSlots; } } set { lock (threadpoolSlots) { threadpoolSlots = value; } } } public int QueuedCount { get { return RequestQueue.Count; } } public int CurrentCount { get { return CurrentRequests.Count; } } // storage for images ready to render private Dictionary RenderReady; // maximum allowed concurrent requests at once const int MAX_TEXTURE_REQUESTS = 10; /// /// /// /// /// public delegate void DownloadFinishedCallback(UUID id, bool success); /// /// /// /// /// /// public delegate void DownloadProgressCallback(UUID image, int recieved, int total); /// Fired when a texture download completes public event DownloadFinishedCallback OnDownloadFinished; /// public event DownloadProgressCallback OnDownloadProgress; private Thread downloadMaster; private bool Running; private AssetManager.ImageReceivedCallback DownloadCallback; private AssetManager.ImageReceiveProgressCallback DownloadProgCallback; /// /// Default constructor /// /// Reference to SecondLife client public TexturePipeline(GridClient client) { Running = true; RequestQueue = new Queue(); CurrentRequests = new Dictionary(MAX_TEXTURE_REQUESTS); RenderReady = new Dictionary(); resetEvents = new AutoResetEvent[MAX_TEXTURE_REQUESTS]; threadpoolSlots = new int[MAX_TEXTURE_REQUESTS]; // pre-configure autoreset events/download slots for (int i = 0; i < MAX_TEXTURE_REQUESTS; i++) { resetEvents[i] = new AutoResetEvent(false); threadpoolSlots[i] = -1; } Client = client; DownloadCallback = new AssetManager.ImageReceivedCallback(Assets_OnImageReceived); DownloadProgCallback = new AssetManager.ImageReceiveProgressCallback(Assets_OnImageReceiveProgress); Client.Assets.OnImageReceived += DownloadCallback; Client.Assets.OnImageReceiveProgress += DownloadProgCallback; // Fire up the texture download thread downloadMaster = new Thread(new ThreadStart(DownloadThread)); downloadMaster.Start(); } public void Shutdown() { Client.Assets.OnImageReceived -= DownloadCallback; Client.Assets.OnImageReceiveProgress -= DownloadProgCallback; RequestQueue.Clear(); for (int i = 0; i < resetEvents.Length; i++) if (resetEvents[i] != null) resetEvents[i].Set(); Running = false; } /// /// Request a texture be downloaded, once downloaded OnImageRenderReady event will be fired /// containing texture key which can be used to retrieve texture with GetTextureToRender method /// /// id of Texture to request public void RequestTexture(UUID textureID) { if (Client.Assets.Cache.HasImage(textureID)) { // Add to rendering dictionary lock (RenderReady) { if (!RenderReady.ContainsKey(textureID)) { RenderReady.Add(textureID, Client.Assets.Cache.GetCachedImage(textureID)); // Let any subscribers know about it if (OnDownloadFinished != null) { OnDownloadFinished(textureID, true); } } else { // This image has already been served up, ignore this request } } } else { lock (RequestQueue) { // Make sure we aren't already downloading the texture if (!RequestQueue.Contains(textureID) && !CurrentRequests.ContainsKey(textureID)) { RequestQueue.Enqueue(textureID); } } } } /// /// retrieve texture information from dictionary /// /// Texture ID /// ImageDownload object public ImageDownload GetTextureToRender(UUID textureID) { ImageDownload renderable = new ImageDownload(); lock (RenderReady) { if (RenderReady.ContainsKey(textureID)) { renderable = RenderReady[textureID]; } else { Logger.Log("Requested texture data for texture that does not exist in dictionary", Helpers.LogLevel.Warning); } return renderable; } } /// /// Remove no longer necessary texture from dictionary /// /// public void RemoveFromPipeline(UUID textureID) { lock (RenderReady) { if (RenderReady.ContainsKey(textureID)) RenderReady.Remove(textureID); } } /// /// Master Download Thread, Queues up downloads in the threadpool /// private void DownloadThread() { int reqNbr; while (Running) { if (RequestQueue.Count > 0) { reqNbr = -1; // find available slot for reset event for (int i = 0; i < threadpoolSlots.Length; i++) { if (threadpoolSlots[i] == -1) { threadpoolSlots[i] = 1; reqNbr = i; break; } } if (reqNbr != -1) { UUID requestID; lock (RequestQueue) requestID = RequestQueue.Dequeue(); Logger.DebugLog(String.Format("Sending Worker thread new download request {0}", reqNbr)); ThreadPool.QueueUserWorkItem(new WaitCallback(textureRequestDoWork), new TaskInfo(requestID, reqNbr)); continue; } } // Queue was empty, let's give up some CPU time Thread.Sleep(500); } } void textureRequestDoWork(Object threadContext) { TaskInfo ti = (TaskInfo)threadContext; lock (CurrentRequests) { if (CurrentRequests.ContainsKey(ti.RequestID)) { threadpoolSlots[ti.RequestNbr] = -1; return; } else { CurrentRequests.Add(ti.RequestID, ti.RequestNbr); } } Logger.DebugLog(String.Format("Worker {0} Requesting {1}", ti.RequestNbr, ti.RequestID)); resetEvents[ti.RequestNbr].Reset(); Client.Assets.RequestImage(ti.RequestID, ImageType.Normal); // don't release this worker slot until texture is downloaded or timeout occurs if (!resetEvents[ti.RequestNbr].WaitOne(30 * 1000, false)) { // Timed out Logger.Log("Worker " + ti.RequestNbr + " Timeout waiting for Texture " + ti.RequestID + " to Download", Helpers.LogLevel.Warning); lock (CurrentRequests) CurrentRequests.Remove(ti.RequestID); } // free up this download slot threadpoolSlots[ti.RequestNbr] = -1; } private void Assets_OnImageReceived(ImageDownload image, AssetTexture asset) { // Free up this slot in the ThreadPool lock (CurrentRequests) { int requestNbr; if (asset != null && CurrentRequests.TryGetValue(image.ID, out requestNbr)) { Logger.DebugLog(String.Format("Worker {0} Downloaded texture {1}", requestNbr, image.ID)); resetEvents[requestNbr].Set(); CurrentRequests.Remove(image.ID); } } if (image.Success) { lock (RenderReady) { if (!RenderReady.ContainsKey(image.ID)) { // Add to rendering dictionary RenderReady.Add(image.ID, image); } } } else { Console.WriteLine(String.Format("Download of texture {0} failed. NotFound={1}", image.ID, image.NotFound)); } // Let any subscribers know about it if (OnDownloadFinished != null) { OnDownloadFinished(image.ID, image.Success); } } private void Assets_OnImageReceiveProgress(UUID image, int lastPacket, int recieved, int total) { if (OnDownloadProgress != null) { OnDownloadProgress(image, recieved, total); } } } }