diff --git a/src/Perpetuum/IO/FileSystem.cs b/src/Perpetuum/IO/FileSystem.cs index f0cb9766..7d40f3d2 100644 --- a/src/Perpetuum/IO/FileSystem.cs +++ b/src/Perpetuum/IO/FileSystem.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Security.Cryptography; namespace Perpetuum.IO { @@ -37,6 +38,53 @@ public void WriteAllBytes(string path, byte[] bytes) File.WriteAllBytes(CreatePath(path),bytes); } + public byte[] WriteAllBytesAndMD5(string path, ReadOnlySpan bytes, int size) + { + // Allocate the resulting array to return. MD5 hash is always 16 bytes. + // This is the only managed allocation in the entire method. + byte[] finalHash = new byte[16]; + + // Initialize incremental hashing without creating heavy managed objects + using (var incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.MD5)) + // Open the file handle directly via the OS to bypass FileStream overhead + using (var handle = File.OpenHandle(CreatePath(path), FileMode.Create, FileAccess.Write, FileShare.None)) + { + // Allocate a local buffer on the thread's stack (isolated from other threads). + // x4096 bytes is an optimal size aligned with most OS disk sector sizes. + Span stackBuffer = stackalloc byte[4096 * size]; + + int position = 0; + int remaining = bytes.Length; + + while (remaining > 0) + { + int chunkSize = Math.Min(remaining, stackBuffer.Length); + + // ATOMIC SNAPSHOT: Copy data from the shared memory into the protected stack buffer. + // Even if other threads modify sharedBytes concurrently, stackBuffer + // captures a stable point-in-time snapshot of this specific chunk. + bytes.Slice(position, chunkSize).CopyTo(stackBuffer); + + // Work strictly with the isolated snapshot + var currentChunk = stackBuffer.Slice(0, chunkSize); + + // Write the exact captured snapshot to the disk + RandomAccess.Write(handle, currentChunk, position); + + // Feed the exact same captured snapshot into the hash engine + incrementalHash.AppendData(currentChunk); + + position += chunkSize; + remaining -= chunkSize; + } + + // Finalize the hash computation directly into the allocated return array + incrementalHash.GetCurrentHash(finalHash); + } + + return finalHash; + } + public void WriteAllLines(string path, IEnumerable lines) { File.WriteAllLines(CreatePath(path), lines); @@ -82,5 +130,19 @@ public override string ToString() { return $"Root: {_root}"; } + + public byte[] MD5SUM(string path) + { + // We use the hash algorithm MD5 + using (var md5 = MD5.Create()) + { + // Using a file stream for reading to calculate the hash + using (var stream = File.OpenRead(CreatePath(path))) + { + // The method itself will read the entire stream to the end + return md5.ComputeHash(stream); + } + } + } } -} \ No newline at end of file +} diff --git a/src/Perpetuum/IO/FileSystemExtensions.cs b/src/Perpetuum/IO/FileSystemExtensions.cs index 2f189837..e410b399 100644 --- a/src/Perpetuum/IO/FileSystemExtensions.cs +++ b/src/Perpetuum/IO/FileSystemExtensions.cs @@ -1,4 +1,7 @@ +using System; using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Perpetuum.IO { @@ -6,7 +9,57 @@ public static class FileSystemExtensions { public static T[] ReadLayer(this IFileSystem fileSystem, string filename) where T : struct { - return fileSystem.ReadAllBytes(CreateLayerPath(filename)).ToArray(); + var path = fileSystem.CreatePath(CreateLayerPath(filename)); + + // Open the stream and read data directly into a T[] to avoid an extra intermediate byte[] + using var stream = File.OpenRead(path); + + var sizeOfT = Marshal.SizeOf(); + if (sizeOfT <= 0) + return Array.Empty(); + + long length = stream.Length; + if (length == 0) + return Array.Empty(); + + int count = (int)(length / sizeOfT); + + // If the file size is not a multiple of sizeof(T), fall back to the previous byte[] reading logic + if (length % sizeOfT != 0) + { + var bytes = fileSystem.ReadAllBytes(CreateLayerPath(filename)); + return bytes.ToArray(); + } + + var result = new T[count]; + + // For blittable types we can read directly into the byte representation of the T[] + if (!RuntimeHelpers.IsReferenceOrContainsReferences()) + { + var span = MemoryMarshal.AsBytes(result.AsSpan()); + + int read = 0; + while (read < span.Length) + { + int n = stream.Read(span.Slice(read)); + if (n == 0) + break; + read += n; + } + + if (read != span.Length) + { + // Read did not complete as expected; fall back to the safe byte[] path + var bytes = fileSystem.ReadAllBytes(CreateLayerPath(filename)); + return bytes.ToArray(); + } + + return result; + } + + // For non-blittable structs (with reference fields) read into a byte buffer and convert + var buffer = fileSystem.ReadAllBytes(CreateLayerPath(filename)); + return buffer.ToArray(); } public static byte[] ReadLayerAsByteArray(this IFileSystem fileSystem, string filename) @@ -14,6 +67,30 @@ public static byte[] ReadLayerAsByteArray(this IFileSystem fileSystem, string fi return fileSystem.ReadAllBytes(CreateLayerPath(filename)); } + /// + /// Returns a hash for the layer file. + /// + /// + /// Layer filename + /// 16 bytes of hash + public static byte[] MD5(this IFileSystem fileSystem, string filename) + { + return fileSystem.MD5SUM(CreateLayerPath(filename)); + } + + /// + /// Writes all bytes to a file on disk, calculating the hash in parallel. + /// + /// + /// Layer filename + /// Layer data as bytes + /// Size fo saved type + /// 16 bytes of hash + public static byte[] WriteLayerAndMD5(this IFileSystem fileSystem, string filename, ReadOnlySpan bytes, int size) + { + return fileSystem.WriteAllBytesAndMD5(CreateLayerPath(filename), bytes, size); + } + private static string CreateLayerPath(string filename) { return Path.Combine("layers", filename); diff --git a/src/Perpetuum/IO/IFileSystem.cs b/src/Perpetuum/IO/IFileSystem.cs index 2f4bff3d..4ad5bc06 100644 --- a/src/Perpetuum/IO/IFileSystem.cs +++ b/src/Perpetuum/IO/IFileSystem.cs @@ -10,6 +10,15 @@ public interface IFileSystem string[] ReadAllLines(string path); void WriteAllBytes(string path, byte[] bytes); + + /// + /// Writes all bytes to a file on disk, calculating the hash in parallel. + /// + /// Path within the game's root directory + /// Bytes to write + /// Size fo saved type + /// MD5 hash + public byte[] WriteAllBytesAndMD5(string path, ReadOnlySpan bytes, int size); void WriteAllLines(string path,IEnumerable lines); void AppendAllText(string path, string text); @@ -21,6 +30,13 @@ public interface IFileSystem string CreatePath(string path); + /// + /// Calculates the hash using the MD5 algorithm + /// + /// Path within the game's root directory + /// 16 bytes of hash + byte[] MD5SUM(string path); + IEnumerable GetFiles(string path, string mask); } -} \ No newline at end of file +} diff --git a/src/Perpetuum/Zones/ZoneExtensions.cs b/src/Perpetuum/Zones/ZoneExtensions.cs index fe9e047b..0b93a7e8 100644 --- a/src/Perpetuum/Zones/ZoneExtensions.cs +++ b/src/Perpetuum/Zones/ZoneExtensions.cs @@ -8,7 +8,7 @@ using Perpetuum.Zones.RemoteControl; using Perpetuum.Zones.Terrains; using System.Drawing; -using System.Security.Cryptography; +using System.Runtime.InteropServices; namespace Perpetuum.Zones { @@ -46,14 +46,14 @@ public T[] LoadLayerData(IZone zone, string name) where T : struct public void SaveLayerToDisk(IZone zone, ILayer layer) where T : struct { string baseFilename = zone.CreateTerrainDataFilename(layer.LayerType.ToString().ToLower(), ""); - - using MD5 md5 = MD5.Create(); string tmpFn = baseFilename + "tmp" + DateTime.Now.Ticks + ".bin"; - byte[] layerData = layer.RawData.ToByteArray(); - _fileSystem.WriteLayer(tmpFn, layerData); + + ReadOnlySpan layerData = MemoryMarshal.AsBytes(layer.RawData.AsSpan()); + var hash = _fileSystem.WriteLayerAndMD5(tmpFn, layerData, layer.SizeInBytes); - if (!md5.ComputeHash(layerData).SequenceEqual(md5.ComputeHash(_fileSystem.ReadLayerAsByteArray(tmpFn)))) + if (!hash.SequenceEqual(_fileSystem.MD5(tmpFn))) { + Logger.Error("Layer not saved. (" + baseFilename + ")"); return; }