diff --git a/source/LibRender2/BaseRenderer.cs b/source/LibRender2/BaseRenderer.cs
index 8f9a404c7..5365d4b39 100644
--- a/source/LibRender2/BaseRenderer.cs
+++ b/source/LibRender2/BaseRenderer.cs
@@ -633,7 +633,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/Objects/ObjectLibrary.cs b/source/LibRender2/Objects/ObjectLibrary.cs
index 5a8471bb8..989cdfb1e 100644
--- a/source/LibRender2/Objects/ObjectLibrary.cs
+++ b/source/LibRender2/Objects/ObjectLibrary.cs
@@ -165,11 +165,17 @@ public void ShowObject(ObjectState State, ObjectType Type)
TextureOrigin daytimeOrigin = State.Prototype.Mesh.Materials[face.Material].DaytimeTexture.Origin;
if (!TextureManager.textureCache.TryGetValue(daytimeOrigin, out daytimeTexture))
{
- Stopwatch sw = Stopwatch.StartNew();
- daytimeOrigin.GetTexture(out daytimeTexture);
- sw.Stop();
- TextureManager.TextureDecodeTime += sw.ElapsedMilliseconds;
- TextureManager.textureCache[daytimeOrigin] = daytimeTexture;
+ lock (BaseRenderer.GdiPlusLock)
+ {
+ if (!TextureManager.textureCache.TryGetValue(daytimeOrigin, out daytimeTexture))
+ {
+ Stopwatch sw = Stopwatch.StartNew();
+ daytimeOrigin.GetTexture(out daytimeTexture);
+ sw.Stop();
+ TextureManager.TextureDecodeTime += sw.ElapsedMilliseconds;
+ TextureManager.textureCache.TryAdd(daytimeOrigin, daytimeTexture);
+ }
+ }
}
TextureTransparencyType transparencyType = TextureTransparencyType.Opaque;
@@ -195,11 +201,17 @@ public void ShowObject(ObjectState State, ObjectType Type)
TextureOrigin nighttimeOrigin = State.Prototype.Mesh.Materials[face.Material].NighttimeTexture.Origin;
if (!TextureManager.textureCache.TryGetValue(nighttimeOrigin, out nighttimeTexture))
{
- Stopwatch sw = Stopwatch.StartNew();
- nighttimeOrigin.GetTexture(out nighttimeTexture);
- sw.Stop();
- TextureManager.TextureDecodeTime += sw.ElapsedMilliseconds;
- TextureManager.textureCache[nighttimeOrigin] = nighttimeTexture;
+ lock (BaseRenderer.GdiPlusLock)
+ {
+ if (!TextureManager.textureCache.TryGetValue(nighttimeOrigin, out nighttimeTexture))
+ {
+ Stopwatch sw = Stopwatch.StartNew();
+ nighttimeOrigin.GetTexture(out nighttimeTexture);
+ sw.Stop();
+ TextureManager.TextureDecodeTime += sw.ElapsedMilliseconds;
+ TextureManager.textureCache.TryAdd(nighttimeOrigin, nighttimeTexture);
+ }
+ }
}
TextureTransparencyType transparencyType = TextureTransparencyType.Opaque;
if (nighttimeTexture != null)
diff --git a/source/LibRender2/Textures/TextureManager.cs b/source/LibRender2/Textures/TextureManager.cs
index cbdd3fff9..a9f2bbd50 100644
--- a/source/LibRender2/Textures/TextureManager.cs
+++ b/source/LibRender2/Textures/TextureManager.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
@@ -23,20 +24,21 @@ public class TextureManager
/// Holds all currently registered textures.
public static Texture[] RegisteredTextures;
/// Holds cached texture origins
- internal static Dictionary textureCache = new Dictionary();
+ internal static ConcurrentDictionary textureCache = new ConcurrentDictionary();
/// Total time spent decoding texture files, in milliseconds.
public static long TextureDecodeTime;
private static Dictionary animatedTextures;
- /// Holds the registered path-based textures, indexed by path.
- private static readonly Dictionary> RegisteredTextureLookup = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ /// Holds the registered path-based textures, indexed by path.
+ /// Values are immutable snapshots, replaced atomically on registration.
+ private static readonly ConcurrentDictionary RegisteredTextureLookup = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
private static readonly object TextureLookupLock = new object();
/// The number of currently registered textures.
- public int RegisteredTexturesCount;
+ public volatile int RegisteredTexturesCount;
internal TextureManager(HostInterface CurrentHost, BaseRenderer Renderer)
{
@@ -66,74 +68,135 @@ public bool RegisterTexture(string path, out Texture handle)
/// Whether registering the texture was successful.
public bool RegisterTexture(string path, TextureParameters parameters, out Texture handle)
{
- /* BUG:
- * The registered textures count very occasional becomes greater than the array length (Texture loader crashes possibly?)
- * This then crashes when we attempt to itinerate the array, so reset it...
- */
- if (RegisteredTexturesCount > RegisteredTextures.Length)
+ /*
+ * Check if the texture is already registered.
+ * If so, return the existing handle.
+ * The stored arrays are immutable, so this read is lock-free.
+ * Only a valid registration is reused, so a transient decode failure
+ * can never poison the lookup for a file which is later decoded fine.
+ * */
+ if (TryFindRegisteredTexture(path, parameters, true, out handle))
{
- RegisteredTexturesCount = RegisteredTextures.Length;
+ return true;
}
/*
- * Check if the texture is already registered.
- * If so, return the existing handle.
+ * Construct the handle (and decode the file) inside the process-wide
+ * GDI+ lock. The image plugins use GDI+ codecs which are not
+ * thread-safe, and parallel route loading registers textures from many
+ * threads concurrently. Serialising the decode also lets a thread that
+ * loses the registration race reuse the winner's handle instead of
+ * decoding the same file again.
* */
- lock (TextureLookupLock)
+ Texture newHandle;
+ lock (BaseRenderer.GdiPlusLock)
{
- if (RegisteredTextureLookup.TryGetValue(path, out List candidates))
+ // Another thread may have completed the registration while we waited.
+ if (TryFindRegisteredTexture(path, parameters, true, out handle))
+ {
+ return true;
+ }
+
+ newHandle = new Texture(path, parameters, currentHost);
+ bool valid = newHandle.PixelFormat != PixelFormat.Invalid && newHandle.DecodedTexture != null;
+
+ /*
+ * Reserve a slot, write the handle and publish the lookup snapshot
+ * atomically. The count is only advanced after the slot has been
+ * written and the array guaranteed to have room for it, so concurrent
+ * readers can never observe a count which exceeds the array length.
+ * */
+ lock (TextureLookupLock)
{
- for (int i = 0; i < candidates.Count; i++)
+ // Prefer an existing valid registration over the handle just decoded.
+ if (TryFindRegisteredTexture(path, parameters, true, out handle))
{
- try
- {
- PathOrigin source = candidates[i].Origin as PathOrigin;
+ return true;
+ }
- if (source != null && source.Parameters == parameters)
- {
- handle = candidates[i];
- return true;
- }
- }
- catch
+ if (!valid)
+ {
+ // Neither our decode nor any existing one is usable. Reuse the
+ // existing invalid handle (if any) rather than registering a
+ // duplicate; render time will retry the decode.
+ if (TryFindRegisteredTexture(path, parameters, false, out handle))
{
- // ignored
+ return true;
}
}
+
+ if (RegisteredTexturesCount >= RegisteredTextures.Length)
+ {
+ Array.Resize(ref RegisteredTextures, Math.Max(RegisteredTextures.Length << 1, RegisteredTexturesCount + 1));
+ }
+ int idx = RegisteredTexturesCount;
+ RegisteredTexturesCount++;
+ RegisteredTextures[idx] = newHandle;
+ handle = newHandle;
+
+ /*
+ * Maintain the registration lookup table.
+ * Publish a new immutable snapshot; readers never take a lock.
+ * */
+ if (RegisteredTextureLookup.TryGetValue(path, out Texture[] currentList))
+ {
+ Texture[] newList = new Texture[currentList.Length + 1];
+ Array.Copy(currentList, newList, currentList.Length);
+ newList[currentList.Length] = newHandle;
+ RegisteredTextureLookup[path] = newList;
+ }
+ else
+ {
+ RegisteredTextureLookup[path] = new[] { newHandle };
+ }
}
}
/*
- * Register the texture and return the newly created handle.
+ * Pre-seed the texture cache with the decoded texture (not the handle).
+ * The handle itself has no decoded bytes, so storing it would cause a null
+ * reference when the transparency type is subsequently queried.
* */
- int idx = GetNextFreeTexture();
- RegisteredTextures[idx] = new Texture(path, parameters, currentHost);
- RegisteredTexturesCount++;
- handle = RegisteredTextures[idx];
+ if (newHandle.PixelFormat != PixelFormat.Invalid && newHandle.DecodedTexture != null)
+ {
+ textureCache.TryAdd(newHandle.Origin, newHandle.DecodedTexture);
+ }
+ return true;
+ }
- lock (TextureLookupLock)
+ /// Looks up a registered path-based texture with the specified parameters.
+ /// The path to the texture.
+ /// The texture parameters.
+ /// Whether an invalid (failed to decode) registration should be ignored.
+ /// Receives the matching handle, if found.
+ /// Whether a matching texture was found.
+ private static bool TryFindRegisteredTexture(string path, TextureParameters parameters, bool requireValid, out Texture handle)
+ {
+ if (RegisteredTextureLookup.TryGetValue(path, out Texture[] candidates))
{
- /*
- * Pre-seed the texture cache with the decoded texture (not the handle).
- * The handle itself has no decoded bytes, so storing it would cause a null
- * reference when the transparency type is subsequently queried.
- * */
- if (handle.PixelFormat != PixelFormat.Invalid && handle.DecodedTexture != null && !textureCache.ContainsKey(handle.Origin))
+ for (int i = 0; i < candidates.Length; i++)
{
- textureCache.Add(handle.Origin, handle.DecodedTexture);
- }
+ try
+ {
+ PathOrigin source = candidates[i].Origin as PathOrigin;
- /*
- * Maintain the registration lookup table.
- * */
- if (!RegisteredTextureLookup.TryGetValue(path, out List list))
- {
- list = new List();
- RegisteredTextureLookup[path] = list;
+ if (source != null && source.Parameters == parameters)
+ {
+ if (!requireValid || candidates[i].PixelFormat != PixelFormat.Invalid)
+ {
+ handle = candidates[i];
+ return true;
+ }
+ }
+ }
+ catch
+ {
+ // ignored
+ }
}
- list.Add(handle);
}
- return true;
+ handle = null;
+ return false;
}
/// Registers a texture and returns a handle to the texture.
@@ -144,10 +207,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 (TextureLookupLock)
+ {
+ int idx = GetNextFreeTexture();
+ RegisteredTextures[idx] = new Texture(texture);
+ RegisteredTexturesCount++;
+ return RegisteredTextures[idx];
+ }
}
/// Registers a texture and returns a handle to the texture.
@@ -160,10 +226,13 @@ public Texture RegisterTexture(Bitmap bitmap, TextureParameters parameters)
/*
* Register the texture and return the newly created handle.
* */
- int idx = GetNextFreeTexture();
- RegisteredTextures[idx] = new Texture(bitmap, parameters);
- RegisteredTexturesCount++;
- return RegisteredTextures[idx];
+ lock (TextureLookupLock)
+ {
+ int idx = GetNextFreeTexture();
+ RegisteredTextures[idx] = new Texture(bitmap, parameters);
+ RegisteredTexturesCount++;
+ return RegisteredTextures[idx];
+ }
}
/// Registers a texture and returns a handle to the texture.
@@ -175,10 +244,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 (TextureLookupLock)
+ {
+ int idx = GetNextFreeTexture();
+ RegisteredTextures[idx] = new Texture(bitmap);
+ RegisteredTexturesCount++;
+ return RegisteredTextures[idx];
+ }
}
@@ -205,10 +277,13 @@ public bool LoadTexture(ref Texture handle, OpenGlTextureWrapMode wrap, int curr
{
if (!animatedTextures.TryGetValue(handle.Origin, out texture))
{
- if (!handle.Origin.GetTexture(out texture))
+ lock (BaseRenderer.GdiPlusLock)
{
- //Loading animated texture barfed
- return false;
+ if (!handle.Origin.GetTexture(out texture))
+ {
+ //Loading animated texture barfed
+ return false;
+ }
}
animatedTextures.Add(handle.Origin, texture);
}
@@ -242,10 +317,31 @@ public bool LoadTexture(ref Texture handle, OpenGlTextureWrapMode wrap, int curr
if (handle.Ignore)
{
- return false;
+ /*
+ * A previous decode failed. If the source file still exists, the
+ * failure may have been transient (e.g. concurrent GDI+ decode
+ * during parallel loading), so retry rather than leaving the
+ * texture permanently white. Only genuinely missing files are
+ * skipped until the file appears (or is unloaded).
+ * */
+ if (handle.Origin is PathOrigin pathOrigin && File.Exists(pathOrigin.Path))
+ {
+ handle.Ignore = false;
+ }
+ else
+ {
+ return false;
+ }
}
- if (texture == null && handle.Origin.GetTexture(out texture) || texture != null)
+ if (texture == null)
+ {
+ lock (BaseRenderer.GdiPlusLock)
+ {
+ handle.Origin.GetTexture(out texture);
+ }
+ }
+ if (texture != null)
{
if (texture.MultipleFrames)
{
@@ -497,10 +593,7 @@ public static void UnloadTexture(ref Texture handle)
handle.Ignore = false;
if (handle.Origin != null)
{
- lock (TextureLookupLock)
- {
- textureCache.Remove(handle.Origin);
- }
+ textureCache.TryRemove(handle.Origin, out _);
}
}
@@ -538,23 +631,17 @@ public void UnloadAllTextures(bool currentlyReloading)
}
if (currentlyReloading)
{
- lock (TextureLookupLock)
+ foreach (TextureOrigin origin in textureCache.Keys.ToList())
{
- foreach (TextureOrigin origin in textureCache.Keys.ToList())
+ if (origin is PathOrigin && !TextureFileUnchanged(origin))
{
- if (origin is PathOrigin && !TextureFileUnchanged(origin))
- {
- textureCache.Remove(origin);
- }
+ textureCache.TryRemove(origin, out _);
}
}
}
else
{
- lock (TextureLookupLock)
- {
- textureCache.Clear();
- }
+ textureCache.Clear();
}
/*
@@ -569,12 +656,17 @@ public void UnloadAllTextures(bool currentlyReloading)
Texture texture = RegisteredTextures[i];
if (texture != null && texture.Origin is PathOrigin pathOrigin)
{
- if (!RegisteredTextureLookup.TryGetValue(pathOrigin.Path, out List list))
+ if (RegisteredTextureLookup.TryGetValue(pathOrigin.Path, out Texture[] currentList))
+ {
+ Texture[] newList = new Texture[currentList.Length + 1];
+ Array.Copy(currentList, newList, currentList.Length);
+ newList[currentList.Length] = texture;
+ RegisteredTextureLookup[pathOrigin.Path] = newList;
+ }
+ else
{
- list = new List();
- RegisteredTextureLookup[pathOrigin.Path] = list;
+ RegisteredTextureLookup[pathOrigin.Path] = new[] { texture };
}
- list.Add(texture);
}
}
}
diff --git a/source/ObjectViewer/Hosts.cs b/source/ObjectViewer/Hosts.cs
index 283c25bb9..5558518ef 100644
--- a/source/ObjectViewer/Hosts.cs
+++ b/source/ObjectViewer/Hosts.cs
@@ -286,20 +286,19 @@ 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;
}
- if (!FailedObjects.Contains(path))
+ if (FailedObjects.TryAdd(path, true))
{
- FailedObjects.Add(path);
Interface.AddMessage(MessageType.Error, false, "Plugin " + Program.CurrentHost.Plugins[i].Title + " returned unsuccessfully at LoadObject");
}
@@ -320,17 +319,15 @@ public override bool LoadObject(string path, System.Text.Encoding Encoding, out
FileInfo f = new FileInfo(path);
if (f.Length == 0)
{
- if (!NullFiles.Contains(Path.GetFileNameWithoutExtension(path).ToLowerInvariant()) && !FailedObjects.Contains(path))
+ if (!NullFiles.Contains(Path.GetFileNameWithoutExtension(path).ToLowerInvariant()) && FailedObjects.TryAdd(path, true))
{
- FailedObjects.Add(path);
Interface.AddMessage(MessageType.Error, false, "Zero-byte object file encountered at " + path);
}
}
else
{
- if (!NullFiles.Contains(Path.GetFileNameWithoutExtension(path).ToLowerInvariant()) && !FailedObjects.Contains(path))
+ if (!NullFiles.Contains(Path.GetFileNameWithoutExtension(path).ToLowerInvariant()) && FailedObjects.TryAdd(path, true))
{
- FailedObjects.Add(path);
Interface.AddMessage(MessageType.Error, false, "No plugin found that is capable of loading object " + path);
}
}
diff --git a/source/ObjectViewer/ProgramS.cs b/source/ObjectViewer/ProgramS.cs
index 48157ab1a..14fc2f7db 100644
--- a/source/ObjectViewer/ProgramS.cs
+++ b/source/ObjectViewer/ProgramS.cs
@@ -341,7 +341,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
diff --git a/source/OpenBVE/System/Host.cs b/source/OpenBVE/System/Host.cs
index b409cfada..1a65faf7f 100644
--- a/source/OpenBVE/System/Host.cs
+++ b/source/OpenBVE/System/Host.cs
@@ -352,7 +352,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;
}
@@ -360,9 +360,8 @@ public override bool LoadStaticObject(string path, System.Text.Encoding Encoding
// may be trying to load in different places, so leave
Interface.AddMessage(MessageType.Error, false, "Attempted to load " + path + " which is an animated object where only static objects are allowed.");
}
- if(!FailedObjects.Contains(path))
+ if (FailedObjects.TryAdd(path, true))
{
- FailedObjects.Add(path);
Interface.AddMessage(MessageType.Error, false, "Plugin " + Program.CurrentHost.Plugins[i].Title + " returned unsuccessfully at LoadObject for file " + path);
}
@@ -375,9 +374,8 @@ public override bool LoadStaticObject(string path, System.Text.Encoding Encoding
}
}
}
- if (!FailedObjects.Contains(path))
+ if (FailedObjects.TryAdd(path, true))
{
- FailedObjects.Add(path);
Interface.AddMessage(MessageType.Error, false, "No plugin found that is capable of loading object " + path);
}
@@ -420,20 +418,19 @@ 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;
}
- if (!FailedObjects.Contains(path))
+ if (FailedObjects.TryAdd(path, true))
{
- FailedObjects.Add(path);
Interface.AddMessage(MessageType.Error, false, "Plugin " + Program.CurrentHost.Plugins[i].Title + " returned unsuccessfully at LoadObject for file " + path);
}
@@ -454,17 +451,15 @@ public override bool LoadObject(string path, System.Text.Encoding Encoding, out
FileInfo f = new FileInfo(path);
if (f.Length == 0)
{
- if (!NullFiles.Contains(Path.GetFileNameWithoutExtension(path).ToLowerInvariant()) && !FailedObjects.Contains(path))
+ if (!NullFiles.Contains(Path.GetFileNameWithoutExtension(path).ToLowerInvariant()) && FailedObjects.TryAdd(path, true))
{
- FailedObjects.Add(path);
Interface.AddMessage(MessageType.Error, false, "Zero-byte object file encountered at " + path);
}
}
else
{
- if (!NullFiles.Contains(Path.GetFileNameWithoutExtension(path).ToLowerInvariant()) && !FailedObjects.Contains(path))
+ if (!NullFiles.Contains(Path.GetFileNameWithoutExtension(path).ToLowerInvariant()) && FailedObjects.TryAdd(path, true))
{
- FailedObjects.Add(path);
Interface.AddMessage(MessageType.Error, false, "No plugin found that is capable of loading object " + path);
}
}
diff --git a/source/OpenBveApi/System/FileSystem.cs b/source/OpenBveApi/System/FileSystem.cs
index fbe223625..ffe79fdf8 100644
--- a/source/OpenBveApi/System/FileSystem.cs
+++ b/source/OpenBveApi/System/FileSystem.cs
@@ -19,6 +19,8 @@ namespace OpenBveApi.FileSystem {
/// Represents the program's organization of files and folders.
public class FileSystem {
+ /// Locks concurrent appends to the log file.
+ private static readonly object LogFileLock = new object();
// --- members ---
@@ -490,22 +492,25 @@ public void ClearLogFile(string version) {
/// The text.
/// Whether a timestamp should be added to the log file
public void AppendToLogFile(string text, bool addTimestamp = true) {
- try
+ lock (LogFileLock)
{
- string file = System.IO.Path.Combine(SettingsFolder, "log.txt");
- if (addTimestamp)
+ try
{
- File.AppendAllText(file, DateTime.Now.ToString("HH:mm:ss") + @" " + text + Environment.NewLine, new UTF8Encoding(false));
+ string file = System.IO.Path.Combine(SettingsFolder, "log.txt");
+ if (addTimestamp)
+ {
+ File.AppendAllText(file, DateTime.Now.ToString("HH:mm:ss") + @" " + text + Environment.NewLine, new UTF8Encoding(false));
+ }
+ else
+ {
+ File.AppendAllText(file, text + Environment.NewLine, new UTF8Encoding(false));
+ }
+
}
- else
+ catch
{
- File.AppendAllText(file, text + Environment.NewLine, new UTF8Encoding(false));
+ // ignored
}
-
- }
- catch
- {
- // ignored
}
}
diff --git a/source/OpenBveApi/System/Hosts/HostInterface.cs b/source/OpenBveApi/System/Hosts/HostInterface.cs
index 0bf02ac56..965609d8f 100644
--- a/source/OpenBveApi/System/Hosts/HostInterface.cs
+++ b/source/OpenBveApi/System/Hosts/HostInterface.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
@@ -137,10 +138,10 @@ private static string DetectUnixKernel()
protected HostInterface(HostApplication host)
{
Application = host;
- StaticObjectCache = new Dictionary, StaticObject>();
- AnimatedObjectCollectionCache = new Dictionary();
+ StaticObjectCache = new ConcurrentDictionary, StaticObject>();
+ AnimatedObjectCollectionCache = new ConcurrentDictionary();
MissingFiles = new HashSet();
- FailedObjects = new HashSet();
+ FailedObjects = new ConcurrentDictionary();
FailedTextures = new HashSet();
if (Platform == HostPlatform.GNULinux)
@@ -177,7 +178,7 @@ public void ClearErrors()
/// Contains a list of missing files encountered
public readonly HashSet MissingFiles;
/// Contains a list of objects which failed to load
- public readonly HashSet FailedObjects;
+ public readonly ConcurrentDictionary FailedObjects;
/// Contains a list of textures which failed to load
public readonly HashSet FailedTextures;
@@ -611,13 +612,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/OpenBveApi/Textures/Textures.ClipRegion.cs b/source/OpenBveApi/Textures/Textures.ClipRegion.cs
index b2db977b0..b04581249 100644
--- a/source/OpenBveApi/Textures/Textures.ClipRegion.cs
+++ b/source/OpenBveApi/Textures/Textures.ClipRegion.cs
@@ -90,5 +90,19 @@ public override bool Equals(object obj)
if (Height != x.Height) return false;
return true;
}
+
+ /// Returns a hash code based on the region coordinates.
+ /// A 32-bit signed integer hash code.
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = Left;
+ hash = (hash * 397) ^ Top;
+ hash = (hash * 397) ^ Width;
+ hash = (hash * 397) ^ Height;
+ return hash;
+ }
+ }
}
}
diff --git a/source/OpenBveApi/Textures/Textures.PathOrigin.cs b/source/OpenBveApi/Textures/Textures.PathOrigin.cs
index 2958d8b57..9700be2ea 100644
--- a/source/OpenBveApi/Textures/Textures.PathOrigin.cs
+++ b/source/OpenBveApi/Textures/Textures.PathOrigin.cs
@@ -90,7 +90,7 @@ public override bool GetTexture(out Texture texture)
if (ReferenceEquals(a, b)) return true;
if (a is null) return false;
if (b is null) return false;
- return a.Path == b.Path;
+ return a.Path == b.Path && a.Parameters == b.Parameters;
}
/// Checks whether two origins are unequal.
@@ -102,7 +102,7 @@ public override bool GetTexture(out Texture texture)
if (ReferenceEquals(a, b)) return false;
if (a is null) return true;
if (b is null) return true;
- return a.Path != b.Path;
+ return a.Path != b.Path || a.Parameters != b.Parameters;
}
/// Checks whether this instance is equal to the specified object.
@@ -113,7 +113,7 @@ public override bool Equals(object obj)
if (ReferenceEquals(this, obj)) return true;
if (obj is null) return false;
if (!(obj is PathOrigin)) return false;
- return Path == ((PathOrigin) obj).Path;
+ return Path == ((PathOrigin) obj).Path && Parameters == ((PathOrigin) obj).Parameters;
}
/// Returns a string representing the absolute on-disk path of this texture
@@ -122,11 +122,16 @@ public override string ToString()
return Path;
}
- /// Returns the hash code based on the path.
+ /// Returns the hash code based on the path and parameters.
/// A 32-bit signed integer hash code.
public override int GetHashCode()
{
- return Path?.GetHashCode() ?? 0;
+ unchecked
+ {
+ int hash = Path != null ? Path.GetHashCode() : 0;
+ hash = (hash * 397) ^ (Parameters != null ? Parameters.GetHashCode() : 0);
+ return hash;
+ }
}
}
}
diff --git a/source/OpenBveApi/Textures/Textures.TextureParameters.cs b/source/OpenBveApi/Textures/Textures.TextureParameters.cs
index b26398556..6296ba5ee 100644
--- a/source/OpenBveApi/Textures/Textures.TextureParameters.cs
+++ b/source/OpenBveApi/Textures/Textures.TextureParameters.cs
@@ -44,6 +44,8 @@ public TextureParameters(TextureClipRegion clipRegion, Color24? transparentColor
if (b is null) return false;
if (a.ClipRegion != b.ClipRegion) return false;
if (a.TransparentColor != b.TransparentColor) return false;
+ if (a.FirstColorTransparent != b.FirstColorTransparent) return false;
+ if (!ReferenceEquals(a.TransparencyTexture, b.TransparencyTexture)) return false;
return true;
}
@@ -58,6 +60,8 @@ public TextureParameters(TextureClipRegion clipRegion, Color24? transparentColor
if (b is null) return true;
if (a.ClipRegion != b.ClipRegion) return true;
if (a.TransparentColor != b.TransparentColor) return true;
+ if (a.FirstColorTransparent != b.FirstColorTransparent) return true;
+ if (!ReferenceEquals(a.TransparencyTexture, b.TransparencyTexture)) return true;
return false;
}
@@ -72,7 +76,23 @@ public override bool Equals(object obj)
TextureParameters x = (TextureParameters) obj;
if (ClipRegion != x.ClipRegion) return false;
if (TransparentColor != x.TransparentColor) return false;
+ if (FirstColorTransparent != x.FirstColorTransparent) return false;
+ if (!ReferenceEquals(TransparencyTexture, x.TransparencyTexture)) return false;
return true;
}
+
+ /// Returns a hash code for the parameter values.
+ /// A 32-bit signed integer hash code.
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = ClipRegion != null ? ClipRegion.GetHashCode() : 0;
+ hash = (hash * 397) ^ (TransparentColor.HasValue ? TransparentColor.Value.GetHashCode() : 0);
+ hash = (hash * 397) ^ FirstColorTransparent.GetHashCode();
+ hash = (hash * 397) ^ (TransparencyTexture != null ? System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(TransparencyTexture) : 0);
+ return hash;
+ }
+ }
}
}
diff --git a/source/Plugins/Object.DirectX/Helpers/Block.cs b/source/Plugins/Object.DirectX/Helpers/Block.cs
index fa5e03713..0b812c5c3 100644
--- a/source/Plugins/Object.DirectX/Helpers/Block.cs
+++ b/source/Plugins/Object.DirectX/Helpers/Block.cs
@@ -445,15 +445,13 @@ public override Block ReadSubBlock()
public override int ReadInt()
{
startPosition = currentPosition;
- string s = getNextValue();
- if (char.IsWhiteSpace(myText[currentPosition]))
+ SkipSeparators();
+ if (TryParseIntFast(out int val))
{
- while (char.IsWhiteSpace(myText[currentPosition]))
- {
- currentPosition++;
- }
+ return val;
}
- if (int.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out int val))
+ string s = getNextValue();
+ if (int.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out val))
{
return val;
}
@@ -463,15 +461,13 @@ public override int ReadInt()
public override ushort ReadUInt16()
{
startPosition = currentPosition;
- string s = getNextValue();
- if (char.IsWhiteSpace(myText[currentPosition]))
+ SkipSeparators();
+ if (TryParseIntFast(out int val))
{
- while (char.IsWhiteSpace(myText[currentPosition]))
- {
- currentPosition++;
- }
+ return (ushort) val;
}
- if (int.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out int val))
+ string s = getNextValue();
+ if (int.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out val))
{
return (ushort) val;
}
@@ -481,15 +477,13 @@ public override ushort ReadUInt16()
public override uint ReadDword()
{
startPosition = currentPosition;
- string s = getNextValue();
- if (char.IsWhiteSpace(myText[currentPosition]))
+ SkipSeparators();
+ if (TryParseUIntFast(out uint val))
{
- while (char.IsWhiteSpace(myText[currentPosition]))
- {
- currentPosition++;
- }
+ return val;
}
- if (uint.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out uint val))
+ string s = getNextValue();
+ if (uint.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out val))
{
return val;
}
@@ -499,9 +493,14 @@ public override uint ReadDword()
public override float ReadSingle()
{
startPosition = currentPosition;
+ SkipSeparators();
+ if (TryParseFloatFast(out float val))
+ {
+ return val;
+ }
string s = getNextValue();
currentPosition++;
- if (float.TryParse(s, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out float val))
+ if (float.TryParse(s, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out val))
{
return val;
}
@@ -509,6 +508,143 @@ public override float ReadSingle()
throw new Exception("Unable to parse " + s + " to a valid float in block " + Token);
}
+ /// Skips any leading whitespace, semicolons or commas.
+ private void SkipSeparators()
+ {
+ while (currentPosition < myText.Length && (char.IsWhiteSpace(myText[currentPosition]) || myText[currentPosition] == ';' || myText[currentPosition] == ','))
+ {
+ currentPosition++;
+ }
+ }
+
+ /// Parses an integer directly from the underlying text, without allocating a substring.
+ private bool TryParseIntFast(out int result)
+ {
+ result = 0;
+ int p = currentPosition;
+ bool neg = false;
+ if (p < myText.Length && (myText[p] == '+' || myText[p] == '-'))
+ {
+ neg = myText[p] == '-';
+ p++;
+ }
+ int digitStart = p;
+ int value = 0;
+ while (p < myText.Length && myText[p] >= '0' && myText[p] <= '9')
+ {
+ if (p - digitStart >= 9)
+ {
+ // too many digits to safely accumulate; fall back to the standard parser
+ return false;
+ }
+ value = value * 10 + (myText[p] - '0');
+ p++;
+ }
+ if (p == digitStart)
+ {
+ return false;
+ }
+ result = neg ? -value : value;
+ currentPosition = p;
+ return true;
+ }
+
+ /// Parses an unsigned integer directly from the underlying text, without allocating a substring.
+ private bool TryParseUIntFast(out uint result)
+ {
+ result = 0;
+ int p = currentPosition;
+ int digitStart = p;
+ uint value = 0;
+ while (p < myText.Length && myText[p] >= '0' && myText[p] <= '9')
+ {
+ if (p - digitStart >= 9)
+ {
+ // too many digits to safely accumulate; fall back to the standard parser
+ return false;
+ }
+ value = value * 10 + (uint)(myText[p] - '0');
+ p++;
+ }
+ if (p == digitStart)
+ {
+ return false;
+ }
+ result = value;
+ currentPosition = p;
+ return true;
+ }
+
+ /// Parses a single-precision float directly from the underlying text, without allocating a substring.
+ private bool TryParseFloatFast(out float result)
+ {
+ result = 0.0f;
+ int p = currentPosition;
+ bool neg = false;
+ if (p < myText.Length && (myText[p] == '+' || myText[p] == '-'))
+ {
+ neg = myText[p] == '-';
+ p++;
+ }
+ bool hasDigit = false;
+ double mantissa = 0.0;
+ int fracDigits = 0;
+ while (p < myText.Length && myText[p] >= '0' && myText[p] <= '9')
+ {
+ hasDigit = true;
+ mantissa = mantissa * 10.0 + (myText[p] - '0');
+ p++;
+ }
+ if (p < myText.Length && myText[p] == '.')
+ {
+ p++;
+ while (p < myText.Length && myText[p] >= '0' && myText[p] <= '9')
+ {
+ hasDigit = true;
+ mantissa = mantissa * 10.0 + (myText[p] - '0');
+ fracDigits++;
+ p++;
+ }
+ }
+ if (!hasDigit)
+ {
+ return false;
+ }
+ int exponent = -fracDigits;
+ if (p < myText.Length && (myText[p] == 'e' || myText[p] == 'E'))
+ {
+ p++;
+ bool expNeg = false;
+ if (p < myText.Length && (myText[p] == '+' || myText[p] == '-'))
+ {
+ expNeg = myText[p] == '-';
+ p++;
+ }
+ int expStart = p;
+ int expValue = 0;
+ while (p < myText.Length && myText[p] >= '0' && myText[p] <= '9')
+ {
+ expValue = expValue * 10 + (myText[p] - '0');
+ p++;
+ }
+ if (p == expStart)
+ {
+ // an exponent marker with no digits is not a valid float token
+ return false;
+ }
+ exponent += expNeg ? -expValue : expValue;
+ }
+ if (p < myText.Length && myText[p] == '.')
+ {
+ // A second decimal point - not a valid float token; let the standard parser report it
+ return false;
+ }
+ double value = exponent != 0 ? mantissa * Math.Pow(10.0, exponent) : mantissa;
+ result = (float)(neg ? -value : value);
+ currentPosition = p;
+ return true;
+ }
+
public override void Skip(int length)
{
//Unused at the minute
diff --git a/source/Plugins/Object.DirectX/Parsers/AssimpXParser.cs b/source/Plugins/Object.DirectX/Parsers/AssimpXParser.cs
index 319851870..42691c052 100644
--- a/source/Plugins/Object.DirectX/Parsers/AssimpXParser.cs
+++ b/source/Plugins/Object.DirectX/Parsers/AssimpXParser.cs
@@ -35,15 +35,11 @@ namespace Plugin
{
internal class AssimpXParser
{
- private static string currentFolder;
- private static string currentFile;
- private static Matrix4D rootMatrix;
-
internal static StaticObject ReadObject(string fileName)
{
- currentFolder = Path.GetDirectoryName(fileName);
- currentFile = fileName;
- rootMatrix = Matrix4D.NoTransformation;
+ string currentFolder = Path.GetDirectoryName(fileName);
+ string currentFile = fileName;
+ Matrix4D rootMatrix = Matrix4D.NoTransformation;
#if !DEBUG
try
@@ -91,7 +87,7 @@ internal static StaticObject ReadObject(string fileName)
// Global
foreach (var mesh in scene.GlobalMeshes)
{
- MeshBuilder(ref obj, ref builder, mesh);
+ MeshBuilder(ref obj, ref builder, mesh, currentFolder, currentFile);
}
if (scene.RootNode != null)
@@ -106,7 +102,7 @@ internal static StaticObject ReadObject(string fileName)
foreach (var mesh in scene.RootNode.Meshes)
{
- MeshBuilder(ref obj, ref builder, mesh);
+ MeshBuilder(ref obj, ref builder, mesh, currentFolder, currentFile);
}
// Children Node
@@ -114,7 +110,7 @@ internal static StaticObject ReadObject(string fileName)
{
Node node = scene.RootNode.Children[i];
SetReferenceMaterials(scene, ref node);
- ChildrenNode(ref obj, ref builder, node);
+ ChildrenNode(ref obj, ref builder, node, currentFolder, currentFile);
}
}
@@ -160,7 +156,7 @@ private static void SetReferenceMaterials(Scene scene, ref Node node)
}
- private static void MeshBuilder(ref StaticObject obj, ref MeshBuilder builder, AssimpNET.X.Mesh mesh)
+ private static void MeshBuilder(ref StaticObject obj, ref MeshBuilder builder, AssimpNET.X.Mesh mesh, string currentFolder, string currentFile)
{
if (builder.Vertices.Count != 0)
{
@@ -326,12 +322,12 @@ private static void MeshBuilder(ref StaticObject obj, ref MeshBuilder builder,
}
}
- private static void ChildrenNode(ref StaticObject obj, ref MeshBuilder builder, Node child)
+ private static void ChildrenNode(ref StaticObject obj, ref MeshBuilder builder, Node child, string currentFolder, string currentFile)
{
foreach (var mesh in child.Meshes)
{
builder.TransformMatrix = child.TrafoMatrix;
- MeshBuilder(ref obj, ref builder, mesh);
+ MeshBuilder(ref obj, ref builder, mesh, currentFolder, currentFile);
if (builder.Vertices.Count != 0)
{
builder.Apply(ref obj, false, false);
@@ -340,7 +336,7 @@ private static void ChildrenNode(ref StaticObject obj, ref MeshBuilder builder,
}
foreach (var grandchild in child.Children)
{
- ChildrenNode(ref obj, ref builder, grandchild);
+ ChildrenNode(ref obj, ref builder, grandchild, currentFolder, currentFile);
}
}
diff --git a/source/Plugins/Object.DirectX/Parsers/NewXParser.cs b/source/Plugins/Object.DirectX/Parsers/NewXParser.cs
index e7e827725..af319d9f9 100644
--- a/source/Plugins/Object.DirectX/Parsers/NewXParser.cs
+++ b/source/Plugins/Object.DirectX/Parsers/NewXParser.cs
@@ -24,6 +24,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
@@ -38,12 +39,37 @@ namespace Plugin
{
internal class NewXParser
{
+ /// Total time spent reading object files from disk, in ticks.
+ internal static long TotalReadMs;
+ /// Total time spent on text preprocessing, in ticks.
+ internal static long TotalPreprocessMs;
+ /// Total time spent parsing block data, in ticks.
+ internal static long TotalParseMs;
+ /// Total time spent applying meshes to objects, in ticks.
+ internal static long TotalApplyMs;
+ /// Total time spent in ReadObject (read + preprocess + parse + apply), in ticks.
+ internal static long TotalReadObjectMs;
+ /// Total time spent in the plugin load call, in ticks.
+ internal static long TotalLoadObjectMs;
+ /// The number of objects parsed.
+ internal static int TotalCount;
+ /// Converts ticks to milliseconds.
+ internal static double TicksToMs(long ticks)
+ {
+ return ticks / (double)Stopwatch.Frequency * 1000.0;
+ }
+
internal static StaticObject ReadObject(string fileName, Encoding encoding)
{
- rootMatrix = Matrix4D.NoTransformation;
- currentFolder = Path.GetDirectoryName(fileName);
- currentFile = fileName;
+ XParseState state = new XParseState
+ {
+ Folder = Path.GetDirectoryName(fileName),
+ File = fileName
+ };
+ Stopwatch readTimer = Stopwatch.StartNew();
byte[] Data = File.ReadAllBytes(fileName);
+ readTimer.Stop();
+ System.Threading.Interlocked.Add(ref TotalReadMs, readTimer.Elapsed.Ticks);
if (Data.Length < 16 || Data[0] != 120 | Data[1] != 111 | Data[2] != 102 | Data[3] != 32)
{
@@ -75,31 +101,57 @@ internal static StaticObject ReadObject(string fileName, Encoding encoding)
if (Data[8] == 116 & Data[9] == 120 & Data[10] == 116 & Data[11] == 32)
{
// textual flavor
- string[] Lines = File.ReadAllLines(fileName, encoding);
- // strip away comments
+ // Single pass over the raw text: strip comments (respecting quoted strings),
+ // collapse runs of whitespace to a single space and append to a single buffer.
+ Stopwatch prepTimer = Stopwatch.StartNew();
+ string Text = encoding.GetString(Data);
+ // Skip the 17 character "xof 0303txt 0032" file header while building the preprocessed text.
+ StringBuilder stripped = new StringBuilder(Text.Length);
bool Quote = false;
- for (int i = 0; i < Lines.Length; i++) {
- for (int j = 0; j < Lines[i].Length; j++) {
- if (Lines[i][j] == '"') Quote = !Quote;
- if (!Quote) {
- if (Lines[i][j] == '#' || j < Lines[i].Length - 1 && Lines[i].Substring(j, 2) == "//") {
- Lines[i] = Lines[i].Substring(0, j);
- break;
- }
+ bool InComment = false;
+ for (int i = 17; i < Text.Length; i++)
+ {
+ char c = Text[i];
+ if (InComment)
+ {
+ if (c == '\n')
+ {
+ InComment = false;
+ Quote = false;
+ AppendSeparator(stripped);
}
+ continue;
}
- //Convert runs of whitespace to single
- var list = Lines[i].Split().Where(s => !string.IsNullOrWhiteSpace(s));
- Lines[i] = string.Join(" ", list);
- }
- StringBuilder Builder = new StringBuilder();
- for (int i = 0; i < Lines.Length; i++) {
- Builder.Append(Lines[i]);
- Builder.Append(' ');
+ if (c == '"')
+ {
+ Quote = !Quote;
+ stripped.Append(c);
+ continue;
+ }
+ if (!Quote && (c == '#' || c == '/' && i + 1 < Text.Length && Text[i + 1] == '/'))
+ {
+ InComment = true;
+ continue;
+ }
+ if (c == '\n')
+ {
+ Quote = false;
+ }
+ if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
+ {
+ AppendSeparator(stripped);
+ continue;
+ }
+ stripped.Append(c);
}
- string Content = Builder.ToString();
- Content = Content.Substring(17).Trim();
- return LoadTextualX(Content);
+ string Content = stripped.ToString();
+ prepTimer.Stop();
+ System.Threading.Interlocked.Add(ref TotalPreprocessMs, prepTimer.Elapsed.Ticks);
+ Stopwatch readObjectTimer = Stopwatch.StartNew();
+ StaticObject result = LoadTextualX(Content, true, state);
+ readObjectTimer.Stop();
+ System.Threading.Interlocked.Add(ref TotalReadObjectMs, readObjectTimer.Elapsed.Ticks);
+ return result;
}
byte[] newData;
@@ -108,7 +160,7 @@ internal static StaticObject ReadObject(string fileName, Encoding encoding)
//Uncompressed binary, so skip the header
newData = new byte[Data.Length - 16];
Array.Copy(Data, 16, newData, 0, Data.Length - 16);
- return LoadBinaryX(newData, floatingPointSize);
+ return LoadBinaryX(newData, floatingPointSize, state);
}
if (Data[8] == 116 & Data[9] == 122 & Data[10] == 105 & Data[11] == 112)
@@ -116,7 +168,7 @@ internal static StaticObject ReadObject(string fileName, Encoding encoding)
// compressed textual flavor
newData = MSZip.Decompress(Data);
string Text = encoding.GetString(newData);
- return LoadTextualX(Text);
+ return LoadTextualX(Text, false, state);
}
if (Data[8] == 98 & Data[9] == 122 & Data[10] == 105 & Data[11] == 112)
@@ -124,7 +176,7 @@ internal static StaticObject ReadObject(string fileName, Encoding encoding)
//Compressed binary
//16 bytes of header, then 8 bytes of padding, followed by the actual compressed data
byte[] uncompressedData = MSZip.Decompress(Data);
- return LoadBinaryX(uncompressedData, floatingPointSize);
+ return LoadBinaryX(uncompressedData, floatingPointSize, state);
}
// unsupported flavor
@@ -132,9 +184,13 @@ internal static StaticObject ReadObject(string fileName, Encoding encoding)
return null;
}
- private static StaticObject LoadTextualX(string Text)
+ private static StaticObject LoadTextualX(string Text, bool preprocessed, XParseState state)
{
- Text = Text.Replace("\r\n", " ").Replace("\n", " ").Replace("\r", " ").Replace("\t", " ").Trim();
+ if (!preprocessed)
+ {
+ Text = Text.Replace("\r\n", " ").Replace("\n", " ").Replace("\r", " ").Replace("\t", " ").Trim();
+ }
+ Stopwatch parseTimer = Stopwatch.StartNew();
StaticObject obj = new StaticObject(Plugin.CurrentHost);
MeshBuilder builder = new MeshBuilder(Plugin.CurrentHost);
Material material = new Material();
@@ -142,32 +198,49 @@ private static StaticObject LoadTextualX(string Text)
while (block.Position() < block.Length() - 5)
{
Block subBlock = block.ReadSubBlock();
- ParseSubBlock(subBlock, ref obj, ref builder, ref material);
+ ParseSubBlock(subBlock, ref obj, ref builder, ref material, state);
}
+ parseTimer.Stop();
+ System.Threading.Interlocked.Add(ref TotalParseMs, parseTimer.Elapsed.Ticks);
+ Stopwatch applyTimer = Stopwatch.StartNew();
builder.Apply(ref obj, false, false);
obj.Mesh.CreateNormals();
- if (rootMatrix != Matrix4D.NoTransformation)
+ applyTimer.Stop();
+ System.Threading.Interlocked.Add(ref TotalApplyMs, applyTimer.Elapsed.Ticks);
+ if (state.RootMatrix != Matrix4D.NoTransformation)
{
- for (int i = transformStart; i < obj.Mesh.Vertices.Length; i++)
+ for (int i = state.TransformStart; i < obj.Mesh.Vertices.Length; i++)
{
- obj.Mesh.Vertices[i].Coordinates.Transform(rootMatrix, false);
+ obj.Mesh.Vertices[i].Coordinates.Transform(state.RootMatrix, false);
}
}
return obj;
}
- private static string currentFolder;
- private static string currentFile;
-
- private static Matrix4D rootMatrix;
- private static int currentLevel = 0;
- private static int transformStart = 0;
- private static VertexElement[] vertexElements;
- private static bool currentMaterialUsed;
+ /// Per-call parse state, allowing multiple objects to be parsed concurrently.
+ private sealed class XParseState
+ {
+ internal string Folder;
+ internal string File;
+ internal Matrix4D RootMatrix = Matrix4D.NoTransformation;
+ internal int Level;
+ internal int TransformStart;
+ internal bool MaterialUsed;
+ /// Key-based material definitions declared at root level in the current file.
+ /// Per-parse, so concurrently parsed files cannot overwrite each other's labels.
+ internal readonly Dictionary RootMaterials = new Dictionary();
+ }
- private static readonly Dictionary rootMaterials = new Dictionary();
+ /// Appends a single space separator, avoiding runs of whitespace.
+ private static void AppendSeparator(StringBuilder sb)
+ {
+ if (sb.Length == 0 || sb[sb.Length - 1] != ' ')
+ {
+ sb.Append(' ');
+ }
+ }
- private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBuilder builder, ref Material material)
+ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBuilder builder, ref Material material, XParseState state)
{
Block subBlock;
switch (block.Token)
@@ -222,19 +295,19 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
}
return;
case TemplateID.Frame:
- currentLevel++;
+ state.Level++;
if (builder.Vertices.Count != 0)
{
builder.Apply(ref obj, false, false);
- if (rootMatrix != Matrix4D.NoTransformation)
+ if (state.RootMatrix != Matrix4D.NoTransformation)
{
- for (int i = transformStart; i < obj.Mesh.Vertices.Length; i++)
+ for (int i = state.TransformStart; i < obj.Mesh.Vertices.Length; i++)
{
- obj.Mesh.Vertices[i].Coordinates.Transform(rootMatrix, false);
+ obj.Mesh.Vertices[i].Coordinates.Transform(state.RootMatrix, false);
}
}
- transformStart = obj.Mesh.Vertices.Length;
- rootMatrix = Matrix4D.NoTransformation;
+ state.TransformStart = obj.Mesh.Vertices.Length;
+ state.RootMatrix = Matrix4D.NoTransformation;
builder = new MeshBuilder(Plugin.CurrentHost);
}
while (block.Position() < block.Length() - 5)
@@ -247,9 +320,9 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
*/
//TemplateID[] validTokens = { TemplateID.Mesh , TemplateID.FrameTransformMatrix, TemplateID.Frame };
subBlock = block.ReadSubBlock();
- ParseSubBlock(subBlock, ref obj, ref builder, ref material);
+ ParseSubBlock(subBlock, ref obj, ref builder, ref material, state);
}
- currentLevel--;
+ state.Level--;
if (builder.Vertices.Count == 0)
{
builder.TransformMatrix = Matrix4D.NoTransformation;
@@ -262,18 +335,18 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
matrixValues[i] = block.ReadSingle();
}
- if (currentLevel > 1)
+ if (state.Level > 1)
{
builder.TransformMatrix = new Matrix4D(matrixValues) * builder.TransformMatrix;
}
else
{
- transformStart = obj.Mesh.Vertices.Length;
- rootMatrix = new Matrix4D(matrixValues);
+ state.TransformStart = obj.Mesh.Vertices.Length;
+ state.RootMatrix = new Matrix4D(matrixValues);
}
break;
case TemplateID.Mesh:
- currentLevel++;
+ state.Level++;
if (builder.Vertices.Count != 0)
{
builder.Apply(ref obj, false, false);
@@ -308,7 +381,7 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
if (block.Position() < block.Length() - 5)
{
subBlock = block.ReadSubBlock();
- ParseSubBlock(subBlock, ref obj, ref builder, ref material);
+ ParseSubBlock(subBlock, ref obj, ref builder, ref material, state);
}
goto NoFaces;
}
@@ -338,10 +411,10 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
while (block.Position() < block.Length() - 5)
{
subBlock = block.ReadSubBlock();
- ParseSubBlock(subBlock, ref obj, ref builder, ref material);
+ ParseSubBlock(subBlock, ref obj, ref builder, ref material, state);
}
- currentLevel--;
+ state.Level--;
break;
case TemplateID.MeshMaterialList:
int nMaterials = block.ReadInt();
@@ -381,12 +454,12 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
Array.Resize(ref builder.Materials, nMaterials + 1);
for (int i = 0; i < nMaterials; i++)
{
- currentMaterialUsed = materialsUsed[i];
+ state.MaterialUsed = materialsUsed[i];
// YUCKY: skip bracket strings
string materialName = block.ReadString();
- if (!rootMaterials.TryGetValue(materialName, out builder.Materials[i + 1]))
+ if (!state.RootMaterials.TryGetValue(materialName, out builder.Materials[i + 1]))
{
- Plugin.CurrentHost.AddMessage(MessageType.Information, false, $"Material {materialName} was not found in DirectX binary file {currentFile}");
+ Plugin.CurrentHost.AddMessage(MessageType.Information, false, $"Material {materialName} was not found in DirectX binary file {state.File}");
builder.Materials[i + 1] = new Material();
}
@@ -402,17 +475,17 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
{
for (int i = 0; i < nMaterials; i++)
{
- currentMaterialUsed = materialsUsed[i];
+ state.MaterialUsed = materialsUsed[i];
try
{
subBlock = block.ReadSubBlock(new[] { TemplateID.Material, TemplateID.TextureKey });
- ParseSubBlock(subBlock, ref obj, ref builder, ref material);
+ ParseSubBlock(subBlock, ref obj, ref builder, ref material, state);
}
catch (Exception ex)
{
if (ex is EndOfStreamException)
{
- Plugin.CurrentHost.AddMessage(MessageType.Information, false, $"{ nMaterials } materials expected, but { i } found in DirectX binary file { currentFile }");
+ Plugin.CurrentHost.AddMessage(MessageType.Information, false, $"{ nMaterials } materials expected, but { i } found in DirectX binary file { state.File }");
}
break;
}
@@ -463,18 +536,20 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
if (block.Position() < block.Length() - 5)
{
subBlock = block.ReadSubBlock(TemplateID.TextureFilename);
- ParseSubBlock(subBlock, ref obj, ref builder, ref newMaterial);
+ ParseSubBlock(subBlock, ref obj, ref builder, ref newMaterial, state);
}
- if (currentLevel == 0)
+ if (state.Level == 0)
{
// Key based material definitions
if (!string.IsNullOrEmpty(block.Label))
{
- rootMaterials[block.Label] = newMaterial;
+ state.RootMaterials[block.Label] = newMaterial;
}
}
else
{
+ // Optimize: Use a list for materials and only update the builder at the end if needed
+ // but to keep it simple, we check if current material matches before resizing
int m = builder.Materials.Length;
Array.Resize(ref builder.Materials, m + 1);
builder.Materials[m] = newMaterial;
@@ -484,7 +559,7 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
string texturePath = block.ReadString();
if (string.IsNullOrEmpty(texturePath))
{
- if (currentMaterialUsed)
+ if (state.MaterialUsed)
{
Plugin.CurrentHost.AddMessage(MessageType.Information, false, $"An empty texture was specified for material {material.Key}");
}
@@ -505,17 +580,17 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
try
{
- material.DaytimeTexture = OpenBveApi.Path.CombineFile(currentFolder, texturePath);
+ material.DaytimeTexture = OpenBveApi.Path.CombineFile(state.Folder, texturePath);
}
catch (Exception e)
{
- if (currentMaterialUsed)
+ if (state.MaterialUsed)
{
- Plugin.CurrentHost.AddMessage(MessageType.Error, false, $"Texture file path {texturePath} in file {currentFile} has the problem: {e.Message}");
+ Plugin.CurrentHost.AddMessage(MessageType.Error, false, $"Texture file path {texturePath} in file {state.File} has the problem: {e.Message}");
}
else
{
- Plugin.CurrentHost.AddMessage(MessageType.Warning, false, $"Referenced, but unused Texture file path {texturePath} for material {material.Key} in file {currentFile} has the problem: {e.Message}");
+ Plugin.CurrentHost.AddMessage(MessageType.Warning, false, $"Referenced, but unused Texture file path {texturePath} for material {material.Key} in file {state.File} has the problem: {e.Message}");
}
material.DaytimeTexture = null;
}
@@ -528,7 +603,7 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
{
byte[] stringBytes = Encoding.GetEncoding(0).GetBytes(texturePath);
string shift_jis_string = Encoding.GetEncoding("shift_jis").GetString(stringBytes);
- material.DaytimeTexture = OpenBveApi.Path.CombineFile(currentFolder, shift_jis_string);
+ material.DaytimeTexture = OpenBveApi.Path.CombineFile(state.Folder, shift_jis_string);
}
catch
{
@@ -538,13 +613,13 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
if (!File.Exists(material.DaytimeTexture) && material.DaytimeTexture != null)
{
- if (currentMaterialUsed)
+ if (state.MaterialUsed)
{
- Plugin.CurrentHost.AddMessage(MessageType.Error, true, $"Texture {material.DaytimeTexture} for material {material.Key} was not found in file {currentFile}");
+ Plugin.CurrentHost.AddMessage(MessageType.Error, true, $"Texture {material.DaytimeTexture} for material {material.Key} was not found in file {state.File}");
}
else
{
- Plugin.CurrentHost.AddMessage(MessageType.Warning, true, $"Referenced, but unused Texture {material.DaytimeTexture} for material {material.Key} was not found in file {currentFile}");
+ Plugin.CurrentHost.AddMessage(MessageType.Warning, true, $"Referenced, but unused Texture {material.DaytimeTexture} for material {material.Key} was not found in file {state.File}");
}
material.DaytimeTexture = null;
}
@@ -631,11 +706,11 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
int ml = builder.Materials.Length;
Array.Resize(ref builder.Materials, ml + 1);
builder.Materials[ml] = new Material();
- rootMaterials.TryGetValue(block.Label, out builder.Materials[ml]);
+ state.RootMaterials.TryGetValue(block.Label, out builder.Materials[ml]);
break;
case TemplateID.DeclData:
int numTemplates = (int)block.ReadDword();
- vertexElements = new VertexElement[numTemplates];
+ VertexElement[] vertexElements = new VertexElement[numTemplates];
for (int i = 0; i < numTemplates; i++)
{
vertexElements[i] = new VertexElement(block.ReadDword(), block.ReadDword(), block.ReadDword(), block.ReadDword());
@@ -659,15 +734,18 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
uint z = block.ReadDword();
Vector3 normal = new Vector3(*(float*)&x, *(float*)&y, *(float*)&z);
+ // Optimize: Avoid O(N^2) search by updating only relevant facial vertices
for (int i = 0; i < builder.Faces.Count; i++)
{
- for (int j = 0; j < builder.Faces[i].Vertices.Length; j++)
+ MeshFace f = builder.Faces[i];
+ for (int j = 0; j < f.Vertices.Length; j++)
{
- if (builder.Faces[i].Vertices[j].Index == currentVertex)
+ if (f.Vertices[j].Index == currentVertex)
{
- builder.Faces[i].Vertices[j].Normal = normal;
+ f.Vertices[j].Normal = normal;
}
}
+ builder.Faces[i] = f;
}
numRemainingDwords -= 3;
break;
@@ -719,7 +797,7 @@ private static void ParseSubBlock(Block block, ref StaticObject obj, ref MeshBui
}
}
- private static StaticObject LoadBinaryX(byte[] objectBytes, int floatingPointSize)
+ private static StaticObject LoadBinaryX(byte[] objectBytes, int floatingPointSize, XParseState state)
{
Block block = new BinaryBlock(objectBytes, floatingPointSize);
StaticObject obj = new StaticObject(Plugin.CurrentHost);
@@ -728,15 +806,15 @@ private static StaticObject LoadBinaryX(byte[] objectBytes, int floatingPointSiz
while (block.Position() < block.Length())
{
Block subBlock = block.ReadSubBlock();
- ParseSubBlock(subBlock, ref obj, ref builder, ref material);
+ ParseSubBlock(subBlock, ref obj, ref builder, ref material, state);
}
builder.Apply(ref obj, false, false);
obj.Mesh.CreateNormals();
- if (rootMatrix != Matrix4D.NoTransformation)
+ if (state.RootMatrix != Matrix4D.NoTransformation)
{
- for (int i = transformStart; i < obj.Mesh.Vertices.Length; i++)
+ for (int i = state.TransformStart; i < obj.Mesh.Vertices.Length; i++)
{
- obj.Mesh.Vertices[i].Coordinates.Transform(rootMatrix, false);
+ obj.Mesh.Vertices[i].Coordinates.Transform(state.RootMatrix, false);
}
}
return obj;
diff --git a/source/Plugins/Object.DirectX/Plugin.cs b/source/Plugins/Object.DirectX/Plugin.cs
index f41981a38..d97cc6db2 100644
--- a/source/Plugins/Object.DirectX/Plugin.cs
+++ b/source/Plugins/Object.DirectX/Plugin.cs
@@ -22,7 +22,10 @@
//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+using System;
+using System.Diagnostics;
using System.IO;
+using System.Threading;
using OpenBveApi.FileSystem;
using OpenBveApi.Hosts;
using OpenBveApi.Interface;
@@ -33,6 +36,7 @@ namespace Plugin
public class Plugin : ObjectInterface
{
internal static HostInterface CurrentHost;
+ internal static FileSystem CurrentFileSystem;
private static XParsers currentXParser = XParsers.Original;
internal static CompatabilityHacks EnabledHacks;
@@ -41,6 +45,7 @@ public class Plugin : ObjectInterface
public override void Load(HostInterface host, FileSystem fileSystem)
{
CurrentHost = host;
+ CurrentFileSystem = fileSystem;
}
public override void SetCompatibilityHacks(CompatabilityHacks enabledHacks)
@@ -60,7 +65,10 @@ public override void SetObjectParser(object parserType)
}
}
- private int pathRecursions;
+ [ThreadStatic]
+ private static int pathRecursions;
+
+ private static readonly Stopwatch wallClock = Stopwatch.StartNew();
public override bool CanLoadObject(string path)
{
@@ -115,7 +123,20 @@ public override bool LoadObject(string path, System.Text.Encoding textEncoding,
case XParsers.NewXParser:
try
{
- unifiedObject = NewXParser.ReadObject(path, textEncoding);
+ Stopwatch sw = Stopwatch.StartNew();
+ unifiedObject = NewXParser.ReadObject(path, textEncoding);
+ sw.Stop();
+ System.Threading.Interlocked.Increment(ref NewXParser.TotalCount);
+ System.Threading.Interlocked.Add(ref NewXParser.TotalLoadObjectMs, sw.Elapsed.Ticks);
+ if (sw.ElapsedMilliseconds > 25)
+ {
+ CurrentFileSystem.AppendToLogFile("Slow .x object: " + path + " (" + sw.ElapsedMilliseconds + " ms)");
+ }
+ if (NewXParser.TotalCount % 100 == 0)
+ {
+ double untimed = NewXParser.TicksToMs(NewXParser.TotalReadObjectMs - (NewXParser.TotalReadMs + NewXParser.TotalPreprocessMs + NewXParser.TotalParseMs + NewXParser.TotalApplyMs));
+ CurrentFileSystem.AppendToLogFile("XParser " + NewXParser.TotalCount + " objects: wall " + wallClock.Elapsed.TotalMilliseconds.ToString("F0") + " ms, load " + NewXParser.TicksToMs(NewXParser.TotalLoadObjectMs).ToString("F0") + " ms, read " + NewXParser.TicksToMs(NewXParser.TotalReadMs).ToString("F0") + " ms, prep " + NewXParser.TicksToMs(NewXParser.TotalPreprocessMs).ToString("F0") + " ms, parse " + NewXParser.TicksToMs(NewXParser.TotalParseMs).ToString("F0") + " ms, apply " + NewXParser.TicksToMs(NewXParser.TotalApplyMs).ToString("F0") + " ms, untimed " + untimed.ToString("F0") + " ms");
+ }
return true;
}
catch
diff --git a/source/Plugins/Route.Bve5/Components/StructureList.cs b/source/Plugins/Route.Bve5/Components/StructureList.cs
index a6abfd3d3..8277a211b 100644
--- a/source/Plugins/Route.Bve5/Components/StructureList.cs
+++ b/source/Plugins/Route.Bve5/Components/StructureList.cs
@@ -25,6 +25,7 @@
using System;
using System.IO;
using System.Linq;
+using System.Threading.Tasks;
using OpenBveApi;
using OpenBveApi.Interface;
using OpenBveApi.Objects;
@@ -63,7 +64,7 @@ private static void LoadStructureList(string FileName, bool PreviewOnly, string
// Some routes with badly optimized objects- Use a much lower threshold to avoid killing the renderer
Plugin.CurrentOptions.ObjectOptimizationBasicThreshold = 2000;
}
- for (int i = 1; i < Lines.Length; i++)
+ Parallel.For(1, Lines.Length, i =>
{
//Cycle through the list of objects
//An object index is formatted as follows:
@@ -72,12 +73,12 @@ private static void LoadStructureList(string FileName, bool PreviewOnly, string
Lines[i] = Lines[i].TrimBVE5Comments();
if (string.IsNullOrEmpty(Lines[i]))
{
- continue;
+ return;
}
if (string.IsNullOrEmpty(Lines[i]))
{
- continue;
+ return;
}
string[] splitStrings = Lines[i].Split(',');
@@ -88,13 +89,16 @@ private static void LoadStructureList(string FileName, bool PreviewOnly, string
// empty object file name
if (string.Equals(Key, "null", StringComparison.InvariantCultureIgnoreCase) || string.Equals(Key, "empty", StringComparison.InvariantCultureIgnoreCase))
{
- RouteData.Objects.Add(Key, new StaticObject(Plugin.CurrentHost));
+ lock (RouteData.Objects)
+ {
+ RouteData.Objects[Key] = new StaticObject(Plugin.CurrentHost);
+ }
}
else
{
Plugin.CurrentHost.AddMessage(MessageType.Warning, false, "BVE5: No object file was specified for key " + Lines[i]);
}
- continue;
+ return;
}
@@ -111,13 +115,16 @@ private static void LoadStructureList(string FileName, bool PreviewOnly, string
if (!File.Exists(FilePath))
{
Plugin.CurrentHost.AddMessage(MessageType.Error, false, "BVE5: Object File " + splitStrings[1] + " with key " + Key + " was not found.");
- continue;
+ return;
}
System.Text.Encoding ObjectEncoding = TextEncoding.GetSystemEncodingFromFile(FilePath);
Plugin.CurrentHost.LoadObject(FilePath, ObjectEncoding, out UnifiedObject obj);
- RouteData.Objects.Add(Key, obj);
- }
+ lock (RouteData.Objects)
+ {
+ RouteData.Objects[Key] = obj;
+ }
+ });
}
}
}
diff --git a/source/Plugins/Route.Bve5/MapParser.cs b/source/Plugins/Route.Bve5/MapParser.cs
index 65b7582a2..892a78ad4 100644
--- a/source/Plugins/Route.Bve5/MapParser.cs
+++ b/source/Plugins/Route.Bve5/MapParser.cs
@@ -23,6 +23,7 @@
//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
using System;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using Bve5_Parsing;
@@ -100,8 +101,10 @@ private static void ParseMap(string FileName, bool PreviewOnly)
throw new Exception("The BVE5 route map file: " + FileName + " was not found");
}
+ Stopwatch fileTimer = Stopwatch.StartNew();
MapParser Parser = new MapParser(FileName, true);
MapData RootData = Parser.Parse();
+ fileTimer.Stop();
System.Threading.Thread.Sleep(1);
if (plugin.Cancel) return;
@@ -110,12 +113,48 @@ private static void ParseMap(string FileName, bool PreviewOnly)
System.Threading.Thread.Sleep(1);
if (plugin.Cancel) return;
+ Stopwatch parseTimer = Stopwatch.StartNew();
ConvertToBlock(FileName, PreviewOnly, RootData, out RouteData RouteData);
+ parseTimer.Stop();
+ Plugin.CurrentHost.PluginParseTime = parseTimer.ElapsedMilliseconds;
System.Threading.Thread.Sleep(1);
if (plugin.Cancel) return;
+ Stopwatch applyTimer = Stopwatch.StartNew();
ApplyRouteData(FileName, PreviewOnly, RouteData);
+ applyTimer.Stop();
+ Plugin.CurrentHost.PluginApplyTime = applyTimer.ElapsedMilliseconds;
+
+ Plugin.FileSystem.AppendToLogFile("Bve5: Map file parse: " + fileTimer.ElapsedMilliseconds + " ms, block conversion: " + parseTimer.ElapsedMilliseconds + " ms, route apply: " + applyTimer.ElapsedMilliseconds + " ms");
+ Plugin.FileSystem.AppendToLogFile("Bve5: Blocks: " + RouteData.Blocks.Count + ", Track elements: " + GetTrackElementCount());
+ }
+
+ private static int GetTrackElementCount()
+ {
+ int count = 0;
+ for (int k = 0; k < Plugin.CurrentRoute.Tracks.Count; k++)
+ {
+ var elements = Plugin.CurrentRoute.Tracks[k].Elements;
+ if (elements == null)
+ {
+ continue;
+ }
+ for (int i = 0; i < elements.Length; i++)
+ {
+ if (elements[i].Events != null)
+ {
+ count++;
+ }
+ }
+ }
+ return count;
+ }
+
+ private static void LogPhase(string name, Stopwatch timer)
+ {
+ timer.Stop();
+ Plugin.FileSystem.AppendToLogFile("Bve5 phase: " + name + ": " + timer.ElapsedMilliseconds + " ms");
}
private static void ConvertToBlock(string FileName, bool PreviewOnly, MapData ParseData, out RouteData RouteData)
@@ -130,35 +169,47 @@ private static void ConvertToBlock(string FileName, bool PreviewOnly, MapData Pa
return;
}
+ Stopwatch phaseTimer = Stopwatch.StartNew();
foreach (string path in ParseData.StationListPaths)
{
LoadStationList(FileName, path, RouteData);
}
+ LogPhase("LoadStationList", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
foreach (string path in ParseData.StructureListPaths)
{
LoadStructureList(FileName, PreviewOnly, path, RouteData);
}
+ LogPhase("LoadStructureList", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
foreach (string path in ParseData.SignalListPaths)
{
LoadSignalList(FileName, PreviewOnly, path, RouteData);
}
+ LogPhase("LoadSignalList", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
foreach (string path in ParseData.SoundListPaths)
{
LoadSoundList(FileName, PreviewOnly, path, RouteData);
}
+ LogPhase("LoadSoundList", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
foreach (string path in ParseData.Sound3DListPaths)
{
LoadSound3DList(FileName, PreviewOnly, path, RouteData);
}
+ LogPhase("LoadSound3DList", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
if (Plugin.CurrentOptions.EnableBve5ScriptedTrain)
{
LoadScriptedTrain(FileName, PreviewOnly, ParseData, RouteData);
}
+ LogPhase("LoadScriptedTrain", phaseTimer);
System.Threading.Thread.Sleep(1);
if (plugin.Cancel) return;
@@ -174,24 +225,50 @@ private static void ConvertToBlock(string FileName, bool PreviewOnly, MapData Pa
*
*/
+ phaseTimer = Stopwatch.StartNew();
ConvertData(ParseData, RouteData, PreviewOnly);
+ LogPhase("ConvertData", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
ConvertTrack(ParseData, RouteData);
+ LogPhase("ConvertTrack", phaseTimer);
System.Threading.Thread.Sleep(1);
if (plugin.Cancel) return;
+ phaseTimer = Stopwatch.StartNew();
ConfirmCurve(RouteData.Blocks);
+ LogPhase("ConfirmCurve", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
ConfirmGradient(RouteData.Blocks);
+ LogPhase("ConfirmGradient", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
ConfirmTrack(RouteData);
+ LogPhase("ConfirmTrack", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
ConfirmStructure(PreviewOnly, ParseData, RouteData);
+ LogPhase("ConfirmStructure", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
ConfirmRepeater(PreviewOnly, ParseData, RouteData);
+ LogPhase("ConfirmRepeater", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
ConfirmSection(PreviewOnly, ParseData, RouteData);
+ LogPhase("ConfirmSection", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
ConfirmSignal(PreviewOnly, ParseData, RouteData);
+ LogPhase("ConfirmSignal", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
ConfirmBeacon(PreviewOnly, ParseData, RouteData);
+ LogPhase("ConfirmBeacon", phaseTimer);
// these require looping through existing blocks, so need to be here at the minute
+ phaseTimer = Stopwatch.StartNew();
ConfirmIrregularity(PreviewOnly, RouteData);
+ LogPhase("ConfirmIrregularity", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
ConfirmAdhesion(PreviewOnly, RouteData);
+ LogPhase("ConfirmAdhesion", phaseTimer);
+ phaseTimer = Stopwatch.StartNew();
ConfirmFlangeNoise(PreviewOnly, ParseData, RouteData);
+ LogPhase("ConfirmFlangeNoise", phaseTimer);
}
private static void ConvertData(MapData parseData, RouteData routeData, bool previewOnly)
diff --git a/source/Plugins/Route.Bve5/MapParser/ApplyRouteData.cs b/source/Plugins/Route.Bve5/MapParser/ApplyRouteData.cs
index d8ede1d76..7af218389 100644
--- a/source/Plugins/Route.Bve5/MapParser/ApplyRouteData.cs
+++ b/source/Plugins/Route.Bve5/MapParser/ApplyRouteData.cs
@@ -37,6 +37,7 @@
using RouteManager2.SignalManager;
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
namespace Route.Bve5
@@ -44,6 +45,16 @@ namespace Route.Bve5
internal static partial class Bve5ScenarioParser
{
internal static Plugin plugin;
+ private static readonly Stopwatch swCrawl = new Stopwatch();
+ private static readonly Stopwatch swCrawlSetup = new Stopwatch();
+ private static readonly Stopwatch swCrawlTurn = new Stopwatch();
+ private static readonly Stopwatch swCrawlRails = new Stopwatch();
+ private static readonly Stopwatch swCrawlFinal = new Stopwatch();
+ private static readonly Stopwatch swEvents = new Stopwatch();
+ private static readonly Stopwatch swSections = new Stopwatch();
+ private static readonly Stopwatch swRailObjects = new Stopwatch();
+ private static readonly Stopwatch swWorldSounds = new Stopwatch();
+ private static readonly Stopwatch swOther = new Stopwatch();
private static void ApplyRouteData(string FileName, bool PreviewOnly, RouteData Data)
{
Plugin.CurrentOptions.UnitOfSpeed = "km/h";
@@ -133,9 +144,9 @@ private static void ApplyRouteData(string FileName, bool PreviewOnly, RouteData
for (int i = 0; i < Data.Blocks.Count; i++)
{
plugin.CurrentProgress = 0.6667 + i * progressFactor;
- if ((i & 15) == 0)
+ if ((i & 63) == 0)
{
- System.Threading.Thread.Sleep(1);
+ System.Threading.Thread.Yield();
if (plugin.Cancel) return;
}
@@ -146,6 +157,7 @@ private static void ApplyRouteData(string FileName, bool PreviewOnly, RouteData
// normalize
Direction.Normalize();
+ swCrawlSetup.Start();
TrackElement WorldTrackElement = Data.Blocks[i].CurrentTrackState;
int n = CurrentTrackLength;
for (int k = 0; k < Plugin.CurrentRoute.Tracks.Count; k++)
@@ -172,6 +184,8 @@ private static void ApplyRouteData(string FileName, bool PreviewOnly, RouteData
}
// background
+ swCrawlSetup.Stop();
+ swEvents.Start();
if (!PreviewOnly)
{
if (!string.IsNullOrEmpty(Data.Blocks[i].Background))
@@ -294,6 +308,8 @@ private static void ApplyRouteData(string FileName, bool PreviewOnly, RouteData
// sections
+ swEvents.Stop();
+ swSections.Start();
if (!PreviewOnly)
{
// sections
@@ -304,6 +320,8 @@ private static void ApplyRouteData(string FileName, bool PreviewOnly, RouteData
}
// rail-aligned objects
+ swSections.Stop();
+ swRailObjects.Start();
if (!PreviewOnly)
{
for (int j = 0; j < Data.Blocks[i].Rails.Count; j++)
@@ -433,6 +451,8 @@ private static void ApplyRouteData(string FileName, bool PreviewOnly, RouteData
}
// turn
+ swRailObjects.Stop();
+ swCrawlTurn.Start();
if (Data.Blocks[i].Turn != 0.0)
{
double ag = -Math.Atan(Data.Blocks[i].Turn);
@@ -446,6 +466,8 @@ private static void ApplyRouteData(string FileName, bool PreviewOnly, RouteData
Plugin.CurrentRoute.Tracks[0].Elements[n].Pitch = Data.Blocks[i].Pitch;
// curves
+ swCrawlTurn.Stop();
+ swCrawlRails.Start();
CalcTransformation(WorldTrackElement.CurveRadius, Data.Blocks[i].Pitch, BlockInterval, ref Direction, out double a, out double c, out double h);
if (!PreviewOnly)
@@ -499,19 +521,25 @@ private static void ApplyRouteData(string FileName, bool PreviewOnly, RouteData
}
// world sounds
+ swCrawlRails.Stop();
+ swWorldSounds.Start();
for (int k = 0; k < Data.Blocks[i].SoundEvents.Count; k++)
{
Data.Blocks[i].SoundEvents[k].Create(Data, n, StartingDistance, Position, Direction);
}
// finalize block
+ swWorldSounds.Stop();
+ swCrawlFinal.Start();
Position.X += Direction.X * c;
Position.Y += h;
Position.Z += Direction.Y * c;
Direction.Rotate(-a);
+ swCrawlFinal.Stop();
}
// transponders
+ swOther.Start();
if (!PreviewOnly)
{
for (int i = 0; i < Data.Blocks.Count; i++)
@@ -643,6 +671,8 @@ private static void ApplyRouteData(string FileName, bool PreviewOnly, RouteData
{
ComputeCantTangents();
}
+ swOther.Stop();
+ Plugin.FileSystem.AppendToLogFile("Bve5: Apply breakdown: setup " + swCrawlSetup.ElapsedMilliseconds + " ms, turn " + swCrawlTurn.ElapsedMilliseconds + " ms, rails " + swCrawlRails.ElapsedMilliseconds + " ms, final " + swCrawlFinal.ElapsedMilliseconds + " ms, events " + swEvents.ElapsedMilliseconds + " ms, sections " + swSections.ElapsedMilliseconds + " ms, railObjects " + swRailObjects.ElapsedMilliseconds + " ms, worldSounds " + swWorldSounds.ElapsedMilliseconds + " ms, other " + swOther.ElapsedMilliseconds + " ms");
}
private static void ComputeCantTangents()
diff --git a/source/Plugins/Route.Bve5/MapParser/ConfirmComponents.cs b/source/Plugins/Route.Bve5/MapParser/ConfirmComponents.cs
index bf96f0012..5823a2bbe 100644
--- a/source/Plugins/Route.Bve5/MapParser/ConfirmComponents.cs
+++ b/source/Plugins/Route.Bve5/MapParser/ConfirmComponents.cs
@@ -25,6 +25,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
using Bve5_Parsing.MapGrammar;
using Bve5_Parsing.MapGrammar.EvaluateData;
using OpenBveApi.Colors;
@@ -421,6 +422,7 @@ private static void ConfirmStructure(bool PreviewOnly, MapData ParseData, RouteD
IList Blocks = RouteData.Blocks;
+ Dictionary> BlockStatements = new Dictionary>();
foreach (Statement Statement in ParseData.Statements)
{
if (Statement.ElementName != MapElementName.Structure)
@@ -428,79 +430,91 @@ private static void ConfirmStructure(bool PreviewOnly, MapData ParseData, RouteD
continue;
}
- switch (Statement.FunctionName)
+ int BlockIndex = RouteData.sortedBlocks.FindBlockIndex(Statement.Distance);
+ if (!BlockStatements.TryGetValue(BlockIndex, out List BlockStatementList))
+ {
+ BlockStatementList = new List();
+ BlockStatements.Add(BlockIndex, BlockStatementList);
+ }
+ BlockStatementList.Add(Statement);
+ }
+
+ Parallel.ForEach(BlockStatements, (blockStatementGroup) =>
+ {
+ int GroupBlockIndex = blockStatementGroup.Key;
+ foreach (Statement Statement in blockStatementGroup.Value)
{
- case MapFunctionName.Put:
- case MapFunctionName.Put0:
+ switch (Statement.FunctionName)
{
- string TrackKey = Statement.GetArgumentValueAsString(ArgumentName.TrackKey);
- if (string.IsNullOrEmpty(TrackKey))
+ case MapFunctionName.Put:
+ case MapFunctionName.Put0:
{
- TrackKey = "0";
- }
+ string TrackKey = Statement.GetArgumentValueAsString(ArgumentName.TrackKey);
+ if (string.IsNullOrEmpty(TrackKey))
+ {
+ TrackKey = "0";
+ }
- if (!RouteData.Objects.ContainsKey(Statement.Key))
- {
- Plugin.CurrentHost.AddMessage(MessageType.Error, true, "BVE5: Structure " + Statement.Key + " was not found on Track " + TrackKey + " at track position " + Statement.Distance + "m");
- continue;
- }
+ if (!RouteData.Objects.ContainsKey(Statement.Key))
+ {
+ Plugin.CurrentHost.AddMessage(MessageType.Error, true, "BVE5: Structure " + Statement.Key + " was not found on Track " + TrackKey + " at track position " + Statement.Distance + "m");
+ continue;
+ }
- if (!RouteData.TrackKeyList.Contains(TrackKey, StringComparer.OrdinalIgnoreCase))
- {
- Plugin.CurrentHost.AddMessage(MessageType.Warning, false, "BVE5: Attempted to place Structure " + Statement.Key + " on the non-existent track " + TrackKey + " at track position " + Statement.Distance + "m");
- TrackKey = "0";
- }
-
- double RX = Statement.GetArgumentValueAsDouble(ArgumentName.RX);
- double RY = Statement.GetArgumentValueAsDouble(ArgumentName.RY);
- double RZ = Statement.GetArgumentValueAsDouble(ArgumentName.RZ);
- int Tilt = Statement.GetArgumentValueAsInt(ArgumentName.Tilt);
- double Span = Statement.GetArgumentValueAsDouble(ArgumentName.Span);
-
- if (Tilt > 3)
- {
- Plugin.CurrentHost.AddMessage(MessageType.Warning, false, "BVE5: Invalid ObjectTransformType for Structure " + Statement.Key + " on track " + TrackKey + " at track position " + Statement.Distance + "m");
- Tilt = 0;
- }
+ if (!RouteData.TrackKeyList.Contains(TrackKey, StringComparer.OrdinalIgnoreCase))
+ {
+ Plugin.CurrentHost.AddMessage(MessageType.Warning, false, "BVE5: Attempted to place Structure " + Statement.Key + " on the non-existent track " + TrackKey + " at track position " + Statement.Distance + "m");
+ TrackKey = "0";
+ }
- int BlockIndex = RouteData.sortedBlocks.FindBlockIndex(Statement.Distance);
+ double RX = Statement.GetArgumentValueAsDouble(ArgumentName.RX);
+ double RY = Statement.GetArgumentValueAsDouble(ArgumentName.RY);
+ double RZ = Statement.GetArgumentValueAsDouble(ArgumentName.RZ);
+ int Tilt = Statement.GetArgumentValueAsInt(ArgumentName.Tilt);
+ double Span = Statement.GetArgumentValueAsDouble(ArgumentName.Span);
- if (!Blocks[BlockIndex].FreeObjects.ContainsKey(TrackKey))
- {
- Blocks[BlockIndex].FreeObjects.Add(TrackKey, new List());
- }
+ if (Tilt > 3)
+ {
+ Plugin.CurrentHost.AddMessage(MessageType.Warning, false, "BVE5: Invalid ObjectTransformType for Structure " + Statement.Key + " on track " + TrackKey + " at track position " + Statement.Distance + "m");
+ Tilt = 0;
+ }
- Vector3 position = new Vector3(Statement.GetArgumentValueAsDouble(ArgumentName.X), Statement.GetArgumentValueAsDouble(ArgumentName.Y), Statement.GetArgumentValueAsDouble(ArgumentName.Z));
- Blocks[BlockIndex].FreeObjects[TrackKey].Add(new FreeObj(Statement.Distance, Statement.Key, position, RY.ToRadians(), -RX.ToRadians(), RZtoRoll(RY, RZ).ToRadians(), (ObjectTransformType)Tilt, Span));
- }
- break;
- case MapFunctionName.PutBetween:
- {
- string[] TrackKeys = new string[2];
- if (!Statement.HasArgument(ArgumentName.TrackKey1) || string.IsNullOrEmpty(TrackKeys[0] = Statement.GetArgumentValueAsString(ArgumentName.TrackKey1)))
- {
- TrackKeys[0] = "0";
+ if (!Blocks[GroupBlockIndex].FreeObjects.ContainsKey(TrackKey))
+ {
+ Blocks[GroupBlockIndex].FreeObjects.Add(TrackKey, new List());
+ }
+
+ Vector3 position = new Vector3(Statement.GetArgumentValueAsDouble(ArgumentName.X), Statement.GetArgumentValueAsDouble(ArgumentName.Y), Statement.GetArgumentValueAsDouble(ArgumentName.Z));
+ Blocks[GroupBlockIndex].FreeObjects[TrackKey].Add(new FreeObj(Statement.Distance, Statement.Key, position, RY.ToRadians(), -RX.ToRadians(), RZtoRoll(RY, RZ).ToRadians(), (ObjectTransformType)Tilt, Span));
}
- if (!Statement.HasArgument(ArgumentName.TrackKey2) || string.IsNullOrEmpty(TrackKeys[1] = Statement.GetArgumentValueAsString(ArgumentName.TrackKey2)))
+ break;
+ case MapFunctionName.PutBetween:
{
- TrackKeys[1] = "0";
- }
+ string[] TrackKeys = new string[2];
+ if (!Statement.HasArgument(ArgumentName.TrackKey1) || string.IsNullOrEmpty(TrackKeys[0] = Statement.GetArgumentValueAsString(ArgumentName.TrackKey1)))
+ {
+ TrackKeys[0] = "0";
+ }
+ if (!Statement.HasArgument(ArgumentName.TrackKey2) || string.IsNullOrEmpty(TrackKeys[1] = Statement.GetArgumentValueAsString(ArgumentName.TrackKey2)))
+ {
+ TrackKeys[1] = "0";
+ }
- if (!RouteData.Objects.ContainsKey(Statement.Key))
- {
- Plugin.CurrentHost.AddMessage(MessageType.Error, true, "BVE5: Structure " + Statement.Key + " was not found for PutBetween Track " + TrackKeys[0] + " and Track " + TrackKeys[1] + " at track position " + Statement.Distance + "m");
- continue;
- }
+ if (!RouteData.Objects.ContainsKey(Statement.Key))
+ {
+ Plugin.CurrentHost.AddMessage(MessageType.Error, true, "BVE5: Structure " + Statement.Key + " was not found for PutBetween Track " + TrackKeys[0] + " and Track " + TrackKeys[1] + " at track position " + Statement.Distance + "m");
+ continue;
+ }
- if (RouteData.TrackKeyList.Contains(TrackKeys[0], StringComparer.OrdinalIgnoreCase) && RouteData.TrackKeyList.Contains(TrackKeys[1]))
- {
- int BlockIndex = RouteData.sortedBlocks.FindBlockIndex(Statement.Distance);
- Blocks[BlockIndex].Cracks.Add(new Crack(Statement.Key, Statement.Distance, TrackKeys[0], TrackKeys[1]));
+ if (RouteData.TrackKeyList.Contains(TrackKeys[0], StringComparer.OrdinalIgnoreCase) && RouteData.TrackKeyList.Contains(TrackKeys[1]))
+ {
+ Blocks[GroupBlockIndex].Cracks.Add(new Crack(Statement.Key, Statement.Distance, TrackKeys[0], TrackKeys[1]));
+ }
}
+ break;
}
- break;
}
- }
+ });
}
private static void ConfirmRepeater(bool PreviewOnly, MapData ParseData, RouteData RouteData)
@@ -510,8 +524,7 @@ private static void ConfirmRepeater(bool PreviewOnly, MapData ParseData, RouteDa
return;
}
- List RepeaterList = new List();
-
+ Dictionary> RepeaterStatements = new Dictionary>(StringComparer.InvariantCultureIgnoreCase);
foreach (Statement Statement in ParseData.Statements)
{
if (Statement.ElementName != MapElementName.Repeater)
@@ -519,22 +532,21 @@ private static void ConfirmRepeater(bool PreviewOnly, MapData ParseData, RouteDa
continue;
}
- if (!RepeaterList.Exists(Repeater => Repeater.Key.Equals(Statement.Key, StringComparison.InvariantCultureIgnoreCase)))
+ if (!RepeaterStatements.TryGetValue(Statement.Key, out List Statements))
{
- RepeaterList.Add(new Repeater(Statement.Key));
+ Statements = new List();
+ RepeaterStatements.Add(Statement.Key, Statements);
}
+ Statements.Add(Statement);
}
- foreach (Repeater Repeater in RepeaterList)
+ foreach (KeyValuePair> RepeaterGroup in RepeaterStatements)
{
+ Repeater Repeater = new Repeater(RepeaterGroup.Key);
double lastDistance = -1;
bool possibleEnd = false;
- foreach (Statement Statement in ParseData.Statements)
+ foreach (Statement Statement in RepeaterGroup.Value)
{
- if (Statement.ElementName != MapElementName.Repeater || !Statement.Key.Equals(Repeater.Key, StringComparison.InvariantCultureIgnoreCase))
- {
- continue;
- }
switch (Statement.FunctionName)
{
diff --git a/source/Plugins/Route.Bve5/MapParser/ConvertComponents.cs b/source/Plugins/Route.Bve5/MapParser/ConvertComponents.cs
index f10c9434b..fd5623973 100644
--- a/source/Plugins/Route.Bve5/MapParser/ConvertComponents.cs
+++ b/source/Plugins/Route.Bve5/MapParser/ConvertComponents.cs
@@ -23,6 +23,8 @@
//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
using System;
+using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using Bve5_Parsing.MapGrammar;
using Bve5_Parsing.MapGrammar.EvaluateData;
@@ -465,17 +467,31 @@ private static void ConvertTrack(MapData ParseData, RouteData RouteData)
{
// Own track is excluded.
+ Dictionary> TrackStatements = new Dictionary>(StringComparer.InvariantCultureIgnoreCase);
+ foreach (Statement Statement in ParseData.Statements)
+ {
+ if (Statement.ElementName != MapElementName.Track)
+ {
+ continue;
+ }
+
+ if (!TrackStatements.TryGetValue(Statement.Key, out List RailStatements))
+ {
+ RailStatements = new List();
+ TrackStatements.Add(Statement.Key, RailStatements);
+ }
+ RailStatements.Add(Statement);
+ }
+
for (int railIndex = 1; railIndex < RouteData.TrackKeyList.Count; railIndex++)
{
string railKey = RouteData.TrackKeyList[railIndex];
- foreach (Statement Statement in ParseData.Statements)
+ Stopwatch railTimer = Stopwatch.StartNew();
+ if (TrackStatements.TryGetValue(railKey, out List ProcessedRailStatements))
{
- if (Statement.ElementName != MapElementName.Track || !Statement.Key.Equals(RouteData.TrackKeyList[railIndex], StringComparison.InvariantCultureIgnoreCase))
+ foreach (Statement Statement in ProcessedRailStatements)
{
- continue;
- }
-
- dynamic d = Statement;
+ dynamic d = Statement;
if (Statement.FunctionName == MapFunctionName.Position || (Statement.HasSubElement && Statement.SubElementName == MapSubElementName.X))
{
@@ -630,6 +646,7 @@ private static void ConvertTrack(MapData ParseData, RouteData RouteData)
}
}
}
+ }
if (!RouteData.Blocks.First().Rails[railKey].InterpolateX)
{
@@ -678,8 +695,11 @@ private static void ConvertTrack(MapData ParseData, RouteData RouteData)
RouteData.Blocks.Last().Rails[railKey].RadiusV = LastInterpolateIndex != -1 ? RouteData.Blocks[LastInterpolateIndex].Rails[railKey].RadiusV : 0.0;
RouteData.Blocks.Last().Rails[railKey].InterpolateY = true;
}
+ railTimer.Stop();
+ Plugin.FileSystem.AppendToLogFile("Bve5 phase: ConvertTrack statements rail " + railKey + ": " + railTimer.ElapsedMilliseconds + " ms");
}
+ Stopwatch passTimer = Stopwatch.StartNew();
for (int j = 1; j < RouteData.TrackKeyList.Count; j++)
{
string railKey = RouteData.TrackKeyList[j];
@@ -720,7 +740,9 @@ private static void ConvertTrack(MapData ParseData, RouteData RouteData)
i = RouteData.sortedBlocks.IndexOfKey(dist) + 1;
}
}
+ LogPhase("ConvertTrack X interp", passTimer);
+ passTimer = Stopwatch.StartNew();
for (int j = 1; j < RouteData.TrackKeyList.Count; j++)
{
string railKey = RouteData.TrackKeyList[j];
@@ -761,7 +783,9 @@ private static void ConvertTrack(MapData ParseData, RouteData RouteData)
i = RouteData.sortedBlocks.IndexOfKey(dist) + 1;
}
}
+ LogPhase("ConvertTrack Y interp", passTimer);
+ passTimer = Stopwatch.StartNew();
for (int j = 1; j < RouteData.TrackKeyList.Count; j++)
{
string railKey = RouteData.TrackKeyList[j];
@@ -808,7 +832,9 @@ private static void ConvertTrack(MapData ParseData, RouteData RouteData)
i = RouteData.sortedBlocks.IndexOfKey(dist) + 1;
}
}
+ LogPhase("ConvertTrack curve transition", passTimer);
+ passTimer = Stopwatch.StartNew();
for (int j = 1; j < RouteData.TrackKeyList.Count; j++)
{
string railKey = RouteData.TrackKeyList[j];
@@ -846,22 +872,23 @@ private static void ConvertTrack(MapData ParseData, RouteData RouteData)
double EndDistance = RouteData.Blocks[i].StartingDistance;
double EndCant = RouteData.Blocks[i].Rails[railKey].CurveCant;
- if (StartCant == EndCant)
- {
- i++;
- continue;
- }
-
- for (double k = StartDistance; k < EndDistance; k += InterpolateInterval)
- {
- RouteData.FindOrAddBlock(k);
- }
+ if (StartCant == EndCant)
+ {
+ i++;
+ continue;
}
- // now use distance to retrieve the *new* index of said block after insertions (+1 to carry on with loop)
- i = RouteData.sortedBlocks.IndexOfKey(dist) + 1;
+ for (double k = StartDistance; k < EndDistance; k += InterpolateInterval)
+ {
+ RouteData.FindOrAddBlock(k);
+ }
}
+
+ // now use distance to retrieve the *new* index of said block after insertions (+1 to carry on with loop)
+ i = RouteData.sortedBlocks.IndexOfKey(dist) + 1;
+ }
}
+ LogPhase("ConvertTrack curve interp", passTimer);
}
private static int CurrentStation = 0;
diff --git a/source/RouteViewer/InterfaceR.cs b/source/RouteViewer/InterfaceR.cs
index f110f5559..a324d41cf 100644
--- a/source/RouteViewer/InterfaceR.cs
+++ b/source/RouteViewer/InterfaceR.cs
@@ -17,7 +17,10 @@ internal static class Interface {
internal static readonly List LogMessages = new List();
internal static void AddMessage(MessageType type, bool fileNotFound, string text) {
- LogMessages.Add(new LogMessage(type, fileNotFound, text));
+ lock (LogMessages)
+ {
+ LogMessages.Add(new LogMessage(type, fileNotFound, text));
+ }
}
}
}
diff --git a/source/RouteViewer/LoadingR.cs b/source/RouteViewer/LoadingR.cs
index 92ba60d46..2586598d2 100644
--- a/source/RouteViewer/LoadingR.cs
+++ b/source/RouteViewer/LoadingR.cs
@@ -138,6 +138,7 @@ private static void LoadEverythingThreaded() {
RouteParseTime = parseTimer.ElapsedMilliseconds;
ParserParseTime = Program.CurrentHost.PluginParseTime;
ParserApplyTime = Program.CurrentHost.PluginApplyTime;
+ Program.FileSystem.AppendToLogFile("Object loading: " + Host.TotalObjectLoadCount + " objects, parse: " + Host.TotalObjectParseMs + " ms, optimize: " + Host.TotalObjectOptimizeMs + " ms");
if (!loaded)
{
diff --git a/source/RouteViewer/System/Host.cs b/source/RouteViewer/System/Host.cs
index 0926422dc..510575a54 100644
--- a/source/RouteViewer/System/Host.cs
+++ b/source/RouteViewer/System/Host.cs
@@ -27,6 +27,15 @@ internal class Host : HostInterface
/// Total time spent registering textures, in milliseconds.
internal static long TextureRegistrationTime;
+ /// Total time spent parsing object files, in milliseconds.
+ internal static long TotalObjectParseMs;
+
+ /// Total time spent optimizing object meshes, in milliseconds.
+ internal static long TotalObjectOptimizeMs;
+
+ /// The number of unique object files parsed.
+ internal static int TotalObjectLoadCount;
+
/// Reports a problem to the host application.
/// The type of problem that is reported.
/// The textual message that describes the problem.
@@ -353,7 +362,8 @@ 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);
+ ValueTuple cacheKey = ValueTuple.Create(path.ToLowerInvariant(), PreserveVertices, File.GetLastWriteTime(path));
+ StaticObjectCache.TryAdd(cacheKey, Object);
return true;
}
@@ -361,9 +371,8 @@ public override bool LoadStaticObject(string path, System.Text.Encoding Encoding
// may be trying to load in different places, so leave
Interface.AddMessage(MessageType.Error, false, "Attempted to load " + path + " which is an animated object where only static objects are allowed.");
}
- if (!FailedObjects.Contains(path))
+ if (FailedObjects.TryAdd(path, true))
{
- FailedObjects.Add(path);
Interface.AddMessage(MessageType.Error, false, "Plugin " + Program.CurrentHost.Plugins[i].Title + " returned unsuccessfully at LoadObject");
}
@@ -380,9 +389,8 @@ public override bool LoadStaticObject(string path, System.Text.Encoding Encoding
}
}
}
- if (!FailedObjects.Contains(path))
+ if (FailedObjects.TryAdd(path, true))
{
- FailedObjects.Add(path);
Interface.AddMessage(MessageType.Error, false, "No plugin found that is capable of loading object " + path);
}
@@ -416,31 +424,38 @@ public override bool LoadObject(string path, System.Text.Encoding Encoding, out
{
try
{
+ Stopwatch parseTimer = Stopwatch.StartNew();
if (Program.CurrentHost.Plugins[i].Object.LoadObject(path, Encoding, out UnifiedObject obj))
{
+ parseTimer.Stop();
+ System.Threading.Interlocked.Add(ref Host.TotalObjectParseMs, parseTimer.ElapsedMilliseconds);
+ System.Threading.Interlocked.Increment(ref Host.TotalObjectLoadCount);
if (obj == null)
{
continue;
}
+ Stopwatch optimizeTimer = Stopwatch.StartNew();
obj.OptimizeObject(false, Interface.CurrentOptions.ObjectOptimizationBasicThreshold, true);
+ optimizeTimer.Stop();
+ System.Threading.Interlocked.Add(ref Host.TotalObjectOptimizeMs, optimizeTimer.ElapsedMilliseconds);
Object = obj;
if (Object is StaticObject staticObject)
{
- StaticObjectCache.Add(ValueTuple.Create(path.ToLowerInvariant(), false, File.GetLastWriteTime(path)), staticObject);
+ ValueTuple cacheKey = ValueTuple.Create(path.ToLowerInvariant(), false, File.GetLastWriteTime(path));
+ StaticObjectCache.TryAdd(cacheKey, staticObject);
return true;
}
if (Object is AnimatedObjectCollection aoc)
{
- AnimatedObjectCollectionCache.Add(path.ToLowerInvariant(), aoc);
+ AnimatedObjectCollectionCache.TryAdd(path.ToLowerInvariant(), aoc);
}
return true;
}
- if (!FailedObjects.Contains(path))
+ if (FailedObjects.TryAdd(path, true))
{
- FailedObjects.Add(path);
Interface.AddMessage(MessageType.Error, false, "Plugin " + Program.CurrentHost.Plugins[i].Title + " returned unsuccessfully at LoadObject");
}
@@ -461,17 +476,15 @@ public override bool LoadObject(string path, System.Text.Encoding Encoding, out
FileInfo f = new FileInfo(path);
if (f.Length == 0)
{
- if (!NullFiles.Contains(Path.GetFileNameWithoutExtension(path).ToLowerInvariant()) && !FailedObjects.Contains(path))
+ if (!NullFiles.Contains(Path.GetFileNameWithoutExtension(path).ToLowerInvariant()) && FailedObjects.TryAdd(path, true))
{
- FailedObjects.Add(path);
Interface.AddMessage(MessageType.Error, false, "Zero-byte object file encountered at " + path);
}
}
else
{
- if (!NullFiles.Contains(Path.GetFileNameWithoutExtension(path).ToLowerInvariant()) && !FailedObjects.Contains(path))
+ if (!NullFiles.Contains(Path.GetFileNameWithoutExtension(path).ToLowerInvariant()) && FailedObjects.TryAdd(path, true))
{
- FailedObjects.Add(path);
Interface.AddMessage(MessageType.Error, false, "No plugin found that is capable of loading object " + path);
}
}