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); } }