diff --git a/Programs/Simian/Extensions/ImageDelivery.cs b/Programs/Simian/Extensions/ImageDelivery.cs index d2d1884f..9868f2d9 100644 --- a/Programs/Simian/Extensions/ImageDelivery.cs +++ b/Programs/Simian/Extensions/ImageDelivery.cs @@ -8,7 +8,7 @@ using OpenMetaverse.Packets; namespace Simian.Extensions { - public struct ImageDownload + public class ImageDownload { public const int FIRST_IMAGE_PACKET_SIZE = 600; public const int IMAGE_PACKET_SIZE = 1000; @@ -16,60 +16,68 @@ namespace Simian.Extensions public AssetTexture Texture; public int DiscardLevel; public float Priority; - public int Packet; + public int CurrentPacket; + public int StopPacket; - public ImageDownload(AssetTexture texture, int discardLevel, float priority) + public ImageDownload(AssetTexture texture, int discardLevel, float priority, int packet) { Texture = texture; - DiscardLevel = discardLevel; + Update(discardLevel, priority, packet); + } + + /// + /// Updates an image transfer with new information and recalculates + /// offsets + /// + /// New requested discard level + /// New requested priority + /// New requested packet offset + public void Update(int discardLevel, float priority, int packet) + { Priority = priority; - Packet = 0; + DiscardLevel = Utils.Clamp(discardLevel, 0, Texture.LayerInfo.Length - 1); + StopPacket = GetPacketForBytePosition(Texture.LayerInfo[(Texture.LayerInfo.Length - 1) - DiscardLevel].End); + CurrentPacket = Utils.Clamp(packet, 1, TexturePacketCount()); } - public int GetRemainingBytes() + /// + /// Returns the total number of packets needed to transfer this texture, + /// including the first packet of size FIRST_IMAGE_PACKET_SIZE + /// + /// Total number of packets needed to transfer this texture + public int TexturePacketCount() { - return GetEndPosition() - GetBytesSent(); + return ((Texture.AssetData.Length - FIRST_IMAGE_PACKET_SIZE + IMAGE_PACKET_SIZE - 1) / IMAGE_PACKET_SIZE) + 1; } - public int GetPacketCount() + /// + /// Returns the current byte offset for this transfer, calculated from + /// the CurrentPacket + /// + /// Current byte offset for this transfer + public int CurrentBytePosition() { - int length = GetRemainingBytes(); - return ((length - FIRST_IMAGE_PACKET_SIZE + IMAGE_PACKET_SIZE - 1) / IMAGE_PACKET_SIZE) + 1; + return FIRST_IMAGE_PACKET_SIZE + (CurrentPacket - 1) * IMAGE_PACKET_SIZE; } - public int GetBytesSent() + /// + /// Returns the size, in bytes, of the last packet. This will be somewhere + /// between 1 and IMAGE_PACKET_SIZE bytes + /// + /// Size of the last packet in the transfer + public int LastPacketSize() { - if (Packet < 1) - return 0; - else - return FIRST_IMAGE_PACKET_SIZE + ((Packet - 1) * IMAGE_PACKET_SIZE); + return Texture.AssetData.Length - (FIRST_IMAGE_PACKET_SIZE + ((TexturePacketCount() - 2) * IMAGE_PACKET_SIZE)); } - public int GetEndPosition() + /// + /// Find the packet number that contains a given byte position + /// + /// Byte position + /// Packet number that contains the given byte position + int GetPacketForBytePosition(int bytePosition) { - if (Texture == null || Texture.LayerInfo == null || Texture.AssetData == null) - throw new InvalidOperationException("Cannot get end position while texture information is null"); - - int layerCount = Texture.LayerInfo.Length; - int requestedLayer = layerCount + DiscardLevel; - - if (requestedLayer == layerCount) - { - // No discard, go to the end of the image data - return Texture.AssetData.Length - 1; - } - else if (requestedLayer >= 0 && requestedLayer < layerCount) - { - return Texture.LayerInfo[requestedLayer].End; - } - else - { - Logger.Log(String.Format( - "DiscardLevel {0} is out of range for texture {1}, which has {2} decoded layer boundaries", - DiscardLevel, Texture.AssetID, Texture.LayerInfo.Length), Helpers.LogLevel.Error); - - return Texture.AssetData.Length - 1; - } + return ((bytePosition - FIRST_IMAGE_PACKET_SIZE + IMAGE_PACKET_SIZE - 1) / IMAGE_PACKET_SIZE); } } @@ -120,115 +128,146 @@ namespace Simian.Extensions for (int i = 0; i < request.RequestImage.Length; i++) { RequestImagePacket.RequestImageBlock block = request.RequestImage[i]; - bool bake = ((ImageType)block.Type == ImageType.Baked); - // Check if we have this image - Asset asset; ImageDownload download; - if (Server.Assets.TryGetAsset(block.Image, out asset) && asset is AssetTexture) + bool downloadFound = CurrentDownloads.TryGetValue(block.Image, out download); + + if (downloadFound) { - if (block.DiscardLevel == -1 && block.DownloadPriority == 0.0f) + lock (download) { - // FIXME: Cancel download - Logger.Log("Canceling image download " + block.Image, Helpers.LogLevel.Info); - } - else - { - lock (CurrentDownloads) + if (block.DiscardLevel == -1 && block.DownloadPriority == 0.0f) { - if (CurrentDownloads.TryGetValue(block.Image, out download)) - { - // Modifying existing download - if (block.DiscardLevel < download.DiscardLevel) - { - // FIXME: Do we need to do something here? - Logger.Log(String.Format("Image download {0} is changing from DiscardLevel {1} to {2}", - block.Image, download.DiscardLevel, block.DiscardLevel), Helpers.LogLevel.Info); - download.DiscardLevel = block.DiscardLevel; - } - - download.Priority = block.DownloadPriority; - - if (block.Packet > 0 && block.Packet < download.Packet) - { - Logger.Log(String.Format("Rolling back image download {0} from packet {1} to {2}", - block.Image, download.Packet, block.Packet), Helpers.LogLevel.Warning); - download.Packet = (int)block.Packet; - } - - // Re-insert this download into the dictionary - CurrentDownloads[block.Image] = download; - } - else - { - // New download - download = new ImageDownload((AssetTexture)asset, block.DiscardLevel, block.DownloadPriority); - download.Packet = (int)block.Packet; - - // Send initial data - ImageDataPacket data = new ImageDataPacket(); - data.ImageID.Codec = (byte)ImageCodec.J2C; - data.ImageID.ID = download.Texture.AssetID; - data.ImageID.Packets = (ushort)download.GetPacketCount(); - data.ImageID.Size = (uint)download.GetRemainingBytes(); - // The Linden Lab servers actually prepend two bytes in this data with the - // size of the following data. It is redundant and ignored by every client, - // so we skip it - data.ImageData = new ImageDataPacket.ImageDataBlock(); - - if (data.ImageID.Packets == 1) - { - // Single packet image - data.ImageData.Data = new byte[download.Texture.AssetData.Length]; - Buffer.BlockCopy(download.Texture.AssetData, download.GetBytesSent(), - data.ImageData.Data, 0, (int)data.ImageID.Size); - } - else - { - // Multi-packet image - data.ImageData.Data = new byte[ImageDownload.FIRST_IMAGE_PACKET_SIZE]; - Buffer.BlockCopy(download.Texture.AssetData, download.GetBytesSent(), - data.ImageData.Data, 0, ImageDownload.FIRST_IMAGE_PACKET_SIZE); - - // Insert this download into the dictionary - CurrentDownloads[block.Image] = download; - - // Send all of the remaining packets - ThreadPool.QueueUserWorkItem( - delegate(object obj) - { - ImagePacketPacket transfer = new ImagePacketPacket(); - transfer.ImageID.ID = block.Image; - //transfer.ImageID.Packet = - } - ); - } - - Server.UDP.SendPacket(agent.AgentID, data, PacketCategory.Texture); - } + Logger.DebugLog(String.Format("Image download {0} is aborting", block.Image)); } + else + { + if (block.DiscardLevel < download.DiscardLevel) + Logger.DebugLog(String.Format("Image download {0} is changing from DiscardLevel {1} to {2}", + block.Image, download.DiscardLevel, block.DiscardLevel)); + + if (block.DownloadPriority != download.Priority) + Logger.DebugLog(String.Format("Image download {0} is changing from Priority {1} to {2}", + block.Image, download.Priority, block.DownloadPriority)); + + if (block.Packet != download.CurrentPacket) + Logger.DebugLog(String.Format("Image download {0} is changing from Packet {1} to {2}", + block.Image, download.CurrentPacket, block.Packet)); + } + + // Update download + download.Update(block.DiscardLevel, block.DownloadPriority, (int)block.Packet); } } + else if (block.DiscardLevel == -1 && block.DownloadPriority == 0.0f) + { + // Aborting a download we are not tracking, ignore + Logger.DebugLog(String.Format("Aborting an image download for untracked image " + block.Image.ToString())); + } else { - // TODO: Technically we should return ImageNotInDatabasePacket, but for now return a default texture - ImageDataPacket imageData = new ImageDataPacket(); - imageData.ImageID.ID = block.Image; - imageData.ImageID.Codec = 1; - imageData.ImageID.Packets = 1; - if (bake) + bool bake = ((ImageType)block.Type == ImageType.Baked); + + // New download, check if we have this image + Asset asset; + if (Server.Assets.TryGetAsset(block.Image, out asset) && asset is AssetTexture) { - Logger.DebugLog(String.Format("Sending default bake texture for {0}", block.Image)); - imageData.ImageData.Data = defaultBakedJP2.AssetData; + download = new ImageDownload((AssetTexture)asset, block.DiscardLevel, block.DownloadPriority, + (int)block.Packet); + + Logger.DebugLog(String.Format( + "Starting new download for {0}, DiscardLevel: {1}, Priority: {2}, Start: {3}, End: {4}, Total: {5}", + block.Image, block.DiscardLevel, block.DownloadPriority, download.CurrentPacket, download.StopPacket, + download.TexturePacketCount())); + + // Send initial data + ImageDataPacket data = new ImageDataPacket(); + data.ImageID.Codec = (byte)ImageCodec.J2C; + data.ImageID.ID = download.Texture.AssetID; + data.ImageID.Packets = (ushort)download.TexturePacketCount(); + data.ImageID.Size = (uint)download.Texture.AssetData.Length; + + // The first bytes of the image are always sent in the ImageData packet + data.ImageData = new ImageDataPacket.ImageDataBlock(); + int imageDataSize = (download.Texture.AssetData.Length >= ImageDownload.FIRST_IMAGE_PACKET_SIZE) ? + ImageDownload.FIRST_IMAGE_PACKET_SIZE : download.Texture.AssetData.Length; + data.ImageData.Data = new byte[imageDataSize]; + Buffer.BlockCopy(download.Texture.AssetData, 0, data.ImageData.Data, 0, imageDataSize); + + Server.UDP.SendPacket(agent.AgentID, data, PacketCategory.Texture); + + // Check if ImagePacket packets need to be sent to complete this transfer + if (download.CurrentPacket <= download.StopPacket) + { + // Insert this download into the dictionary + lock (CurrentDownloads) + CurrentDownloads[block.Image] = download; + + // Send all of the remaining packets + ThreadPool.QueueUserWorkItem( + delegate(object obj) + { + while (download.CurrentPacket <= download.StopPacket) + { + if (download.Priority == 0.0f && download.DiscardLevel == -1) + break; + + lock (download) + { + int imagePacketSize = (download.CurrentPacket == download.TexturePacketCount() - 1) ? + download.LastPacketSize() : ImageDownload.IMAGE_PACKET_SIZE; + + ImagePacketPacket transfer = new ImagePacketPacket(); + transfer.ImageID.ID = block.Image; + transfer.ImageID.Packet = (ushort)download.CurrentPacket; + transfer.ImageData.Data = new byte[imagePacketSize]; + Buffer.BlockCopy(download.Texture.AssetData, download.CurrentBytePosition(), + transfer.ImageData.Data, 0, imagePacketSize); + + Server.UDP.SendPacket(agent.AgentID, transfer, PacketCategory.Texture); + + ++download.CurrentPacket; + } + } + + Logger.DebugLog("Completed image transfer for " + block.Image.ToString()); + + // Transfer is complete, remove the reference + lock (CurrentDownloads) + CurrentDownloads.Remove(block.Image); + } + ); + } } else { - Logger.DebugLog(String.Format("Sending default texture for {0}", block.Image)); - imageData.ImageData.Data = defaultJP2.AssetData; - } - imageData.ImageID.Size = (uint)imageData.ImageData.Data.Length; + Logger.Log("Request for a missing texture " + block.Image.ToString(), Helpers.LogLevel.Warning); - Server.UDP.SendPacket(agent.AgentID, imageData, PacketCategory.Texture); + ImageNotInDatabasePacket notfound = new ImageNotInDatabasePacket(); + notfound.ImageID.ID = block.Image; + Server.UDP.SendPacket(agent.AgentID, notfound, PacketCategory.Texture); + + /* + // TODO: Technically we should return ImageNotInDatabasePacket, but for now return a default texture + ImageDataPacket imageData = new ImageDataPacket(); + imageData.ImageID.ID = block.Image; + imageData.ImageID.Codec = 1; + imageData.ImageID.Packets = 1; + if (bake) + { + Logger.DebugLog(String.Format("Sending default bake texture for {0}", block.Image)); + imageData.ImageData.Data = defaultBakedJP2.AssetData; + } + else + { + Logger.DebugLog(String.Format("Sending default texture for {0}", block.Image)); + imageData.ImageData.Data = defaultJP2.AssetData; + } + imageData.ImageID.Size = (uint)imageData.ImageData.Data.Length; + + Server.UDP.SendPacket(agent.AgentID, imageData, PacketCategory.Texture); + */ + } } } }