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 ---