From 0d85b9976d74a168e4c2fd739468fa71b06b0fe5 Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:08:05 +0700 Subject: [PATCH] Implement parallel route loading, performance stats, and build fixes --- source/LibRender2/BaseRenderer.cs | 2 +- source/LibRender2/Menu/Menu.OptionType.cs | 6 +- .../LibRender2/Menu/MenuEntries/MenuOption.cs | 8 +- source/LibRender2/Textures/TextureManager.cs | 70 +++++++----- source/ObjectViewer/Graphics/NewRendererS.cs | 1 + source/ObjectViewer/Hosts.cs | 5 +- source/ObjectViewer/ProgramS.cs | 7 +- source/ObjectViewer/formOptions.Designer.cs | 23 ++++ source/ObjectViewer/formOptions.cs | 2 + source/OpenBVE/Game/Menu/Menu.SingleMenu.cs | 5 +- source/OpenBVE/Game/RouteInfoOverlay.cs | 21 +++- source/OpenBVE/System/Host.cs | 6 +- source/OpenBVE/System/Interface.cs | 10 +- source/OpenBVE/System/Loading.cs | 10 +- source/OpenBVE/System/Options.cs | 2 + .../UserInterface/formMain.Designer.cs | 14 +++ source/OpenBVE/UserInterface/formMain.cs | 2 + source/OpenBveApi/Interface/LoadingStats.cs | 17 +++ source/OpenBveApi/OpenBveApi.csproj | 1 + .../System/BaseOptions.OptionsKey.cs | 1 + source/OpenBveApi/System/BaseOptions.cs | 2 + .../OpenBveApi/System/Hosts/HostInterface.cs | 28 ++--- .../CsvRwRouteParser.CompatibilityObjects.cs | 7 +- .../Plugins/Route.CsvRw/CsvRwRouteParser.cs | 107 ++++++++++++++++++ source/RouteViewer/LoadingR.cs | 10 ++ source/RouteViewer/NewRendererR.cs | 7 +- source/RouteViewer/System/Host.cs | 6 +- source/RouteViewer/formOptions.Designer.cs | 24 ++++ source/RouteViewer/formOptions.cs | 2 + source/SoundManager/Sounds.cs | 74 ++++++------ 30 files changed, 378 insertions(+), 102 deletions(-) create mode 100644 source/OpenBveApi/Interface/LoadingStats.cs diff --git a/source/LibRender2/BaseRenderer.cs b/source/LibRender2/BaseRenderer.cs index b291bf5d33..aea953767e 100644 --- a/source/LibRender2/BaseRenderer.cs +++ b/source/LibRender2/BaseRenderer.cs @@ -882,7 +882,7 @@ public void Reset() { if (!File.Exists(keys[i].Item1) || File.GetLastWriteTime(keys[i].Item1) != keys[i].Item3) { - currentHost.StaticObjectCache.Remove(keys[i]); + currentHost.StaticObjectCache.TryRemove(keys[i], out _); } } TextureManager.UnloadAllTextures(true); diff --git a/source/LibRender2/Menu/Menu.OptionType.cs b/source/LibRender2/Menu/Menu.OptionType.cs index 6f5d01a112..6c96805527 100644 --- a/source/LibRender2/Menu/Menu.OptionType.cs +++ b/source/LibRender2/Menu/Menu.OptionType.cs @@ -1,4 +1,4 @@ -//Simplified BSD License (BSD-2-Clause) +//Simplified BSD License (BSD-2-Clause) // //Copyright (c) 2024, Maurizo M. Gavioli, The OpenBVE Project // @@ -46,6 +46,8 @@ public enum OptionType /// Sets whether to automatically reload the current objects AutoReloadObjects, /// Sets the shadow quality - ShadowQuality + ShadowQuality, + /// Sets whether to use parallel route loading + ParallelRouteLoading } } diff --git a/source/LibRender2/Menu/MenuEntries/MenuOption.cs b/source/LibRender2/Menu/MenuEntries/MenuOption.cs index 6bb95e63dd..b267983e3e 100644 --- a/source/LibRender2/Menu/MenuEntries/MenuOption.cs +++ b/source/LibRender2/Menu/MenuEntries/MenuOption.cs @@ -1,4 +1,4 @@ -//Simplified BSD License (BSD-2-Clause) +//Simplified BSD License (BSD-2-Clause) // //Copyright (c) 2024, Maurizo M. Gavioli, The OpenBVE Project // @@ -169,6 +169,9 @@ public MenuOption(AbstractMenu menu, OptionType type, string text, object[] entr break; } return; + case OptionType.ParallelRouteLoading: + CurrentlySelectedOption = BaseMenu.CurrentOptions.ParallelRouteLoading ? 0 : 1; + return; } CurrentlySelectedOption = 0; } @@ -316,6 +319,9 @@ public void Flip() } BaseMenu.Renderer.InitializeShadows(); break; + case OptionType.ParallelRouteLoading: + BaseMenu.CurrentOptions.ParallelRouteLoading = !BaseMenu.CurrentOptions.ParallelRouteLoading; + break; } diff --git a/source/LibRender2/Textures/TextureManager.cs b/source/LibRender2/Textures/TextureManager.cs index e9c7ba88dc..64c59c63d8 100644 --- a/source/LibRender2/Textures/TextureManager.cs +++ b/source/LibRender2/Textures/TextureManager.cs @@ -22,6 +22,7 @@ public class TextureManager /// Holds all currently registered textures. public static Texture[] RegisteredTextures; + private static readonly object texturesLock = new object(); /// Holds cached texture origins internal static Dictionary textureCache = new Dictionary(); @@ -106,37 +107,40 @@ public bool RegisterTexture(string path, TextureParameters parameters, out Textu * Check if the texture is already registered. * If so, return the existing handle. * */ - for (int i = 0; i < RegisteredTexturesCount; i++) + lock (texturesLock) { - if (RegisteredTextures[i] != null) + for (int i = 0; i < RegisteredTexturesCount; i++) { - try + if (RegisteredTextures[i] != null) { - //The only exceptions thrown were these when it barfed - PathOrigin source = RegisteredTextures[i].Origin as PathOrigin; - - if (source != null && source.Path.Equals(path, StringComparison.InvariantCultureIgnoreCase) && source.Parameters == parameters) + try { - handle = RegisteredTextures[i]; - return true; + //The only exceptions thrown were these when it barfed + PathOrigin source = RegisteredTextures[i].Origin as PathOrigin; + + if (source != null && source.Path.Equals(path, StringComparison.InvariantCultureIgnoreCase) && source.Parameters == parameters) + { + handle = RegisteredTextures[i]; + return true; + } + } + catch + { + // ignored } } - catch - { - // ignored - } + } + /* + * Register the texture and return the newly created handle. + * */ + int idx = GetNextFreeTexture(); + RegisteredTextures[idx] = new Texture(path, parameters, currentHost); + RegisteredTexturesCount++; + handle = RegisteredTextures[idx]; + return true; } - - /* - * Register the texture and return the newly created handle. - * */ - int idx = GetNextFreeTexture(); - RegisteredTextures[idx] = new Texture(path, parameters, currentHost); - RegisteredTexturesCount++; - handle = RegisteredTextures[idx]; - return true; } /// Registers a texture and returns a handle to the texture. @@ -147,10 +151,13 @@ public Texture RegisterTexture(Texture texture) /* * Register the texture and return the newly created handle. * */ - int idx = GetNextFreeTexture(); - RegisteredTextures[idx] = new Texture(texture); - RegisteredTexturesCount++; - return RegisteredTextures[idx]; + lock (texturesLock) + { + int idx = GetNextFreeTexture(); + RegisteredTextures[idx] = new Texture(texture); + RegisteredTexturesCount++; + return RegisteredTextures[idx]; + } } /// Registers a texture and returns a handle to the texture. @@ -178,10 +185,13 @@ public Texture RegisterTexture(Bitmap bitmap) /* * Register the texture and return the newly created handle. * */ - int idx = GetNextFreeTexture(); - RegisteredTextures[idx] = new Texture(bitmap); - RegisteredTexturesCount++; - return RegisteredTextures[idx]; + lock (texturesLock) + { + int idx = GetNextFreeTexture(); + RegisteredTextures[idx] = new Texture(bitmap); + RegisteredTexturesCount++; + return RegisteredTextures[idx]; + } } diff --git a/source/ObjectViewer/Graphics/NewRendererS.cs b/source/ObjectViewer/Graphics/NewRendererS.cs index bccff7d620..8862bf0f48 100644 --- a/source/ObjectViewer/Graphics/NewRendererS.cs +++ b/source/ObjectViewer/Graphics/NewRendererS.cs @@ -423,6 +423,7 @@ private void RenderOverlays(double timeElapsed) OpenGlString.Draw(Fonts.SmallFont, $"Current frame rate: {FrameRate.ToString("0.0", culture)}fps", new Vector2(4, Screen.Height - 88), TextAlignment.TopLeft, Color128.White, true); OpenGlString.Draw(Fonts.SmallFont, $"Total opaque faces: {opaqueFaces}", new Vector2(4, Screen.Height - 76), TextAlignment.TopLeft, Color128.White, true); OpenGlString.Draw(Fonts.SmallFont, $"Total alpha faces: {alphaFaces}", new Vector2(4, Screen.Height - 64), TextAlignment.TopLeft, Color128.White, true); + OpenGlString.Draw(Fonts.SmallFont, $"Total Loading: {LoadingStats.TotalLoadingTime:0.0} ms", new Vector2(4 * scaleFactor, Screen.Height - 146), TextAlignment.TopLeft, Color128.Yellow, true); } } } diff --git a/source/ObjectViewer/Hosts.cs b/source/ObjectViewer/Hosts.cs index d13a373080..4a1b5d0a4e 100644 --- a/source/ObjectViewer/Hosts.cs +++ b/source/ObjectViewer/Hosts.cs @@ -7,6 +7,7 @@ using OpenBveApi.Hosts; using OpenBveApi.Interface; using OpenBveApi.Math; +using OpenBveApi.Graphics; using OpenBveApi.Objects; using OpenBveApi.Routes; using OpenBveApi.Textures; @@ -286,13 +287,13 @@ public override bool LoadObject(string path, System.Text.Encoding Encoding, out if (Object is StaticObject staticObject) { - StaticObjectCache.Add(ValueTuple.Create(path.ToLowerInvariant(), false, File.GetLastWriteTime(path)), staticObject); + StaticObjectCache.TryAdd(ValueTuple.Create(path.ToLowerInvariant(), false, File.GetLastWriteTime(path)), staticObject); return true; } if (Object is AnimatedObjectCollection aoc) { - AnimatedObjectCollectionCache.Add(path.ToLowerInvariant(), aoc); + AnimatedObjectCollectionCache.TryAdd(path.ToLowerInvariant(), aoc); } return true; diff --git a/source/ObjectViewer/ProgramS.cs b/source/ObjectViewer/ProgramS.cs index b6d73b84c2..f06e153fb4 100644 --- a/source/ObjectViewer/ProgramS.cs +++ b/source/ObjectViewer/ProgramS.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Windows.Forms; @@ -321,6 +322,7 @@ internal static void MouseMovement() internal static void RefreshObjects(bool autoReload = false) { + Stopwatch sw = Stopwatch.StartNew(); LightingRelative = -1.0; // Prune cache to allow actual reloading of modified files @@ -338,7 +340,7 @@ internal static void RefreshObjects(bool autoReload = false) } foreach (var key in staticKeysToRemove) { - CurrentHost.StaticObjectCache.Remove(key); + CurrentHost.StaticObjectCache.TryRemove(key, out _); } CurrentHost.AnimatedObjectCollectionCache.Clear(); // Let TextureManager check for texture changes @@ -456,6 +458,9 @@ internal static void RefreshObjects(bool autoReload = false) Renderer.GameWindow.Title = "Object Viewer"; } LastReloadTime = DateTime.UtcNow; + sw.Stop(); + LoadingStats.TotalLoadingTime = sw.Elapsed.TotalMilliseconds; + Interface.AddMessage(MessageType.Information, false, $"Object(s) loaded in {LoadingStats.TotalLoadingTime:0.0} ms"); UpdateWatchers(); } diff --git a/source/ObjectViewer/formOptions.Designer.cs b/source/ObjectViewer/formOptions.Designer.cs index f1be27bf12..d11779847c 100644 --- a/source/ObjectViewer/formOptions.Designer.cs +++ b/source/ObjectViewer/formOptions.Designer.cs @@ -51,6 +51,8 @@ private void InitializeComponent() this.labelInterpolationMode = new System.Windows.Forms.Label(); this.labelInterpolationSettings = new System.Windows.Forms.Label(); this.InterpolationMode = new System.Windows.Forms.ComboBox(); + this.labelParallel = new System.Windows.Forms.Label(); + this.checkBoxParallel = new System.Windows.Forms.CheckBox(); this.tabPageKeys = new System.Windows.Forms.TabPage(); this.labelControls = new System.Windows.Forms.Label(); this.comboBoxBackwards = new System.Windows.Forms.ComboBox(); @@ -117,6 +119,8 @@ private void InitializeComponent() // // tabPageOptions // + this.tabPageOptions.Controls.Add(this.checkBoxParallel); + this.tabPageOptions.Controls.Add(this.labelParallel); this.tabPageOptions.Controls.Add(this.checkBoxAutoReload); this.tabPageOptions.Controls.Add(this.labelAutoReloadChanged); this.tabPageOptions.Controls.Add(this.comboBoxOptimizeObjects); @@ -785,6 +789,23 @@ private void InitializeComponent() this.checkBoxAutoReload.TabIndex = 49; this.checkBoxAutoReload.UseVisualStyleBackColor = true; // + // labelParallel + // + this.labelParallel.Location = new System.Drawing.Point(10, 352); + this.labelParallel.Name = "labelParallel"; + this.labelParallel.Size = new System.Drawing.Size(131, 28); + this.labelParallel.TabIndex = 50; + this.labelParallel.Text = "Parallel Route Loading:"; + // + // checkBoxParallel + // + this.checkBoxParallel.AutoSize = true; + this.checkBoxParallel.Location = new System.Drawing.Point(265, 356); + this.checkBoxParallel.Name = "checkBoxParallel"; + this.checkBoxParallel.Size = new System.Drawing.Size(15, 14); + this.checkBoxParallel.TabIndex = 51; + this.checkBoxParallel.UseVisualStyleBackColor = true; + // // formOptions // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); @@ -879,5 +900,7 @@ private void InitializeComponent() private System.Windows.Forms.NumericUpDown numericUpDownShadowNormalBias; private System.Windows.Forms.CheckBox checkBoxAutoReload; private System.Windows.Forms.Label labelAutoReloadChanged; + private System.Windows.Forms.Label labelParallel; + private System.Windows.Forms.CheckBox checkBoxParallel; } } diff --git a/source/ObjectViewer/formOptions.cs b/source/ObjectViewer/formOptions.cs index e49b5b3750..d4fe193f89 100644 --- a/source/ObjectViewer/formOptions.cs +++ b/source/ObjectViewer/formOptions.cs @@ -85,6 +85,7 @@ private formOptions() comboBoxBackwards.DataSource = Enum.GetValues(typeof(Key)); comboBoxBackwards.SelectedItem = Interface.CurrentOptions.CameraMoveBackward; checkBoxAutoReload.Checked = Interface.CurrentOptions.AutoReloadObjects; + checkBoxParallel.Checked = Interface.CurrentOptions.ParallelRouteLoading; } private void InitializeSunSliders() @@ -241,6 +242,7 @@ private void CloseButton_Click(object sender, EventArgs e) Interface.CurrentOptions.CameraMoveForward = (Key)comboBoxForwards.SelectedItem; Interface.CurrentOptions.CameraMoveBackward = (Key)comboBoxBackwards.SelectedItem; Interface.CurrentOptions.AutoReloadObjects = checkBoxAutoReload.Checked; + Interface.CurrentOptions.ParallelRouteLoading = checkBoxParallel.Checked; // Saving shadow settings switch (comboBoxShadowResolution.SelectedIndex) diff --git a/source/OpenBVE/Game/Menu/Menu.SingleMenu.cs b/source/OpenBVE/Game/Menu/Menu.SingleMenu.cs index bb21c98535..be8d206085 100644 --- a/source/OpenBVE/Game/Menu/Menu.SingleMenu.cs +++ b/source/OpenBVE/Game/Menu/Menu.SingleMenu.cs @@ -228,7 +228,7 @@ public SingleMenu(AbstractMenu menu, MenuType menuType, int data = 0, double Max Align = TextAlignment.TopLeft; break; case MenuType.Options: - Items = new MenuEntry[11]; + Items = new MenuEntry[12]; Items[0] = new MenuCaption(menu, Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"panel","options"})); Items[1] = new MenuOption(menu, OptionType.ScreenResolution, Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","resolution"}), Program.Renderer.Screen.AvailableResolutions.ToArray()); Items[2] = new MenuOption(menu, OptionType.FullScreen, Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","display_mode_fullscreen"}), new[] { "true", "false" }); @@ -254,7 +254,8 @@ public SingleMenu(AbstractMenu menu, MenuType menuType, int data = 0, double Max Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "shadows_resolution_high" }), Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "shadows_resolution_ultra" }) }); - Items[10] = new MenuCommand(menu, Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"menu","back"}), MenuTag.MenuBack, 0); + Items[10] = new MenuOption(menu, OptionType.ParallelRouteLoading, "Parallel route loading", new[] { "true", "false" }); + Items[11] = new MenuCommand(menu, Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"menu","back"}), MenuTag.MenuBack, 0); Align = TextAlignment.TopLeft; break; case MenuType.RouteList: diff --git a/source/OpenBVE/Game/RouteInfoOverlay.cs b/source/OpenBVE/Game/RouteInfoOverlay.cs index 290c65686b..6ab6aa1154 100644 --- a/source/OpenBVE/Game/RouteInfoOverlay.cs +++ b/source/OpenBVE/Game/RouteInfoOverlay.cs @@ -1,9 +1,10 @@ -using LibRender2; +using LibRender2; using OpenBveApi.Colors; using OpenTK.Graphics.OpenGL; using OpenBveApi.Textures; using OpenBveApi.Interface; using OpenBveApi.Math; +using OpenBveApi.Graphics; namespace OpenBve { @@ -20,6 +21,7 @@ private enum OverlayState None = 0, Map, Gradient, + LoadingStats, NumOf } @@ -97,6 +99,23 @@ public void Show() Program.Renderer.Rectangle.Draw(null, new Vector2(Pos.X, gradientSize.Y / 2), new Vector2(gradientPosWidth, gradientSize.Y / 2), gradientPosBar); break; + case OverlayState.LoadingStats: + double x = 4.0; + double y = 4.0; + double scale = Program.Renderer.ScaleFactor.X; + Program.Renderer.OpenGlString.Draw(Program.Renderer.Fonts.SmallFont, "Loading Performance Stats:", new Vector2(x * scale, y * scale), TextAlignment.TopLeft, Color128.Yellow, true); + y += 20.0; + Program.Renderer.OpenGlString.Draw(Program.Renderer.Fonts.SmallFont, $"Route Parsing Time: {OpenBveApi.Interface.LoadingStats.RouteParseTime:0.0} ms", new Vector2(x * scale, y * scale), TextAlignment.TopLeft, Color128.White, true); + y += 20.0; + Program.Renderer.OpenGlString.Draw(Program.Renderer.Fonts.SmallFont, $"Object Preload Time: {OpenBveApi.Interface.LoadingStats.ObjectPreloadTime:0.0} ms", new Vector2(x * scale, y * scale), TextAlignment.TopLeft, Color128.White, true); + y += 20.0; + Program.Renderer.OpenGlString.Draw(Program.Renderer.Fonts.SmallFont, $"Unique Objects Found: {OpenBveApi.Interface.LoadingStats.ObjectsFound}", new Vector2(x * scale, y * scale), TextAlignment.TopLeft, Color128.White, true); + y += 20.0; + if (OpenBveApi.Interface.LoadingStats.TotalLoadingTime > 0) + { + Program.Renderer.OpenGlString.Draw(Program.Renderer.Fonts.SmallFont, $"Total Loading Time: {OpenBveApi.Interface.LoadingStats.TotalLoadingTime:0.0} ms", new Vector2(x * scale, y * scale), TextAlignment.TopLeft, Color128.White, true); + } + break; } } diff --git a/source/OpenBVE/System/Host.cs b/source/OpenBVE/System/Host.cs index 0d38a37534..8ddc481ef7 100644 --- a/source/OpenBVE/System/Host.cs +++ b/source/OpenBVE/System/Host.cs @@ -356,7 +356,7 @@ public override bool LoadStaticObject(string path, System.Text.Encoding Encoding { staticObject.OptimizeObject(PreserveVertices, Interface.CurrentOptions.ObjectOptimizationBasicThreshold, Interface.CurrentOptions.ObjectOptimizationVertexCulling); Object = staticObject; - StaticObjectCache.Add(ValueTuple.Create(path.ToLowerInvariant(), PreserveVertices, File.GetLastWriteTime(path)), Object); + StaticObjectCache.TryAdd(ValueTuple.Create(path.ToLowerInvariant(), PreserveVertices, File.GetLastWriteTime(path)), Object); return true; } @@ -424,13 +424,13 @@ public override bool LoadObject(string path, System.Text.Encoding Encoding, out if (Object is StaticObject staticObject) { - StaticObjectCache.Add(ValueTuple.Create(path.ToLowerInvariant(), false, File.GetLastWriteTime(path)), staticObject); + StaticObjectCache.TryAdd(ValueTuple.Create(path.ToLowerInvariant(), false, File.GetLastWriteTime(path)), staticObject); return true; } if (Object is AnimatedObjectCollection aoc) { - AnimatedObjectCollectionCache.Add(path.ToLowerInvariant(), aoc); + AnimatedObjectCollectionCache.TryAdd(path.ToLowerInvariant(), aoc); } return true; diff --git a/source/OpenBVE/System/Interface.cs b/source/OpenBVE/System/Interface.cs index 0ca05cb2bc..78af1028e7 100644 --- a/source/OpenBVE/System/Interface.cs +++ b/source/OpenBVE/System/Interface.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using OpenBveApi; using OpenBveApi.Interface; @@ -9,9 +9,11 @@ internal static partial class Interface { internal static void AddMessage(MessageType messageType, bool fileNotFound, string messageText) { if (messageType == MessageType.Warning && !CurrentOptions.ShowWarningMessages) return; if (messageType == MessageType.Error && !CurrentOptions.ShowErrorMessages) return; - LogMessages.Add(new LogMessage(messageType, fileNotFound, messageText)); - Program.FileSystem.AppendToLogFile(messageText); - + lock (LogMessages) + { + LogMessages.Add(new LogMessage(messageType, fileNotFound, messageText)); + Program.FileSystem.AppendToLogFile(messageText); + } } /// Parses a string into OpenBVE's internal time representation (Seconds since midnight on the first day) diff --git a/source/OpenBVE/System/Loading.cs b/source/OpenBVE/System/Loading.cs index 617c34988a..16ebf387a6 100644 --- a/source/OpenBVE/System/Loading.cs +++ b/source/OpenBVE/System/Loading.cs @@ -12,6 +12,7 @@ using OpenBveApi.Runtime; using OpenBveApi.Trains; using OpenBveApi.Routes; +using System.Diagnostics; using RouteManager2; using TrainManager; using TrainManager.Car; @@ -324,7 +325,7 @@ private static void LoadThreaded() { Complete = true; } private static void LoadEverythingThreaded() { - + Stopwatch sw = Stopwatch.StartNew(); string railwayFolder = GetRailwayFolder(CurrentRouteFile); string objectFolder = Path.CombineDirectory(railwayFolder, "Object"); string soundFolder = Path.CombineDirectory(railwayFolder, "Sound"); @@ -504,6 +505,13 @@ private static void LoadEverythingThreaded() { } + sw.Stop(); + LoadingStats.TotalLoadingTime = sw.Elapsed.TotalMilliseconds; + Interface.AddMessage(MessageType.Information, false, "--- Loading Performance Summary ---"); + Interface.AddMessage(MessageType.Information, false, $"Route Parsing: {LoadingStats.RouteParseTime:0.0} ms"); + Interface.AddMessage(MessageType.Information, false, $"Object Preload: {LoadingStats.ObjectPreloadTime:0.0} ms ({LoadingStats.ObjectsFound} unique objects)"); + Interface.AddMessage(MessageType.Information, false, $"Total Loading: {LoadingStats.TotalLoadingTime:0.0} ms"); + Interface.AddMessage(MessageType.Information, false, "-----------------------------------"); } } diff --git a/source/OpenBVE/System/Options.cs b/source/OpenBVE/System/Options.cs index 0761b7a52a..0f88e1a3d1 100644 --- a/source/OpenBVE/System/Options.cs +++ b/source/OpenBVE/System/Options.cs @@ -292,6 +292,7 @@ public override void Save(string fileName) Builder.AppendLine("isUseNewRenderer = " + (IsUseNewRenderer ? "true" : "false")); Builder.AppendLine("forwardsCompatibleContext = " + (ForceForwardsCompatibleContext ? "true" : "false")); Builder.AppendLine("uiscalefactor = " + UserInterfaceScaleFactor); + Builder.AppendLine("parallelrouteloading = " + (ParallelRouteLoading ? "true" : "false")); Builder.AppendLine(); Builder.AppendLine("[quality]"); Builder.AppendLine("interpolation = " + Interpolation); @@ -471,6 +472,7 @@ internal static void LoadOptions() block.TryGetValue(OptionsKey.ViewingDistance, ref Interface.CurrentOptions.ViewingDistance, NumberRange.Positive); block.TryGetValue(OptionsKey.QuadLeafSize, ref Interface.CurrentOptions.QuadTreeLeafSize, NumberRange.Positive); block.TryGetValue(OptionsKey.UIScaleFactor, ref CurrentOptions.UserInterfaceScaleFactor); + block.GetValue(OptionsKey.ParallelRouteLoading, out CurrentOptions.ParallelRouteLoading); break; case OptionsSection.Quality: block.GetEnumValue(OptionsKey.Interpolation, out Interface.CurrentOptions.Interpolation); diff --git a/source/OpenBVE/UserInterface/formMain.Designer.cs b/source/OpenBVE/UserInterface/formMain.Designer.cs index 5c6ee6810e..b8c93bec43 100644 --- a/source/OpenBVE/UserInterface/formMain.Designer.cs +++ b/source/OpenBVE/UserInterface/formMain.Designer.cs @@ -132,6 +132,7 @@ private void InitializeComponent() { this.labelTimeAcceleration = new System.Windows.Forms.Label(); this.updownTimeAccelerationFactor = new System.Windows.Forms.NumericUpDown(); this.checkBoxIsUseNewRenderer = new System.Windows.Forms.CheckBox(); + this.checkBoxParallelRouteLoading = new System.Windows.Forms.CheckBox(); this.checkBoxLoadInAdvance = new System.Windows.Forms.CheckBox(); this.groupBoxPackageOptions = new System.Windows.Forms.GroupBox(); this.buttonMSTSTrainsetDirectory = new System.Windows.Forms.Button(); @@ -1826,6 +1827,7 @@ private void InitializeComponent() { this.groupBoxAdvancedOptions.Controls.Add(this.labelTimeAcceleration); this.groupBoxAdvancedOptions.Controls.Add(this.updownTimeAccelerationFactor); this.groupBoxAdvancedOptions.Controls.Add(this.checkBoxIsUseNewRenderer); + this.groupBoxAdvancedOptions.Controls.Add(this.checkBoxParallelRouteLoading); this.groupBoxAdvancedOptions.Controls.Add(this.checkBoxLoadInAdvance); this.groupBoxAdvancedOptions.ForeColor = System.Drawing.Color.Black; this.groupBoxAdvancedOptions.Location = new System.Drawing.Point(8, 190); @@ -1936,6 +1938,17 @@ private void InitializeComponent() { this.checkBoxIsUseNewRenderer.Text = "Disable OpenGL display lists"; this.checkBoxIsUseNewRenderer.UseVisualStyleBackColor = true; // + // checkBoxParallelRouteLoading + // + this.checkBoxParallelRouteLoading.AutoSize = true; + this.checkBoxParallelRouteLoading.ForeColor = System.Drawing.Color.Black; + this.checkBoxParallelRouteLoading.Location = new System.Drawing.Point(8, 81); + this.checkBoxParallelRouteLoading.Name = "checkBoxParallelRouteLoading"; + this.checkBoxParallelRouteLoading.Size = new System.Drawing.Size(133, 17); + this.checkBoxParallelRouteLoading.TabIndex = 11; + this.checkBoxParallelRouteLoading.Text = "Parallel route loading"; + this.checkBoxParallelRouteLoading.UseVisualStyleBackColor = true; + // // checkBoxLoadInAdvance // this.checkBoxLoadInAdvance.AutoSize = true; @@ -6746,6 +6759,7 @@ private void InitializeComponent() { private System.Windows.Forms.ComboBox comboboxCursor; private System.Windows.Forms.CheckBox checkBoxPanel2Extended; private System.Windows.Forms.CheckBox checkBoxIsUseNewRenderer; + private System.Windows.Forms.CheckBox checkBoxParallelRouteLoading; private System.Windows.Forms.CheckBox checkBoxLoadInAdvance; private System.Windows.Forms.GroupBox groupBoxRailDriver; private System.Windows.Forms.Label labelRailDriverCalibration; diff --git a/source/OpenBVE/UserInterface/formMain.cs b/source/OpenBVE/UserInterface/formMain.cs index 08372424c2..b42572c7f2 100644 --- a/source/OpenBVE/UserInterface/formMain.cs +++ b/source/OpenBVE/UserInterface/formMain.cs @@ -458,6 +458,7 @@ private void formMain_Load(object sender, EventArgs e) checkBoxLoadInAdvance.Checked = Interface.CurrentOptions.LoadInAdvance; checkBoxUnloadTextures.Checked = Interface.CurrentOptions.UnloadUnusedTextures; checkBoxIsUseNewRenderer.Checked = Interface.CurrentOptions.IsUseNewRenderer; + checkBoxParallelRouteLoading.Checked = Interface.CurrentOptions.ParallelRouteLoading; // Shadow Resolution switch (Interface.CurrentOptions.ShadowResolution) { @@ -1179,6 +1180,7 @@ private void formMain_FormClosing() Interface.CurrentOptions.OldTransparencyMode = checkBoxTransparencyFix.Checked; Interface.CurrentOptions.EnableBveTsHacks = checkBoxHacks.Checked; Interface.CurrentOptions.IsUseNewRenderer = checkBoxIsUseNewRenderer.Checked; + Interface.CurrentOptions.ParallelRouteLoading = checkBoxParallelRouteLoading.Checked; // Shadow Resolution switch (comboboxShadowResolution.SelectedIndex) { diff --git a/source/OpenBveApi/Interface/LoadingStats.cs b/source/OpenBveApi/Interface/LoadingStats.cs new file mode 100644 index 0000000000..8d3715c6b0 --- /dev/null +++ b/source/OpenBveApi/Interface/LoadingStats.cs @@ -0,0 +1,17 @@ +namespace OpenBveApi.Interface +{ + /// Provides statistics about the loading process. + public static class LoadingStats + { + /// The time taken to parse the route file in milliseconds. + public static double RouteParseTime; + /// The time taken to preload objects in milliseconds. + public static double ObjectPreloadTime; + /// The number of unique objects found during preloading. + public static int ObjectsFound; + /// The number of objects actually loaded. + public static int ObjectsLoaded; + /// The total time taken for the loading process in milliseconds. + public static double TotalLoadingTime; + } +} diff --git a/source/OpenBveApi/OpenBveApi.csproj b/source/OpenBveApi/OpenBveApi.csproj index 12a06fcffc..5bc4d5e569 100644 --- a/source/OpenBveApi/OpenBveApi.csproj +++ b/source/OpenBveApi/OpenBveApi.csproj @@ -121,6 +121,7 @@ + diff --git a/source/OpenBveApi/System/BaseOptions.OptionsKey.cs b/source/OpenBveApi/System/BaseOptions.OptionsKey.cs index 67a8371c50..322ca0a5c0 100644 --- a/source/OpenBveApi/System/BaseOptions.OptionsKey.cs +++ b/source/OpenBveApi/System/BaseOptions.OptionsKey.cs @@ -35,6 +35,7 @@ public enum OptionsKey QuadLeafSize, UIScaleFactor, AutoReloadObjects, + ParallelRouteLoading, // Quality Interpolation, AnisotropicFilteringLevel, diff --git a/source/OpenBveApi/System/BaseOptions.cs b/source/OpenBveApi/System/BaseOptions.cs index a6c0e56cf3..2db067ffa3 100644 --- a/source/OpenBveApi/System/BaseOptions.cs +++ b/source/OpenBveApi/System/BaseOptions.cs @@ -134,6 +134,8 @@ public abstract class BaseOptions public int UserInterfaceScaleFactor; /// Whether loaded objects are automatically reloaded on change public bool AutoReloadObjects; + /// Whether to use multi-threaded object preloading when loading a route + public bool ParallelRouteLoading = true; /// Saves the options to the specified filename /// The filename to save the options to diff --git a/source/OpenBveApi/System/Hosts/HostInterface.cs b/source/OpenBveApi/System/Hosts/HostInterface.cs index e4f3ba2aab..1799b19a45 100644 --- a/source/OpenBveApi/System/Hosts/HostInterface.cs +++ b/source/OpenBveApi/System/Hosts/HostInterface.cs @@ -13,6 +13,7 @@ using OpenBveApi.Textures; using OpenBveApi.Trains; using OpenBveApi.World; +using System.Collections.Concurrent; using SoundHandle = OpenBveApi.Sounds.SoundHandle; namespace OpenBveApi.Hosts { @@ -123,11 +124,11 @@ private static string DetectUnixKernel() protected HostInterface(HostApplication host) { Application = host; - StaticObjectCache = new Dictionary, StaticObject>(); - AnimatedObjectCollectionCache = new Dictionary(); - MissingFiles = new List(); - FailedObjects = new List(); - FailedTextures = new List(); + StaticObjectCache = new ConcurrentDictionary, StaticObject>(); + AnimatedObjectCollectionCache = new ConcurrentDictionary(); + MissingFiles = new ConcurrentBag(); + FailedObjects = new ConcurrentBag(); + FailedTextures = new ConcurrentBag(); if (Platform == HostPlatform.GNULinux) { @@ -154,18 +155,17 @@ public virtual void ReportProblem(ProblemType type, string text) /// Clears the error log public void ClearErrors() { - MissingFiles.Clear(); - FailedObjects.Clear(); - FailedTextures.Clear(); - + while (MissingFiles.TryTake(out _)) ; + while (FailedObjects.TryTake(out _)) ; + while (FailedTextures.TryTake(out _)) ; } /// Contains a list of missing files encountered - public readonly List MissingFiles; + public readonly ConcurrentBag MissingFiles; /// Contains a list of objects which failed to load - public readonly List FailedObjects; + public readonly ConcurrentBag FailedObjects; /// Contains a list of textures which failed to load - public readonly List FailedTextures; + public readonly ConcurrentBag FailedTextures; /// Queries the dimensions of a texture. /// The path to the file or folder that contains the texture. @@ -604,13 +604,13 @@ public virtual void UpdateCustomTimetable(Texture Daytime, Texture Nighttime) /// /// Dictionary of StaticObject with Path and PreserveVertices as keys. /// - public readonly Dictionary, StaticObject> StaticObjectCache; + public readonly ConcurrentDictionary, StaticObject> StaticObjectCache; /// /// Dictionary of AnimatedObjectCollection with Path as key. /// - public readonly Dictionary AnimatedObjectCollectionCache; + public readonly ConcurrentDictionary AnimatedObjectCollectionCache; /// Adds a marker texture to the host application's display /// The texture to add diff --git a/source/Plugins/Route.CsvRw/CsvRwRouteParser.CompatibilityObjects.cs b/source/Plugins/Route.CsvRw/CsvRwRouteParser.CompatibilityObjects.cs index 3562c56d98..b239a96618 100644 --- a/source/Plugins/Route.CsvRw/CsvRwRouteParser.CompatibilityObjects.cs +++ b/source/Plugins/Route.CsvRw/CsvRwRouteParser.CompatibilityObjects.cs @@ -1,4 +1,4 @@ -using Formats.OpenBve; +using Formats.OpenBve; using Formats.OpenBve.XML; using OpenBveApi; using OpenBveApi.Interface; @@ -6,6 +6,7 @@ using System; using System.ComponentModel.Design; using System.Linq; +using System.Threading; using System.Xml; namespace CsvRwRouteParser @@ -158,7 +159,7 @@ internal static bool LocateObject(ref string fileName, string objectPath) { Plugin.CurrentHost.AddMessage(MessageType.Warning, false, CompatibilityObjects.AvailableReplacements[i].Message); } - CompatibilityObjectsUsed++; + Interlocked.Increment(ref CompatibilityObjectsUsed); return true; } } @@ -249,7 +250,7 @@ internal static bool LocateSound(ref string fileName, string objectPath) { Plugin.CurrentHost.AddMessage(MessageType.Warning, false, CompatibilityObjects.AvailableSounds[i].Message); } - CompatibilityObjectsUsed++; + Interlocked.Increment(ref CompatibilityObjectsUsed); return true; } } diff --git a/source/Plugins/Route.CsvRw/CsvRwRouteParser.cs b/source/Plugins/Route.CsvRw/CsvRwRouteParser.cs index ffa4f86a76..aa5b56ee7e 100644 --- a/source/Plugins/Route.CsvRw/CsvRwRouteParser.cs +++ b/source/Plugins/Route.CsvRw/CsvRwRouteParser.cs @@ -11,6 +11,10 @@ using RouteManager2.Climate; using RouteManager2.SignalManager; using RouteManager2.Stations; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Globalization; namespace CsvRwRouteParser { internal partial class Parser { @@ -138,6 +142,7 @@ internal void ParseRoute(string fileName, System.Text.Encoding Encoding, string } private void ParseRouteForData(string FileName, System.Text.Encoding Encoding, ref RouteData Data, bool PreviewOnly) { + Stopwatch sw = Stopwatch.StartNew(); //Read the entire routefile into memory List Lines = System.IO.File.ReadAllLines(FileName, Encoding).ToList(); PreprocessSplitIntoExpressions(FileName, Lines, out Expression[] Expressions, true); @@ -148,8 +153,12 @@ private void ParseRouteForData(string FileName, System.Text.Encoding Encoding, r Data.UnitOfSpeed = 0.277777777777778; PreprocessOptions(Expressions, ref Data, ref UnitOfLength, PreviewOnly); PreprocessSortByTrackPosition(UnitOfLength, ref Expressions); + if (Plugin.Cancel) return; + PreloadObjects(Expressions); ParseRouteForData(FileName, Encoding, Expressions, UnitOfLength, ref Data, PreviewOnly); CurrentRoute.UnitOfLength = UnitOfLength; + sw.Stop(); + LoadingStats.RouteParseTime = sw.Elapsed.TotalMilliseconds; } private int freeObjCount; @@ -538,5 +547,103 @@ private void ParseRouteForData(string FileName, System.Text.Encoding Encoding, E } } } + + private void PreloadObjects(Expression[] expressions) + { + if (Plugin.Cancel) + { + return; + } + Stopwatch sw = Stopwatch.StartNew(); + ConcurrentDictionary objectsToLoad = new ConcurrentDictionary(); + ParallelOptions parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Plugin.CurrentOptions.ParallelRouteLoading ? -1 : 1 }; + + Parallel.ForEach(expressions, parallelOptions, (expression) => + { + if (Plugin.Cancel || string.IsNullOrEmpty(expression.Text)) + { + return; + } + + string text = expression.Text; + // Quick check to skip clearly irrelevant lines + // Most object/sound commands contain a dot or start with a known namespace + if (text.IndexOf('.') == -1 && + text.IndexOf("Structure", StringComparison.OrdinalIgnoreCase) == -1 && + text.IndexOf("Signal", StringComparison.OrdinalIgnoreCase) == -1 && + text.IndexOf("Background", StringComparison.OrdinalIgnoreCase) == -1) + { + return; + } + + string command, argumentSequence; + expression.SeparateCommandsAndArguments(out command, out argumentSequence, Culture, false, IsRW, ""); + if (command == null) + { + return; + } + + string cmdLower = command.ToLowerInvariant(); + bool isFileCommand = false; + + // Structure namespace + if (cmdLower.StartsWith("structure.", StringComparison.Ordinal)) + { + if (cmdLower.EndsWith(".load", StringComparison.Ordinal) || + cmdLower.Contains(".rail") || + cmdLower.Contains(".freeobj") || + cmdLower.Contains(".ground") || + cmdLower.Contains(".wall") || + cmdLower.Contains(".dike") || + cmdLower.Contains(".form") || + cmdLower.Contains(".roof") || + cmdLower.Contains(".crack")) + { + isFileCommand = true; + } + } + // Signal and Background namespaces + else if (cmdLower.Equals("signal", StringComparison.Ordinal) || + cmdLower.Equals("background", StringComparison.Ordinal) || + cmdLower.StartsWith("background.", StringComparison.Ordinal)) + { + isFileCommand = true; + } + + if (isFileCommand) + { + string[] args = SplitArguments(argumentSequence); + if (args.Length > 0 && !string.IsNullOrEmpty(args[0])) + { + string f = args[0]; + if (LocateObject(ref f, ObjectPath)) + { + objectsToLoad.TryAdd(f, (byte)0); + } + } + } + }); + + if (Plugin.Cancel) + { + return; + } + + // Now load them in parallel + // This will populate the Host's cache thread-safely + Parallel.ForEach(objectsToLoad.Keys, parallelOptions, (file) => + { + if (Plugin.Cancel) + { + return; + } + // UnifiedObject handles both static and animated objects + UnifiedObject obj; + Plugin.CurrentHost.LoadObject(file, System.Text.Encoding.UTF8, out obj); + }); + sw.Stop(); + LoadingStats.ObjectPreloadTime = sw.Elapsed.TotalMilliseconds; + LoadingStats.ObjectsFound = objectsToLoad.Count; + } } } diff --git a/source/RouteViewer/LoadingR.cs b/source/RouteViewer/LoadingR.cs index ed29a0d785..760b3c98c1 100644 --- a/source/RouteViewer/LoadingR.cs +++ b/source/RouteViewer/LoadingR.cs @@ -6,6 +6,7 @@ // ╚═════════════════════════════════════════════════════════════╝ using System; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -16,6 +17,7 @@ using OpenBveApi.Routes; using OpenBveApi.Runtime; using OpenBveApi.Textures; +using OpenBveApi.Interface; using RouteManager2; namespace RouteViewer { @@ -180,6 +182,7 @@ internal static void LoadAsynchronously(string RouteFile, Encoding RouteEncoding } private static void LoadEverythingThreaded() { + Stopwatch sw = Stopwatch.StartNew(); string RailwayFolder = GetRailwayFolder(CurrentRouteFile); string ObjectFolder = Path.CombineDirectory(RailwayFolder, "Object"); string SoundFolder = Path.CombineDirectory(RailwayFolder, "Sound"); @@ -244,6 +247,13 @@ private static void LoadEverythingThreaded() { Program.Renderer.CameraTrackFollower.UpdateAbsolute(FirstStationPosition, true, false); 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); + sw.Stop(); + OpenBveApi.Interface.LoadingStats.TotalLoadingTime = sw.Elapsed.TotalMilliseconds; + Interface.AddMessage(MessageType.Information, false, "--- Loading Performance Summary ---"); + Interface.AddMessage(MessageType.Information, false, $"Route Parsing: {OpenBveApi.Interface.LoadingStats.RouteParseTime:0.0} ms"); + Interface.AddMessage(MessageType.Information, false, $"Object Preload: {OpenBveApi.Interface.LoadingStats.ObjectPreloadTime:0.0} ms ({OpenBveApi.Interface.LoadingStats.ObjectsFound} unique objects)"); + Interface.AddMessage(MessageType.Information, false, $"Total Loading: {OpenBveApi.Interface.LoadingStats.TotalLoadingTime:0.0} ms"); + Interface.AddMessage(MessageType.Information, false, "-----------------------------------"); Complete = true; } diff --git a/source/RouteViewer/NewRendererR.cs b/source/RouteViewer/NewRendererR.cs index d1d888a460..a58bc21579 100644 --- a/source/RouteViewer/NewRendererR.cs +++ b/source/RouteViewer/NewRendererR.cs @@ -790,7 +790,12 @@ private void RenderOverlays(double timeElapsed) OpenGlString.Draw(Fonts.SmallFont, $"Total opaque faces: {VisibleObjects.OpaqueFaces.Count}", new Vector2(4, Screen.Height - 76), TextAlignment.TopLeft, Color128.White, true); OpenGlString.Draw(Fonts.SmallFont, $"Total alpha faces: {VisibleObjects.AlphaFaces.Count}", new Vector2(4, Screen.Height - 64), TextAlignment.TopLeft, Color128.White, true); } - + double y = Screen.Height - 150; + OpenGlString.Draw(Fonts.SmallFont, $"Route Parsing: {LoadingStats.RouteParseTime:0.0} ms", new Vector2(4 * scaleFactor, y), TextAlignment.TopLeft, Color128.Yellow, true); + y -= 12; + OpenGlString.Draw(Fonts.SmallFont, $"Obj Preloading: {LoadingStats.ObjectPreloadTime:0.0} ms ({LoadingStats.ObjectsFound} unique)", new Vector2(4 * scaleFactor, y), TextAlignment.TopLeft, Color128.Yellow, true); + y -= 12; + OpenGlString.Draw(Fonts.SmallFont, $"Total Loading: {LoadingStats.TotalLoadingTime:0.0} ms", new Vector2(4 * scaleFactor, y), TextAlignment.TopLeft, Color128.Yellow, true); } } } diff --git a/source/RouteViewer/System/Host.cs b/source/RouteViewer/System/Host.cs index 5f75b6f937..ab87bee900 100644 --- a/source/RouteViewer/System/Host.cs +++ b/source/RouteViewer/System/Host.cs @@ -346,7 +346,7 @@ public override bool LoadStaticObject(string path, System.Text.Encoding Encoding { staticObject.OptimizeObject(PreserveVertices, Interface.CurrentOptions.ObjectOptimizationBasicThreshold, true); Object = staticObject; - StaticObjectCache.Add(ValueTuple.Create(path.ToLowerInvariant(), PreserveVertices, File.GetLastWriteTime(path)), Object); + StaticObjectCache.TryAdd(ValueTuple.Create(path.ToLowerInvariant(), PreserveVertices, File.GetLastWriteTime(path)), Object); return true; } @@ -420,13 +420,13 @@ public override bool LoadObject(string path, System.Text.Encoding Encoding, out if (Object is StaticObject staticObject) { - StaticObjectCache.Add(ValueTuple.Create(path.ToLowerInvariant(), false, File.GetLastWriteTime(path)), staticObject); + StaticObjectCache.TryAdd(ValueTuple.Create(path.ToLowerInvariant(), false, File.GetLastWriteTime(path)), staticObject); return true; } if (Object is AnimatedObjectCollection aoc) { - AnimatedObjectCollectionCache.Add(path.ToLowerInvariant(), aoc); + AnimatedObjectCollectionCache.TryAdd(path.ToLowerInvariant(), aoc); } return true; diff --git a/source/RouteViewer/formOptions.Designer.cs b/source/RouteViewer/formOptions.Designer.cs index 9f174a0a2c..257b24e690 100644 --- a/source/RouteViewer/formOptions.Designer.cs +++ b/source/RouteViewer/formOptions.Designer.cs @@ -60,6 +60,8 @@ private void InitializeComponent() this.comboBoxNewXParser = new System.Windows.Forms.ComboBox(); this.label15 = new System.Windows.Forms.Label(); this.numericUpDownViewingDistance = new System.Windows.Forms.NumericUpDown(); + this.labelParallel = new System.Windows.Forms.Label(); + this.checkBoxParallel = new System.Windows.Forms.CheckBox(); this.labelShadowResolution = new System.Windows.Forms.Label(); this.comboBoxShadowResolution = new System.Windows.Forms.ComboBox(); this.labelShadowDistance = new System.Windows.Forms.Label(); @@ -133,6 +135,8 @@ private void InitializeComponent() this.tabPageOptions.Controls.Add(this.comboBoxNewXParser); this.tabPageOptions.Controls.Add(this.label14); this.tabPageOptions.Controls.Add(this.comboBoxNewObjParser); + this.tabPageOptions.Controls.Add(this.labelParallel); + this.tabPageOptions.Controls.Add(this.checkBoxParallel); this.tabPageOptions.Controls.Add(this.label15); this.tabPageOptions.Controls.Add(this.numericUpDownViewingDistance); this.tabPageOptions.AutoScroll = true; @@ -489,6 +493,24 @@ private void InitializeComponent() 0, 0}); // + // labelParallel + // + this.labelParallel.AutoSize = true; + this.labelParallel.Location = new System.Drawing.Point(6, 417); + this.labelParallel.Name = "labelParallel"; + this.labelParallel.Size = new System.Drawing.Size(120, 13); + this.labelParallel.TabIndex = 36; + this.labelParallel.Text = "Parallel Route Loading:"; + // + // checkBoxParallel + // + this.checkBoxParallel.AutoSize = true; + this.checkBoxParallel.Location = new System.Drawing.Point(248, 417); + this.checkBoxParallel.Name = "checkBoxParallel"; + this.checkBoxParallel.Size = new System.Drawing.Size(15, 14); + this.checkBoxParallel.TabIndex = 35; + this.checkBoxParallel.UseVisualStyleBackColor = true; + // // comboBoxShadowResolution // this.comboBoxShadowResolution.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; @@ -767,6 +789,8 @@ private void InitializeComponent() private System.Windows.Forms.ComboBox comboBoxNewXParser; private System.Windows.Forms.Label label15; private System.Windows.Forms.NumericUpDown numericUpDownViewingDistance; + private System.Windows.Forms.Label labelParallel; + private System.Windows.Forms.CheckBox checkBoxParallel; private System.Windows.Forms.Label labelShadowResolution; private System.Windows.Forms.ComboBox comboBoxShadowResolution; private System.Windows.Forms.Label labelShadowDistance; diff --git a/source/RouteViewer/formOptions.cs b/source/RouteViewer/formOptions.cs index f7da8fa35e..0cfce0c728 100644 --- a/source/RouteViewer/formOptions.cs +++ b/source/RouteViewer/formOptions.cs @@ -29,6 +29,7 @@ public FormOptions() comboBoxNewXParser.SelectedIndex = (int) Interface.CurrentOptions.CurrentXParser; comboBoxNewObjParser.SelectedIndex = (int) Interface.CurrentOptions.CurrentObjParser; numericUpDownViewingDistance.Value = Interface.CurrentOptions.ViewingDistance; + checkBoxParallel.Checked = Interface.CurrentOptions.ParallelRouteLoading; // Shadows switch (Interface.CurrentOptions.ShadowResolution) @@ -232,6 +233,7 @@ private void button1_Click(object sender, EventArgs e) Interface.CurrentOptions.LoadingLogo = checkBoxLogo.Checked; Interface.CurrentOptions.LoadingBackground = checkBoxBackgrounds.Checked; Interface.CurrentOptions.LoadingProgressBar = checkBoxProgressBar.Checked; + Interface.CurrentOptions.ParallelRouteLoading = checkBoxParallel.Checked; XParsers xParser = (XParsers)comboBoxNewXParser.SelectedIndex; ObjParsers objParser = (ObjParsers)comboBoxNewObjParser.SelectedIndex; diff --git a/source/SoundManager/Sounds.cs b/source/SoundManager/Sounds.cs index da197f7978..87b64ab0a2 100644 --- a/source/SoundManager/Sounds.cs +++ b/source/SoundManager/Sounds.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -31,6 +31,8 @@ public abstract partial class SoundsBase /// The number of sound buffers. private int BufferCount; + private readonly object buffersLock = new object(); + /// A list of all sound sources. protected internal static SoundSource[] Sources = new SoundSource[16]; @@ -240,33 +242,36 @@ public SoundBuffer RegisterBuffer(string path, double radius) { return null; } - for (int i = 0; i < BufferCount; i++) + lock (buffersLock) { - if (!(Buffers[i].Origin is PathOrigin)) + for (int i = 0; i < BufferCount; i++) { - continue; - } + if (!(Buffers[i].Origin is PathOrigin)) + { + continue; + } - if (((PathOrigin)Buffers[i].Origin).Path == path) + if (((PathOrigin)Buffers[i].Origin).Path == path) + { + return Buffers[i]; + } + } + if (Buffers.Length == BufferCount) { - return Buffers[i]; + Array.Resize(ref Buffers, Buffers.Length << 1); } - } - if (Buffers.Length == BufferCount) - { - Array.Resize(ref Buffers, Buffers.Length << 1); - } - try - { - Buffers[BufferCount] = new SoundBuffer(CurrentHost, path, radius); - } - catch - { - return null; + try + { + Buffers[BufferCount] = new SoundBuffer(CurrentHost, path, radius); + } + catch + { + return null; + } + BufferCount++; + return Buffers[BufferCount - 1]; } - BufferCount++; - return Buffers[BufferCount - 1]; } /// Registers a sound buffer and returns a handle to the buffer. @@ -275,21 +280,24 @@ public SoundBuffer RegisterBuffer(string path, double radius) /// The handle to the sound buffer. public SoundBuffer RegisterBuffer(Sound data, double radius) { - if (Buffers.Length == BufferCount) + lock (buffersLock) { - Array.Resize(ref Buffers, Buffers.Length << 1); - } + if (Buffers.Length == BufferCount) + { + Array.Resize(ref Buffers, Buffers.Length << 1); + } - try - { - Buffers[BufferCount] = new SoundBuffer(data, radius); - } - catch - { - return null; + try + { + Buffers[BufferCount] = new SoundBuffer(data, radius); + } + catch + { + return null; + } + BufferCount++; + return Buffers[BufferCount - 1]; } - BufferCount++; - return Buffers[BufferCount - 1]; } // --- loading buffers ---