If reusing dynamic textures, do not reuse small data length textures that fall below current viewer discard level 2 thresholds.

Viewer LL 3.3.4 and before sometimes fail to properly redisplay dynamic textures that have a small data length compared to pixel size when pulled from cache.
This appears to happen when the data length is smaller than the estimate discard level 2 size the viewer uses when making this GetTexture request.
This commit works around this by always regenerating dynamic textures that fall below this threshold rather than reusing them if ReuseDynamicTextures = true
This can be controlled by the [Textures] ReuseDynamicLowDataTextures config setting which defaults to false.
integration
Justin Clark-Casey (justincc) 2012-09-06 22:12:05 +01:00
parent 9f914327c6
commit 8f02fd926e
8 changed files with 188 additions and 46 deletions

View File

@ -42,7 +42,7 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture
{ {
public class DynamicTextureModule : IRegionModule, IDynamicTextureManager public class DynamicTextureModule : IRegionModule, IDynamicTextureManager
{ {
// private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private const int ALL_SIDES = -1; private const int ALL_SIDES = -1;
@ -54,6 +54,17 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture
/// </summary> /// </summary>
public bool ReuseTextures { get; set; } public bool ReuseTextures { get; set; }
/// <summary>
/// If false, then textures which have a low data size are not reused when ReuseTextures = true.
/// </summary>
/// <remarks>
/// LL viewers 3.3.4 and before appear to not fully render textures pulled from the viewer cache if those
/// textures have a relatively high pixel surface but a small data size. Typically, this appears to happen
/// if the data size is smaller than the viewer's discard level 2 size estimate. So if this is setting is
/// false, textures smaller than the calculation in IsSizeReuseable are always regenerated rather than reused
/// to work around this problem.</remarks>
public bool ReuseLowDataTextures { get; set; }
private Dictionary<UUID, Scene> RegisteredScenes = new Dictionary<UUID, Scene>(); private Dictionary<UUID, Scene> RegisteredScenes = new Dictionary<UUID, Scene>();
private Dictionary<string, IDynamicTextureRender> RenderPlugins = private Dictionary<string, IDynamicTextureRender> RenderPlugins =
@ -83,18 +94,17 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture
/// <summary> /// <summary>
/// Called by code which actually renders the dynamic texture to supply texture data. /// Called by code which actually renders the dynamic texture to supply texture data.
/// </summary> /// </summary>
/// <param name="id"></param> /// <param name="updaterId"></param>
/// <param name="data"></param> /// <param name="texture"></param>
/// <param name="isReuseable">True if the data generated can be reused for subsequent identical requests</param> public void ReturnData(UUID updaterId, IDynamicTexture texture)
public void ReturnData(UUID id, byte[] data, bool isReuseable)
{ {
DynamicTextureUpdater updater = null; DynamicTextureUpdater updater = null;
lock (Updaters) lock (Updaters)
{ {
if (Updaters.ContainsKey(id)) if (Updaters.ContainsKey(updaterId))
{ {
updater = Updaters[id]; updater = Updaters[updaterId];
} }
} }
@ -103,11 +113,16 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture
if (RegisteredScenes.ContainsKey(updater.SimUUID)) if (RegisteredScenes.ContainsKey(updater.SimUUID))
{ {
Scene scene = RegisteredScenes[updater.SimUUID]; Scene scene = RegisteredScenes[updater.SimUUID];
UUID newTextureID = updater.DataReceived(data, scene); UUID newTextureID = updater.DataReceived(texture.Data, scene);
if (ReuseTextures && isReuseable && !updater.BlendWithOldTexture) if (ReuseTextures
&& !updater.BlendWithOldTexture
&& texture.IsReuseable
&& (ReuseLowDataTextures || IsDataSizeReuseable(texture)))
{
m_reuseableDynamicTextures.Store( m_reuseableDynamicTextures.Store(
GenerateReusableTextureKey(updater.BodyData, updater.Params), newTextureID); GenerateReusableTextureKey(texture.InputCommands, texture.InputParams), newTextureID);
}
} }
} }
@ -123,6 +138,27 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture
} }
} }
/// <summary>
/// Determines whether the texture is reuseable based on its data size.
/// </summary>
/// <remarks>
/// This is a workaround for a viewer bug where very small data size textures relative to their pixel size
/// are not redisplayed properly when pulled from cache. The calculation here is based on the typical discard
/// level of 2, a 'rate' of 0.125 and 4 components (which makes for a factor of 0.5).
/// </remarks>
/// <returns></returns>
private bool IsDataSizeReuseable(IDynamicTexture texture)
{
// Console.WriteLine("{0} {1}", texture.Size.Width, texture.Size.Height);
int discardLevel2DataThreshold = (int)Math.Ceiling((texture.Size.Width >> 2) * (texture.Size.Height >> 2) * 0.5);
// m_log.DebugFormat(
// "[DYNAMIC TEXTURE MODULE]: Discard level 2 threshold {0}, texture data length {1}",
// discardLevel2DataThreshold, texture.Data.Length);
return discardLevel2DataThreshold < texture.Data.Length;
}
public UUID AddDynamicTextureURL(UUID simID, UUID primID, string contentType, string url, public UUID AddDynamicTextureURL(UUID simID, UUID primID, string contentType, string url,
string extraParams, int updateTimer) string extraParams, int updateTimer)
{ {
@ -293,7 +329,10 @@ namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture
{ {
IConfig texturesConfig = config.Configs["Textures"]; IConfig texturesConfig = config.Configs["Textures"];
if (texturesConfig != null) if (texturesConfig != null)
{
ReuseTextures = texturesConfig.GetBoolean("ReuseDynamicTextures", false); ReuseTextures = texturesConfig.GetBoolean("ReuseDynamicTextures", false);
ReuseLowDataTextures = texturesConfig.GetBoolean("ReuseDynamicLowDataTextures", false);
}
if (!RegisteredScenes.ContainsKey(scene.RegionInfo.RegionID)) if (!RegisteredScenes.ContainsKey(scene.RegionInfo.RegionID))
{ {

View File

@ -32,6 +32,7 @@ using System.Net;
using Nini.Config; using Nini.Config;
using OpenMetaverse; using OpenMetaverse;
using OpenMetaverse.Imaging; using OpenMetaverse.Imaging;
using OpenSim.Region.CoreModules.Scripting.DynamicTexture;
using OpenSim.Region.Framework.Interfaces; using OpenSim.Region.Framework.Interfaces;
using OpenSim.Region.Framework.Scenes; using OpenSim.Region.Framework.Scenes;
using log4net; using log4net;
@ -73,12 +74,12 @@ namespace OpenSim.Region.CoreModules.Scripting.LoadImageURL
// return false; // return false;
// } // }
public byte[] ConvertUrl(string url, string extraParams) public IDynamicTexture ConvertUrl(string url, string extraParams)
{ {
return null; return null;
} }
public byte[] ConvertData(string bodyData, string extraParams) public IDynamicTexture ConvertData(string bodyData, string extraParams)
{ {
return null; return null;
} }
@ -171,11 +172,11 @@ namespace OpenSim.Region.CoreModules.Scripting.LoadImageURL
private void HttpRequestReturn(IAsyncResult result) private void HttpRequestReturn(IAsyncResult result)
{ {
RequestState state = (RequestState) result.AsyncState; RequestState state = (RequestState) result.AsyncState;
WebRequest request = (WebRequest) state.Request; WebRequest request = (WebRequest) state.Request;
Stream stream = null; Stream stream = null;
byte[] imageJ2000 = new byte[0]; byte[] imageJ2000 = new byte[0];
Size newSize = new Size(0, 0);
try try
{ {
@ -188,37 +189,43 @@ namespace OpenSim.Region.CoreModules.Scripting.LoadImageURL
try try
{ {
Bitmap image = new Bitmap(stream); Bitmap image = new Bitmap(stream);
Size newsize;
// TODO: make this a bit less hard coded // TODO: make this a bit less hard coded
if ((image.Height < 64) && (image.Width < 64)) if ((image.Height < 64) && (image.Width < 64))
{ {
newsize = new Size(32, 32); newSize.Width = 32;
newSize.Height = 32;
} }
else if ((image.Height < 128) && (image.Width < 128)) else if ((image.Height < 128) && (image.Width < 128))
{ {
newsize = new Size(64, 64); newSize.Width = 64;
newSize.Height = 64;
} }
else if ((image.Height < 256) && (image.Width < 256)) else if ((image.Height < 256) && (image.Width < 256))
{ {
newsize = new Size(128, 128); newSize.Width = 128;
newSize.Height = 128;
} }
else if ((image.Height < 512 && image.Width < 512)) else if ((image.Height < 512 && image.Width < 512))
{ {
newsize = new Size(256, 256); newSize.Width = 256;
newSize.Height = 256;
} }
else if ((image.Height < 1024 && image.Width < 1024)) else if ((image.Height < 1024 && image.Width < 1024))
{ {
newsize = new Size(512, 512); newSize.Width = 512;
newSize.Height = 512;
} }
else else
{ {
newsize = new Size(1024, 1024); newSize.Width = 1024;
newSize.Height = 1024;
} }
Bitmap resize = new Bitmap(image, newsize); using (Bitmap resize = new Bitmap(image, newSize))
{
imageJ2000 = OpenJPEG.EncodeFromImage(resize, true); imageJ2000 = OpenJPEG.EncodeFromImage(resize, true);
}
} }
catch (Exception) catch (Exception)
{ {
@ -233,7 +240,6 @@ namespace OpenSim.Region.CoreModules.Scripting.LoadImageURL
} }
catch (WebException) catch (WebException)
{ {
} }
finally finally
{ {
@ -243,10 +249,13 @@ namespace OpenSim.Region.CoreModules.Scripting.LoadImageURL
} }
} }
m_log.DebugFormat("[LOADIMAGEURLMODULE] Returning {0} bytes of image data for request {1}", m_log.DebugFormat("[LOADIMAGEURLMODULE]: Returning {0} bytes of image data for request {1}",
imageJ2000.Length, state.RequestID); imageJ2000.Length, state.RequestID);
m_textureManager.ReturnData(state.RequestID, imageJ2000, false); m_textureManager.ReturnData(
state.RequestID,
new OpenSim.Region.CoreModules.Scripting.DynamicTexture.DynamicTexture(
request.RequestUri, null, imageJ2000, newSize, false));
} }
#region Nested type: RequestState #region Nested type: RequestState

View File

@ -57,6 +57,7 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender.Tests
m_dtm = new DynamicTextureModule(); m_dtm = new DynamicTextureModule();
m_dtm.ReuseTextures = reuseTextures; m_dtm.ReuseTextures = reuseTextures;
// m_dtm.ReuseLowDataTextures = reuseTextures;
m_vrm = new VectorRenderModule(); m_vrm = new VectorRenderModule();
@ -201,6 +202,7 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender.Tests
public void TestRepeatSameDrawReusingTexture() public void TestRepeatSameDrawReusingTexture()
{ {
TestHelpers.InMethod(); TestHelpers.InMethod();
// TestHelpers.EnableLogging();
string dtText = "PenColour BLACK; MoveTo 40,220; FontSize 32; Text Hello World;"; string dtText = "PenColour BLACK; MoveTo 40,220; FontSize 32; Text Hello World;";
@ -228,6 +230,46 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender.Tests
Assert.That(firstDynamicTextureID, Is.EqualTo(so.RootPart.Shape.Textures.GetFace(0).TextureID)); Assert.That(firstDynamicTextureID, Is.EqualTo(so.RootPart.Shape.Textures.GetFace(0).TextureID));
} }
/// <summary>
/// Test a low data dynamically generated texture such that it is treated as a low data texture that causes
/// problems for current viewers.
/// </summary>
/// <remarks>
/// As we do not set DynamicTextureModule.ReuseLowDataTextures = true in this test, it should not reuse the
/// texture
/// </remarks>
[Test]
public void TestRepeatSameDrawLowDataTexture()
{
TestHelpers.InMethod();
// TestHelpers.EnableLogging();
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,
"1024",
0);
UUID firstDynamicTextureID = so.RootPart.Shape.Textures.GetFace(0).TextureID;
m_dtm.AddDynamicTextureData(
m_scene.RegionInfo.RegionID,
so.UUID,
m_vrm.GetContentType(),
dtText,
"1024",
0);
Assert.That(firstDynamicTextureID, Is.Not.EqualTo(so.RootPart.Shape.Textures.GetFace(0).TextureID));
}
[Test] [Test]
public void TestRepeatSameDrawDifferentExtraParamsReusingTexture() public void TestRepeatSameDrawDifferentExtraParamsReusingTexture()
{ {

View File

@ -35,6 +35,7 @@ using System.Net;
using Nini.Config; using Nini.Config;
using OpenMetaverse; using OpenMetaverse;
using OpenMetaverse.Imaging; using OpenMetaverse.Imaging;
using OpenSim.Region.CoreModules.Scripting.DynamicTexture;
using OpenSim.Region.Framework.Interfaces; using OpenSim.Region.Framework.Interfaces;
using OpenSim.Region.Framework.Scenes; using OpenSim.Region.Framework.Scenes;
using log4net; using log4net;
@ -85,20 +86,14 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender
// return lines.Any((str, r) => str.StartsWith("Image")); // return lines.Any((str, r) => str.StartsWith("Image"));
// } // }
public byte[] ConvertUrl(string url, string extraParams) public IDynamicTexture ConvertUrl(string url, string extraParams)
{ {
return null; return null;
} }
public byte[] ConvertData(string bodyData, string extraParams) public IDynamicTexture ConvertData(string bodyData, string extraParams)
{ {
bool reuseable; return Draw(bodyData, extraParams);
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) public bool AsyncConvertUrl(UUID id, string url, string extraParams)
@ -109,10 +104,7 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender
public bool AsyncConvertData(UUID id, string bodyData, string extraParams) public bool AsyncConvertData(UUID id, string bodyData, string extraParams)
{ {
// XXX: This isn't actually being done asynchronously! // XXX: This isn't actually being done asynchronously!
bool reuseable; m_textureManager.ReturnData(id, ConvertData(bodyData, extraParams));
byte[] data = ConvertData(bodyData, extraParams, out reuseable);
m_textureManager.ReturnData(id, data, reuseable);
return true; return true;
} }
@ -191,7 +183,7 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender
#endregion #endregion
private byte[] Draw(string data, string extraParams, out bool reuseable) private IDynamicTexture Draw(string data, string extraParams)
{ {
// 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 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 // we will now support multiple comma seperated params in the form width:256,height:512,alpha:255
@ -334,6 +326,7 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender
Bitmap bitmap = null; Bitmap bitmap = null;
Graphics graph = null; Graphics graph = null;
bool reuseable = false;
try try
{ {
@ -396,7 +389,8 @@ namespace OpenSim.Region.CoreModules.Scripting.VectorRender
e.Message, e.StackTrace); e.Message, e.StackTrace);
} }
return imageJ2000; return new OpenSim.Region.CoreModules.Scripting.DynamicTexture.DynamicTexture(
data, extraParams, imageJ2000, new Size(width, height), reuseable);
} }
finally finally
{ {

View File

@ -43,7 +43,7 @@ using OpenSim.Tests.Common;
namespace OpenSim.Region.CoreModules.ServiceConnectorsOut.Grid.Tests namespace OpenSim.Region.CoreModules.ServiceConnectorsOut.Grid.Tests
{ {
[TestFixture] [TestFixture]
public class GridConnectorsTests public class GridConnectorsTests : OpenSimTestCase
{ {
LocalGridServicesConnector m_LocalConnector; LocalGridServicesConnector m_LocalConnector;
private void SetUp() private void SetUp()

View File

@ -51,7 +51,7 @@ using RegionSettings = OpenSim.Framework.RegionSettings;
namespace OpenSim.Region.CoreModules.World.Archiver.Tests namespace OpenSim.Region.CoreModules.World.Archiver.Tests
{ {
[TestFixture] [TestFixture]
public class ArchiverTests public class ArchiverTests : OpenSimTestCase
{ {
private Guid m_lastRequestId; private Guid m_lastRequestId;
private string m_lastErrorMessage; private string m_lastErrorMessage;

View File

@ -25,6 +25,8 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
using System;
using System.Drawing;
using System.IO; using System.IO;
using OpenMetaverse; using OpenMetaverse;
@ -33,7 +35,14 @@ namespace OpenSim.Region.Framework.Interfaces
public interface IDynamicTextureManager public interface IDynamicTextureManager
{ {
void RegisterRender(string handleType, IDynamicTextureRender render); void RegisterRender(string handleType, IDynamicTextureRender render);
void ReturnData(UUID id, byte[] data, bool isReuseable);
/// <summary>
/// Used by IDynamicTextureRender implementations to return renders
/// </summary>
/// <param name='id'></param>
/// <param name='data'></param>
/// <param name='isReuseable'></param>
void ReturnData(UUID id, IDynamicTexture texture);
UUID AddDynamicTextureURL(UUID simID, UUID primID, string contentType, string url, string extraParams, UUID AddDynamicTextureURL(UUID simID, UUID primID, string contentType, string url, string extraParams,
int updateTimer); int updateTimer);
@ -125,11 +134,53 @@ namespace OpenSim.Region.Framework.Interfaces
// /// <param name='extraParams'></param> // /// <param name='extraParams'></param>
// bool AlwaysIdenticalConversion(string bodyData, string extraParams); // bool AlwaysIdenticalConversion(string bodyData, string extraParams);
byte[] ConvertUrl(string url, string extraParams); IDynamicTexture ConvertUrl(string url, string extraParams);
byte[] ConvertData(string bodyData, string extraParams); IDynamicTexture ConvertData(string bodyData, string extraParams);
bool AsyncConvertUrl(UUID id, string url, string extraParams); bool AsyncConvertUrl(UUID id, string url, string extraParams);
bool AsyncConvertData(UUID id, string bodyData, string extraParams); bool AsyncConvertData(UUID id, string bodyData, string extraParams);
void GetDrawStringSize(string text, string fontName, int fontSize, void GetDrawStringSize(string text, string fontName, int fontSize,
out double xSize, out double ySize); out double xSize, out double ySize);
} }
public interface IDynamicTexture
{
/// <summary>
/// Input commands used to generate this data.
/// </summary>
/// <remarks>
/// Null if input commands were not used.
/// </remarks>
string InputCommands { get; }
/// <summary>
/// Uri used to generate this data.
/// </summary>
/// <remarks>
/// Null if a uri was not used.
/// </remarks>
Uri InputUri { get; }
/// <summary>
/// Extra input params used to generate this data.
/// </summary>
string InputParams { get; }
/// <summary>
/// Texture data.
/// </summary>
byte[] Data { get; }
/// <summary>
/// Size of texture.
/// </summary>
Size Size { get; }
/// <summary>
/// Signal whether the texture is reuseable (i.e. whether the same input data will always generate the same
/// texture).
/// </summary>
bool IsReuseable { get; }
}
} }

View File

@ -703,6 +703,13 @@
; Default is false. ; Default is false.
ReuseDynamicTextures = false ReuseDynamicTextures = false
; If true, then textures generated dynamically that have a low data size relative to their pixel size are not reused
; This is to workaround an apparent LL 3.3.4 and earlier viewer bug where such textures are not redisplayed properly when pulled from the viewer cache.
; Only set this to true if you are sure that all the viewers using your simulator will not suffer from this problem.
; This setting only has an affect is ReuseDynamicTextures = true
; Default is false
ReuseDynamicLowDataTextures = false
[ODEPhysicsSettings] [ODEPhysicsSettings]
; ## ; ##