diff --git a/BizHawk.Client.Common/BizHawk.Client.Common.csproj b/BizHawk.Client.Common/BizHawk.Client.Common.csproj
index ef15df436a..b350e94fe5 100644
--- a/BizHawk.Client.Common/BizHawk.Client.Common.csproj
+++ b/BizHawk.Client.Common/BizHawk.Client.Common.csproj
@@ -157,6 +157,7 @@
+
diff --git a/BizHawk.Client.Common/FrameworkFastZipWriter.cs b/BizHawk.Client.Common/FrameworkFastZipWriter.cs
new file mode 100644
index 0000000000..8612b66b44
--- /dev/null
+++ b/BizHawk.Client.Common/FrameworkFastZipWriter.cs
@@ -0,0 +1,296 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BizHawk.Client.Common
+{
+ ///
+ /// this almost works, but it loses all of its speed advantages over FrameworkZipWriter from slow CRC calculation.
+ ///
+ public class FrameworkFastZipWriter : IZipWriter
+ {
+ private Stream _output;
+ private readonly CompressionLevel _level;
+
+ private byte[] _localHeader;
+ private List _endBlobs = new List();
+ private byte[] _fileHeaderTemplate;
+ private int _numEntries;
+ private bool _disposed;
+
+ private class CRC32Stream : Stream
+ {
+ // Lookup table for speed.
+ private static readonly uint[] Crc32Table;
+
+ static CRC32Stream()
+ {
+ Crc32Table = new uint[256];
+ for (uint i = 0; i < 256; ++i)
+ {
+ uint crc = i;
+ for (int j = 8; j > 0; --j)
+ {
+ if ((crc & 1) == 1)
+ {
+ crc = (crc >> 1) ^ 0xEDB88320;
+ }
+ else
+ {
+ crc >>= 1;
+ }
+ }
+
+ Crc32Table[i] = crc;
+ }
+ }
+
+ private uint _crc = 0xffffffff;
+ private int _count = 0;
+ private Stream _baseStream;
+
+ public int Size => _count;
+ public uint Crc => ~_crc;
+
+ public CRC32Stream(Stream baseStream)
+ {
+ _baseStream = baseStream;
+ }
+
+ private void CalculateByte(byte b)
+ {
+ _crc = (_crc >> 8) ^ Crc32Table[b ^ (_crc & 0xff)];
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ for (int i = offset; i < offset + count; i++)
+ {
+ CalculateByte(buffer[i]);
+ }
+ _count += count;
+ _baseStream.Write(buffer, offset, count);
+ }
+
+ public override void WriteByte(byte value)
+ {
+ CalculateByte(value);
+ _count++;
+ _baseStream.WriteByte(value);
+ }
+
+ public override void Flush()
+ {
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override bool CanRead => false;
+
+ public override bool CanSeek => false;
+
+ public override bool CanWrite => true;
+
+ public override long Length
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override long Position
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+
+ set
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+
+ public FrameworkFastZipWriter(string path, int compressionlevel)
+ {
+ _output = new FileStream(path, FileMode.Create, FileAccess.Write);
+ if (compressionlevel == 0)
+ throw new NotImplementedException();
+ //_level = CompressionLevel.NoCompression;
+ else if (compressionlevel < 5)
+ _level = CompressionLevel.Fastest;
+ else
+ _level = CompressionLevel.Optimal;
+
+ var dt = DateTime.Now;
+ var mtime = dt.Second >> 1
+ | dt.Minute << 5
+ | dt.Hour << 11;
+ var mdate = dt.Day
+ | dt.Month << 5
+ | (dt.Year - 1980) << 9;
+
+ var modifiedDate = new byte[]
+ {
+ (byte)(mtime & 0xff),
+ (byte)(mtime >> 8),
+ (byte)(mdate & 0xff),
+ (byte)(mdate >> 8)
+ };
+
+ _localHeader = new byte[]
+ {
+ 0x50, 0x4b, 0x03, 0x04, // signature
+ 0x14, 0x00, // version
+ 0x08, 0x00, // flags: has data descriptor
+ 0x08, 0x00, // method: deflate
+ modifiedDate[0], modifiedDate[1], // mod time
+ modifiedDate[2], modifiedDate[3], // mod date
+ 0x00, 0x00, 0x00, 0x00, // crc32
+ 0x00, 0x00, 0x00, 0x00, // compressed size
+ 0x00, 0x00, 0x00, 0x00, // uncompressed size
+ 0x00, 0x00, // filename length
+ 0x00, 0x00, // extra field length
+ };
+
+ _fileHeaderTemplate = new byte[]
+ {
+ 0x50, 0x4b, 0x01, 0x02, // signature
+ 0x17, 0x03, // ??
+ 0x14, 0x00, // version
+ 0x08, 0x00, // flags: has data descriptor
+ 0x08, 0x00, // method: deflate
+ modifiedDate[0], modifiedDate[1], // mod time
+ modifiedDate[2], modifiedDate[3], // mod date
+ 0x00, 0x00, 0x00, 0x00, // crc32
+ 0x00, 0x00, 0x00, 0x00, // compressed size
+ 0x00, 0x00, 0x00, 0x00, // uncompressed size
+ 0x00, 0x00, // filename length
+ 0x00, 0x00, // extra field length
+ 0x00, 0x00, // file comment length
+ 0x00, 0x00, // disk #,
+ 0x00, 0x00, // internal attributes
+ 0x00, 0x00, 0x00, 0x00, // external attributes
+ 0x00, 0x00, 0x00, 0x00, // local header offset
+ };
+ }
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ WriteFooter();
+ _output.Dispose();
+ _output = null;
+ _disposed = true;
+ }
+ }
+
+ public void WriteItem(string name, Action callback)
+ {
+ var nameb = Encoding.ASCII.GetBytes(name);
+ _localHeader[26] = (byte)nameb.Length;
+ _localHeader[27] = (byte)(nameb.Length >> 8);
+
+ var localHeaderOffset = (int)(_output.Position);
+
+ _output.Write(_localHeader, 0, _localHeader.Length);
+ _output.Write(nameb, 0, nameb.Length);
+
+ var fileStart = (int)(_output.Position);
+
+ var s2 = new DeflateStream(_output, _level, true);
+ var s3 = new CRC32Stream(s2);
+ callback(s3);
+ s2.Flush();
+
+ var fileEnd = (int)(_output.Position);
+
+ var crc = s3.Crc;
+ var compressedSize = fileEnd - fileStart;
+ var uncompressedSize = s3.Size;
+ var descriptor = new byte[]
+ {
+ (byte)crc,
+ (byte)(crc >> 8),
+ (byte)(crc >> 16),
+ (byte)(crc >> 24),
+ (byte)compressedSize,
+ (byte)(compressedSize >> 8),
+ (byte)(compressedSize >> 16),
+ (byte)(compressedSize >> 24),
+ (byte)uncompressedSize,
+ (byte)(uncompressedSize >> 8),
+ (byte)(uncompressedSize >> 16),
+ (byte)(uncompressedSize >> 24)
+ };
+ _output.Write(descriptor, 0, descriptor.Length);
+
+ var fileHeader = (byte[])_fileHeaderTemplate.Clone();
+
+ fileHeader[28] = (byte)nameb.Length;
+ fileHeader[29] = (byte)(nameb.Length >> 8);
+ Array.Copy(descriptor, 0, fileHeader, 16, 12);
+ fileHeader[42] = (byte)localHeaderOffset;
+ fileHeader[43] = (byte)(localHeaderOffset >> 8);
+ fileHeader[44] = (byte)(localHeaderOffset >> 16);
+ fileHeader[45] = (byte)(localHeaderOffset >> 24);
+
+ _endBlobs.Add(fileHeader);
+ _endBlobs.Add(nameb);
+ _numEntries++;
+ }
+
+ private void WriteFooter()
+ {
+ var centralHeaderOffset = (int)(_output.Position);
+
+ foreach (var blob in _endBlobs)
+ _output.Write(blob, 0, blob.Length);
+
+ var centralHeaderEnd = (int)(_output.Position);
+
+ var centralHeaderSize = centralHeaderEnd - centralHeaderOffset;
+
+ var footer = new byte[]
+ {
+ 0x50, 0x4b, 0x05, 0x06, // signature
+ 0x00, 0x00, // disk number
+ 0x00, 0x00, // central record disk number
+ (byte)_numEntries, (byte)(_numEntries >> 8), // number of entries on disk
+ (byte)_numEntries, (byte)(_numEntries >> 8), // number of entries total
+ (byte)centralHeaderSize,
+ (byte)(centralHeaderSize >> 8),
+ (byte)(centralHeaderSize >> 16),
+ (byte)(centralHeaderSize >> 24), // central directory size
+ (byte)centralHeaderOffset,
+ (byte)(centralHeaderOffset >> 8),
+ (byte)(centralHeaderOffset >> 16),
+ (byte)(centralHeaderOffset >> 24), // central directory offset
+ 0x07, 0x00, // comment length
+ 0x42, 0x69, 0x7a, 0x48, 0x61, 0x77, 0x6b // comment
+ };
+
+ _output.Write(footer, 0, footer.Length);
+ }
+ }
+}