From c0d68d0aa4f2c9c5432bb128ca5fd30b2fc110ff Mon Sep 17 00:00:00 2001 From: "Justin Clark-Casey (justincc)" Date: Tue, 28 Aug 2012 23:06:53 +0100 Subject: [PATCH] Add experimental DynamicTextureModule.ReuseTextures flag, currently only configurable on compile. Disabled (status quo) by default. This flag makes the dynamic texture module reuse cache previously dynamically generated textures given the same input commands and extra params for 24 hours. This occurs as long as those commands would always generate the same texture (e.g. they do not contain commands to fetch data from the web). This makes texture changing faster as a viewer-cached texture uuid is sent and may reduce simulator load in regions with generation of lots of dynamic textures. A downside is that this stops expiry of old temporary dynamic textures from the cache, Another downside is that a jpeg2000 generation that partially failed is currently not regenerated until restart or after 24 hours. --- .../DynamicTexture/DynamicTextureModule.cs | 169 +++++++++++++----- .../LoadImageURL/LoadImageURLModule.cs | 10 +- .../Tests/VectorRenderModuleTests.cs | 125 ++++++++++++- .../VectorRender/VectorRenderModule.cs | 48 ++++- .../Interfaces/IDynamicTextureManager.cs | 14 +- 5 files changed, 310 insertions(+), 56 deletions(-) diff --git a/OpenSim/Region/CoreModules/Scripting/DynamicTexture/DynamicTextureModule.cs b/OpenSim/Region/CoreModules/Scripting/DynamicTexture/DynamicTextureModule.cs index 18bd0186b8..13b74983b1 100644 --- a/OpenSim/Region/CoreModules/Scripting/DynamicTexture/DynamicTextureModule.cs +++ b/OpenSim/Region/CoreModules/Scripting/DynamicTexture/DynamicTextureModule.cs @@ -49,6 +49,11 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture public const int DISP_EXPIRE = 1; public const int DISP_TEMP = 2; + /// + /// If true then where possible dynamic textures are reused. + /// + public bool ReuseTextures { get; set; } + private Dictionary RegisteredScenes = new Dictionary(); private Dictionary RenderPlugins = @@ -56,6 +61,15 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture private Dictionary Updaters = new Dictionary(); + /// + /// Record dynamic textures that we can reuse for a given data and parameter combination rather than + /// regenerate. + /// + /// + /// Key is string.Format("{0}{1}", data + /// + private Cache m_reuseableDynamicTextures; + #region IDynamicTextureManager Members public void RegisterRender(string handleType, IDynamicTextureRender render) @@ -71,7 +85,8 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture /// /// /// - public void ReturnData(UUID id, byte[] data) + /// True if the data generated can be reused for subsequent identical requests + public void ReturnData(UUID id, byte[] data, bool isReuseable) { DynamicTextureUpdater updater = null; @@ -88,7 +103,11 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture if (RegisteredScenes.ContainsKey(updater.SimUUID)) { Scene scene = RegisteredScenes[updater.SimUUID]; - updater.DataReceived(data, scene); + UUID newTextureID = updater.DataReceived(data, scene); + + if (ReuseTextures && isReuseable && !updater.BlendWithOldTexture) + m_reuseableDynamicTextures.Store( + GenerateReusableTextureKey(updater.BodyData, updater.Params), newTextureID); } } @@ -169,6 +188,11 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture { if (RenderPlugins.ContainsKey(contentType)) { + // If we want to reuse dynamic textures then we have to ignore any request from the caller to expire + // them. + if (ReuseTextures) + disp = disp & ~DISP_EXPIRE; + DynamicTextureUpdater updater = new DynamicTextureUpdater(); updater.SimUUID = simID; updater.PrimID = primID; @@ -183,21 +207,49 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture updater.Url = "Local image"; updater.Disp = disp; - lock (Updaters) + object reusableTextureUUID = null; + + if (ReuseTextures) + reusableTextureUUID + = m_reuseableDynamicTextures.Get(GenerateReusableTextureKey(data, extraParams)); + + // We cannot reuse a dynamic texture if the data is going to be blended with something already there. + if (reusableTextureUUID == null || updater.BlendWithOldTexture) { - if (!Updaters.ContainsKey(updater.UpdaterID)) + lock (Updaters) { - Updaters.Add(updater.UpdaterID, updater); + if (!Updaters.ContainsKey(updater.UpdaterID)) + { + Updaters.Add(updater.UpdaterID, updater); + } + } + + RenderPlugins[contentType].AsyncConvertData(updater.UpdaterID, data, extraParams); + } + else + { + // No need to add to updaters as the texture is always the same. Not that this functionality + // apppears to be implemented anyway. + if (RegisteredScenes.ContainsKey(updater.SimUUID)) + { + SceneObjectPart part = RegisteredScenes[updater.SimUUID].GetSceneObjectPart(updater.PrimID); + + if (part != null) + updater.UpdatePart(part, (UUID)reusableTextureUUID); } } - RenderPlugins[contentType].AsyncConvertData(updater.UpdaterID, data, extraParams); return updater.UpdaterID; } return UUID.Zero; } + private string GenerateReusableTextureKey(string data, string extraParams) + { + return string.Format("{0}{1}", data, extraParams); + } + public void GetDrawStringSize(string contentType, string text, string fontName, int fontSize, out double xSize, out double ySize) { @@ -224,6 +276,12 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture public void PostInitialise() { +// ReuseTextures = true; + if (ReuseTextures) + { + m_reuseableDynamicTextures = new Cache(CacheMedium.Memory, CacheStrategy.Conservative); + m_reuseableDynamicTextures.DefaultTTL = new TimeSpan(24, 0, 0); + } } public void Close() @@ -268,10 +326,61 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture BodyData = null; } + /// + /// Update the given part with the new texture. + /// + /// + /// The old texture UUID. + /// + public UUID UpdatePart(SceneObjectPart part, UUID textureID) + { + UUID oldID; + + lock (part) + { + // mostly keep the values from before + Primitive.TextureEntry tmptex = part.Shape.Textures; + + // FIXME: Need to return the appropriate ID if only a single face is replaced. + oldID = tmptex.DefaultTexture.TextureID; + + if (Face == ALL_SIDES) + { + oldID = tmptex.DefaultTexture.TextureID; + tmptex.DefaultTexture.TextureID = textureID; + } + else + { + try + { + Primitive.TextureEntryFace texface = tmptex.CreateFace((uint)Face); + texface.TextureID = textureID; + tmptex.FaceTextures[Face] = texface; + } + catch (Exception) + { + tmptex.DefaultTexture.TextureID = textureID; + } + } + + // I'm pretty sure we always want to force this to true + // I'm pretty sure noone whats to set fullbright true if it wasn't true before. + // tmptex.DefaultTexture.Fullbright = true; + + part.UpdateTextureEntry(tmptex.GetBytes()); + } + + return oldID; + } + /// /// Called once new texture data has been received for this updater. /// - public void DataReceived(byte[] data, Scene scene) + /// + /// + /// True if the data given is reuseable. + /// The asset UUID given to the incoming data. + public UUID DataReceived(byte[] data, Scene scene) { SceneObjectPart part = scene.GetSceneObjectPart(PrimID); @@ -281,7 +390,8 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture String.Format("DynamicTextureModule: Error preparing image using URL {0}", Url); scene.SimChat(Utils.StringToBytes(msg), ChatTypeEnum.Say, 0, part.ParentGroup.RootPart.AbsolutePosition, part.Name, part.UUID, false); - return; + + return UUID.Zero; } byte[] assetData = null; @@ -323,52 +433,23 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture cacheLayerDecode = null; } - UUID oldID = UUID.Zero; - - lock (part) - { - // mostly keep the values from before - Primitive.TextureEntry tmptex = part.Shape.Textures; - - // remove the old asset from the cache - oldID = tmptex.DefaultTexture.TextureID; - - if (Face == ALL_SIDES) - { - tmptex.DefaultTexture.TextureID = asset.FullID; - } - else - { - try - { - Primitive.TextureEntryFace texface = tmptex.CreateFace((uint)Face); - texface.TextureID = asset.FullID; - tmptex.FaceTextures[Face] = texface; - } - catch (Exception) - { - tmptex.DefaultTexture.TextureID = asset.FullID; - } - } - - // I'm pretty sure we always want to force this to true - // I'm pretty sure noone whats to set fullbright true if it wasn't true before. - // tmptex.DefaultTexture.Fullbright = true; - - part.UpdateTextureEntry(tmptex.GetBytes()); - } + UUID oldID = UpdatePart(part, asset.FullID); if (oldID != UUID.Zero && ((Disp & DISP_EXPIRE) != 0)) { - if (oldAsset == null) oldAsset = scene.AssetService.Get(oldID.ToString()); + if (oldAsset == null) + oldAsset = scene.AssetService.Get(oldID.ToString()); + if (oldAsset != null) { - if (oldAsset.Temporary == true) + if (oldAsset.Temporary) { scene.AssetService.Delete(oldID.ToString()); } } } + + return asset.FullID; } private byte[] BlendTextures(byte[] frontImage, byte[] backImage, bool setNewAlpha, byte newAlpha) diff --git a/OpenSim/Region/CoreModules/Scripting/LoadImageURL/LoadImageURLModule.cs b/OpenSim/Region/CoreModules/Scripting/LoadImageURL/LoadImageURLModule.cs index 6e38b3f44c..2b3a0f29ff 100644 --- a/OpenSim/Region/CoreModules/Scripting/LoadImageURL/LoadImageURLModule.cs +++ b/OpenSim/Region/CoreModules/Scripting/LoadImageURL/LoadImageURLModule.cs @@ -67,6 +67,12 @@ namespace OpenSim.Region.CoreModules.Scripting.LoadImageURL return true; } +// public bool AlwaysIdenticalConversion(string bodyData, string extraParams) +// { +// // We don't support conversion of body data. +// return false; +// } + public byte[] ConvertUrl(string url, string extraParams) { return null; @@ -236,9 +242,11 @@ namespace OpenSim.Region.CoreModules.Scripting.LoadImageURL stream.Close(); } } + m_log.DebugFormat("[LOADIMAGEURLMODULE] Returning {0} bytes of image data for request {1}", imageJ2000.Length, state.RequestID); - m_textureManager.ReturnData(state.RequestID, imageJ2000); + + m_textureManager.ReturnData(state.RequestID, imageJ2000, false); } #region Nested type: RequestState diff --git a/OpenSim/Region/CoreModules/Scripting/VectorRender/Tests/VectorRenderModuleTests.cs b/OpenSim/Region/CoreModules/Scripting/VectorRender/Tests/VectorRenderModuleTests.cs index 180ea9f015..b50c0bd7cd 100644 --- a/OpenSim/Region/CoreModules/Scripting/VectorRender/Tests/VectorRenderModuleTests.cs +++ b/OpenSim/Region/CoreModules/Scripting/VectorRender/Tests/VectorRenderModuleTests.cs @@ -51,12 +51,15 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender.Tests DynamicTextureModule m_dtm; VectorRenderModule m_vrm; - [SetUp] - public void SetUp() + private void SetupScene(bool reuseTextures) { m_scene = new SceneHelpers().SetupScene(); + m_dtm = new DynamicTextureModule(); + m_dtm.ReuseTextures = reuseTextures; + m_vrm = new VectorRenderModule(); + SceneHelpers.SetupSceneModules(m_scene, m_dtm, m_vrm); } @@ -65,6 +68,7 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender.Tests { TestHelpers.InMethod(); + SetupScene(false); SceneObjectGroup so = SceneHelpers.AddSceneObject(m_scene); UUID originalTextureID = so.RootPart.Shape.Textures.GetFace(0).TextureID; @@ -86,6 +90,7 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender.Tests string dtText = "PenColour BLACK; MoveTo 40,220; FontSize 32; Text Hello World;"; + SetupScene(false); SceneObjectGroup so = SceneHelpers.AddSceneObject(m_scene); m_dtm.AddDynamicTextureData( @@ -116,6 +121,7 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender.Tests string dtText = "PenColour BLACK; MoveTo 40,220; FontSize 32; Text Hello World;"; + SetupScene(false); SceneObjectGroup so = SceneHelpers.AddSceneObject(m_scene); m_dtm.AddDynamicTextureData( @@ -147,6 +153,121 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender.Tests string dtText = "PenColour BLACK; MoveTo 40,220; FontSize 32; Text Hello World; Image http://localhost/shouldnotexist.png"; + SetupScene(false); + SceneObjectGroup so = SceneHelpers.AddSceneObject(m_scene); + + m_dtm.AddDynamicTextureData( + m_scene.RegionInfo.RegionID, + so.UUID, + m_vrm.GetContentType(), + dtText, + "", + 0); + + UUID firstDynamicTextureID = so.RootPart.Shape.Textures.GetFace(0).TextureID; + + m_dtm.AddDynamicTextureData( + m_scene.RegionInfo.RegionID, + so.UUID, + m_vrm.GetContentType(), + dtText, + "", + 0); + + Assert.That(firstDynamicTextureID, Is.Not.EqualTo(so.RootPart.Shape.Textures.GetFace(0).TextureID)); + } + + [Test] + public void TestDrawReusingTexture() + { + TestHelpers.InMethod(); + + SetupScene(true); + SceneObjectGroup so = SceneHelpers.AddSceneObject(m_scene); + UUID originalTextureID = so.RootPart.Shape.Textures.GetFace(0).TextureID; + + m_dtm.AddDynamicTextureData( + m_scene.RegionInfo.RegionID, + so.UUID, + m_vrm.GetContentType(), + "PenColour BLACK; MoveTo 40,220; FontSize 32; Text Hello World;", + "", + 0); + + Assert.That(originalTextureID, Is.Not.EqualTo(so.RootPart.Shape.Textures.GetFace(0).TextureID)); + } + + [Test] + public void TestRepeatSameDrawReusingTexture() + { + TestHelpers.InMethod(); + + string dtText = "PenColour BLACK; MoveTo 40,220; FontSize 32; Text Hello World;"; + + SetupScene(true); + SceneObjectGroup so = SceneHelpers.AddSceneObject(m_scene); + + m_dtm.AddDynamicTextureData( + m_scene.RegionInfo.RegionID, + so.UUID, + m_vrm.GetContentType(), + dtText, + "", + 0); + + UUID firstDynamicTextureID = so.RootPart.Shape.Textures.GetFace(0).TextureID; + + m_dtm.AddDynamicTextureData( + m_scene.RegionInfo.RegionID, + so.UUID, + m_vrm.GetContentType(), + dtText, + "", + 0); + + Assert.That(firstDynamicTextureID, Is.EqualTo(so.RootPart.Shape.Textures.GetFace(0).TextureID)); + } + + [Test] + public void TestRepeatSameDrawDifferentExtraParamsReusingTexture() + { + TestHelpers.InMethod(); + + string dtText = "PenColour BLACK; MoveTo 40,220; FontSize 32; Text Hello World;"; + + SetupScene(true); + SceneObjectGroup so = SceneHelpers.AddSceneObject(m_scene); + + m_dtm.AddDynamicTextureData( + m_scene.RegionInfo.RegionID, + so.UUID, + m_vrm.GetContentType(), + dtText, + "", + 0); + + UUID firstDynamicTextureID = so.RootPart.Shape.Textures.GetFace(0).TextureID; + + m_dtm.AddDynamicTextureData( + m_scene.RegionInfo.RegionID, + so.UUID, + m_vrm.GetContentType(), + dtText, + "alpha:250", + 0); + + Assert.That(firstDynamicTextureID, Is.Not.EqualTo(so.RootPart.Shape.Textures.GetFace(0).TextureID)); + } + + [Test] + public void TestRepeatSameDrawContainingImageReusingTexture() + { + TestHelpers.InMethod(); + + string dtText + = "PenColour BLACK; MoveTo 40,220; FontSize 32; Text Hello World; Image http://localhost/shouldnotexist.png"; + + SetupScene(true); SceneObjectGroup so = SceneHelpers.AddSceneObject(m_scene); m_dtm.AddDynamicTextureData( diff --git a/OpenSim/Region/CoreModules/Scripting/VectorRender/VectorRenderModule.cs b/OpenSim/Region/CoreModules/Scripting/VectorRender/VectorRenderModule.cs index 3a758c591a..4268f2e98a 100644 --- a/OpenSim/Region/CoreModules/Scripting/VectorRender/VectorRenderModule.cs +++ b/OpenSim/Region/CoreModules/Scripting/VectorRender/VectorRenderModule.cs @@ -30,6 +30,7 @@ using System.Drawing; using System.Drawing.Imaging; using System.Globalization; using System.IO; +using System.Linq; using System.Net; using Nini.Config; using OpenMetaverse; @@ -73,6 +74,12 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender return true; } +// public bool AlwaysIdenticalConversion(string bodyData, string extraParams) +// { +// string[] lines = GetLines(bodyData); +// return lines.Any((str, r) => str.StartsWith("Image")); +// } + public byte[] ConvertUrl(string url, string extraParams) { return null; @@ -80,7 +87,13 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender public byte[] ConvertData(string bodyData, string extraParams) { - return Draw(bodyData, extraParams); + bool reuseable; + return Draw(bodyData, extraParams, out reuseable); + } + + private byte[] ConvertData(string bodyData, string extraParams, out bool reuseable) + { + return Draw(bodyData, extraParams, out reuseable); } public bool AsyncConvertUrl(UUID id, string url, string extraParams) @@ -91,7 +104,11 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender public bool AsyncConvertData(UUID id, string bodyData, string extraParams) { // XXX: This isn't actually being done asynchronously! - m_textureManager.ReturnData(id, ConvertData(bodyData, extraParams)); + bool reuseable; + byte[] data = ConvertData(bodyData, extraParams, out reuseable); + + m_textureManager.ReturnData(id, data, reuseable); + return true; } @@ -162,7 +179,7 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender #endregion - private byte[] Draw(string data, string extraParams) + private byte[] Draw(string data, string extraParams, out bool reuseable) { // We need to cater for old scripts that didnt use extraParams neatly, they use either an integer size which represents both width and height, or setalpha // we will now support multiple comma seperated params in the form width:256,height:512,alpha:255 @@ -343,7 +360,7 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender } } - GDIDraw(data, graph, altDataDelim); + GDIDraw(data, graph, altDataDelim, out reuseable); } byte[] imageJ2000 = new byte[0]; @@ -434,8 +451,21 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender } */ - private void GDIDraw(string data, Graphics graph, char dataDelim) + /// + /// Split input data into discrete command lines. + /// + /// + /// + /// + private string[] GetLines(string data, char dataDelim) { + char[] lineDelimiter = { dataDelim }; + return data.Split(lineDelimiter); + } + + private void GDIDraw(string data, Graphics graph, char dataDelim, out bool reuseable) + { + reuseable = true; Point startPoint = new Point(0, 0); Point endPoint = new Point(0, 0); Pen drawPen = null; @@ -450,11 +480,9 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender myFont = new Font(fontName, fontSize); myBrush = new SolidBrush(Color.Black); - char[] lineDelimiter = {dataDelim}; char[] partsDelimiter = {','}; - string[] lines = data.Split(lineDelimiter); - foreach (string line in lines) + foreach (string line in GetLines(data, dataDelim)) { string nextLine = line.Trim(); //replace with switch, or even better, do some proper parsing @@ -485,6 +513,10 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender } else if (nextLine.StartsWith("Image")) { + // We cannot reuse any generated texture involving fetching an image via HTTP since that image + // can change. + reuseable = false; + float x = 0; float y = 0; GetParams(partsDelimiter, ref nextLine, 5, ref x, ref y); diff --git a/OpenSim/Region/Framework/Interfaces/IDynamicTextureManager.cs b/OpenSim/Region/Framework/Interfaces/IDynamicTextureManager.cs index 773ba737ca..1a3bcbbc71 100644 --- a/OpenSim/Region/Framework/Interfaces/IDynamicTextureManager.cs +++ b/OpenSim/Region/Framework/Interfaces/IDynamicTextureManager.cs @@ -33,7 +33,7 @@ namespace OpenSim.Region.Framework.Interfaces public interface IDynamicTextureManager { void RegisterRender(string handleType, IDynamicTextureRender render); - void ReturnData(UUID id, byte[] data); + void ReturnData(UUID id, byte[] data, bool isReuseable); UUID AddDynamicTextureURL(UUID simID, UUID primID, string contentType, string url, string extraParams, int updateTimer); @@ -113,6 +113,18 @@ namespace OpenSim.Region.Framework.Interfaces string GetName(); string GetContentType(); bool SupportsAsynchronous(); + +// /// +// /// Return true if converting the input body and extra params data will always result in the same byte[] array +// /// +// /// +// /// This method allows the caller to use a previously generated asset if it has one. +// /// +// /// +// /// +// /// +// bool AlwaysIdenticalConversion(string bodyData, string extraParams); + byte[] ConvertUrl(string url, string extraParams); byte[] ConvertData(string bodyData, string extraParams); bool AsyncConvertUrl(UUID id, string url, string extraParams);