diff --git a/source/LibRender2/Textures/TextureManager.cs b/source/LibRender2/Textures/TextureManager.cs index a7eb1518a..c6c507f02 100644 --- a/source/LibRender2/Textures/TextureManager.cs +++ b/source/LibRender2/Textures/TextureManager.cs @@ -231,6 +231,8 @@ public bool LoadTexture(ref Texture handle, OpenGlTextureWrapMode wrap, int curr } //Set last access time + bool compress = renderer.currentOptions.CompressTextures; + if (texture != null) { handle = texture; @@ -256,6 +258,38 @@ public bool LoadTexture(ref Texture handle, OpenGlTextureWrapMode wrap, int curr } //if (texture.BitsPerPixel == 32) { + if (compress) + { + double savedFactor = 0; + switch (texture.PixelFormat) + { + case PixelFormat.Grayscale: + savedFactor = 0.5; + break; + case PixelFormat.GrayscaleAlpha: + savedFactor = 1.0; + break; + case PixelFormat.RGB: + savedFactor = 2.5; + break; + case PixelFormat.RGBAlpha: + savedFactor = 3.0; + break; + } + bool alreadyTracked = false; + for (int w = 0; w < 4; w++) + { + if (handle.OpenGlTextures[w].Valid) + { + alreadyTracked = true; + break; + } + } + if (!alreadyTracked) + { + Texture.TotalVramSavedBytes += (long)(texture.Width * texture.Height * savedFactor * (texture.MultipleFrames ? texture.TotalFrames : 1)); + } + } int[] names = new int[1]; GL.GenTextures(1, names); GL.BindTexture(TextureTarget.Texture2D, names[0]); @@ -342,7 +376,7 @@ public bool LoadTexture(ref Texture handle, OpenGlTextureWrapMode wrap, int curr // n.b. Make sure to set the unpack alignment as otherwise we corrupt textures where stride > width GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1); GL.TexImage2D(TextureTarget.Texture2D, 0, - noLuminanceChannel ? PixelInternalFormat.R8 : PixelInternalFormat.Luminance, + noLuminanceChannel ? (compress ? PixelInternalFormat.CompressedRed : PixelInternalFormat.R8) : (compress ? PixelInternalFormat.CompressedLuminance : PixelInternalFormat.Luminance), texture.Width, texture.Height, 0, noLuminanceChannel ? OpenTK.Graphics.OpenGL.PixelFormat.Red : OpenTK.Graphics.OpenGL.PixelFormat.Luminance, PixelType.UnsignedByte, texture.Bytes); @@ -358,7 +392,7 @@ public bool LoadTexture(ref Texture handle, OpenGlTextureWrapMode wrap, int curr // n.b. Make sure to set the unpack alignment as otherwise we corrupt textures where stride > width GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1); GL.TexImage2D(TextureTarget.Texture2D, 0, - PixelInternalFormat.Rgb8, + compress ? PixelInternalFormat.CompressedRgb : PixelInternalFormat.Rgb8, texture.Width, texture.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Rgb, PixelType.UnsignedByte, texture.Bytes); @@ -386,7 +420,7 @@ public bool LoadTexture(ref Texture handle, OpenGlTextureWrapMode wrap, int curr // n.b. Must reset the unpack alignment in case of changes GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4); GL.TexImage2D(TextureTarget.Texture2D, 0, - PixelInternalFormat.Rgb8, + compress ? PixelInternalFormat.CompressedRgb : PixelInternalFormat.Rgb8, texture.Width, texture.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Rgb, PixelType.UnsignedByte, newBytes); @@ -420,7 +454,7 @@ public bool LoadTexture(ref Texture handle, OpenGlTextureWrapMode wrap, int curr j += stride - 3 * texture.Width; GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4); GL.TexImage2D(TextureTarget.Texture2D, 0, - PixelInternalFormat.Rgba8, + compress ? PixelInternalFormat.CompressedRgba : PixelInternalFormat.Rgba8, texture.Width, texture.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Rgba, PixelType.UnsignedByte, newBytes); @@ -430,7 +464,7 @@ public bool LoadTexture(ref Texture handle, OpenGlTextureWrapMode wrap, int curr { GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1); GL.TexImage2D(TextureTarget.Texture2D, 0, - PixelInternalFormat.LuminanceAlpha, + compress ? PixelInternalFormat.CompressedLuminanceAlpha : PixelInternalFormat.LuminanceAlpha, texture.Width, texture.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.LuminanceAlpha, PixelType.UnsignedByte, texture.Bytes); @@ -444,7 +478,7 @@ public bool LoadTexture(ref Texture handle, OpenGlTextureWrapMode wrap, int curr // n.b. Must reset the unpack alignment in case of changes GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4); GL.TexImage2D(TextureTarget.Texture2D, 0, - PixelInternalFormat.Rgba8, + compress ? PixelInternalFormat.CompressedRgba : PixelInternalFormat.Rgba8, texture.Width, texture.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Rgba, PixelType.UnsignedByte, texture.Bytes); diff --git a/source/ObjectViewer/Hosts.cs b/source/ObjectViewer/Hosts.cs index 283c25bb9..8c6ba2700 100644 --- a/source/ObjectViewer/Hosts.cs +++ b/source/ObjectViewer/Hosts.cs @@ -113,6 +113,10 @@ public override bool LoadTexture(string path, TextureParameters parameters, out if (Program.CurrentHost.Plugins[i].Texture.LoadTexture(path, out texture)) { texture.CompatibleTransparencyMode = false; texture = texture.ApplyParameters(parameters); + if (Interface.CurrentOptions.MaxTextureSize > 0 && !path.StartsWith(Program.FileSystem.DataFolder, StringComparison.OrdinalIgnoreCase)) + { + texture.Downscale(Interface.CurrentOptions.MaxTextureSize); + } return true; } if(!FailedTextures.Contains(path)) diff --git a/source/ObjectViewer/Options.cs b/source/ObjectViewer/Options.cs index 3162aed6b..bd44c86cc 100644 --- a/source/ObjectViewer/Options.cs +++ b/source/ObjectViewer/Options.cs @@ -87,6 +87,8 @@ public override void Save(string fileName) Builder.AppendLine("anisotropicfilteringlevel = " + AnisotropicFilteringLevel.ToString(Culture)); Builder.AppendLine("antialiasinglevel = " + AntiAliasingLevel.ToString(Culture)); Builder.AppendLine("transparencyMode = " + ((int)TransparencyMode).ToString(Culture)); + Builder.AppendLine("compresstextures = " + (CompressTextures ? "true" : "false")); + Builder.AppendLine("maxtexturesize = " + MaxTextureSize.ToString(Culture)); Builder.AppendLine("shadowresolution = " + (int)ShadowResolution); Builder.AppendLine("shadowdrawdistance = " + ShadowDrawDistance); Builder.AppendLine("shadowcascades = " + (int)ShadowCascades); @@ -180,6 +182,8 @@ internal static void LoadOptions() block.TryGetValue(OptionsKey.AnisotropicFilteringLevel, ref Interface.CurrentOptions.AnisotropicFilteringLevel); block.TryGetValue(OptionsKey.AntiAliasingLevel, ref Interface.CurrentOptions.AntiAliasingLevel); block.GetEnumValue(OptionsKey.TransparencyMode, out Interface.CurrentOptions.TransparencyMode); + block.GetValue(OptionsKey.CompressTextures, out Interface.CurrentOptions.CompressTextures); + block.TryGetValue(OptionsKey.MaxTextureSize, ref Interface.CurrentOptions.MaxTextureSize, NumberRange.NonNegative); block.TryGetEnumValue(OptionsKey.ShadowResolution, ref Interface.CurrentOptions.ShadowResolution); block.TryGetEnumValue(OptionsKey.ShadowDrawDistance, ref Interface.CurrentOptions.ShadowDrawDistance); block.TryGetEnumValue(OptionsKey.ShadowCascades, ref Interface.CurrentOptions.ShadowCascades); diff --git a/source/ObjectViewer/ProgramS.cs b/source/ObjectViewer/ProgramS.cs index 9d53fb7cc..ed46e6fb7 100644 --- a/source/ObjectViewer/ProgramS.cs +++ b/source/ObjectViewer/ProgramS.cs @@ -320,6 +320,8 @@ internal static void MouseMovement() internal static void RefreshObjects(bool autoReload = false) { LightingRelative = -1.0; + OpenBveApi.Textures.Texture.TotalRamSavedBytes = 0; + OpenBveApi.Textures.Texture.TotalVramSavedBytes = 0; // Prune cache to allow actual reloading of modified files var staticKeysToRemove = new List>(); @@ -455,6 +457,11 @@ internal static void RefreshObjects(bool autoReload = false) } LastReloadTime = DateTime.UtcNow; UpdateWatchers(); + if (OpenBveApi.Textures.Texture.TotalRamSavedBytes > 0 || OpenBveApi.Textures.Texture.TotalVramSavedBytes > 0) + { + Console.WriteLine($"[Memory Optimization] RAM Saved from resizing: {OpenBveApi.Textures.Texture.TotalRamSavedBytes / 1024.0 / 1024.0:F2} MB"); + Console.WriteLine($"[Memory Optimization] Estimated VRAM Saved from compression: {OpenBveApi.Textures.Texture.TotalVramSavedBytes / 1024.0 / 1024.0:F2} MB"); + } } /// Checks if any of the loaded files have been updated externally. diff --git a/source/OpenBVE/System/Host.cs b/source/OpenBVE/System/Host.cs index 8903de456..27b08d2fd 100644 --- a/source/OpenBVE/System/Host.cs +++ b/source/OpenBVE/System/Host.cs @@ -160,6 +160,10 @@ public override bool LoadTexture(string path, TextureParameters parameters, out texture.CompatibleTransparencyMode = oldTransparencyMode; texture = texture.ApplyParameters(parameters); + if (Interface.CurrentOptions.MaxTextureSize > 0 && !path.StartsWith(Program.FileSystem.DataFolder, StringComparison.OrdinalIgnoreCase)) + { + texture.Downscale(Interface.CurrentOptions.MaxTextureSize); + } return true; } if (!FailedTextures.Contains(path)) diff --git a/source/OpenBVE/System/Loading.cs b/source/OpenBVE/System/Loading.cs index 2874f6528..53e1e12bc 100644 --- a/source/OpenBVE/System/Loading.cs +++ b/source/OpenBVE/System/Loading.cs @@ -68,6 +68,8 @@ internal static void LoadAsynchronously(string routeFile, Encoding routeEncoding // members Cancel = false; Complete = false; + OpenBveApi.Textures.Texture.TotalRamSavedBytes = 0; + OpenBveApi.Textures.Texture.TotalVramSavedBytes = 0; CurrentRouteFile = routeFile; CurrentRouteEncoding = routeEncoding; CurrentTrainFolder = trainFolder; @@ -239,6 +241,11 @@ private static void LoadThreaded() { Thread.Sleep(10); } Complete = true; + if (OpenBveApi.Textures.Texture.TotalRamSavedBytes > 0 || OpenBveApi.Textures.Texture.TotalVramSavedBytes > 0) + { + Console.WriteLine($"[Memory Optimization] RAM Saved from resizing: {OpenBveApi.Textures.Texture.TotalRamSavedBytes / 1024.0 / 1024.0:F2} MB"); + Console.WriteLine($"[Memory Optimization] Estimated VRAM Saved from compression: {OpenBveApi.Textures.Texture.TotalVramSavedBytes / 1024.0 / 1024.0:F2} MB"); + } } private static void LoadEverythingThreaded() { diff --git a/source/OpenBVE/System/Options.cs b/source/OpenBVE/System/Options.cs index db2bd96ce..618fe884a 100644 --- a/source/OpenBVE/System/Options.cs +++ b/source/OpenBVE/System/Options.cs @@ -311,8 +311,9 @@ public override void Save(string fileName) Builder.AppendLine("anisotropicFilteringLevel = " + AnisotropicFilteringLevel.ToString(Culture)); Builder.AppendLine("anisotropicFilteringMaximum = " + AnisotropicFilteringMaximum.ToString(Culture)); Builder.AppendLine("antiAliasingLevel = " + AntiAliasingLevel.ToString(Culture)); - Builder.AppendLine("transparencyMode = " + ((int)TransparencyMode).ToString(Culture)); Builder.AppendLine("oldtransparencymode = " + (OldTransparencyMode ? "true" : "false")); + Builder.AppendLine("compresstextures = " + (CompressTextures ? "true" : "false")); + Builder.AppendLine("maxtexturesize = " + MaxTextureSize.ToString(Culture)); Builder.AppendLine("viewingDistance = " + ViewingDistance.ToString(Culture)); Builder.AppendLine("nearclipscenery = " + NearClipScenery.ToString(Culture)); Builder.AppendLine("nearclipcab = " + NearClipCab.ToString(Culture)); @@ -517,6 +518,8 @@ internal static void LoadOptions() block.TryGetValue(OptionsKey.AntiAliasingLevel, ref Interface.CurrentOptions.AntiAliasingLevel); block.GetEnumValue(OptionsKey.TransparencyMode, out Interface.CurrentOptions.TransparencyMode); block.GetValue(OptionsKey.OldTransparencyMode, out CurrentOptions.OldTransparencyMode); + block.GetValue(OptionsKey.CompressTextures, out CurrentOptions.CompressTextures); + block.TryGetValue(OptionsKey.MaxTextureSize, ref CurrentOptions.MaxTextureSize, NumberRange.NonNegative); block.TryGetValue(OptionsKey.ViewingDistance, ref Interface.CurrentOptions.ViewingDistance, NumberRange.Positive); block.TryGetValue(OptionsKey.QuadLeafSize, ref Interface.CurrentOptions.QuadTreeLeafSize, NumberRange.Positive); block.TryGetValue(OptionsKey.NearClipScenery, ref Interface.CurrentOptions.NearClipScenery, NumberRange.Positive); diff --git a/source/OpenBveApi/System/BaseOptions.OptionsKey.cs b/source/OpenBveApi/System/BaseOptions.OptionsKey.cs index 9d93fac26..51c599e9d 100644 --- a/source/OpenBveApi/System/BaseOptions.OptionsKey.cs +++ b/source/OpenBveApi/System/BaseOptions.OptionsKey.cs @@ -50,6 +50,8 @@ public enum OptionsKey OldTransparencyMode, MotionBlur, FPSLimit, + CompressTextures, + MaxTextureSize, ShadowResolution, ShadowDrawDistance, ShadowCascades, diff --git a/source/OpenBveApi/System/BaseOptions.cs b/source/OpenBveApi/System/BaseOptions.cs index 135f66022..6af702658 100644 --- a/source/OpenBveApi/System/BaseOptions.cs +++ b/source/OpenBveApi/System/BaseOptions.cs @@ -137,6 +137,10 @@ public abstract class BaseOptions public int UserInterfaceScaleFactor; /// Whether loaded objects are automatically reloaded on change public bool AutoReloadObjects; + /// Whether to use texture compression to save VRAM + public bool CompressTextures; + /// The maximum texture size allowed, or 0 for unlimited + public int MaxTextureSize; /// The near clipping plane for scenery public double NearClipScenery = 0.5; diff --git a/source/OpenBveApi/Textures/Textures.cs b/source/OpenBveApi/Textures/Textures.cs index 6e029c0f5..4738aeaab 100644 --- a/source/OpenBveApi/Textures/Textures.cs +++ b/source/OpenBveApi/Textures/Textures.cs @@ -434,5 +434,83 @@ public void InvertLightness() } } } + + /// The total number of RAM bytes saved by downscaling textures. + public static long TotalRamSavedBytes = 0; + /// The total number of VRAM bytes saved by compressing textures. + public static long TotalVramSavedBytes = 0; + + /// Downscales the texture to fit within the specified maximum size, keeping aspect ratio. + public void Downscale(int maxTextureSize) + { + if (Ignore || MyBytes == null || Size.X <= maxTextureSize && Size.Y <= maxTextureSize) + { + return; + } + int oldWidth = (int)Size.X; + int oldHeight = (int)Size.Y; + int newWidth = oldWidth; + int newHeight = oldHeight; + if (newWidth > newHeight) + { + newHeight = (int)System.Math.Round((double)newHeight * maxTextureSize / newWidth); + newWidth = maxTextureSize; + } + else + { + newWidth = (int)System.Math.Round((double)newWidth * maxTextureSize / newHeight); + newHeight = maxTextureSize; + } + if (newWidth < 1) newWidth = 1; + if (newHeight < 1) newHeight = 1; + + int bpp = (int)PixelFormat; + for (int frame = 0; frame < MyBytes.Length; frame++) + { + byte[] oldBytes = MyBytes[frame]; + if (oldBytes == null || oldBytes.Length != oldWidth * oldHeight * bpp) + { + continue; + } + byte[] newBytes = new byte[newWidth * newHeight * bpp]; + + for (int y = 0; y < newHeight; y++) + { + float tempY = (float)y * oldHeight / newHeight; + int yFloor = (int)System.Math.Floor(tempY); + int yCeil = System.Math.Min(oldHeight - 1, yFloor + 1); + float dy = tempY - yFloor; + + for (int x = 0; x < newWidth; x++) + { + float tempX = (float)x * oldWidth / newWidth; + int yFloor2 = (int)System.Math.Floor(tempX); + int xCeil = System.Math.Min(oldWidth - 1, yFloor2 + 1); + float dx = tempX - yFloor2; + + for (int c = 0; c < bpp; c++) + { + int idx00 = (yFloor * oldWidth + yFloor2) * bpp + c; + int idx10 = (yFloor * oldWidth + xCeil) * bpp + c; + int idx01 = (yCeil * oldWidth + yFloor2) * bpp + c; + int idx11 = (yCeil * oldWidth + xCeil) * bpp + c; + + float val = oldBytes[idx00] * (1 - dx) * (1 - dy) + + oldBytes[idx10] * dx * (1 - dy) + + oldBytes[idx01] * (1 - dx) * dy + + oldBytes[idx11] * dx * dy; + + newBytes[(y * newWidth + x) * bpp + c] = (byte)System.Math.Min(255, System.Math.Max(0, val)); + } + } + } + MyBytes[frame] = newBytes; + } + long bytesBefore = (long)oldWidth * oldHeight * bpp * MyBytes.Length; + long bytesAfter = (long)newWidth * newHeight * bpp * MyBytes.Length; + TotalRamSavedBytes += (bytesBefore - bytesAfter); + Size.X = newWidth; + Size.Y = newHeight; + } } } diff --git a/source/RouteViewer/LoadingR.cs b/source/RouteViewer/LoadingR.cs index f79ba18d3..0358af984 100644 --- a/source/RouteViewer/LoadingR.cs +++ b/source/RouteViewer/LoadingR.cs @@ -55,6 +55,8 @@ internal static void Load(string routeFile, Encoding routeEncoding, byte[] textu Program.Renderer.GameWindow.TargetRenderFrequency = 0; // reset Game.Reset(); + OpenBveApi.Textures.Texture.TotalRamSavedBytes = 0; + OpenBveApi.Textures.Texture.TotalVramSavedBytes = 0; Program.Renderer.Loading.InitLoading(Program.FileSystem.GetDataFolder("In-game"), typeof(NewRenderer).Assembly.GetName().Version.ToString(), Interface.CurrentOptions.LoadingLogo, Interface.CurrentOptions.LoadingProgressBar); if (textureBytes != null && textureBytes.Length > 0) { @@ -167,6 +169,11 @@ private static void LoadEverythingThreaded() { Program.Renderer.Camera.Alignment = new CameraAlignment(new Vector3(0.0, 2.5, 0.0), 0.0, 0.0, 0.0, FirstStationPosition, 1.0); World.UpdateAbsoluteCamera(0.0); Complete = true; + if (Texture.TotalRamSavedBytes > 0 || Texture.TotalVramSavedBytes > 0) + { + Console.WriteLine($"[Memory Optimization] RAM Saved from resizing: {Texture.TotalRamSavedBytes / 1024.0 / 1024.0:F2} MB"); + Console.WriteLine($"[Memory Optimization] Estimated VRAM Saved from compression: {Texture.TotalVramSavedBytes / 1024.0 / 1024.0:F2} MB"); + } } } diff --git a/source/RouteViewer/Options.cs b/source/RouteViewer/Options.cs index defcfd3b4..9de1aa49a 100644 --- a/source/RouteViewer/Options.cs +++ b/source/RouteViewer/Options.cs @@ -45,6 +45,8 @@ public override void Save(string fileName) Builder.AppendLine("anisotropicfilteringlevel = " + AnisotropicFilteringLevel.ToString(Culture)); Builder.AppendLine("antialiasinglevel = " + AntiAliasingLevel.ToString(Culture)); Builder.AppendLine("transparencyMode = " + ((int)TransparencyMode).ToString(Culture)); + Builder.AppendLine("compresstextures = " + (CompressTextures ? "true" : "false")); + Builder.AppendLine("maxtexturesize = " + MaxTextureSize.ToString(Culture)); Builder.AppendLine("viewingdistance = " + ViewingDistance); Builder.AppendLine("nearclipbase = " + NearClipBase.ToString(Culture)); Builder.AppendLine("quadleafsize = " + QuadTreeLeafSize); @@ -129,6 +131,8 @@ internal static void LoadOptions() block.TryGetValue(OptionsKey.AnisotropicFilteringLevel, ref Interface.CurrentOptions.AnisotropicFilteringLevel); block.TryGetValue(OptionsKey.AntiAliasingLevel, ref Interface.CurrentOptions.AntiAliasingLevel); block.GetEnumValue(OptionsKey.TransparencyMode, out Interface.CurrentOptions.TransparencyMode); + block.GetValue(OptionsKey.CompressTextures, out Interface.CurrentOptions.CompressTextures); + block.TryGetValue(OptionsKey.MaxTextureSize, ref Interface.CurrentOptions.MaxTextureSize, NumberRange.NonNegative); block.TryGetValue(OptionsKey.ViewingDistance, ref Interface.CurrentOptions.ViewingDistance, NumberRange.Positive); block.TryGetValue(OptionsKey.QuadLeafSize, ref Interface.CurrentOptions.QuadTreeLeafSize, NumberRange.Positive); block.TryGetValue(OptionsKey.NearClipBase, ref Interface.CurrentOptions.NearClipBase, NumberRange.Positive); diff --git a/source/RouteViewer/System/Host.cs b/source/RouteViewer/System/Host.cs index cd0f3a920..f94ab9a96 100644 --- a/source/RouteViewer/System/Host.cs +++ b/source/RouteViewer/System/Host.cs @@ -140,6 +140,10 @@ public override bool LoadTexture(string path, TextureParameters parameters, out { texture.CompatibleTransparencyMode = false; texture = texture.ApplyParameters(parameters); + if (Interface.CurrentOptions.MaxTextureSize > 0 && !path.StartsWith(Program.FileSystem.DataFolder, StringComparison.OrdinalIgnoreCase)) + { + texture.Downscale(Interface.CurrentOptions.MaxTextureSize); + } return true; } if (!FailedTextures.Contains(path))