├── .github └── FUNDING.yml ├── JamesFrowen.BitPacking ├── Source │ ├── AssemblyInfo.cs │ ├── JamesFrowen.BitPacking.asmdef.meta │ ├── BitMask.cs.meta │ ├── AssemblyInfo.cs.meta │ ├── NetworkReader.cs.meta │ ├── NetworkWriter.cs.meta │ ├── Packers │ │ ├── ZigZag.cs.meta │ │ ├── BitHelper.cs.meta │ │ ├── FloatPacker.cs.meta │ │ ├── Vector2Packer.cs.meta │ │ ├── Vector3Packer.cs.meta │ │ ├── QuaternionPacker.cs.meta │ │ ├── VarFloatPacker.cs.meta │ │ ├── VarIntBlocksPacker.cs.meta │ │ ├── VarVector3Packer.cs.meta │ │ ├── VariableIntPacker.cs.meta │ │ ├── ZigZag.cs │ │ ├── VarFloatPacker.cs │ │ ├── VarVector3Packer.cs │ │ ├── Vector2Packer.cs │ │ ├── VarIntBlocksPacker.cs │ │ ├── Vector3Packer.cs │ │ ├── BitHelper.cs │ │ ├── VariableIntPacker.cs │ │ ├── FloatPacker.cs │ │ └── QuaternionPacker.cs │ ├── Logging │ │ ├── LogHelper.cs.meta │ │ └── LogHelper.cs │ ├── JamesFrowen.BitPacking.asmdef │ ├── BitMask.cs │ ├── NetworkReader.cs │ └── NetworkWriter.cs └── JamesFrowen.BitPacking.csproj ├── JamesFrowen.BitPacking.Tests ├── Tests │ ├── Packers.meta │ ├── Mirage.Tests.BitPacking.asmdef.meta │ ├── TestRandom.cs.meta │ ├── BitPackingTests.cs.meta │ ├── BitMaskHelperTests.cs.meta │ ├── BitPackingProperties.cs.meta │ ├── Packers │ │ ├── BitHelperTest.cs.meta │ │ ├── FloatPackerTests.cs.meta │ │ ├── PackerTestBase.cs.meta │ │ ├── UintPackerTests.cs.meta │ │ ├── Vector2PackerTests.cs.meta │ │ ├── Vector3PackerTests.cs.meta │ │ ├── FloatPackerCreateTest.cs.meta │ │ ├── QuaternionPackerTests.cs.meta │ │ ├── UintBlockPackerTests.cs.meta │ │ ├── UintPackerCreateTest.cs.meta │ │ ├── unsignedFloatPackerTests.cs.meta │ │ ├── PackerTestBase.cs │ │ ├── unsignedFloatPackerTests.cs │ │ ├── BitHelperTest.cs │ │ ├── UintPackerTests.cs │ │ ├── FloatPackerCreateTest.cs │ │ ├── UintBlockPackerTests.cs │ │ ├── Vector2PackerTests.cs │ │ ├── Vector3PackerTests.cs │ │ ├── FloatPackerTests.cs │ │ ├── UintPackerCreateTest.cs │ │ └── QuaternionPackerTests.cs │ ├── BitPackingCopyFromOtherTests.cs.meta │ ├── BitPackingReusingWriterTests.cs.meta │ ├── TestRandom.cs │ ├── Mirage.Tests.BitPacking.asmdef │ ├── BitMaskHelperTests.cs │ ├── BitPackingProperties.cs │ ├── BitPackingReusingWriterTests.cs │ ├── BitPackingCopyFromOtherTests.cs │ └── BitPackingTests.cs └── JamesFrowen.BitPacking.Tests.csproj ├── README.md ├── LICENSE ├── BitPacking.sln ├── .gitignore └── .editorconfig /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: James-Frowen 2 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | 4 | [assembly: AssemblyVersion("1.0.0")] 5 | 6 | [assembly: InternalsVisibleTo("JamesFrowen.BitPacking.Tests")] 7 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2629728c95c038a4984b7ab3d5bf9e84 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/JamesFrowen.BitPacking.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2e1ab74547cbbc742a86c84cdf148326 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Mirage.Tests.BitPacking.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 467e21328cb049d4bbcbbbf6adcc6d2f 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/BitMask.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e8ce6cd3bc8829743bc1b67cf937135d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/TestRandom.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0b44b3fdecf4fc14b85a016d9e1b1a8f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/AssemblyInfo.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3f5bdf9082a94734f8f84aac99bbe80f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/NetworkReader.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1610f05ec5bd14d6882e689f7372596a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/NetworkWriter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 48d2207bcef1f4477b624725f075f9bd 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/ZigZag.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fced96ad9167cb14a875d6dace9f55bc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/BitPackingTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ab6374cb6820dd4488f5fe62d1b816a7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Logging/LogHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 67a0af8480afd5c49aba9e766b29fbb3 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/BitHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1ba9a01829f61d6428bb3e276ac9272a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/FloatPacker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f44847fc059d5464eba7bd04088ddfa1 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/Vector2Packer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a26a59a805a1d6e47a9e41a9aa5e8108 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/Vector3Packer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d2f6b7a4b33422e47b03898a06a14c7d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/BitMaskHelperTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 923082b03eea0934e972aade59852606 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/BitPackingProperties.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5a96aaa0a5f3acb4a83b659c28316658 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/BitHelperTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 782d32bee66d34649b2d40b6a111d20d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/QuaternionPacker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 62c06dfd1d38c294badc710697aff611 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/VarFloatPacker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 96a76eaf70b1dcd47b1cfda1c98d4420 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/VarIntBlocksPacker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 124da2a56c14a924bb6bda5663c0c6f2 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/VarVector3Packer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 14e1e42c2a63d324c83b7aaf67d324fa 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/VariableIntPacker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 93822b4a356267942b8457394696ba08 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/FloatPackerTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e683c337f85dc4d418e6c71c6f8ac5c4 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/PackerTestBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0d9edfce25a6a2d4bbd86782e43eae21 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/UintPackerTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5bb3717be6b5a6143b7ac20ce5ddc740 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/Vector2PackerTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 95e840159b32e0347b99ff97e90d3e2b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/Vector3PackerTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cbc014213f9098643b1058db26baa5ee 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BitPacking 2 | A c# Bit packing implementation for unity and [Mirage Networking](https://github.com/MirageNet/Mirage) and [PositionSyncSystem](https://github.com/James-Frowen/NetworkPositionSync) 3 | 4 | For an up-to-date version of this code see [this Mirage folder](https://github.com/MirageNet/Mirage/tree/main/Assets/Mirage/Runtime/Serialization) 5 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/BitPackingCopyFromOtherTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0fbe9dbb117513140978987f0be0beda 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/BitPackingReusingWriterTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d600aaf09129e234c9ccd9b98223cdc9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/FloatPackerCreateTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a14c571ce3156ac4f9fbc8629c2fabc0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/QuaternionPackerTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3a126e0a10498944d9b63c0c021fbd41 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/UintBlockPackerTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 043048420116fc54582eaf333e5bf6d8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/UintPackerCreateTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ba07df19a3e3850409d63c96e0b3cab0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/unsignedFloatPackerTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f4c565f91c3e4874ea8f6fb642c0cd99 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/JamesFrowen.BitPacking.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JamesFrowen.BitPacking", 3 | "references": [], 4 | "optionalUnityReferences": [], 5 | "includePlatforms": [], 6 | "excludePlatforms": [], 7 | "allowUnsafeCode": true, 8 | "overrideReferences": false, 9 | "precompiledReferences": [], 10 | "autoReferenced": true, 11 | "defineConstraints": [] 12 | } -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/TestRandom.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Mirage.Tests.BitPacking 4 | { 5 | public static class TestRandom 6 | { 7 | static Random random = new Random(); 8 | public static float Range(float a, float b) 9 | { 10 | return (float)((random.NextDouble() * (b - a)) + a); 11 | } 12 | public static int Range(int a, int b) 13 | { 14 | return random.Next(a, b); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Mirage.Tests.BitPacking.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mirage.Tests.BitPacking", 3 | "rootNamespace": "", 4 | "references": [ 5 | "Mirage", 6 | "UnityEngine.TestRunner", 7 | "UnityEditor.TestRunner" 8 | ], 9 | "includePlatforms": [ 10 | "Editor" 11 | ], 12 | "excludePlatforms": [], 13 | "allowUnsafeCode": true, 14 | "overrideReferences": true, 15 | "precompiledReferences": [ 16 | "nunit.framework.dll" 17 | ], 18 | "autoReferenced": false, 19 | "defineConstraints": [ 20 | "UNITY_INCLUDE_TESTS" 21 | ], 22 | "versionDefines": [], 23 | "noEngineReferences": false 24 | } -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/PackerTestBase.cs: -------------------------------------------------------------------------------- 1 | using Mirage.Serialization; 2 | using NUnit.Framework; 3 | 4 | namespace Mirage.Tests.Runtime.Serialization.Packers 5 | { 6 | public class PackerTestBase 7 | { 8 | public readonly NetworkWriter writer = new NetworkWriter(1300); 9 | private readonly NetworkReader reader = new NetworkReader(); 10 | 11 | [TearDown] 12 | public virtual void TearDown() 13 | { 14 | writer.Reset(); 15 | reader.Dispose(); 16 | } 17 | 18 | /// 19 | /// Gets Reader using the current data inside writer 20 | /// 21 | /// 22 | public NetworkReader GetReader() 23 | { 24 | reader.Reset(writer.ToArraySegment()); 25 | return reader; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/JamesFrowen.BitPacking.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | false 6 | 7 | 8 | 9 | true 10 | TRACE;BIT_PACKING_NO_DEBUG 11 | 12 | 13 | 14 | true 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ..\..\..\..\UnityEditors\2019.4.14f1\Editor\Data\Managed\UnityEngine.dll 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/ZigZag.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Mirage.Serialization 4 | { 5 | /// 6 | /// See zigzag encoding
7 | ///
8 | public static class ZigZag 9 | { 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public static uint Encode(int v) 12 | { 13 | return (uint)((v >> 31) ^ (v << 1)); 14 | } 15 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 16 | public static ulong Encode(long v) 17 | { 18 | return (ulong)((v >> 63) ^ (v << 1)); 19 | } 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | public static int Decode(uint v) 22 | { 23 | return (int)((v >> 1) ^ -(v & 1)); 24 | } 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | public static long Decode(ulong v) 27 | { 28 | return ((long)(v >> 1)) ^ -((long)v & 1); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 James Frowen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/unsignedFloatPackerTests.cs: -------------------------------------------------------------------------------- 1 | using Mirage.Serialization; 2 | using NUnit.Framework; 3 | 4 | namespace Mirage.Tests.Runtime.Serialization.Packers 5 | { 6 | public class UnsignedFloatPackerTests : PackerTestBase 7 | { 8 | private FloatPacker packer; 9 | private float max; 10 | private float precsion; 11 | 12 | [SetUp] 13 | public void Setup() 14 | { 15 | max = 100; 16 | precsion = 1 / 1000f; 17 | packer = new FloatPacker(max, precsion, false); 18 | } 19 | 20 | [Test] 21 | public void ClampsToZero() 22 | { 23 | packer.Pack(writer, -4.5f); 24 | var outValue = packer.Unpack(GetReader()); 25 | 26 | Assert.That(outValue, Is.Zero); 27 | } 28 | 29 | [Test] 30 | public void CanWriteNearMax() 31 | { 32 | const float value = 99.5f; 33 | packer.Pack(writer, value); 34 | var outValue = packer.Unpack(GetReader()); 35 | 36 | Assert.That(outValue, Is.EqualTo(value).Within(precsion)); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/JamesFrowen.BitPacking.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | false 11 | true 12 | TRACE;BIT_PACKING_NO_DEBUG 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ..\..\..\..\UnityEditors\2019.4.14f1\Editor\Data\Managed\UnityEngine.dll 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Logging/LogHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace JamesFrowen.BitPacking 4 | { 5 | public static class LogHelper 6 | { 7 | public static Action Log = Console.WriteLine; 8 | 9 | public static void LogBits(uint n) 10 | { 11 | var builder = new System.Text.StringBuilder(); 12 | for (int i = sizeof(uint) * 8 - 1; i >= 0; i--) 13 | { 14 | uint shift = n >> i; 15 | uint masked = shift & 0b1; 16 | builder.Append(masked); 17 | } 18 | 19 | Log(builder.ToString()); 20 | } 21 | 22 | public static void LogBits(ulong n) 23 | { 24 | var builder = new System.Text.StringBuilder(); 25 | for (int i = sizeof(ulong) * 8 - 1; i >= 0; i--) 26 | { 27 | ulong shift = n >> i; 28 | ulong masked = shift & 0b1; 29 | builder.Append(masked); 30 | } 31 | 32 | Log(builder.ToString()); 33 | } 34 | 35 | public static void LogHex(ArraySegment segment) 36 | => LogHex(segment.Array, segment.Offset, segment.Count); 37 | 38 | public static void LogHex(byte[] bytes, int? offset = null, int? count = null) 39 | { 40 | Log(BitConverter.ToString(bytes, offset ?? 0, count ?? bytes.Length)); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/BitMaskHelperTests.cs: -------------------------------------------------------------------------------- 1 | using Mirage.Serialization; 2 | using NUnit.Framework; 3 | 4 | namespace Mirage.Tests.Runtime.Serialization 5 | { 6 | public class BitMaskHelperTests 7 | { 8 | /// 9 | /// slow way of creating correct mask 10 | /// 11 | private static ulong slowMask(int bits) 12 | { 13 | ulong mask = 0; 14 | for (var i = 0; i < bits; i++) 15 | { 16 | mask |= 1ul << i; 17 | } 18 | 19 | return mask; 20 | } 21 | 22 | [Test] 23 | [Description("manually checking edge cases to be sure")] 24 | public void MaskValueIsCorrect0() 25 | { 26 | var mask = BitMask.Mask(0); 27 | Assert.That(mask, Is.EqualTo(0x0)); 28 | } 29 | 30 | [Test] 31 | [Description("manually checking edge cases to be sure")] 32 | public void MaskValueIsCorrect63() 33 | { 34 | var mask = BitMask.Mask(63); 35 | Assert.That(mask, Is.EqualTo(0x7FFF_FFFF_FFFF_FFFF)); 36 | } 37 | 38 | [Test] 39 | [Description("manually checking edge cases to be sure")] 40 | public void MaskValueIsCorrect64() 41 | { 42 | var mask = BitMask.Mask(64); 43 | Assert.That(mask, Is.EqualTo(0xFFFF_FFFF_FFFF_FFFF)); 44 | } 45 | 46 | [Test] 47 | public void MaskValueIsCorrect([Range(0, 64)] int bits) 48 | { 49 | var mask = BitMask.Mask(bits); 50 | var expected = slowMask(bits); 51 | Assert.That(mask, Is.EqualTo(expected), $" mask:{mask:X}\nexpected:{expected:X}"); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/BitHelperTest.cs: -------------------------------------------------------------------------------- 1 | using Mirage.Serialization; 2 | using NUnit.Framework; 3 | 4 | namespace Mirage.Tests.Runtime.Serialization.Packers 5 | { 6 | public class BitHelperTest 7 | { 8 | [Test] 9 | [TestCase(2, 0.05f, ExpectedResult = 7)] 10 | [TestCase(100, 0.1f, ExpectedResult = 11)] 11 | [TestCase(.707f, 0.002f, ExpectedResult = 10)] 12 | [TestCase(1000, 0.1f, ExpectedResult = 15)] 13 | [TestCase(2000, 0.01f, ExpectedResult = 19)] 14 | // 1023 is 10 bits, but max is -+ so 11 bits 15 | [TestCase(1023, 1, ExpectedResult = 11)] 16 | [TestCase(16, 0.5f, ExpectedResult = 7)] 17 | public int ReturnCorrectBitCountForMaxPrecision(float max, float precision) 18 | { 19 | return BitHelper.BitCount(max, precision); 20 | } 21 | 22 | [Test] 23 | [TestCase(0b1UL, ExpectedResult = 1)] 24 | [TestCase(0b10UL, ExpectedResult = 2)] 25 | [TestCase(0b11UL, ExpectedResult = 2)] 26 | [TestCase(0b100UL, ExpectedResult = 3)] 27 | [TestCase(0b101UL, ExpectedResult = 3)] 28 | [TestCase(0b110UL, ExpectedResult = 3)] 29 | [TestCase(0b111UL, ExpectedResult = 3)] 30 | [TestCase(8UL, ExpectedResult = 4)] 31 | [TestCase(15UL, ExpectedResult = 4)] 32 | [TestCase(16UL, ExpectedResult = 5)] 33 | [TestCase(31UL, ExpectedResult = 5)] 34 | [TestCase(32UL, ExpectedResult = 6)] 35 | [TestCase(63UL, ExpectedResult = 6)] 36 | [TestCase(255UL, ExpectedResult = 8)] 37 | [TestCase(256UL, ExpectedResult = 9)] 38 | public int ReturnCorrectBitCountForRange(ulong range) 39 | { 40 | return BitHelper.BitCount(range); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/VarFloatPacker.cs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 James Frowen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | using UnityEngine; 26 | 27 | namespace Mirage.Serialization 28 | { 29 | /// 30 | /// Packs a float using and 31 | /// 32 | public sealed class VarFloatPacker 33 | { 34 | private readonly int _blockSize; 35 | private readonly float _precision; 36 | private readonly float _inversePrecision; 37 | 38 | public VarFloatPacker(float precision, int blockSize) 39 | { 40 | _precision = precision; 41 | _blockSize = blockSize; 42 | _inversePrecision = 1 / precision; 43 | } 44 | 45 | public void Pack(NetworkWriter writer, float value) 46 | { 47 | var scaled = Mathf.RoundToInt(value * _inversePrecision); 48 | var zig = ZigZag.Encode(scaled); 49 | VarIntBlocksPacker.Pack(writer, zig, _blockSize); 50 | } 51 | 52 | public float Unpack(NetworkReader reader) 53 | { 54 | var zig = (uint)VarIntBlocksPacker.Unpack(reader, _blockSize); 55 | var scaled = ZigZag.Decode(zig); 56 | return scaled * _precision; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/VarVector3Packer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 James Frowen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | using UnityEngine; 26 | 27 | namespace Mirage.Serialization 28 | { 29 | /// 30 | /// Packs a vector3 using and 31 | /// 32 | public sealed class VarVector3Packer 33 | { 34 | private readonly VarFloatPacker _xPacker; 35 | private readonly VarFloatPacker _yPacker; 36 | private readonly VarFloatPacker _zPacker; 37 | 38 | public VarVector3Packer(Vector3 precision, int blocksize) 39 | { 40 | _xPacker = new VarFloatPacker(precision.x, blocksize); 41 | _yPacker = new VarFloatPacker(precision.y, blocksize); 42 | _zPacker = new VarFloatPacker(precision.z, blocksize); 43 | } 44 | 45 | public void Pack(NetworkWriter writer, Vector3 position) 46 | { 47 | _xPacker.Pack(writer, position.x); 48 | _yPacker.Pack(writer, position.y); 49 | _zPacker.Pack(writer, position.z); 50 | } 51 | 52 | public Vector3 Unpack(NetworkReader reader) 53 | { 54 | Vector3 value = default; 55 | value.x = _xPacker.Unpack(reader); 56 | value.y = _yPacker.Unpack(reader); 57 | value.z = _zPacker.Unpack(reader); 58 | return value; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/Vector2Packer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 James Frowen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | using UnityEngine; 26 | 27 | namespace Mirage.Serialization 28 | { 29 | public sealed class Vector2Packer 30 | { 31 | private readonly FloatPacker _xPacker; 32 | private readonly FloatPacker _yPacker; 33 | 34 | public Vector2Packer(float xMax, float yMax, int xBitCount, int yBitCount) 35 | { 36 | _xPacker = new FloatPacker(xMax, xBitCount); 37 | _yPacker = new FloatPacker(yMax, yBitCount); 38 | } 39 | public Vector2Packer(float xMax, float yMax, float xPrecision, float yPrecision) 40 | { 41 | _xPacker = new FloatPacker(xMax, xPrecision); 42 | _yPacker = new FloatPacker(yMax, yPrecision); 43 | } 44 | public Vector2Packer(Vector2 max, Vector2 precision) 45 | { 46 | _xPacker = new FloatPacker(max.x, precision.x); 47 | _yPacker = new FloatPacker(max.y, precision.y); 48 | } 49 | 50 | public void Pack(NetworkWriter writer, Vector2 value) 51 | { 52 | _xPacker.Pack(writer, value.x); 53 | _yPacker.Pack(writer, value.y); 54 | } 55 | 56 | public Vector2 Unpack(NetworkReader reader) 57 | { 58 | Vector2 value = default; 59 | value.x = _xPacker.Unpack(reader); 60 | value.y = _yPacker.Unpack(reader); 61 | return value; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/BitMask.cs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 James Frowen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | using System.Runtime.CompilerServices; 26 | 27 | namespace Mirage.Serialization 28 | { 29 | public static class BitMask 30 | { 31 | /// 32 | /// Creates mask for 33 | /// 34 | /// (showing 32 bits for simplify, result is 64 bit) 35 | ///
36 | /// Example bits = 4 => mask = 00000000_00000000_00000000_00001111 37 | ///
38 | /// Example bits = 10 => mask = 00000000_00000000_00000011_11111111 39 | ///
40 | ///
41 | /// 42 | /// 43 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 44 | public static ulong Mask(int bits) 45 | { 46 | return bits == 0 ? 0 : ulong.MaxValue >> (64 - bits); 47 | } 48 | 49 | /// 50 | /// Creates Mask either side of start and end 51 | /// Note this mask is only valid for start [0..63] and end [0..64] 52 | /// 53 | /// 54 | /// 55 | /// 56 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 57 | public static ulong OuterMask(int start, int end) 58 | { 59 | return (ulong.MaxValue << start) ^ (ulong.MaxValue >> (64 - end)); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/VarIntBlocksPacker.cs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 James Frowen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | using System; 26 | using System.Runtime.CompilerServices; 27 | 28 | namespace Mirage.Serialization 29 | { 30 | public static class VarIntBlocksPacker 31 | { 32 | // todo needs doc comments 33 | // todo neeeds tests 34 | 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | public static void Pack(NetworkWriter writer, ulong value, int blockSize) 37 | { 38 | // always writes atleast 1 block 39 | var count = 1; 40 | var checkValue = value >> blockSize; 41 | while (checkValue != 0) 42 | { 43 | count++; 44 | checkValue >>= blockSize; 45 | } 46 | // count = 1, write = b0, (1<<(1-1) -1 => 1<<0 -1) => 1 -1 => 0) 47 | // count = 2, write = b01 48 | // count = 3, write = b011, (1<<(3-1) -1 => 1<<2 -1) => 100 - 1 => 011) 49 | writer.Write((1ul << (count - 1)) - 1, count); 50 | writer.Write(value, Math.Min(64, blockSize * count)); 51 | } 52 | 53 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 54 | public static ulong Unpack(NetworkReader reader, int blockSize) 55 | { 56 | var blocks = 1; 57 | // read bits till we see a zero 58 | while (reader.ReadBoolean()) 59 | { 60 | blocks++; 61 | } 62 | 63 | return reader.Read(Math.Min(64, blocks * blockSize)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/UintPackerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Mirage.Serialization; 3 | using NUnit.Framework; 4 | using Random = System.Random; 5 | 6 | namespace Mirage.Tests.Runtime.Serialization.Packers 7 | { 8 | [TestFixture(50ul, 1_000ul, null)] 9 | [TestFixture(250ul, 10_000ul, null)] 10 | [TestFixture(500ul, 100_000ul, null)] 11 | [TestFixture(50ul, 1_000ul, 10_000_000ul)] 12 | [TestFixture(250ul, 10_000ul, 10_000_000ul)] 13 | [TestFixture(500ul, 100_000ul, 10_000_000ul)] 14 | public class UintPackerTests : PackerTestBase 15 | { 16 | private readonly Random random = new Random(); 17 | private readonly VarIntPacker packer; 18 | private readonly ulong max; 19 | 20 | public UintPackerTests(ulong smallValue, ulong mediumValue, ulong? largeValue) 21 | { 22 | if (largeValue.HasValue) 23 | { 24 | packer = new VarIntPacker(smallValue, mediumValue, largeValue.Value, false); 25 | max = largeValue.Value; 26 | } 27 | else 28 | { 29 | packer = new VarIntPacker(smallValue, mediumValue); 30 | max = ulong.MaxValue; 31 | } 32 | } 33 | 34 | private ulong GetRandonUlongBias() 35 | { 36 | return (ulong)(Math.Abs(random.NextDouble() - random.NextDouble()) * max); 37 | } 38 | 39 | private uint GetRandonUintBias() 40 | { 41 | return (uint)(Math.Abs(random.NextDouble() - random.NextDouble()) * Math.Min(max, uint.MaxValue)); 42 | } 43 | 44 | private ushort GetRandonUshortBias() 45 | { 46 | return (ushort)(Math.Abs(random.NextDouble() - random.NextDouble()) * Math.Min(max, ushort.MaxValue)); 47 | } 48 | 49 | [Test] 50 | [Repeat(1000)] 51 | public void UnpacksCorrectUlongValue() 52 | { 53 | var start = GetRandonUlongBias(); 54 | packer.PackUlong(writer, start); 55 | var unpacked = packer.UnpackUlong(GetReader()); 56 | 57 | Assert.That(unpacked, Is.EqualTo(start)); 58 | } 59 | 60 | [Test] 61 | [Repeat(1000)] 62 | public void UnpacksCorrectUintValue() 63 | { 64 | var start = GetRandonUintBias(); 65 | packer.PackUint(writer, start); 66 | var unpacked = packer.UnpackUint(GetReader()); 67 | 68 | Assert.That(unpacked, Is.EqualTo(start)); 69 | } 70 | 71 | [Test] 72 | [Repeat(1000)] 73 | public void UnpacksCorrectUshortValue() 74 | { 75 | var start = GetRandonUshortBias(); 76 | packer.PackUshort(writer, start); 77 | var unpacked = packer.UnpackUshort(GetReader()); 78 | 79 | Assert.That(unpacked, Is.EqualTo(start)); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/Vector3Packer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 James Frowen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | using UnityEngine; 26 | 27 | namespace Mirage.Serialization 28 | { 29 | public sealed class Vector3Packer 30 | { 31 | private readonly FloatPacker _xPacker; 32 | private readonly FloatPacker _yPacker; 33 | private readonly FloatPacker _zPacker; 34 | 35 | public Vector3Packer(float xMax, float yMax, float zMax, int xBitCount, int yBitCount, int zBitCount) 36 | { 37 | _xPacker = new FloatPacker(xMax, xBitCount); 38 | _yPacker = new FloatPacker(yMax, yBitCount); 39 | _zPacker = new FloatPacker(zMax, zBitCount); 40 | } 41 | public Vector3Packer(float xMax, float yMax, float zMax, float xPrecision, float yPrecision, float zPrecision) 42 | { 43 | _xPacker = new FloatPacker(xMax, xPrecision); 44 | _yPacker = new FloatPacker(yMax, yPrecision); 45 | _zPacker = new FloatPacker(zMax, zPrecision); 46 | } 47 | public Vector3Packer(Vector3 max, Vector3 precision) 48 | { 49 | _xPacker = new FloatPacker(max.x, precision.x); 50 | _yPacker = new FloatPacker(max.y, precision.y); 51 | _zPacker = new FloatPacker(max.z, precision.z); 52 | } 53 | 54 | public void Pack(NetworkWriter writer, Vector3 value) 55 | { 56 | _xPacker.Pack(writer, value.x); 57 | _yPacker.Pack(writer, value.y); 58 | _zPacker.Pack(writer, value.z); 59 | } 60 | 61 | public Vector3 Unpack(NetworkReader reader) 62 | { 63 | Vector3 value = default; 64 | value.x = _xPacker.Unpack(reader); 65 | value.y = _yPacker.Unpack(reader); 66 | value.z = _zPacker.Unpack(reader); 67 | return value; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/FloatPackerCreateTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Mirage.Serialization; 3 | using NUnit.Framework; 4 | using UnityEngine; 5 | using Range = NUnit.Framework.RangeAttribute; 6 | 7 | namespace Mirage.Tests.Runtime.Serialization.Packers 8 | { 9 | public class FloatPackerCreateTest : PackerTestBase 10 | { 11 | [Test] 12 | [TestCase(1, ExpectedResult = 8)] 13 | [TestCase(0.1f, ExpectedResult = 11)] 14 | [TestCase(0.01f, ExpectedResult = 15)] 15 | public int CreateUsingPrecsion(float precision) 16 | { 17 | var packer = new FloatPacker(100, precision); 18 | 19 | packer.Pack(writer, 1f); 20 | return writer.BitPosition; 21 | } 22 | 23 | [Test] 24 | [TestCase(1, ExpectedResult = 7)] 25 | [TestCase(0.1f, ExpectedResult = 10)] 26 | [TestCase(0.01f, ExpectedResult = 14)] 27 | public int BitCountIsLessForUnSigned(float precision) 28 | { 29 | var packer = new FloatPacker(100, precision, false); 30 | 31 | packer.Pack(writer, 1f); 32 | return writer.BitPosition; 33 | } 34 | 35 | [Test] 36 | public void PackFromBitCountPacksToCorrectCount([Range(1, 30)] int bitCount, [Values(true, false)] bool signed) 37 | { 38 | var packer = new FloatPacker(100, bitCount, signed); 39 | 40 | packer.Pack(writer, 1f); 41 | 42 | Assert.That(writer.BitPosition, Is.EqualTo(bitCount)); 43 | } 44 | 45 | [Test] 46 | public void ThrowsIfBitCountIsLessThan1([Range(-10, 0)] int bitCount, [Values(true, false)] bool signed) 47 | { 48 | var exception = Assert.Throws(() => 49 | { 50 | _ = new FloatPacker(10, bitCount, signed); 51 | }); 52 | 53 | var expected = new ArgumentException("Bit count is too low, bit count should be between 1 and 30", "bitCount"); 54 | Assert.That(exception, Has.Message.EqualTo(expected.Message)); 55 | } 56 | 57 | [Test] 58 | public void ThrowsIfBitCountIsGreaterThan30([Range(31, 40)] int bitCount, [Values(true, false)] bool signed) 59 | { 60 | var exception = Assert.Throws(() => 61 | { 62 | _ = new FloatPacker(10, bitCount, signed); 63 | }); 64 | 65 | var expected = new ArgumentException("Bit count is too high, bit count should be between 1 and 30", "bitCount"); 66 | Assert.That(exception, Has.Message.EqualTo(expected.Message)); 67 | } 68 | 69 | [Test] 70 | public void ThrowsIfMaxIsZero([Values(true, false)] bool signed) 71 | { 72 | var exception = Assert.Throws(() => 73 | { 74 | _ = new FloatPacker(0, 1, signed); 75 | }); 76 | 77 | var expected = new ArgumentException("Max can not be 0", "max"); 78 | Assert.That(exception, Has.Message.EqualTo(expected.Message)); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/UintBlockPackerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Mirage.Serialization; 3 | using NUnit.Framework; 4 | using Random = System.Random; 5 | 6 | namespace Mirage.Tests.Runtime.Serialization.Packers 7 | { 8 | [TestFixture(4)] 9 | [TestFixture(7)] 10 | [TestFixture(8)] 11 | [TestFixture(12)] 12 | [TestFixture(16)] 13 | public class UintBlockPackerTests : PackerTestBase 14 | { 15 | private readonly Random random = new Random(); 16 | private readonly int blockSize; 17 | 18 | public UintBlockPackerTests(int blockSize) 19 | { 20 | this.blockSize = blockSize; 21 | } 22 | 23 | private ulong GetRandonUlongBias() 24 | { 25 | return (ulong)(Math.Abs(random.NextDouble() - random.NextDouble()) * ulong.MaxValue); 26 | } 27 | 28 | private uint GetRandonUintBias() 29 | { 30 | return (uint)(Math.Abs(random.NextDouble() - random.NextDouble()) * uint.MaxValue); 31 | } 32 | 33 | private ushort GetRandonUshortBias() 34 | { 35 | return (ushort)(Math.Abs(random.NextDouble() - random.NextDouble()) * ushort.MaxValue); 36 | } 37 | 38 | [Test] 39 | [Repeat(1000)] 40 | public void UnpacksCorrectUlongValue() 41 | { 42 | var start = GetRandonUlongBias(); 43 | VarIntBlocksPacker.Pack(writer, start, blockSize); 44 | var unpacked = VarIntBlocksPacker.Unpack(GetReader(), blockSize); 45 | 46 | Assert.That(unpacked, Is.EqualTo(start)); 47 | } 48 | 49 | [Test] 50 | [Repeat(1000)] 51 | public void UnpacksCorrectUintValue() 52 | { 53 | var start = GetRandonUintBias(); 54 | VarIntBlocksPacker.Pack(writer, start, blockSize); 55 | var unpacked = VarIntBlocksPacker.Unpack(GetReader(), blockSize); 56 | 57 | Assert.That(unpacked, Is.EqualTo(start)); 58 | } 59 | 60 | [Test] 61 | [Repeat(1000)] 62 | public void UnpacksCorrectUshortValue() 63 | { 64 | var start = GetRandonUshortBias(); 65 | VarIntBlocksPacker.Pack(writer, start, blockSize); 66 | var unpacked = VarIntBlocksPacker.Unpack(GetReader(), blockSize); 67 | 68 | Assert.That(unpacked, Is.EqualTo(start)); 69 | } 70 | 71 | [Test] 72 | public void WritesNplus1BitsPerBlock() 73 | { 74 | var zero = 0u; 75 | VarIntBlocksPacker.Pack(writer, zero, blockSize); 76 | Assert.That(writer.BitPosition, Is.EqualTo(blockSize + 1)); 77 | 78 | var unpacked = VarIntBlocksPacker.Unpack(GetReader(), blockSize); 79 | Assert.That(unpacked, Is.EqualTo(zero)); 80 | } 81 | 82 | [Test] 83 | public void WritesNplus1BitsPerBlock_bigger() 84 | { 85 | var aboveBlockSize = (1u << blockSize) + 1u; 86 | VarIntBlocksPacker.Pack(writer, aboveBlockSize, blockSize); 87 | Assert.That(writer.BitPosition, Is.EqualTo(2 * (blockSize + 1))); 88 | 89 | var unpacked = VarIntBlocksPacker.Unpack(GetReader(), blockSize); 90 | Assert.That(unpacked, Is.EqualTo(aboveBlockSize)); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /BitPacking.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30717.126 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JamesFrowen.BitPacking", "JamesFrowen.BitPacking\JamesFrowen.BitPacking.csproj", "{82F3D519-6EB4-4B68-9D97-DC0F944C5136}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JamesFrowen.BitPacking.Tests", "JamesFrowen.BitPacking.Tests\JamesFrowen.BitPacking.Tests.csproj", "{A5308AB2-1557-4AEE-A9B9-F4C5CD23AEAB}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A38990DC-0C3C-45DA-9B73-6F3BE86D4361}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Debug|x64 = Debug|x64 19 | Debug|x86 = Debug|x86 20 | Release|Any CPU = Release|Any CPU 21 | Release|x64 = Release|x64 22 | Release|x86 = Release|x86 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {82F3D519-6EB4-4B68-9D97-DC0F944C5136}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {82F3D519-6EB4-4B68-9D97-DC0F944C5136}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {82F3D519-6EB4-4B68-9D97-DC0F944C5136}.Debug|x64.ActiveCfg = Debug|Any CPU 28 | {82F3D519-6EB4-4B68-9D97-DC0F944C5136}.Debug|x64.Build.0 = Debug|Any CPU 29 | {82F3D519-6EB4-4B68-9D97-DC0F944C5136}.Debug|x86.ActiveCfg = Debug|Any CPU 30 | {82F3D519-6EB4-4B68-9D97-DC0F944C5136}.Debug|x86.Build.0 = Debug|Any CPU 31 | {82F3D519-6EB4-4B68-9D97-DC0F944C5136}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {82F3D519-6EB4-4B68-9D97-DC0F944C5136}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {82F3D519-6EB4-4B68-9D97-DC0F944C5136}.Release|x64.ActiveCfg = Release|Any CPU 34 | {82F3D519-6EB4-4B68-9D97-DC0F944C5136}.Release|x64.Build.0 = Release|Any CPU 35 | {82F3D519-6EB4-4B68-9D97-DC0F944C5136}.Release|x86.ActiveCfg = Release|Any CPU 36 | {82F3D519-6EB4-4B68-9D97-DC0F944C5136}.Release|x86.Build.0 = Release|Any CPU 37 | {A5308AB2-1557-4AEE-A9B9-F4C5CD23AEAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {A5308AB2-1557-4AEE-A9B9-F4C5CD23AEAB}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {A5308AB2-1557-4AEE-A9B9-F4C5CD23AEAB}.Debug|x64.ActiveCfg = Debug|Any CPU 40 | {A5308AB2-1557-4AEE-A9B9-F4C5CD23AEAB}.Debug|x64.Build.0 = Debug|Any CPU 41 | {A5308AB2-1557-4AEE-A9B9-F4C5CD23AEAB}.Debug|x86.ActiveCfg = Debug|Any CPU 42 | {A5308AB2-1557-4AEE-A9B9-F4C5CD23AEAB}.Debug|x86.Build.0 = Debug|Any CPU 43 | {A5308AB2-1557-4AEE-A9B9-F4C5CD23AEAB}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {A5308AB2-1557-4AEE-A9B9-F4C5CD23AEAB}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {A5308AB2-1557-4AEE-A9B9-F4C5CD23AEAB}.Release|x64.ActiveCfg = Release|Any CPU 46 | {A5308AB2-1557-4AEE-A9B9-F4C5CD23AEAB}.Release|x64.Build.0 = Release|Any CPU 47 | {A5308AB2-1557-4AEE-A9B9-F4C5CD23AEAB}.Release|x86.ActiveCfg = Release|Any CPU 48 | {A5308AB2-1557-4AEE-A9B9-F4C5CD23AEAB}.Release|x86.Build.0 = Release|Any CPU 49 | EndGlobalSection 50 | GlobalSection(SolutionProperties) = preSolution 51 | HideSolutionNode = FALSE 52 | EndGlobalSection 53 | GlobalSection(ExtensibilityGlobals) = postSolution 54 | SolutionGuid = {DACFC036-4A8D-4097-9B77-57C70FB3ECEC} 55 | EndGlobalSection 56 | EndGlobal 57 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/BitHelper.cs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 James Frowen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | using System; 26 | using System.Runtime.CompilerServices; 27 | using UnityEngine; 28 | 29 | namespace Mirage.Serialization 30 | { 31 | public static class BitHelper 32 | { 33 | /// 34 | /// Gets the number of bits need for in range negative to positive 35 | /// 36 | /// WARNING: these methods are not fast, dont use in hotpath 37 | /// 38 | /// 39 | /// 40 | /// lowest precision required, bit count will round up so real precision might be higher 41 | /// 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | public static int BitCount(float max, float precision) 44 | { 45 | return BitCount(max, precision, true); 46 | } 47 | 48 | /// 49 | /// Gets the number of bits need for in range 50 | /// If signed then range is negative max to positive max, If unsigned then 0 to max 51 | /// 52 | /// WARNING: these methods are not fast, dont use in hotpath 53 | /// 54 | /// 55 | /// 56 | /// lowest precision required, bit count will round up so real precision might be higher 57 | /// 58 | public static int BitCount(float max, float precision, bool signed) 59 | { 60 | float multiplier = signed ? 2 : 1; 61 | return Mathf.FloorToInt(Mathf.Log(multiplier * max / precision, 2)) + 1; 62 | } 63 | 64 | /// 65 | /// Gets the number of bits need for 66 | /// 67 | /// WARNING: these methods are not fast, dont use in hotpath 68 | /// 69 | /// 70 | /// 71 | /// lowest precision required, bit count will round up so real precision might be higher 72 | /// 73 | public static int BitCount(ulong max) 74 | { 75 | return (int)Math.Floor(Math.Log(max, 2)) + 1; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/BitPackingProperties.cs: -------------------------------------------------------------------------------- 1 | using Mirage.Serialization; 2 | using NUnit.Framework; 3 | 4 | namespace Mirage.Tests.Runtime.Serialization 5 | { 6 | public class BitPackingProperties 7 | { 8 | private readonly NetworkWriter writer = new NetworkWriter(1300); 9 | private readonly NetworkReader reader = new NetworkReader(); 10 | private readonly byte[] sampleData = new byte[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 11 | private const int BITS_PER_BYTE = 8; 12 | 13 | [TearDown] 14 | public void TearDown() 15 | { 16 | writer.Reset(); 17 | reader.Dispose(); 18 | } 19 | 20 | 21 | [Test] 22 | public void WriterBitPositionStartsAtZero() 23 | { 24 | Assert.That(writer.BitPosition, Is.EqualTo(0)); 25 | } 26 | 27 | [Test] 28 | public void WriterByteLengthStartsAtZero() 29 | { 30 | Assert.That(writer.ByteLength, Is.EqualTo(0)); 31 | } 32 | 33 | [Test] 34 | public void ReaderBitPositionStartsStartsAtZero() 35 | { 36 | reader.Reset(sampleData); 37 | Assert.That(reader.BitPosition, Is.EqualTo(0)); 38 | } 39 | 40 | [Test] 41 | public void ReaderBytePositionStartsStartsAtZero() 42 | { 43 | reader.Reset(sampleData); 44 | Assert.That(reader.BytePosition, Is.EqualTo(0)); 45 | } 46 | 47 | [Test] 48 | public void ReaderBitLengthStartsStartsAtArrayLength() 49 | { 50 | reader.Reset(sampleData); 51 | Assert.That(reader.BitLength, Is.EqualTo(sampleData.Length * BITS_PER_BYTE)); 52 | } 53 | 54 | 55 | 56 | [Test] 57 | public void WriterBitPositionIncreasesAfterWriting() 58 | { 59 | writer.Write(0, 15); 60 | Assert.That(writer.BitPosition, Is.EqualTo(15)); 61 | 62 | writer.Write(0, 50); 63 | Assert.That(writer.BitPosition, Is.EqualTo(65)); 64 | } 65 | 66 | [Test] 67 | public void WriterByteLengthIncreasesAfterWriting_ShouldRoundUp() 68 | { 69 | writer.Write(0, 15); 70 | Assert.That(writer.ByteLength, Is.EqualTo(2)); 71 | 72 | writer.Write(0, 50); 73 | Assert.That(writer.ByteLength, Is.EqualTo(9)); 74 | } 75 | 76 | [Test] 77 | public void ReaderBitPositionIncreasesAfterReading() 78 | { 79 | reader.Reset(sampleData); 80 | _ = reader.Read(15); 81 | Assert.That(reader.BitPosition, Is.EqualTo(15)); 82 | 83 | _ = reader.Read(50); 84 | Assert.That(reader.BitPosition, Is.EqualTo(65)); 85 | } 86 | 87 | [Test] 88 | public void ReaderBytePositionIncreasesAfterReading_ShouldRoundUp() 89 | { 90 | reader.Reset(sampleData); 91 | _ = reader.Read(15); 92 | Assert.That(reader.BytePosition, Is.EqualTo(2)); 93 | 94 | _ = reader.Read(50); 95 | Assert.That(reader.BytePosition, Is.EqualTo(9)); 96 | } 97 | 98 | [Test] 99 | public void ReaderBitLengthDoesnotIncreasesAfterReading() 100 | { 101 | reader.Reset(sampleData); 102 | _ = reader.Read(15); 103 | Assert.That(reader.BitLength, Is.EqualTo(sampleData.Length * BITS_PER_BYTE)); 104 | 105 | _ = reader.Read(50); 106 | Assert.That(reader.BitLength, Is.EqualTo(sampleData.Length * BITS_PER_BYTE)); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/Vector2PackerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using Mirage.Serialization; 5 | using NUnit.Framework; 6 | using UnityEngine; 7 | using Random = Mirage.Tests.BitPacking.TestRandom; 8 | 9 | namespace Mirage.Tests.Runtime.Serialization.Packers 10 | { 11 | public class Vector2PackerTests : PackerTestBase 12 | { 13 | private static IEnumerable WriteCorrectNumberOfBitsCases() 14 | { 15 | yield return new TestCaseData(Vector2.one * 100, Vector2.one * 0.1f).Returns(11 * 2); 16 | yield return new TestCaseData(Vector2.one * 200, Vector2.one * 0.1f).Returns(12 * 2); 17 | yield return new TestCaseData(Vector2.one * 200, Vector2.one * 0.05f).Returns(13 * 2); 18 | yield return new TestCaseData(new Vector2(100, 50), Vector2.one * 0.1f).Returns(11 + 10); 19 | yield return new TestCaseData(new Vector2(100, 50), new Vector2(0.1f, 0.2f)).Returns(11 + 9); 20 | } 21 | 22 | [Test] 23 | [TestCaseSource(nameof(WriteCorrectNumberOfBitsCases))] 24 | public int WriteCorrectNumberOfBits(Vector2 max, Vector2 precision) 25 | { 26 | var packer = new Vector2Packer(max, precision); 27 | packer.Pack(writer, Vector2.zero); 28 | return writer.BitPosition; 29 | } 30 | 31 | private static IEnumerable ThrowsIfAnyMaxIsZeroCases() 32 | { 33 | yield return new TestCaseData(new Vector2(100, 0), Vector2.one * 0.1f); 34 | yield return new TestCaseData(new Vector2(0, 100), Vector2.one * 0.1f); 35 | } 36 | 37 | [Test] 38 | [TestCaseSource(nameof(ThrowsIfAnyMaxIsZeroCases))] 39 | public void ThrowsIfAnyMaxIsZero(Vector2 max, Vector2 precision) 40 | { 41 | var exception = Assert.Throws(() => 42 | { 43 | _ = new Vector2Packer(max, precision); 44 | }); 45 | 46 | var expected = new ArgumentException("Max can not be 0", "max"); 47 | Assert.That(exception, Has.Message.EqualTo(expected.Message)); 48 | } 49 | 50 | private static IEnumerable UnpacksToSaveValueCases() 51 | { 52 | yield return new TestCaseData(Vector2.one * 100, Vector2.one * 0.1f); 53 | yield return new TestCaseData(Vector2.one * 200, Vector2.one * 0.1f); 54 | yield return new TestCaseData(Vector2.one * 200, Vector2.one * 0.05f); 55 | yield return new TestCaseData(new Vector2(100, 50), Vector2.one * 0.1f); 56 | yield return new TestCaseData(new Vector2(100, 50), new Vector2(0.1f, 0.2f)); 57 | } 58 | 59 | [Test] 60 | [TestCaseSource(nameof(UnpacksToSaveValueCases))] 61 | [Repeat(100)] 62 | public void UnpacksToSaveValue(Vector2 max, Vector2 precision) 63 | { 64 | var packer = new Vector2Packer(max, precision); 65 | var expected = new Vector2( 66 | Random.Range(-max.x, -max.x), 67 | Random.Range(-max.y, -max.y) 68 | ); 69 | 70 | packer.Pack(writer, expected); 71 | var unpacked = packer.Unpack(GetReader()); 72 | 73 | Assert.That(unpacked.x, Is.EqualTo(expected.x).Within(precision.x)); 74 | Assert.That(unpacked.y, Is.EqualTo(expected.y).Within(precision.y)); 75 | } 76 | 77 | [Test] 78 | [TestCaseSource(nameof(UnpacksToSaveValueCases))] 79 | public void ZeroUnpacksAsZero(Vector2 max, Vector2 precision) 80 | { 81 | var packer = new Vector2Packer(max, precision); 82 | var zero = Vector2.zero; 83 | 84 | packer.Pack(writer, zero); 85 | var unpacked = packer.Unpack(GetReader()); 86 | 87 | Assert.That(unpacked, Is.EqualTo(zero)); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/Vector3PackerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using Mirage.Serialization; 5 | using NUnit.Framework; 6 | using UnityEngine; 7 | using Random = Mirage.Tests.BitPacking.TestRandom; 8 | 9 | namespace Mirage.Tests.Runtime.Serialization.Packers 10 | { 11 | public class Vector3PackerTests : PackerTestBase 12 | { 13 | private static IEnumerable WriteCorrectNumberOfBitsCases() 14 | { 15 | yield return new TestCaseData(Vector3.one * 100, Vector3.one * 0.1f).Returns(11 * 3); 16 | yield return new TestCaseData(Vector3.one * 200, Vector3.one * 0.1f).Returns(12 * 3); 17 | yield return new TestCaseData(Vector3.one * 200, Vector3.one * 0.05f).Returns(13 * 3); 18 | yield return new TestCaseData(new Vector3(100, 50, 100), Vector3.one * 0.1f).Returns(11 + 10 + 11); 19 | yield return new TestCaseData(new Vector3(100, 50, 100), new Vector3(0.1f, 0.2f, 0.1f)).Returns(11 + 9 + 11); 20 | yield return new TestCaseData(new Vector3(100, 50, 200), Vector3.one * 0.1f).Returns(11 + 10 + 12); 21 | yield return new TestCaseData(new Vector3(100, 50, 200), new Vector3(0.1f, 0.2f, 0.05f)).Returns(11 + 9 + 13); 22 | } 23 | 24 | [Test] 25 | [TestCaseSource(nameof(WriteCorrectNumberOfBitsCases))] 26 | public int WriteCorrectNumberOfBits(Vector3 max, Vector3 precision) 27 | { 28 | var packer = new Vector3Packer(max, precision); 29 | packer.Pack(writer, Vector3.zero); 30 | return writer.BitPosition; 31 | } 32 | 33 | private static IEnumerable ThrowsIfAnyMaxIsZeroCases() 34 | { 35 | yield return new TestCaseData(new Vector3(100, 0, 100), Vector3.one * 0.1f); 36 | yield return new TestCaseData(new Vector3(0, 100, 100), Vector3.one * 0.1f); 37 | yield return new TestCaseData(new Vector3(100, 100, 0), Vector3.one * 0.1f); 38 | } 39 | 40 | [Test] 41 | [TestCaseSource(nameof(ThrowsIfAnyMaxIsZeroCases))] 42 | public void ThrowsIfAnyMaxIsZero(Vector3 max, Vector3 precision) 43 | { 44 | var exception = Assert.Throws(() => 45 | { 46 | _ = new Vector3Packer(max, precision); 47 | }); 48 | 49 | var expected = new ArgumentException("Max can not be 0", "max"); 50 | Assert.That(exception, Has.Message.EqualTo(expected.Message)); 51 | } 52 | 53 | private static IEnumerable UnpacksToSaveValueCases() 54 | { 55 | yield return new TestCaseData(Vector3.one * 100, Vector3.one * 0.1f); 56 | yield return new TestCaseData(Vector3.one * 200, Vector3.one * 0.1f); 57 | yield return new TestCaseData(Vector3.one * 200, Vector3.one * 0.05f); 58 | yield return new TestCaseData(new Vector3(100, 50, 100), Vector3.one * 0.1f); 59 | yield return new TestCaseData(new Vector3(100, 50, 100), new Vector3(0.1f, 0.2f, 0.1f)); 60 | } 61 | 62 | [Test] 63 | [TestCaseSource(nameof(UnpacksToSaveValueCases))] 64 | [Repeat(100)] 65 | public void UnpacksToSaveValue(Vector3 max, Vector3 precision) 66 | { 67 | var packer = new Vector3Packer(max, precision); 68 | var expected = new Vector3( 69 | Random.Range(-max.x, -max.x), 70 | Random.Range(-max.y, -max.y), 71 | Random.Range(-max.z, -max.z) 72 | ); 73 | 74 | packer.Pack(writer, expected); 75 | var unpacked = packer.Unpack(GetReader()); 76 | 77 | Assert.That(unpacked.x, Is.EqualTo(expected.x).Within(precision.x)); 78 | Assert.That(unpacked.y, Is.EqualTo(expected.y).Within(precision.y)); 79 | Assert.That(unpacked.z, Is.EqualTo(expected.z).Within(precision.z)); 80 | } 81 | 82 | [Test] 83 | [TestCaseSource(nameof(UnpacksToSaveValueCases))] 84 | public void ZeroUnpacksAsZero(Vector3 max, Vector3 precision) 85 | { 86 | var packer = new Vector3Packer(max, precision); 87 | var zero = Vector3.zero; 88 | 89 | packer.Pack(writer, zero); 90 | var unpacked = packer.Unpack(GetReader()); 91 | 92 | Assert.That(unpacked, Is.EqualTo(zero)); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/FloatPackerTests.cs: -------------------------------------------------------------------------------- 1 | using Mirage.Serialization; 2 | using NUnit.Framework; 3 | using Random = Mirage.Tests.BitPacking.TestRandom; 4 | 5 | namespace Mirage.Tests.Runtime.Serialization.Packers 6 | { 7 | [TestFixture(100, 0.1f, true)] 8 | [TestFixture(500, 0.02f, true)] 9 | [TestFixture(2000, 0.05f, true)] 10 | [TestFixture(1.5f, 0.01f, true)] 11 | [TestFixture(100_000, 30, true)] 12 | 13 | [TestFixture(100, 0.1f, false)] 14 | [TestFixture(500, 0.02f, false)] 15 | [TestFixture(2000, 0.05f, false)] 16 | [TestFixture(1.5f, 0.01f, false)] 17 | [TestFixture(100_000, 30, false)] 18 | public class FloatPackerTests : PackerTestBase 19 | { 20 | private readonly FloatPacker packer; 21 | private readonly float max; 22 | private readonly float min; 23 | private readonly float precsion; 24 | private readonly bool signed; 25 | 26 | public FloatPackerTests(float max, float precsion, bool signed) 27 | { 28 | this.max = max; 29 | min = signed ? -max : 0; 30 | this.precsion = precsion; 31 | this.signed = signed; 32 | packer = new FloatPacker(max, precsion, signed); 33 | } 34 | 35 | private float GetRandomFloat() 36 | { 37 | return Random.Range(min, max); 38 | } 39 | 40 | 41 | [Test] 42 | // takes about 1 seconds per 1000 values (including all fixtures) 43 | [Repeat(1000)] 44 | public void UnpackedValueIsWithinPrecision() 45 | { 46 | var start = GetRandomFloat(); 47 | var packed = packer.Pack(start); 48 | var unpacked = packer.Unpack(packed); 49 | 50 | Assert.That(unpacked, Is.EqualTo(start).Within(precsion)); 51 | } 52 | 53 | [Test] 54 | public void ValueOverMaxWillBeUnpackedAsMax() 55 | { 56 | var start = max * 1.2f; 57 | var packed = packer.Pack(start); 58 | var unpacked = packer.Unpack(packed); 59 | 60 | Assert.That(unpacked, Is.EqualTo(max).Within(precsion)); 61 | } 62 | 63 | [Test] 64 | public void ValueUnderNegativeMaxWillBeUnpackedAsNegativeMax() 65 | { 66 | var start = max * -1.2f; 67 | var packed = packer.Pack(start); 68 | var unpacked = packer.Unpack(packed); 69 | 70 | Assert.That(unpacked, Is.EqualTo(min).Within(precsion)); 71 | } 72 | 73 | [Test] 74 | public void ZeroUnpackToExactlyZero() 75 | { 76 | const float zero = 0; 77 | var packed = packer.Pack(zero); 78 | var unpacked = packer.Unpack(packed); 79 | 80 | Assert.That(unpacked, Is.EqualTo(zero)); 81 | } 82 | 83 | 84 | [Test] 85 | [Repeat(100)] 86 | public void UnpackedValueIsWithinPrecisionUsingWriter() 87 | { 88 | var start = GetRandomFloat(); 89 | packer.Pack(writer, start); 90 | var unpacked = packer.Unpack(GetReader()); 91 | 92 | Assert.That(unpacked, Is.EqualTo(start).Within(precsion)); 93 | } 94 | 95 | [Test] 96 | public void ValueOverMaxWillBeUnpackedUsingWriterAsMax() 97 | { 98 | var start = max * 1.2f; 99 | packer.Pack(writer, start); 100 | var unpacked = packer.Unpack(GetReader()); 101 | 102 | Assert.That(unpacked, Is.EqualTo(max).Within(precsion)); 103 | } 104 | 105 | [Test] 106 | public void ValueUnderNegativeMaxWillBeUnpackedUsingWriterAsNegativeMax() 107 | { 108 | var start = max * -1.2f; 109 | packer.Pack(writer, start); 110 | var unpacked = packer.Unpack(GetReader()); 111 | 112 | Assert.That(unpacked, Is.EqualTo(min).Within(precsion)); 113 | } 114 | 115 | 116 | [Test] 117 | [Repeat(100)] 118 | public void UnpackedValueIsWithinPrecisionNoClamp() 119 | { 120 | var start = GetRandomFloat(); 121 | var packed = packer.PackNoClamp(start); 122 | var unpacked = packer.Unpack(packed); 123 | 124 | Assert.That(unpacked, Is.EqualTo(start).Within(precsion)); 125 | } 126 | 127 | [Test] 128 | [Repeat(100)] 129 | public void UnpackedValueIsWithinPrecisionNoClampUsingWriter() 130 | { 131 | var start = GetRandomFloat(); 132 | packer.PackNoClamp(writer, start); 133 | var unpacked = packer.Unpack(GetReader()); 134 | 135 | Assert.That(unpacked, Is.EqualTo(start).Within(precsion)); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/BitPackingReusingWriterTests.cs: -------------------------------------------------------------------------------- 1 | using Mirage.Serialization; 2 | using NUnit.Framework; 3 | 4 | namespace Mirage.Tests.Runtime.Serialization 5 | { 6 | public class BitPackingResizeTest 7 | { 8 | private NetworkWriter writer; 9 | private NetworkReader reader; 10 | 11 | [SetUp] 12 | public void SetUp() 13 | { 14 | writer = new NetworkWriter(1300, true); 15 | reader = new NetworkReader(); 16 | } 17 | 18 | [TearDown] 19 | public void TearDown() 20 | { 21 | // we have to clear these each time so that capactity doesn't effect other tests 22 | writer.Reset(); 23 | writer = null; 24 | reader.Dispose(); 25 | reader = null; 26 | } 27 | 28 | [Test] 29 | public void ResizesIfWritingOverCapacity() 30 | { 31 | var overCapacity = (1300 / 8) + 10; 32 | Assert.That(writer.ByteCapacity, Is.EqualTo(1304), "is first multiple of 8 over 1300"); 33 | for (var i = 0; i < overCapacity; i++) 34 | { 35 | writer.WriteUInt64((ulong)i); 36 | } 37 | 38 | Assert.That(writer.ByteCapacity, Is.EqualTo(1304 * 2), "should double in size"); 39 | } 40 | 41 | 42 | [Test] 43 | public void WillResizeMultipleTimes() 44 | { 45 | var overCapacity = ((1300 / 8) + 10) * 10; // 1720 * 8 = 13760 bytes 46 | 47 | Assert.That(writer.ByteCapacity, Is.EqualTo(1304), "is first multiple of 8 over 1300"); 48 | for (var i = 0; i < overCapacity; i++) 49 | { 50 | writer.WriteUInt64((ulong)i); 51 | } 52 | 53 | 54 | Assert.That(writer.ByteCapacity, Is.EqualTo(20_864), "should double each time it goes over capacity"); 55 | } 56 | 57 | [Test] 58 | public void ResizedArrayContainsAllData() 59 | { 60 | var overCapacity = (1300 / 8) + 10; 61 | for (var i = 0; i < overCapacity; i++) 62 | { 63 | writer.WriteUInt64((ulong)i); 64 | } 65 | 66 | 67 | var segment = writer.ToArraySegment(); 68 | reader.Reset(segment); 69 | for (var i = 0; i < overCapacity; i++) 70 | { 71 | Assert.That(reader.ReadUInt64(), Is.EqualTo((ulong)i)); 72 | } 73 | } 74 | } 75 | public class BitPackingReusingWriterTests 76 | { 77 | private NetworkWriter writer; 78 | private NetworkReader reader; 79 | 80 | [OneTimeSetUp] 81 | public void OneTimeSetUp() 82 | { 83 | writer = new NetworkWriter(1300); 84 | reader = new NetworkReader(); 85 | } 86 | 87 | [TearDown] 88 | public void TearDown() 89 | { 90 | writer.Reset(); 91 | reader.Dispose(); 92 | } 93 | 94 | 95 | [Test] 96 | public void WriteUShortAfterReset() 97 | { 98 | ushort value1 = 0b0101; 99 | ushort value2 = 0x1000; 100 | 101 | // write first value 102 | writer.WriteUInt16(value1); 103 | 104 | reader.Reset(writer.ToArray()); 105 | var out1 = reader.ReadUInt16(); 106 | Assert.That(out1, Is.EqualTo(value1)); 107 | 108 | // reset and write 2nd value 109 | writer.Reset(); 110 | 111 | writer.WriteUInt16(value2); 112 | 113 | reader.Reset(writer.ToArray()); 114 | var out2 = reader.ReadUInt16(); 115 | Assert.That(out2, Is.EqualTo(value2), "Value 2 was incorrect"); 116 | } 117 | 118 | [Test] 119 | [TestCase(0b0101ul, 0x1000ul)] 120 | [TestCase(0xffff_0000_ffff_fffful, 0x0000_ffff_1111_0000ul)] 121 | public void WriteULongAfterReset(ulong value1, ulong value2) 122 | { 123 | // write first value 124 | writer.WriteUInt64(value1); 125 | 126 | reader.Reset(writer.ToArray()); 127 | var out1 = reader.ReadUInt64(); 128 | Assert.That(out1, Is.EqualTo(value1)); 129 | 130 | // reset and write 2nd value 131 | writer.Reset(); 132 | 133 | writer.WriteUInt64(value2); 134 | 135 | reader.Reset(writer.ToArray()); 136 | var out2 = reader.ReadUInt64(); 137 | Assert.That(out2, Is.EqualTo(value2), "Value 2 was incorrect"); 138 | } 139 | 140 | [Test] 141 | [TestCase(0b0101ul, 0x1000ul)] 142 | [TestCase(0xffff_0000_ffff_fffful, 0x0000_ffff_1111_0000ul)] 143 | public void WriteULongWriteBitsAfterReset(ulong value1, ulong value2) 144 | { 145 | // write first value 146 | writer.Write(value1, 64); 147 | 148 | reader.Reset(writer.ToArray()); 149 | var out1 = reader.Read(64); 150 | Assert.That(out1, Is.EqualTo(value1)); 151 | 152 | // reset and write 2nd value 153 | writer.Reset(); 154 | 155 | writer.Write(value2, 64); 156 | 157 | reader.Reset(writer.ToArray()); 158 | var out2 = reader.Read(64); 159 | Assert.That(out2, Is.EqualTo(value2), "Value 2 was incorrect"); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/BitPackingCopyFromOtherTests.cs: -------------------------------------------------------------------------------- 1 | using Mirage.Serialization; 2 | using NUnit.Framework; 3 | using Random = Mirage.Tests.BitPacking.TestRandom; 4 | 5 | namespace Mirage.Tests.Runtime.Serialization 6 | { 7 | public class BitPackingCopyFromOtherTests 8 | { 9 | private NetworkWriter writer; 10 | private NetworkWriter otherWriter; 11 | private NetworkReader reader; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | writer = new NetworkWriter(1300); 17 | otherWriter = new NetworkWriter(1300); 18 | reader = new NetworkReader(); 19 | } 20 | 21 | [TearDown] 22 | public void TearDown() 23 | { 24 | writer.Reset(); 25 | otherWriter.Reset(); 26 | reader.Dispose(); 27 | } 28 | 29 | [Test] 30 | public void CopyFromOtherWriterAligned() 31 | { 32 | otherWriter.Write(1, 8); 33 | otherWriter.Write(2, 8); 34 | otherWriter.Write(3, 8); 35 | otherWriter.Write(4, 8); 36 | otherWriter.Write(5, 8); 37 | 38 | 39 | writer.CopyFromWriter(otherWriter, 0, 5 * 8); 40 | 41 | var segment = writer.ToArraySegment(); 42 | reader.Reset(segment); 43 | 44 | Assert.That(reader.Read(8), Is.EqualTo(1)); 45 | Assert.That(reader.Read(8), Is.EqualTo(2)); 46 | Assert.That(reader.Read(8), Is.EqualTo(3)); 47 | Assert.That(reader.Read(8), Is.EqualTo(4)); 48 | Assert.That(reader.Read(8), Is.EqualTo(5)); 49 | } 50 | 51 | [Test] 52 | public void CopyFromOtherWriterUnAligned() 53 | { 54 | otherWriter.Write(1, 6); 55 | otherWriter.Write(2, 7); 56 | otherWriter.Write(3, 8); 57 | otherWriter.Write(4, 9); 58 | otherWriter.Write(5, 10); 59 | 60 | writer.Write(1, 3); 61 | 62 | writer.CopyFromWriter(otherWriter, 0, 40); 63 | 64 | var segment = writer.ToArraySegment(); 65 | reader.Reset(segment); 66 | 67 | Assert.That(reader.Read(3), Is.EqualTo(1)); 68 | Assert.That(reader.Read(6), Is.EqualTo(1)); 69 | Assert.That(reader.Read(7), Is.EqualTo(2)); 70 | Assert.That(reader.Read(8), Is.EqualTo(3)); 71 | Assert.That(reader.Read(9), Is.EqualTo(4)); 72 | Assert.That(reader.Read(10), Is.EqualTo(5)); 73 | } 74 | 75 | [Test] 76 | [Repeat(100)] 77 | public void CopyFromOtherWriterUnAlignedBig() 78 | { 79 | var value1 = (ulong)Random.Range(0, 20000); 80 | var value2 = (ulong)Random.Range(0, 20000); 81 | var value3 = (ulong)Random.Range(0, 20000); 82 | var value4 = (ulong)Random.Range(0, 20000); 83 | var value5 = (ulong)Random.Range(0, 20000); 84 | otherWriter.Write(value1, 46); 85 | otherWriter.Write(value2, 47); 86 | otherWriter.Write(value3, 48); 87 | otherWriter.Write(value4, 49); 88 | otherWriter.Write(value5, 50); 89 | 90 | writer.WriteUInt64(5); 91 | writer.Write(1, 3); 92 | writer.WriteByte(171); 93 | 94 | writer.CopyFromWriter(otherWriter, 0, 240); 95 | 96 | var segment = writer.ToArraySegment(); 97 | reader.Reset(segment); 98 | 99 | Assert.That(reader.ReadUInt64(), Is.EqualTo(5ul)); 100 | Assert.That(reader.Read(3), Is.EqualTo(1)); 101 | Assert.That(reader.ReadByte(), Is.EqualTo(171)); 102 | Assert.That(reader.Read(46), Is.EqualTo(value1), "Random value 1 not correct"); 103 | Assert.That(reader.Read(47), Is.EqualTo(value2), "Random value 2 not correct"); 104 | Assert.That(reader.Read(48), Is.EqualTo(value3), "Random value 3 not correct"); 105 | Assert.That(reader.Read(49), Is.EqualTo(value4), "Random value 4 not correct"); 106 | Assert.That(reader.Read(50), Is.EqualTo(value5), "Random value 5 not correct"); 107 | } 108 | 109 | [Test] 110 | [Repeat(100)] 111 | public void CopyFromOtherWriterUnAlignedBigOtherUnaligned() 112 | { 113 | for (var i = 0; i < 10; i++) 114 | { 115 | otherWriter.Write(12, 20); 116 | } 117 | 118 | 119 | var value1 = (ulong)Random.Range(0, 20000); 120 | var value2 = (ulong)Random.Range(0, 20000); 121 | var value3 = (ulong)Random.Range(0, 20000); 122 | var value4 = (ulong)Random.Range(0, 20000); 123 | var value5 = (ulong)Random.Range(0, 20000); 124 | otherWriter.Write(value1, 46); 125 | otherWriter.Write(value2, 47); 126 | otherWriter.Write(value3, 48); 127 | otherWriter.Write(value4, 49); 128 | otherWriter.Write(value5, 50); 129 | 130 | writer.WriteUInt64(5); 131 | writer.Write(1, 3); 132 | writer.WriteByte(171); 133 | 134 | writer.CopyFromWriter(otherWriter, 200, 240); 135 | 136 | var segment = writer.ToArraySegment(); 137 | reader.Reset(segment); 138 | 139 | Assert.That(reader.ReadUInt64(), Is.EqualTo(5ul)); 140 | Assert.That(reader.Read(3), Is.EqualTo(1)); 141 | Assert.That(reader.ReadByte(), Is.EqualTo(171)); 142 | Assert.That(reader.Read(46), Is.EqualTo(value1), "Random value 1 not correct"); 143 | Assert.That(reader.Read(47), Is.EqualTo(value2), "Random value 2 not correct"); 144 | Assert.That(reader.Read(48), Is.EqualTo(value3), "Random value 3 not correct"); 145 | Assert.That(reader.Read(49), Is.EqualTo(value4), "Random value 4 not correct"); 146 | Assert.That(reader.Read(50), Is.EqualTo(value5), "Random value 5 not correct"); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/UintPackerCreateTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Mirage.Serialization; 3 | using NUnit.Framework; 4 | 5 | namespace Mirage.Tests.Runtime.Serialization.Packers 6 | { 7 | public class UintPackerCreateTest : PackerTestBase 8 | { 9 | [Test] 10 | [TestCase(0ul, 50ul, 1000ul, ExpectedResult = 6 + 1)] 11 | [TestCase(60ul, 50ul, 1000ul, ExpectedResult = 6 + 1, Description = "50 rounds up to 64, so v=60 is still small")] 12 | [TestCase(100ul, 50ul, 1000ul, ExpectedResult = 10 + 2)] 13 | [TestCase(100ul, 500ul, 100000ul, ExpectedResult = 9 + 1)] 14 | [TestCase(501ul, 500ul, 100000ul, ExpectedResult = 9 + 1)] 15 | [TestCase(5_000ul, 500ul, 100000ul, ExpectedResult = 17 + 2)] 16 | [TestCase(5_000_000ul, 500ul, 100000ul, ExpectedResult = 64 + 2)] 17 | public int CreatesUsing2Values(ulong inValue, ulong smallValue, ulong mediumValue) 18 | { 19 | var packer = new VarIntPacker(smallValue, mediumValue); 20 | packer.PackUlong(writer, inValue); 21 | return writer.BitPosition; 22 | } 23 | 24 | 25 | [Test] 26 | [TestCase(0ul, 50ul, 1000ul, 50_000ul, ExpectedResult = 6 + 1)] 27 | [TestCase(60ul, 50ul, 1000ul, 50_000ul, ExpectedResult = 6 + 1, Description = "50 rounds up to 64, so v=60 is still small")] 28 | [TestCase(100ul, 50ul, 1000ul, 50_000ul, ExpectedResult = 10 + 2)] 29 | [TestCase(5000ul, 50ul, 1000ul, 50_000ul, ExpectedResult = 16 + 2)] 30 | [TestCase(100ul, 500ul, 100_000ul, 50_000_000ul, ExpectedResult = 9 + 1)] 31 | [TestCase(501ul, 500ul, 100_000ul, 50_000_000ul, ExpectedResult = 9 + 1)] 32 | [TestCase(5_000ul, 500ul, 100_000ul, 50_000_000ul, ExpectedResult = 17 + 2)] 33 | [TestCase(5_000_000ul, 500ul, 100_000ul, 50_000_000ul, ExpectedResult = 26 + 2)] 34 | public int CreatesUsing3Values(ulong inValue, ulong smallValue, ulong mediumValue, ulong largeValue) 35 | { 36 | var packer = new VarIntPacker(smallValue, mediumValue, largeValue); 37 | packer.PackUlong(writer, inValue); 38 | return writer.BitPosition; 39 | } 40 | 41 | 42 | [Test] 43 | public void ThrowsIfSmallBitIsZero() 44 | { 45 | var exception = Assert.Throws(() => 46 | { 47 | _ = VarIntPacker.FromBitCount(0, 10); 48 | }); 49 | var expected = new ArgumentException("Small value can not be zero", "smallBits"); 50 | Assert.That(exception, Has.Message.EqualTo(expected.Message)); 51 | } 52 | [Test] 53 | public void ThrowsIfMediumLessThanSmall() 54 | { 55 | var exception = Assert.Throws(() => 56 | { 57 | _ = VarIntPacker.FromBitCount(6, 5); 58 | }); 59 | var expected = new ArgumentException("Medium value must be greater than small value", "mediumBits"); 60 | Assert.That(exception, Has.Message.EqualTo(expected.Message)); 61 | } 62 | [Test] 63 | public void ThrowsIfLargeLessThanMedium() 64 | { 65 | var exception = Assert.Throws(() => 66 | { 67 | _ = VarIntPacker.FromBitCount(4, 10, 8); 68 | }); 69 | var expected = new ArgumentException("Large value must be greater than medium value", "largeBits"); 70 | Assert.That(exception, Has.Message.EqualTo(expected.Message)); 71 | } 72 | [Test] 73 | public void ThrowsIfLargeIsOver64() 74 | { 75 | var exception = Assert.Throws(() => 76 | { 77 | _ = VarIntPacker.FromBitCount(5, 10, 65); 78 | }); 79 | var expected = new ArgumentException("Large bits must be 64 or less", "largeBits"); 80 | Assert.That(exception, Has.Message.EqualTo(expected.Message)); 81 | } 82 | [Test] 83 | public void ThrowsIfMediumIsOver62() 84 | { 85 | var exception = Assert.Throws(() => 86 | { 87 | _ = VarIntPacker.FromBitCount(5, 63); 88 | }); 89 | var expected = new ArgumentException("Medium bits must be 62 or less", "mediumBits"); 90 | Assert.That(exception, Has.Message.EqualTo(expected.Message)); 91 | } 92 | 93 | [Test] 94 | public void ThrowsWhenValueIsOverLargeValue() 95 | { 96 | var packer = VarIntPacker.FromBitCount(1, 2, 3, true); 97 | var exception1 = Assert.Throws(() => 98 | { 99 | packer.PackUlong(writer, 20); 100 | }); 101 | var exception2 = Assert.Throws(() => 102 | { 103 | packer.PackUint(writer, 20); 104 | }); 105 | var exception3 = Assert.Throws(() => 106 | { 107 | packer.PackUlong(writer, 20); 108 | }); 109 | var expected = new ArgumentOutOfRangeException("value", 20, $"Value is over max of {7}"); 110 | Assert.That(exception1, Has.Message.EqualTo(expected.Message)); 111 | Assert.That(exception2, Has.Message.EqualTo(expected.Message)); 112 | Assert.That(exception3, Has.Message.EqualTo(expected.Message)); 113 | } 114 | 115 | [Test] 116 | [TestCase(20ul, 3)] 117 | [TestCase(260ul, 8)] 118 | [TestCase(50_000ul, 10)] 119 | public void WritesMaxIfOverLargeValue(ulong inValue, int largeBits) 120 | { 121 | var max = BitMask.Mask(largeBits); 122 | var packer = VarIntPacker.FromBitCount(1, 2, largeBits, false); 123 | Assert.DoesNotThrow(() => 124 | { 125 | packer.PackUlong(writer, inValue); 126 | packer.PackUint(writer, (uint)inValue); 127 | packer.PackUlong(writer, (ushort)inValue); 128 | }); 129 | var reader = GetReader(); 130 | var unpack1 = packer.UnpackUlong(reader); 131 | var unpack2 = packer.UnpackUint(reader); 132 | var unpack3 = packer.UnpackUshort(reader); 133 | 134 | Assert.That(unpack1, Is.EqualTo(max)); 135 | Assert.That(unpack2, Is.EqualTo(max)); 136 | Assert.That(unpack3, Is.EqualTo(max)); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/BitPackingTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using Mirage.Serialization; 4 | using NUnit.Framework; 5 | 6 | namespace Mirage.Tests.Runtime.Serialization 7 | { 8 | public class BitPackingTests 9 | { 10 | private NetworkWriter writer; 11 | private NetworkReader reader; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | // dont allow resizing for this test, because we test throw 17 | writer = new NetworkWriter(1300, false); 18 | reader = new NetworkReader(); 19 | } 20 | 21 | [TearDown] 22 | public void TearDown() 23 | { 24 | writer.Reset(); 25 | reader.Dispose(); 26 | } 27 | 28 | [Test] 29 | [TestCase(0ul)] 30 | [TestCase(1ul)] 31 | [TestCase(0x_FFFF_FFFF_12Ul)] 32 | public void WritesCorrectUlongValue(ulong value) 33 | { 34 | writer.Write(value, 64); 35 | reader.Reset(writer.ToArray()); 36 | 37 | var result = reader.Read(64); 38 | Assert.That(result, Is.EqualTo(value)); 39 | } 40 | 41 | [Test] 42 | [TestCase(0u)] 43 | [TestCase(1u)] 44 | [TestCase(0x_FFFF_FF12U)] 45 | public void WritesCorrectUIntValue(uint value) 46 | { 47 | writer.Write(value, 32); 48 | reader.Reset(writer.ToArray()); 49 | 50 | var result = reader.Read(32); 51 | Assert.That(result, Is.EqualTo(value)); 52 | } 53 | 54 | [Test] 55 | [TestCase(0u, 10, 2u, 5)] 56 | [TestCase(10u, 10, 36u, 15)] 57 | [TestCase(1u, 1, 250u, 8)] 58 | public void WritesCorrectValues(uint value1, int bits1, uint value2, int bits2) 59 | { 60 | writer.Write(value1, bits1); 61 | writer.Write(value2, bits2); 62 | reader.Reset(writer.ToArray()); 63 | 64 | var result1 = reader.Read(bits1); 65 | var result2 = reader.Read(bits2); 66 | Assert.That(result1, Is.EqualTo(value1)); 67 | Assert.That(result2, Is.EqualTo(value2)); 68 | } 69 | 70 | 71 | [Test] 72 | public void CanWriteToBufferLimit() 73 | { 74 | for (var i = 0; i < 208; i++) 75 | { 76 | writer.Write((ulong)i, 50); 77 | } 78 | 79 | var result = writer.ToArray(); 80 | 81 | // written bits/8 82 | var expectedLength = (208 * 50) / 8; 83 | Assert.That(result, Has.Length.EqualTo(expectedLength)); 84 | } 85 | 86 | [Test, Description("Real buffer size is 1304 because 1300 rounds up")] 87 | public void WriterThrowIfWritesTooMuch() 88 | { 89 | // write 1296 up to last word 90 | for (var i = 0; i < 162; i++) 91 | { 92 | writer.Write((ulong)i, 64); 93 | } 94 | 95 | writer.Write(0, 63); 96 | 97 | Assert.DoesNotThrow(() => 98 | { 99 | writer.Write(0, 1); 100 | }); 101 | 102 | var exception = Assert.Throws(() => 103 | { 104 | writer.Write(0, 1); 105 | }); 106 | const int max = 162 * 64 + 64; 107 | Assert.That(exception, Has.Message.EqualTo($"Can not write over end of buffer, new length {max + 1}, capacity {max}")); 108 | } 109 | 110 | [Test] 111 | [Repeat(10)] 112 | public void WritesAllValueSizesCorrectly([Range(0, 63)] int startPosition, [Range(0, 64)] int valueBits) 113 | { 114 | var randomValue = ULongRandom.Next(); 115 | writer.Write(0, startPosition); 116 | 117 | var maskedValue = randomValue & BitMask.Mask(valueBits); 118 | 119 | writer.Write(randomValue, valueBits); 120 | reader.Reset(writer.ToArray()); 121 | 122 | _ = reader.Read(startPosition); 123 | var result = reader.Read(valueBits); 124 | Assert.That(result, Is.EqualTo(maskedValue)); 125 | } 126 | 127 | [Test] 128 | public void WritesAllMasksCorrectly() 129 | { 130 | // we can't use [range] args because we have to skip cases where end is over 64 131 | var count = 0; 132 | for (var start = 0; start < 64; start++) 133 | { 134 | for (var bits = 0; bits < 64; bits++) 135 | { 136 | var end = start + bits; 137 | if (end > 64) 138 | { 139 | continue; 140 | } 141 | 142 | var expected = SlowMask(start, end); 143 | var actual = BitMask.OuterMask(start, end); 144 | count++; 145 | if (expected != actual) 146 | { 147 | Assert.Fail($"Failed, start:{start} bits:{bits}"); 148 | } 149 | } 150 | } 151 | 152 | // define to allow debug.log to be skipped when running outside of unity 153 | #if !BIT_PACKING_NO_DEBUG 154 | UnityEngine.Debug.Log($"{count} masks tested"); 155 | #endif 156 | } 157 | 158 | /// 159 | /// Slower but correct mask 160 | /// 161 | /// 162 | /// 163 | /// 164 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 165 | public static ulong SlowMask(int start, int end) 166 | { 167 | // old mask, doesn't work when bitposition before/after is multiple of 64 168 | // so we need to check if values == 0 before shifting masks 169 | var mask1 = start == 0 ? 0ul : (ulong.MaxValue >> (64 - start)); 170 | // note: new position can not be 0, so no need to worry about 171 | var mask2 = (end & 0b11_1111) == 0 ? 0ul : (ulong.MaxValue << end /*we can use full position here as c# will mask it to just 6 bits*/); 172 | // mask either side of value, eg writing 4 bits at position 3: 111...111_1000_0111 173 | var mask = mask1 | mask2; 174 | return mask; 175 | } 176 | } 177 | 178 | public static class ULongRandom 179 | { 180 | private static Random rand; 181 | private static byte[] bytes; 182 | 183 | public static unsafe ulong Next() 184 | { 185 | if (rand == null) 186 | { 187 | rand = new System.Random(); 188 | bytes = new byte[8]; 189 | } 190 | 191 | rand.NextBytes(bytes); 192 | fixed (byte* ptr = &bytes[0]) 193 | { 194 | return *(ulong*)ptr; 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/VariableIntPacker.cs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 James Frowen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | using System; 26 | using System.Runtime.CompilerServices; 27 | 28 | namespace Mirage.Serialization 29 | { 30 | public sealed class VarIntPacker 31 | { 32 | // todo needs doc comments 33 | // todo need attribute to validate large bits based on pack type (eg if packing ushort, make sure largebits is 16 or less) 34 | 35 | private readonly int _smallBitCount; 36 | private readonly int _mediumBitsCount; 37 | private readonly int _largeBitsCount; 38 | private readonly ulong _smallValue; 39 | private readonly ulong _mediumValue; 40 | private readonly ulong _largeValue; 41 | private readonly bool _throwIfOverLarge; 42 | 43 | public VarIntPacker(ulong smallValue, ulong mediumValue) 44 | : this(BitHelper.BitCount(smallValue), BitHelper.BitCount(mediumValue), 64, false) { } 45 | public VarIntPacker(ulong smallValue, ulong mediumValue, ulong largeValue, bool throwIfOverLarge = true) 46 | : this(BitHelper.BitCount(smallValue), BitHelper.BitCount(mediumValue), BitHelper.BitCount(largeValue), throwIfOverLarge) { } 47 | 48 | public static VarIntPacker FromBitCount(int smallBits, int mediumBits) 49 | => FromBitCount(smallBits, mediumBits, 64, false); 50 | public static VarIntPacker FromBitCount(int smallBits, int mediumBits, int largeBits, bool throwIfOverLarge = true) 51 | => new VarIntPacker(smallBits, mediumBits, largeBits, throwIfOverLarge); 52 | 53 | private VarIntPacker(int smallBits, int mediumBits, int largeBits, bool throwIfOverLarge) 54 | { 55 | _throwIfOverLarge = throwIfOverLarge; 56 | if (smallBits == 0) throw new ArgumentException("Small value can not be zero", nameof(smallBits)); 57 | if (smallBits >= mediumBits) throw new ArgumentException("Medium value must be greater than small value", nameof(mediumBits)); 58 | if (mediumBits >= largeBits) throw new ArgumentException("Large value must be greater than medium value", nameof(largeBits)); 59 | if (largeBits > 64) throw new ArgumentException("Large bits must be 64 or less", nameof(largeBits)); 60 | // force medium to also be 62 or less so we can use 1 write call (2 bits to say its medium + 62 value bits 61 | if (mediumBits > 62) throw new ArgumentException("Medium bits must be 62 or less", nameof(mediumBits)); 62 | 63 | _smallBitCount = smallBits; 64 | _mediumBitsCount = mediumBits; 65 | _largeBitsCount = largeBits; 66 | 67 | // mask is also max value for n bits 68 | _smallValue = BitMask.Mask(smallBits); 69 | _mediumValue = BitMask.Mask(mediumBits); 70 | _largeValue = BitMask.Mask(largeBits); 71 | } 72 | 73 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 74 | public void PackUlong(NetworkWriter writer, ulong value) 75 | { 76 | pack(writer, value, 64); 77 | } 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | public void PackUint(NetworkWriter writer, uint value) 80 | { 81 | pack(writer, value, 32); 82 | } 83 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 84 | public void PackUshort(NetworkWriter writer, ushort value) 85 | { 86 | pack(writer, value, 16); 87 | } 88 | 89 | private void pack(NetworkWriter writer, ulong value, int maxBits) 90 | { 91 | if (value <= _smallValue) 92 | { 93 | // start with b0 to say small, then value 94 | writer.Write(value << 1, _smallBitCount + 1); 95 | } 96 | else if (value <= _mediumValue) 97 | { 98 | // start with b01 to say medium, then value 99 | writer.Write((value << 2) | 0b01, _mediumBitsCount + 2); 100 | } 101 | else if (value <= _largeValue) 102 | { 103 | // start with b11 to say large, then value 104 | // use 2 write calls here because bitCount could be 64 105 | writer.Write(0b11, 2); 106 | writer.Write(value, Math.Min(maxBits, _largeBitsCount)); 107 | } 108 | else 109 | { 110 | if (_throwIfOverLarge) 111 | { 112 | throw new ArgumentOutOfRangeException(nameof(value), value, $"Value is over max of {_largeValue}"); 113 | } 114 | else 115 | { 116 | // if no throw write MaxValue 117 | // we dont want to write value here because it will be masked and lose some high bits 118 | // need 2 write calls here because max is 64+2 bits 119 | writer.Write(0b11, 2); 120 | writer.Write(ulong.MaxValue, Math.Min(maxBits, _largeBitsCount)); 121 | } 122 | } 123 | } 124 | 125 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 126 | public ulong UnpackUlong(NetworkReader reader) 127 | { 128 | return unpack(reader, 64); 129 | } 130 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 131 | public uint UnpackUint(NetworkReader reader) 132 | { 133 | return (uint)unpack(reader, 32); 134 | } 135 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 136 | public ushort UnpackUshort(NetworkReader reader) 137 | { 138 | return (ushort)unpack(reader, 16); 139 | } 140 | 141 | private ulong unpack(NetworkReader reader, int maxBits) 142 | { 143 | if (!reader.ReadBoolean()) 144 | { 145 | return reader.Read(_smallBitCount); 146 | } 147 | else 148 | { 149 | if (!reader.ReadBoolean()) 150 | { 151 | return reader.Read(_mediumBitsCount); 152 | } 153 | else 154 | { 155 | return reader.Read(Math.Min(_largeBitsCount, maxBits)); 156 | } 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.csproj.meta 2 | *.sln.meta 3 | JamesFrowen.BitPacking.meta 4 | JamesFrowen.BitPacking.Tests.meta 5 | JamesFrowen.BitPacking/Source.meta 6 | JamesFrowen.BitPacking.Tests/Tests.meta 7 | README.md.meta 8 | LICENSE.meta 9 | 10 | # Created by https://www.toptal.com/developers/gitignore/api/dotnetcore,vs 11 | # Edit at https://www.toptal.com/developers/gitignore?templates=dotnetcore,vs 12 | 13 | ### DotnetCore ### 14 | # .NET Core build folders 15 | bin/ 16 | obj/ 17 | 18 | # Common node modules locations 19 | /node_modules 20 | /wwwroot/node_modules 21 | 22 | ### vs ### 23 | ## Ignore Visual Studio temporary files, build results, and 24 | ## files generated by popular Visual Studio add-ons. 25 | ## 26 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 27 | 28 | # User-specific files 29 | *.rsuser 30 | *.suo 31 | *.user 32 | *.userosscache 33 | *.sln.docstates 34 | 35 | # User-specific files (MonoDevelop/Xamarin Studio) 36 | *.userprefs 37 | 38 | # Mono auto generated files 39 | mono_crash.* 40 | 41 | # Build results 42 | [Dd]ebug/ 43 | [Dd]ebugPublic/ 44 | [Rr]elease/ 45 | [Rr]eleases/ 46 | x64/ 47 | x86/ 48 | [Aa][Rr][Mm]/ 49 | [Aa][Rr][Mm]64/ 50 | bld/ 51 | [Bb]in/ 52 | [Oo]bj/ 53 | [Ll]og/ 54 | [Ll]ogs/ 55 | 56 | # Visual Studio 2015/2017 cache/options directory 57 | .vs/ 58 | # Uncomment if you have tasks that create the project's static files in wwwroot 59 | #wwwroot/ 60 | 61 | # Visual Studio 2017 auto generated files 62 | Generated\ Files/ 63 | 64 | # MSTest test Results 65 | [Tt]est[Rr]esult*/ 66 | [Bb]uild[Ll]og.* 67 | 68 | # NUnit 69 | *.VisualState.xml 70 | TestResult.xml 71 | nunit-*.xml 72 | 73 | # Build Results of an ATL Project 74 | [Dd]ebugPS/ 75 | [Rr]eleasePS/ 76 | dlldata.c 77 | 78 | # Benchmark Results 79 | BenchmarkDotNet.Artifacts/ 80 | 81 | # .NET Core 82 | project.lock.json 83 | project.fragment.lock.json 84 | artifacts/ 85 | 86 | # StyleCop 87 | StyleCopReport.xml 88 | 89 | # Files built by Visual Studio 90 | *_i.c 91 | *_p.c 92 | *_h.h 93 | *.ilk 94 | *.obj 95 | *.iobj 96 | *.pch 97 | *.pdb 98 | *.ipdb 99 | *.pgc 100 | *.pgd 101 | *.rsp 102 | *.sbr 103 | *.tlb 104 | *.tli 105 | *.tlh 106 | *.tmp 107 | *.tmp_proj 108 | *_wpftmp.csproj 109 | *.log 110 | *.vspscc 111 | *.vssscc 112 | .builds 113 | *.pidb 114 | *.svclog 115 | *.scc 116 | 117 | # Chutzpah Test files 118 | _Chutzpah* 119 | 120 | # Visual C++ cache files 121 | ipch/ 122 | *.aps 123 | *.ncb 124 | *.opendb 125 | *.opensdf 126 | *.sdf 127 | *.cachefile 128 | *.VC.db 129 | *.VC.VC.opendb 130 | 131 | # Visual Studio profiler 132 | *.psess 133 | *.vsp 134 | *.vspx 135 | *.sap 136 | 137 | # Visual Studio Trace Files 138 | *.e2e 139 | 140 | # TFS 2012 Local Workspace 141 | $tf/ 142 | 143 | # Guidance Automation Toolkit 144 | *.gpState 145 | 146 | # ReSharper is a .NET coding add-in 147 | _ReSharper*/ 148 | *.[Rr]e[Ss]harper 149 | *.DotSettings.user 150 | 151 | # TeamCity is a build add-in 152 | _TeamCity* 153 | 154 | # DotCover is a Code Coverage Tool 155 | *.dotCover 156 | 157 | # AxoCover is a Code Coverage Tool 158 | .axoCover/* 159 | !.axoCover/settings.json 160 | 161 | # Coverlet is a free, cross platform Code Coverage Tool 162 | coverage*[.json, .xml, .info] 163 | 164 | # Visual Studio code coverage results 165 | *.coverage 166 | *.coveragexml 167 | 168 | # NCrunch 169 | _NCrunch_* 170 | .*crunch*.local.xml 171 | nCrunchTemp_* 172 | 173 | # MightyMoose 174 | *.mm.* 175 | AutoTest.Net/ 176 | 177 | # Web workbench (sass) 178 | .sass-cache/ 179 | 180 | # Installshield output folder 181 | [Ee]xpress/ 182 | 183 | # DocProject is a documentation generator add-in 184 | DocProject/buildhelp/ 185 | DocProject/Help/*.HxT 186 | DocProject/Help/*.HxC 187 | DocProject/Help/*.hhc 188 | DocProject/Help/*.hhk 189 | DocProject/Help/*.hhp 190 | DocProject/Help/Html2 191 | DocProject/Help/html 192 | 193 | # Click-Once directory 194 | publish/ 195 | 196 | # Publish Web Output 197 | *.[Pp]ublish.xml 198 | *.azurePubxml 199 | # Note: Comment the next line if you want to checkin your web deploy settings, 200 | # but database connection strings (with potential passwords) will be unencrypted 201 | *.pubxml 202 | *.publishproj 203 | 204 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 205 | # checkin your Azure Web App publish settings, but sensitive information contained 206 | # in these scripts will be unencrypted 207 | PublishScripts/ 208 | 209 | # NuGet Packages 210 | *.nupkg 211 | # NuGet Symbol Packages 212 | *.snupkg 213 | # The packages folder can be ignored because of Package Restore 214 | **/[Pp]ackages/* 215 | # except build/, which is used as an MSBuild target. 216 | !**/[Pp]ackages/build/ 217 | # Uncomment if necessary however generally it will be regenerated when needed 218 | #!**/[Pp]ackages/repositories.config 219 | # NuGet v3's project.json files produces more ignorable files 220 | *.nuget.props 221 | *.nuget.targets 222 | 223 | # Microsoft Azure Build Output 224 | csx/ 225 | *.build.csdef 226 | 227 | # Microsoft Azure Emulator 228 | ecf/ 229 | rcf/ 230 | 231 | # Windows Store app package directories and files 232 | AppPackages/ 233 | BundleArtifacts/ 234 | Package.StoreAssociation.xml 235 | _pkginfo.txt 236 | *.appx 237 | *.appxbundle 238 | *.appxupload 239 | 240 | # Visual Studio cache files 241 | # files ending in .cache can be ignored 242 | *.[Cc]ache 243 | # but keep track of directories ending in .cache 244 | !?*.[Cc]ache/ 245 | 246 | # Others 247 | ClientBin/ 248 | ~$* 249 | *~ 250 | *.dbmdl 251 | *.dbproj.schemaview 252 | *.jfm 253 | *.pfx 254 | *.publishsettings 255 | orleans.codegen.cs 256 | 257 | # Including strong name files can present a security risk 258 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 259 | #*.snk 260 | 261 | # Since there are multiple workflows, uncomment next line to ignore bower_components 262 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 263 | #bower_components/ 264 | 265 | # RIA/Silverlight projects 266 | Generated_Code/ 267 | 268 | # Backup & report files from converting an old project file 269 | # to a newer Visual Studio version. Backup files are not needed, 270 | # because we have git ;-) 271 | _UpgradeReport_Files/ 272 | Backup*/ 273 | UpgradeLog*.XML 274 | UpgradeLog*.htm 275 | ServiceFabricBackup/ 276 | *.rptproj.bak 277 | 278 | # SQL Server files 279 | *.mdf 280 | *.ldf 281 | *.ndf 282 | 283 | # Business Intelligence projects 284 | *.rdl.data 285 | *.bim.layout 286 | *.bim_*.settings 287 | *.rptproj.rsuser 288 | *- [Bb]ackup.rdl 289 | *- [Bb]ackup ([0-9]).rdl 290 | *- [Bb]ackup ([0-9][0-9]).rdl 291 | 292 | # Microsoft Fakes 293 | FakesAssemblies/ 294 | 295 | # GhostDoc plugin setting file 296 | *.GhostDoc.xml 297 | 298 | # Node.js Tools for Visual Studio 299 | .ntvs_analysis.dat 300 | node_modules/ 301 | 302 | # Visual Studio 6 build log 303 | *.plg 304 | 305 | # Visual Studio 6 workspace options file 306 | *.opt 307 | 308 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 309 | *.vbw 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # End of https://www.toptal.com/developers/gitignore/api/dotnetcore,vs -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/FloatPacker.cs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 James Frowen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | using System; 26 | using System.Runtime.CompilerServices; 27 | using UnityEngine; 28 | 29 | namespace Mirage.Serialization 30 | { 31 | /// 32 | /// Helps compresses a float into a reduced number of bits 33 | /// 34 | public sealed class FloatPacker 35 | { 36 | private readonly int _bitCount; 37 | private readonly float _multiplier_pack; 38 | private readonly float _multiplier_unpack; 39 | private readonly uint _mask; 40 | private readonly int _toNegative; 41 | 42 | /// max positive value, any uint value over this will be negative 43 | private readonly uint _midPoint; 44 | private readonly float _positiveMax; 45 | private readonly float _negativeMax; 46 | 47 | /// lowest precision, actual precision will be caculated from number of bits used 48 | public FloatPacker(float max, float lowestPrecision) : this(max, lowestPrecision, true) { } 49 | 50 | public FloatPacker(float max, int bitCount) : this(max, bitCount, true) { } 51 | 52 | /// 53 | /// lowest precision, actual precision will be caculated from number of bits used 54 | /// if negative values will be allowed or not 55 | public FloatPacker(float max, float lowestPrecision, bool signed) : this(max, BitHelper.BitCount(max, lowestPrecision, signed), signed) { } 56 | 57 | /// if negative values will be allowed or not 58 | public FloatPacker(float max, int bitCount, bool signed) 59 | { 60 | _bitCount = bitCount; 61 | // not sure what max bit count should be, 62 | // but 30 seems reasonable since an unpacked float is already 32 63 | if (max == 0) throw new ArgumentException("Max can not be 0", nameof(max)); 64 | if (bitCount < 1) throw new ArgumentException("Bit count is too low, bit count should be between 1 and 30", nameof(bitCount)); 65 | if (bitCount > 30) throw new ArgumentException("Bit count is too high, bit count should be between 1 and 30", nameof(bitCount)); 66 | 67 | _mask = (1u << bitCount) - 1u; 68 | 69 | if (signed) 70 | { 71 | _midPoint = (1u << (bitCount - 1)) - 1u; 72 | _toNegative = (int)(_mask + 1u); 73 | 74 | _positiveMax = max; 75 | _negativeMax = -max; 76 | } 77 | else // unsigned 78 | { 79 | _midPoint = (1u << (bitCount)) - 1u; 80 | _toNegative = 0; 81 | 82 | _positiveMax = max; 83 | _negativeMax = 0; 84 | } 85 | 86 | _multiplier_pack = _midPoint / max; 87 | _multiplier_unpack = 1 / _multiplier_pack; 88 | } 89 | 90 | 91 | /// 92 | /// Packs a float value into a uint 93 | /// Clamps the value within min/max range 94 | /// 95 | /// 96 | /// 97 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 98 | public uint Pack(float value) 99 | { 100 | if (value >= _positiveMax) value = _positiveMax; 101 | if (value <= _negativeMax) value = _negativeMax; 102 | return PackNoClamp(value); 103 | } 104 | 105 | /// 106 | /// Packs and Writes a float value 107 | /// Clamps the value within min/max range 108 | /// 109 | /// 110 | /// 111 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 112 | public void Pack(NetworkWriter writer, float value) 113 | { 114 | if (value >= _positiveMax) value = _positiveMax; 115 | if (value <= _negativeMax) value = _negativeMax; 116 | PackNoClamp(writer, value); 117 | } 118 | 119 | /// 120 | /// Packs a float value into a uint without clamping it in range 121 | /// 122 | /// WARNING: only use this method if value is always in range. Out of range values may not be unpacked correctly 123 | /// 124 | /// 125 | /// 126 | /// 127 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 128 | public uint PackNoClamp(float value) 129 | { 130 | return (uint)Mathf.RoundToInt(value * _multiplier_pack) & _mask; 131 | } 132 | 133 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 134 | public void PackNoClamp(NetworkWriter writer, float value) 135 | { 136 | // dont need to mask value here because the Write function will mask it 137 | writer.Write((uint)Mathf.RoundToInt(value * _multiplier_pack), _bitCount); 138 | } 139 | 140 | 141 | /// 142 | /// Unpacks uint value to float 143 | /// 144 | /// 145 | /// 146 | /// 147 | /// Positive and Negative values need to be unpacked differnely so that they both keep same precision 148 | /// 149 | /// Example 10 bits (max uint = 1023): 150 | ///
151 | /// p = precision 152 | /// 153 | /// Positive values have uint range 0 to 511 154 | ///
155 | /// Unpacked: 0 to max 156 | ///
157 | /// 158 | /// Negative values have uint range 512 to 1023 159 | ///
160 | /// Unpacked using same as positive: max+p to max*2+p 161 | ///
162 | /// but we want range -max to -p 163 | ///
164 | /// so we need to subtrack -1024 so range is -512 to -1 165 | ///
166 | /// then scale to unpack to -max to -p 167 | ///
168 | ///
169 | ///
170 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 171 | public float Unpack(uint value) 172 | { 173 | if (value <= _midPoint) // positive 174 | { 175 | // 0 -> 511 176 | // 0 -> (max) 177 | return value * _multiplier_unpack; 178 | } 179 | else // negative 180 | { 181 | // max = 1024 182 | // 512 -> 1023 183 | // -max -> 0 184 | 185 | // doing `value - max*2` cause: 186 | // -512 -> -1 187 | return ((int)value - _toNegative) * _multiplier_unpack; 188 | } 189 | } 190 | 191 | /// 192 | /// Reads and unpacks float value 193 | /// 194 | /// 195 | /// 196 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 197 | public float Unpack(NetworkReader reader) 198 | { 199 | return Unpack((uint)reader.Read(_bitCount)); 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/Packers/QuaternionPacker.cs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 James Frowen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | using System; 26 | using System.Runtime.CompilerServices; 27 | using UnityEngine; 28 | 29 | namespace Mirage.Serialization 30 | { 31 | public sealed class QuaternionPacker 32 | { 33 | /// Default packer using 9 bits per element, 29 bits total 34 | public static readonly QuaternionPacker Default9 = new QuaternionPacker(9); 35 | /// Default packer using 10 bits per element, 32 bits total 36 | public static readonly QuaternionPacker Default10 = new QuaternionPacker(10); 37 | 38 | public static uint PackAsInt(Quaternion value) 39 | { 40 | return (uint)Default10.Pack(value); 41 | } 42 | public static Quaternion UnpackFromInt(uint value) 43 | { 44 | return Default10.Unpack(value); 45 | } 46 | 47 | /// 48 | /// 1 / sqrt(2) 49 | /// 50 | private const float MAX_VALUE = 1f / 1.414214f; 51 | 52 | /// 53 | /// bit count per element writen 54 | /// 55 | private readonly int _bitCountPerElement; 56 | 57 | /// 58 | /// total bit count for Quaternion 59 | /// 60 | /// count = 3 * perElement + 2; 61 | /// 62 | /// 63 | private readonly int _totalBitCount; 64 | private readonly uint _readMask; 65 | private readonly FloatPacker _floatPacker; 66 | 67 | /// 10 per "smallest 3" is good enough for most people 68 | public QuaternionPacker(int quaternionBitLength = 10) 69 | { 70 | // (this.BitLength - 1) because pack sign by itself 71 | _bitCountPerElement = quaternionBitLength; 72 | _totalBitCount = 2 + (quaternionBitLength * 3); 73 | _floatPacker = new FloatPacker(MAX_VALUE, quaternionBitLength); 74 | _readMask = (uint)BitMask.Mask(_bitCountPerElement); 75 | } 76 | 77 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 78 | public void Pack(NetworkWriter writer, Quaternion value) 79 | { 80 | writer.Write(Pack(value), _totalBitCount); 81 | } 82 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 83 | public ulong Pack(Quaternion value) 84 | { 85 | QuickNormalize(ref value); 86 | 87 | FindLargestIndex(ref value, out var index); 88 | 89 | GetSmallerDimensions(index, ref value, out var a, out var b, out var c); 90 | 91 | // largest needs to be positive to be calculated by reader 92 | // if largest is negative flip sign of others because Q = -Q 93 | if (value[(int)index] < 0) 94 | { 95 | a = -a; 96 | b = -b; 97 | c = -c; 98 | } 99 | 100 | // todo, should we be rounding down for abc? because if they are rounded up their sum may be greater than largest 101 | 102 | ulong combine = 0; 103 | // write Index as (3-i) so that Quaternion.identity will be all zeros 104 | combine |= (ulong)(3 - index) << (_bitCountPerElement * 3); 105 | combine |= (ulong)_floatPacker.PackNoClamp(a) << (_bitCountPerElement * 2); 106 | combine |= (ulong)_floatPacker.PackNoClamp(b) << _bitCountPerElement; 107 | combine |= _floatPacker.PackNoClamp(c); 108 | return combine; 109 | } 110 | 111 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 112 | internal static void QuickNormalize(ref Quaternion quaternion) 113 | { 114 | var dot = 115 | (quaternion.x * quaternion.x) + 116 | (quaternion.y * quaternion.y) + 117 | (quaternion.z * quaternion.z) + 118 | (quaternion.w * quaternion.w); 119 | 120 | const float allowedEpsilon = 1E-5f; 121 | const float minAllowed = 1 - allowedEpsilon; 122 | const float maxAllowed = 1 + allowedEpsilon; 123 | // only normalize if dot product is outside allowed range 124 | if (minAllowed > dot || maxAllowed < dot) 125 | { 126 | var dotSqrt = (float)Math.Sqrt(dot); 127 | // rotation is 0 128 | if (dotSqrt < allowedEpsilon) 129 | { 130 | // identity 131 | quaternion.x = 0; 132 | quaternion.y = 0; 133 | quaternion.z = 0; 134 | quaternion.w = 1; 135 | } 136 | else 137 | { 138 | var iDotSqrt = 1 / dotSqrt; 139 | quaternion.x *= iDotSqrt; 140 | quaternion.y *= iDotSqrt; 141 | quaternion.z *= iDotSqrt; 142 | quaternion.w *= iDotSqrt; 143 | } 144 | } 145 | } 146 | 147 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 148 | internal static void FindLargestIndex(ref Quaternion quaternion, out uint index) 149 | { 150 | var x2 = quaternion.x * quaternion.x; 151 | var y2 = quaternion.y * quaternion.y; 152 | var z2 = quaternion.z * quaternion.z; 153 | var w2 = quaternion.w * quaternion.w; 154 | 155 | index = 0; 156 | var current = x2; 157 | // check vs sq to avoid doing mathf.abs 158 | if (y2 > current) 159 | { 160 | index = 1; 161 | current = y2; 162 | } 163 | if (z2 > current) 164 | { 165 | index = 2; 166 | current = z2; 167 | } 168 | if (w2 > current) 169 | { 170 | index = 3; 171 | } 172 | } 173 | 174 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 175 | private static void GetSmallerDimensions(uint largestIndex, ref Quaternion quaternion, out float a, out float b, out float c) 176 | { 177 | switch (largestIndex) 178 | { 179 | case 0: 180 | a = quaternion.y; 181 | b = quaternion.z; 182 | c = quaternion.w; 183 | return; 184 | case 1: 185 | a = quaternion.x; 186 | b = quaternion.z; 187 | c = quaternion.w; 188 | return; 189 | case 2: 190 | a = quaternion.x; 191 | b = quaternion.y; 192 | c = quaternion.w; 193 | return; 194 | case 3: 195 | a = quaternion.x; 196 | b = quaternion.y; 197 | c = quaternion.z; 198 | return; 199 | default: 200 | ThrowIfOutOfRange(); 201 | a = b = c = default; 202 | return; 203 | } 204 | } 205 | 206 | private static void ThrowIfOutOfRange() => throw new IndexOutOfRangeException("Invalid Quaternion index!"); 207 | 208 | 209 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 210 | public Quaternion Unpack(NetworkReader reader) 211 | { 212 | return Unpack(reader.Read(_totalBitCount)); 213 | } 214 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 215 | public Quaternion Unpack(ulong combine) 216 | { 217 | // Index writen as (3-i) 218 | // so (3-c) to decode 219 | var index = 3 - (uint)(combine >> (_bitCountPerElement * 3)); 220 | 221 | var a = _floatPacker.Unpack((uint)(combine >> (_bitCountPerElement * 2)) & _readMask); 222 | var b = _floatPacker.Unpack((uint)(combine >> (_bitCountPerElement * 1)) & _readMask); 223 | var c = _floatPacker.Unpack((uint)combine & _readMask); 224 | 225 | var l2 = 1 - ((a * a) + (b * b) + (c * c)); 226 | var largest = (float)Math.Sqrt(l2); 227 | // this Quaternion should already be normallized because of the way that largest is calculated 228 | switch (index) 229 | { 230 | case 0: 231 | return new Quaternion(largest, a, b, c); 232 | case 1: 233 | return new Quaternion(a, largest, b, c); 234 | case 2: 235 | return new Quaternion(a, b, largest, c); 236 | case 3: 237 | return new Quaternion(a, b, c, largest); 238 | default: 239 | ThrowIfOutOfRange(); 240 | return default; 241 | } 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking.Tests/Tests/Packers/QuaternionPackerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using Mirage.Serialization; 4 | using NUnit.Framework; 5 | using NUnit.Framework.Constraints; 6 | using UnityEngine; 7 | using Random = Mirage.Tests.BitPacking.TestRandom; 8 | using Range = NUnit.Framework.RangeAttribute; 9 | 10 | namespace Mirage.Tests.Runtime.Serialization.Packers 11 | { 12 | public class QuaternionPackerTests : PackerTestBase 13 | { 14 | private static Quaternion GetRandomQuaternion() 15 | { 16 | return GetRandomQuaternionNotNormalized().normalized; 17 | } 18 | 19 | private static Quaternion GetRandomQuaternionNotNormalized() 20 | { 21 | return new Quaternion( 22 | Random.Range(-1, 1), 23 | Random.Range(-1, 1), 24 | Random.Range(-1, 1), 25 | Random.Range(-1, 1) 26 | ); 27 | } 28 | 29 | 30 | [Test] 31 | public void IdentityIsUnpackedAsIdentity() 32 | { 33 | var packer = new QuaternionPacker(10); 34 | packer.Pack(writer, Quaternion.identity); 35 | var unpack = packer.Unpack(GetReader()); 36 | 37 | Assert.That(unpack, Is.EqualTo(Quaternion.identity)); 38 | } 39 | 40 | private static IEnumerable ReturnsCorrectIndexCases() 41 | { 42 | var values = new List() { 0.1f, 0.2f, 0.3f, 0.4f }; 43 | // abcd are index 44 | // testing all permutation, index can only be used once 45 | for (var a = 0; a < 4; a++) 46 | { 47 | for (var b = 0; b < 4; b++) 48 | { 49 | if (b == a) { continue; } 50 | 51 | for (var c = 0; c < 4; c++) 52 | { 53 | if (c == a || c == b) { continue; } 54 | 55 | for (var d = 0; d < 4; d++) 56 | { 57 | if (d == a || d == b || d == c) { continue; } 58 | 59 | uint largest = 0; 60 | // index 3 is the largest, 61 | if (a == 3) { largest = 0; } 62 | if (b == 3) { largest = 1; } 63 | if (c == 3) { largest = 2; } 64 | if (d == 3) { largest = 3; } 65 | yield return new TestCaseData(new Quaternion(values[a], values[b], values[c], values[d]).normalized) 66 | .Returns(largest); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | [Test] 74 | [TestCaseSource(nameof(ReturnsCorrectIndexCases))] 75 | public uint ReturnsCorrectIndex(Quaternion q) 76 | { 77 | QuaternionPacker.FindLargestIndex(ref q, out var index); 78 | return index; 79 | } 80 | 81 | private static IEnumerable PackAndUnpackCases() 82 | { 83 | for (var i = 8; i < 12; i++) 84 | { 85 | yield return new TestCaseData(i, Quaternion.identity); 86 | yield return new TestCaseData(i, Quaternion.Euler(25, 30, 0)); 87 | yield return new TestCaseData(i, Quaternion.Euler(-50, 30, 90)); 88 | yield return new TestCaseData(i, Quaternion.Euler(90, 90, 180)); 89 | yield return new TestCaseData(i, Quaternion.Euler(-20, 0, 45)); 90 | yield return new TestCaseData(i, Quaternion.Euler(80, 30, -45)); 91 | } 92 | } 93 | 94 | [Test] 95 | [TestCaseSource(nameof(PackAndUnpackCases))] 96 | #if !UNITY_EDITOR 97 | [Ignore("Quaternion.Euler Requires unity engine to run")] 98 | #endif 99 | public void PackAndUnpack(int bits, Quaternion inValue) 100 | { 101 | RunPackAndUnpack(bits, inValue); 102 | } 103 | 104 | 105 | [Test] 106 | [Repeat(1000)] 107 | #if !UNITY_EDITOR 108 | [Ignore("Quaternion.Euler Requires unity engine to run")] 109 | #endif 110 | public void PackAndUnpackRepeat([Range(8, 12)] int bits) 111 | { 112 | var inValue = GetRandomQuaternion(); 113 | 114 | RunPackAndUnpack(bits, inValue); 115 | } 116 | 117 | [Test] 118 | [Repeat(1000)] 119 | #if !UNITY_EDITOR 120 | [Ignore("Quaternion.Euler Requires unity engine to run")] 121 | #endif 122 | public void PackAndUnpackRepeatNotNormalized([Range(8, 12)] int bits) 123 | { 124 | var inValueNotNormalized = GetRandomQuaternionNotNormalized(); 125 | 126 | RunPackAndUnpack(bits, inValueNotNormalized); 127 | } 128 | 129 | private void RunPackAndUnpack(int bits, Quaternion inValueNotNormalized) 130 | { 131 | var inValue = inValueNotNormalized.normalized; 132 | 133 | // precision for 1 element 134 | var max = (1 / Mathf.Sqrt(2)); 135 | var precision = 2 * max / ((1 << bits) - 1); 136 | // allow extra precision because largest is caculated using the other 3 values so may be out side of precision 137 | precision *= 2; 138 | 139 | var packer = new QuaternionPacker(bits); 140 | 141 | packer.Pack(writer, inValueNotNormalized); 142 | 143 | var outValue = packer.Unpack(GetReader()); 144 | //Debug.Log($"Packed: ({inValue.x:0.000},{inValue.y:0.000},{inValue.z:0.000},{inValue.w:0.000}) " + 145 | // $"UnPacked: ({outValue.x:0.000},{outValue.y:0.000},{outValue.z:0.000},{outValue.w:0.000})"); 146 | 147 | Assert.That(outValue.x, Is.Not.NaN, "x was NaN"); 148 | Assert.That(outValue.y, Is.Not.NaN, "y was NaN"); 149 | Assert.That(outValue.z, Is.Not.NaN, "z was NaN"); 150 | Assert.That(outValue.w, Is.Not.NaN, "w was NaN"); 151 | 152 | var assertSign = getAssertSign(inValue, outValue); 153 | 154 | Assert.That(outValue.x, IsUnSignedEqualWithIn(inValue.x), $"x off by {Mathf.Abs(assertSign * inValue.x - outValue.x)}"); 155 | Assert.That(outValue.y, IsUnSignedEqualWithIn(inValue.y), $"y off by {Mathf.Abs(assertSign * inValue.y - outValue.y)}"); 156 | Assert.That(outValue.z, IsUnSignedEqualWithIn(inValue.z), $"z off by {Mathf.Abs(assertSign * inValue.z - outValue.z)}"); 157 | Assert.That(outValue.w, IsUnSignedEqualWithIn(inValue.w), $"w off by {Mathf.Abs(assertSign * inValue.w - outValue.w)}"); 158 | 159 | var inVec = inValue * Vector3.forward; 160 | var outVec = outValue * Vector3.forward; 161 | 162 | // allow for extra precision when rotating vector 163 | Assert.AreEqual(inVec.x, outVec.x, precision * 2, $"vx off by {Mathf.Abs(inVec.x - outVec.x)}"); 164 | Assert.AreEqual(inVec.y, outVec.y, precision * 2, $"vy off by {Mathf.Abs(inVec.y - outVec.y)}"); 165 | Assert.AreEqual(inVec.z, outVec.z, precision * 2, $"vz off by {Mathf.Abs(inVec.z - outVec.z)}"); 166 | 167 | 168 | EqualConstraint IsUnSignedEqualWithIn(float v) 169 | { 170 | return Is.EqualTo(v).Within(precision).Or.EqualTo(assertSign * v).Within(precision); 171 | } 172 | } 173 | 174 | 175 | [Test] 176 | [Repeat(1000)] 177 | public void FastNormalize() 178 | { 179 | var q1 = GetRandomQuaternionNotNormalized(); 180 | // create copy here so it can be used in ref without chanigng q1 181 | var q2 = q1; 182 | QuaternionPacker.QuickNormalize(ref q2); 183 | 184 | Assert.That(q2, Is.EqualTo(q1.normalized)); 185 | } 186 | 187 | /// 188 | /// sign used to validate values (in/out are different, then flip values 189 | /// 190 | /// 191 | /// 192 | /// 193 | private static float getAssertSign(Quaternion inValue, Quaternion outValue) 194 | { 195 | // keep same index for in/out because largest *might* have chagned if all elements are equal 196 | QuaternionPacker.FindLargestIndex(ref inValue, out var index); 197 | 198 | var inLargest = inValue[(int)index]; 199 | var outLargest = outValue[(int)index]; 200 | // flip sign of A if largest is is negative 201 | // Q == (-Q) 202 | var inSign = Mathf.Sign(inLargest); 203 | var outSign = Mathf.Sign(outLargest); 204 | 205 | float assertSign = inSign == outSign ? 1 : -1; 206 | return assertSign; 207 | } 208 | 209 | 210 | private static IEnumerable PackToIntCases() 211 | { 212 | yield return new TestCaseData(Quaternion.identity); 213 | yield return new TestCaseData(Quaternion.Euler(25, 30, 0)); 214 | yield return new TestCaseData(Quaternion.Euler(-50, 30, 90)); 215 | yield return new TestCaseData(Quaternion.Euler(90, 90, 180)); 216 | yield return new TestCaseData(Quaternion.Euler(-20, 0, 45)); 217 | yield return new TestCaseData(Quaternion.Euler(80, 30, -45)); 218 | } 219 | [Test] 220 | [TestCaseSource(nameof(PackToIntCases))] 221 | #if !UNITY_EDITOR 222 | [Ignore("Quaternion.Euler Requires unity engine to run")] 223 | #endif 224 | public void PackAsInt(Quaternion inValue) 225 | { 226 | // precision for 1 element 227 | var max = (1 / Mathf.Sqrt(2)); 228 | var precision = 2 * max / ((1 << 10) - 1); 229 | // allow extra precision because largest is caculated using the other 3 values so may be out side of precision 230 | precision *= 2; 231 | 232 | var encoded = QuaternionPacker.PackAsInt(inValue); 233 | var outValue = QuaternionPacker.UnpackFromInt(encoded); 234 | 235 | //Debug.Log($"Packed: ({inValue.x:0.000},{inValue.y:0.000},{inValue.z:0.000},{inValue.w:0.000}) " + 236 | // $"UnPacked: ({outValue.x:0.000},{outValue.y:0.000},{outValue.z:0.000},{outValue.w:0.000})"); 237 | 238 | Assert.That(outValue.x, Is.Not.NaN, "x was NaN"); 239 | Assert.That(outValue.y, Is.Not.NaN, "y was NaN"); 240 | Assert.That(outValue.z, Is.Not.NaN, "z was NaN"); 241 | Assert.That(outValue.w, Is.Not.NaN, "w was NaN"); 242 | 243 | var assertSign = getAssertSign(inValue, outValue); 244 | 245 | Assert.That(outValue.x, IsUnSignedEqualWithIn(inValue.x), $"x off by {Mathf.Abs(assertSign * inValue.x - outValue.x)}"); 246 | Assert.That(outValue.y, IsUnSignedEqualWithIn(inValue.y), $"y off by {Mathf.Abs(assertSign * inValue.y - outValue.y)}"); 247 | Assert.That(outValue.z, IsUnSignedEqualWithIn(inValue.z), $"z off by {Mathf.Abs(assertSign * inValue.z - outValue.z)}"); 248 | Assert.That(outValue.w, IsUnSignedEqualWithIn(inValue.w), $"w off by {Mathf.Abs(assertSign * inValue.w - outValue.w)}"); 249 | 250 | var inVec = inValue * Vector3.forward; 251 | var outVec = outValue * Vector3.forward; 252 | 253 | // allow for extra precision when rotating vector 254 | Assert.AreEqual(inVec.x, outVec.x, precision * 2, $"vx off by {Mathf.Abs(inVec.x - outVec.x)}"); 255 | Assert.AreEqual(inVec.y, outVec.y, precision * 2, $"vy off by {Mathf.Abs(inVec.y - outVec.y)}"); 256 | Assert.AreEqual(inVec.z, outVec.z, precision * 2, $"vz off by {Mathf.Abs(inVec.z - outVec.z)}"); 257 | 258 | 259 | EqualConstraint IsUnSignedEqualWithIn(float v) 260 | { 261 | return Is.EqualTo(v).Within(precision).Or.EqualTo(assertSign * v).Within(precision); 262 | } 263 | } 264 | 265 | [Test] 266 | [Description("Quaternion is a likely value, so packing to 0 will be useful compression")] 267 | public void PackIdentityShouldBeZero() 268 | { 269 | var encoded = QuaternionPacker.PackAsInt(Quaternion.identity); 270 | Assert.That(encoded, Is.Zero); 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/NetworkReader.cs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 James Frowen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | using System; 26 | using System.IO; 27 | using System.Runtime.CompilerServices; 28 | using System.Runtime.InteropServices; 29 | 30 | namespace Mirage.Serialization 31 | { 32 | /// 33 | /// Bit writer, writes values to a buffer on a bit level 34 | /// Use to reduce memory allocation 35 | /// 36 | public unsafe class NetworkReader : IDisposable 37 | { 38 | private byte[] _managedBuffer; 39 | private GCHandle _handle; 40 | private ulong* _longPtr; 41 | private bool _needsDisposing; 42 | 43 | /// Current read position 44 | private int _bitPosition; 45 | 46 | /// Offset of given buffer 47 | private int _bitOffset; 48 | 49 | /// Length of given buffer 50 | private int _bitLength; 51 | 52 | /// 53 | /// Size of buffer that is being read from 54 | /// 55 | public int BitLength 56 | { 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | get => _bitLength; 59 | } 60 | 61 | /// 62 | /// Current bit position for reading from buffer 63 | /// 64 | public int BitPosition 65 | { 66 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 67 | get => _bitPosition; 68 | } 69 | /// 70 | /// Current rounded up to nearest multiple of 8 71 | /// 72 | public int BytePosition 73 | { 74 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 75 | // rounds up to nearest 8 76 | // add to 3 last bits, 77 | // if any are 1 then it will roll over 4th bit. 78 | // if all are 0, then nothing happens 79 | get => (_bitPosition + 0b111) >> 3; 80 | } 81 | 82 | public NetworkReader() { } 83 | 84 | ~NetworkReader() 85 | { 86 | Dispose(false); 87 | } 88 | /// true if called from IDisposable 89 | protected virtual void Dispose(bool disposing) 90 | { 91 | if (!_needsDisposing) return; 92 | 93 | _handle.Free(); 94 | _longPtr = null; 95 | _needsDisposing = false; 96 | 97 | if (disposing) 98 | { 99 | // clear manged stuff here because we no longer want reader to keep reference to buffer 100 | _bitLength = 0; 101 | _managedBuffer = null; 102 | } 103 | } 104 | public void Dispose() 105 | { 106 | Dispose(true); 107 | } 108 | 109 | public void Reset(ArraySegment segment) 110 | { 111 | Reset(segment.Array, segment.Offset, segment.Count); 112 | } 113 | public void Reset(byte[] array) 114 | { 115 | Reset(array, 0, array.Length); 116 | } 117 | public void Reset(byte[] array, int position, int length) 118 | { 119 | if (array == null) 120 | throw new ArgumentNullException(nameof(array), "Cant use null array in Reader"); 121 | 122 | if (_needsDisposing) 123 | { 124 | // dispose old handler first 125 | // false here so we dont release reader back to pool 126 | Dispose(false); 127 | } 128 | 129 | // reset disposed bool, as it can be disposed again after reset 130 | _needsDisposing = true; 131 | 132 | _bitPosition = position * 8; 133 | _bitOffset = position * 8; 134 | _bitLength = _bitPosition + (length * 8); 135 | _managedBuffer = array; 136 | _handle = GCHandle.Alloc(_managedBuffer, GCHandleType.Pinned); 137 | _longPtr = (ulong*)_handle.AddrOfPinnedObject(); 138 | } 139 | 140 | /// 141 | /// Can read atleast 1 bit 142 | /// 143 | /// 144 | public bool CanRead() 145 | { 146 | return _bitPosition < _bitLength; 147 | } 148 | 149 | /// 150 | /// Can atleast bits 151 | /// 152 | /// 153 | public bool CanReadBits(int readCount) 154 | { 155 | return (_bitPosition + readCount) <= _bitLength; 156 | } 157 | 158 | /// 159 | /// Can atleast bytes 160 | /// 161 | /// 162 | /// 163 | public bool CanReadBytes(int readCount) 164 | { 165 | return (_bitPosition + (readCount * 8)) <= _bitLength; 166 | } 167 | 168 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 169 | private void CheckNewLength(int newPosition) 170 | { 171 | if (newPosition > _bitLength) 172 | { 173 | ThrowPositionOverLength(newPosition); 174 | } 175 | } 176 | 177 | private void ThrowPositionOverLength(int newPosition) 178 | { 179 | throw new EndOfStreamException($"Can not read over end of buffer, new position {newPosition}, length {_bitLength} bits"); 180 | } 181 | 182 | private void PadToByte() 183 | { 184 | _bitPosition = BytePosition << 3; 185 | } 186 | 187 | 188 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 189 | public bool ReadBoolean() 190 | { 191 | return ReadBooleanAsUlong() == 1UL; 192 | } 193 | 194 | /// 195 | /// Writes first bit of to buffer 196 | /// 197 | /// 198 | public ulong ReadBooleanAsUlong() 199 | { 200 | var newPosition = _bitPosition + 1; 201 | CheckNewLength(newPosition); 202 | 203 | var ptr = _longPtr + (_bitPosition >> 6); 204 | var result = ((*ptr) >> _bitPosition) & 0b1; 205 | 206 | _bitPosition = newPosition; 207 | return result; 208 | } 209 | 210 | 211 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 212 | public sbyte ReadSByte() => (sbyte)ReadByte(); 213 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 214 | public byte ReadByte() => (byte)ReadUnmasked(8); 215 | 216 | 217 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 218 | public short ReadInt16() => (short)ReadUInt16(); 219 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 220 | public ushort ReadUInt16() => (ushort)ReadUnmasked(16); 221 | 222 | 223 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 224 | public int ReadInt32() => (int)ReadUInt32(); 225 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 226 | public uint ReadUInt32() => (uint)ReadUnmasked(32); 227 | 228 | 229 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 230 | public long ReadInt64() => (long)ReadUInt64(); 231 | public ulong ReadUInt64() 232 | { 233 | var newPosition = _bitPosition + 64; 234 | CheckNewLength(newPosition); 235 | 236 | var bitsInLong = _bitPosition & 0b11_1111; 237 | ulong result; 238 | if (bitsInLong == 0) 239 | { 240 | var ptr1 = _longPtr + (_bitPosition >> 6); 241 | result = *ptr1; 242 | } 243 | else 244 | { 245 | var bitsLeft = 64 - bitsInLong; 246 | 247 | var ptr1 = _longPtr + (_bitPosition >> 6); 248 | var ptr2 = ptr1 + 1; 249 | 250 | // eg use byte, read 6 =>bitPosition=5, bitsLeft=3, newPos=1 251 | // r1 = aaab_bbbb => 0000_0aaa 252 | // r2 = cccc_caaa => ccaa_a000 253 | // r = r1|r2 => ccaa_aaaa 254 | // we mask this result later 255 | 256 | var r1 = (*ptr1) >> _bitPosition; 257 | var r2 = (*ptr2) << bitsLeft; 258 | result = r1 | r2; 259 | } 260 | 261 | _bitPosition = newPosition; 262 | 263 | // dont need to mask this result because should be reading all 64 bits 264 | return result; 265 | } 266 | 267 | 268 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 269 | public float ReadSingle() 270 | { 271 | var uValue = ReadUInt32(); 272 | return *(float*)&uValue; 273 | } 274 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 275 | public double ReadDouble() 276 | { 277 | var uValue = ReadUInt64(); 278 | return *(double*)&uValue; 279 | } 280 | 281 | 282 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 283 | public ulong Read(int bits) 284 | { 285 | if (bits == 0) return 0; 286 | // mask so we dont returns extra bits 287 | return ReadUnmasked(bits) & (ulong.MaxValue >> (64 - bits)); 288 | } 289 | 290 | private ulong ReadUnmasked(int bits) 291 | { 292 | var newPosition = _bitPosition + bits; 293 | CheckNewLength(newPosition); 294 | 295 | var bitsInLong = _bitPosition & 0b11_1111; 296 | var bitsLeft = 64 - bitsInLong; 297 | 298 | ulong result; 299 | if (bitsLeft >= bits) 300 | { 301 | var ptr = _longPtr + (_bitPosition >> 6); 302 | result = (*ptr) >> bitsInLong; 303 | } 304 | else 305 | { 306 | var ptr1 = _longPtr + (_bitPosition >> 6); 307 | var ptr2 = ptr1 + 1; 308 | 309 | // eg use byte, read 6 =>bitPosition=5, bitsLeft=3, newPos=1 310 | // r1 = aaab_bbbb => 0000_0aaa 311 | // r2 = cccc_caaa => ccaa_a000 312 | // r = r1|r2 => ccaa_aaaa 313 | // we mask this result later 314 | 315 | var r1 = (*ptr1) >> bitsInLong; 316 | var r2 = (*ptr2) << bitsLeft; 317 | result = r1 | r2; 318 | } 319 | _bitPosition = newPosition; 320 | 321 | return result; 322 | } 323 | 324 | /// 325 | /// Reads n from buffer at 326 | /// 327 | /// number of bits in value to write 328 | /// where to write bits 329 | public ulong ReadAtPosition(int bits, int bitPosition) 330 | { 331 | // check length here so this methods throws instead of the read below 332 | CheckNewLength(bitPosition + bits); 333 | 334 | var currentPosition = _bitPosition; 335 | _bitPosition = bitPosition; 336 | var result = Read(bits); 337 | _bitPosition = currentPosition; 338 | 339 | return result; 340 | } 341 | 342 | 343 | /// 344 | /// Moves the internal bit position 345 | /// For most usecases it is safer to use 346 | /// WARNING: When reading from earlier position make sure to move position back to end of buffer after reading 347 | /// 348 | /// 349 | /// throws when is less than 350 | public void MoveBitPosition(int newPosition) 351 | { 352 | if (newPosition < _bitOffset) 353 | { 354 | throw new ArgumentOutOfRangeException(nameof(newPosition), newPosition, $"New position can not be less than buffer offset, Buffer offset: {_bitOffset}"); 355 | } 356 | CheckNewLength(newPosition); 357 | _bitPosition = newPosition; 358 | } 359 | 360 | 361 | /// 362 | /// 363 | /// Moves position to nearest byte then copies struct from that position 364 | /// 365 | /// 366 | /// 367 | /// 368 | public void PadAndCopy(out T value) where T : unmanaged 369 | { 370 | PadToByte(); 371 | var newPosition = _bitPosition + (8 * sizeof(T)); 372 | CheckNewLength(newPosition); 373 | 374 | var startPtr = ((byte*)_longPtr) + (_bitPosition >> 3); 375 | 376 | value = *(T*)startPtr; 377 | _bitPosition = newPosition; 378 | } 379 | 380 | /// 381 | /// 382 | /// Moves position to nearest byte then copies bytes from that position 383 | /// 384 | /// 385 | /// 386 | /// 387 | /// 388 | public void ReadBytes(byte[] array, int offset, int length) 389 | { 390 | PadToByte(); 391 | var newPosition = _bitPosition + (8 * length); 392 | CheckNewLength(newPosition); 393 | 394 | // todo benchmark this vs Marshal.Copy or for loop 395 | Buffer.BlockCopy(_managedBuffer, BytePosition, array, offset, length); 396 | _bitPosition = newPosition; 397 | } 398 | 399 | public ArraySegment ReadBytesSegment(int count) 400 | { 401 | PadToByte(); 402 | var newPosition = _bitPosition + (8 * count); 403 | CheckNewLength(newPosition); 404 | 405 | var result = new ArraySegment(_managedBuffer, BytePosition, count); 406 | _bitPosition = newPosition; 407 | return result; 408 | } 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /JamesFrowen.BitPacking/Source/NetworkWriter.cs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 James Frowen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | using System; 26 | using System.Runtime.CompilerServices; 27 | using System.Runtime.InteropServices; 28 | using UnityEngine; 29 | 30 | namespace Mirage.Serialization 31 | { 32 | /// 33 | /// Bit writer, writes values to a buffer on a bit level 34 | /// Use to reduce memory allocation 35 | /// 36 | public unsafe class NetworkWriter 37 | { 38 | /// 39 | /// Max buffer size = 0.5MB 40 | /// 41 | private const int MAX_BUFFER_SIZE = 524_288; 42 | private byte[] _managedBuffer; 43 | private int _bitCapacity; 44 | 45 | /// Allow internal buffer to resize if capcity is reached 46 | private readonly bool _allowResize; 47 | private GCHandle _handle; 48 | private ulong* _longPtr; 49 | private bool _needsDisposing; 50 | private int _bitPosition; 51 | 52 | /// 53 | /// Size limit of buffer 54 | /// 55 | public int ByteCapacity 56 | { 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | // see ByteLength for comment on math 59 | get => (_bitCapacity + 0b111) >> 3; 60 | } 61 | 62 | /// 63 | /// Current rounded up to nearest multiple of 8 64 | /// To set byte position use multiple by 8 65 | /// 66 | public int ByteLength 67 | { 68 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 69 | // rounds up to nearest 8 70 | // add to 3 last bits, 71 | // if any are 1 then it will roll over 4th bit. 72 | // if all are 0, then nothing happens 73 | get => (_bitPosition + 0b111) >> 3; 74 | } 75 | 76 | /// 77 | /// Current bit position for writing to buffer 78 | /// To set bit position use 79 | /// 80 | public int BitPosition 81 | { 82 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 83 | get => _bitPosition; 84 | } 85 | 86 | public NetworkWriter(int minByteCapacity) : this(minByteCapacity, true) { } 87 | public NetworkWriter(int minByteCapacity, bool allowResize) 88 | { 89 | _allowResize = allowResize; 90 | 91 | // ensure capacity is multiple of 8 92 | var ulongCapacity = Mathf.CeilToInt(minByteCapacity / (float)sizeof(ulong)); 93 | var byteCapacity = ulongCapacity * sizeof(ulong); 94 | 95 | _bitCapacity = byteCapacity * 8; 96 | _managedBuffer = new byte[byteCapacity]; 97 | 98 | CreateHandle(); 99 | } 100 | 101 | 102 | ~NetworkWriter() 103 | { 104 | FreeHandle(); 105 | } 106 | 107 | private void ResizeBuffer(int minBitCapacity) 108 | { 109 | // +7 to round up to next byte 110 | var minByteCapacity = (minBitCapacity + 7) / 8; 111 | var size = _managedBuffer.Length; 112 | while (size < minByteCapacity) 113 | { 114 | size *= 2; 115 | if (size > MAX_BUFFER_SIZE) 116 | { 117 | throw new InvalidOperationException($"Can not resize buffer to {size} bytes because it is above max value of {MAX_BUFFER_SIZE}"); 118 | } 119 | } 120 | 121 | // define to allow debug.log to be skipped when running outside of unity 122 | #if !BIT_PACKING_NO_DEBUG 123 | Debug.LogWarning($"Resizing buffer, new size:{size} bytes"); 124 | #endif 125 | 126 | FreeHandle(); 127 | 128 | Array.Resize(ref _managedBuffer, size); 129 | _bitCapacity = size * 8; 130 | 131 | CreateHandle(); 132 | } 133 | 134 | private void CreateHandle() 135 | { 136 | if (_needsDisposing) FreeHandle(); 137 | 138 | _handle = GCHandle.Alloc(_managedBuffer, GCHandleType.Pinned); 139 | _longPtr = (ulong*)_handle.AddrOfPinnedObject(); 140 | _needsDisposing = true; 141 | } 142 | 143 | /// 144 | /// Frees the handle for the buffer 145 | /// In order for to work This class can not have . Instead we call this method from finalize 146 | /// 147 | private void FreeHandle() 148 | { 149 | if (!_needsDisposing) return; 150 | 151 | _handle.Free(); 152 | _longPtr = null; 153 | _needsDisposing = false; 154 | } 155 | 156 | public void Reset() 157 | { 158 | _bitPosition = 0; 159 | } 160 | 161 | /// 162 | /// Copies internal buffer to new Array 163 | /// To reduce Allocations use instead 164 | /// 165 | /// 166 | public byte[] ToArray() 167 | { 168 | var data = new byte[ByteLength]; 169 | Buffer.BlockCopy(_managedBuffer, 0, data, 0, ByteLength); 170 | return data; 171 | } 172 | public ArraySegment ToArraySegment() 173 | { 174 | return new ArraySegment(_managedBuffer, 0, ByteLength); 175 | } 176 | 177 | 178 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 179 | private void CheckCapacity(int newLength) 180 | { 181 | if (newLength > _bitCapacity) 182 | { 183 | if (_allowResize) 184 | { 185 | ResizeBuffer(newLength); 186 | } 187 | else 188 | { 189 | ThrowLengthOverCapacity(newLength); 190 | } 191 | } 192 | } 193 | 194 | private void ThrowLengthOverCapacity(int newLength) 195 | { 196 | throw new InvalidOperationException($"Can not write over end of buffer, new length {newLength}, capacity {_bitCapacity}"); 197 | } 198 | 199 | private void PadToByte() 200 | { 201 | _bitPosition = ByteLength << 3; 202 | } 203 | 204 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 205 | public void WriteBoolean(bool value) 206 | { 207 | WriteBoolean(value ? 1UL : 0UL); 208 | } 209 | /// 210 | /// Writes first bit of to buffer 211 | /// 212 | /// 213 | public void WriteBoolean(ulong value) 214 | { 215 | var newPosition = _bitPosition + 1; 216 | CheckCapacity(newPosition); 217 | 218 | var bitsInLong = _bitPosition & 0b11_1111; 219 | 220 | var ptr = _longPtr + (_bitPosition >> 6); 221 | *ptr = ( 222 | *ptr & ( 223 | // start with 0000_0001 224 | // shift by number in bit, eg 5 => 0010_0000 225 | // then not 1101_1111 226 | ~(1UL << bitsInLong) 227 | ) 228 | ) | ((value & 0b1) << bitsInLong); 229 | 230 | _bitPosition = newPosition; 231 | } 232 | 233 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 234 | public void WriteSByte(sbyte value) => WriteByte((byte)value); 235 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 236 | public void WriteByte(byte value) => WriterUnmasked(value, 8); 237 | 238 | 239 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 240 | public void WriteInt16(short value) => WriteUInt16((ushort)value); 241 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 242 | public void WriteUInt16(ushort value) => WriterUnmasked(value, 16); 243 | 244 | 245 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 246 | public void WriteInt32(int value) => WriteUInt32((uint)value); 247 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 248 | public void WriteUInt32(uint value) => WriterUnmasked(value, 32); 249 | 250 | 251 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 252 | public void WriteInt64(long value) => WriteUInt64((ulong)value); 253 | public void WriteUInt64(ulong value) 254 | { 255 | var newPosition = _bitPosition + 64; 256 | CheckCapacity(newPosition); 257 | 258 | var bitsInLong = _bitPosition & 0b11_1111; 259 | 260 | if (bitsInLong == 0) 261 | { 262 | var ptr1 = _longPtr + (_bitPosition >> 6); 263 | *ptr1 = value; 264 | } 265 | else 266 | { 267 | var bitsLeft = 64 - bitsInLong; 268 | 269 | var ptr1 = _longPtr + (_bitPosition >> 6); 270 | var ptr2 = ptr1 + 1; 271 | 272 | *ptr1 = (*ptr1 & (ulong.MaxValue >> bitsLeft)) | (value << bitsInLong); 273 | *ptr2 = (*ptr2 & (ulong.MaxValue << newPosition)) | (value >> bitsLeft); 274 | } 275 | _bitPosition = newPosition; 276 | } 277 | 278 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 279 | public void WriteSingle(float value) => WriteUInt32(*(uint*)&value); 280 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 281 | public void WriteDouble(double value) => WriteUInt64(*(ulong*)&value); 282 | 283 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 284 | public void Write(ulong value, int bits) 285 | { 286 | if (bits == 0) return; 287 | // mask so we dont overwrite 288 | WriterUnmasked(value & BitMask.Mask(bits), bits); 289 | } 290 | 291 | private void WriterUnmasked(ulong value, int bits) 292 | { 293 | var newPosition = _bitPosition + bits; 294 | CheckCapacity(newPosition); 295 | 296 | var bitsInLong = _bitPosition & 0b11_1111; 297 | var bitsLeft = 64 - bitsInLong; 298 | 299 | if (bitsLeft >= bits) 300 | { 301 | var ptr = _longPtr + (_bitPosition >> 6); 302 | 303 | *ptr = (*ptr & BitMask.OuterMask(_bitPosition, newPosition)) | (value << bitsInLong); 304 | } 305 | else 306 | { 307 | var ptr1 = _longPtr + (_bitPosition >> 6); 308 | var ptr2 = ptr1 + 1; 309 | 310 | *ptr1 = (*ptr1 & (ulong.MaxValue >> bitsLeft)) | (value << bitsInLong); 311 | *ptr2 = (*ptr2 & (ulong.MaxValue << newPosition)) | (value >> bitsLeft); 312 | } 313 | _bitPosition = newPosition; 314 | } 315 | 316 | /// 317 | /// Same as expect position given is in bytes instead of bits 318 | /// WARNING: When writing to bytes instead of bits make sure you are able to read at the right position when deserializing as it might cause data to be misaligned 319 | /// 320 | /// 321 | /// 322 | /// 323 | public void WriteAtBytePosition(ulong value, int bits, int bytePosition) 324 | { 325 | WriteAtPosition(value, bits, bytePosition * 8); 326 | } 327 | 328 | /// 329 | /// Writes n from to 330 | /// This methods can be used to go back to a previous position to write length or other flags to the buffer after other data has been written 331 | /// WARNING: This method does not change the internal position so will not change the overall length if writing past internal position 332 | /// 333 | /// value to write 334 | /// number of bits in value to write 335 | /// where to write bits 336 | public void WriteAtPosition(ulong value, int bits, int bitPosition) 337 | { 338 | // check length here so this methods throws instead of the write below 339 | // this is so that it is more obvious that the position arg for this method is invalid 340 | CheckCapacity(bitPosition + bits); 341 | 342 | // moves position to arg, then write, then reset position 343 | var currentPosition = _bitPosition; 344 | _bitPosition = bitPosition; 345 | Write(value, bits); 346 | _bitPosition = currentPosition; 347 | } 348 | 349 | 350 | /// 351 | /// Moves the internal bit position 352 | /// For most usecases it is safer to use 353 | /// WARNING: When writing to earlier position make sure to move position back to end of buffer after writing because position is also used as length 354 | /// 355 | /// 356 | public void MoveBitPosition(int newPosition) 357 | { 358 | CheckCapacity(newPosition); 359 | _bitPosition = newPosition; 360 | } 361 | 362 | /// 363 | /// 364 | /// Moves position to nearest byte then copies struct to that position 365 | /// 366 | /// 367 | /// 368 | /// 369 | public void PadAndCopy(in T value) where T : unmanaged 370 | { 371 | PadToByte(); 372 | var newPosition = _bitPosition + (8 * sizeof(T)); 373 | CheckCapacity(newPosition); 374 | 375 | var startPtr = ((byte*)_longPtr) + (_bitPosition >> 3); 376 | 377 | var ptr = (T*)startPtr; 378 | *ptr = value; 379 | _bitPosition = newPosition; 380 | } 381 | 382 | /// 383 | /// 384 | /// Moves position to nearest byte then writes bytes to that position 385 | /// 386 | /// 387 | /// 388 | /// 389 | /// 390 | public void WriteBytes(byte[] array, int offset, int length) 391 | { 392 | PadToByte(); 393 | var newPosition = _bitPosition + (8 * length); 394 | CheckCapacity(newPosition); 395 | 396 | // todo benchmark this vs Marshal.Copy or for loop 397 | Buffer.BlockCopy(array, offset, _managedBuffer, ByteLength, length); 398 | _bitPosition = newPosition; 399 | } 400 | 401 | /// 402 | /// Copies all data from 403 | /// 404 | /// 405 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 406 | public void CopyFromWriter(NetworkWriter other) 407 | { 408 | CopyFromWriter(other, 0, other.BitPosition); 409 | } 410 | 411 | /// 412 | /// Copies bits from starting at 413 | /// 414 | /// 415 | /// 416 | /// 417 | public void CopyFromWriter(NetworkWriter other, int otherBitPosition, int bitLength) 418 | { 419 | var newBit = _bitPosition + bitLength; 420 | CheckCapacity(newBit); 421 | 422 | var ulongPos = otherBitPosition >> 6; 423 | var otherPtr = other._longPtr + ulongPos; 424 | 425 | 426 | var firstBitOffset = otherBitPosition & 0b11_1111; 427 | 428 | // first align other 429 | if (firstBitOffset != 0) 430 | { 431 | var bitsToCopyFromFirst = Math.Min(64 - firstBitOffset, bitLength); 432 | 433 | // if offset is 10, then we want to shift value by 10 to remove un-needed bits 434 | var firstValue = *otherPtr >> firstBitOffset; 435 | 436 | Write(firstValue, bitsToCopyFromFirst); 437 | 438 | bitLength -= bitsToCopyFromFirst; 439 | otherPtr++; 440 | } 441 | 442 | // write aligned with other 443 | while (bitLength > 64) 444 | { 445 | WriteUInt64(*otherPtr); 446 | 447 | bitLength -= 64; 448 | otherPtr++; 449 | } 450 | 451 | // write left over others 452 | // if bitlength == 0 then write will return 453 | Write(*otherPtr, bitLength); 454 | 455 | // define to allow debug.log to be skipped when running outside of unity 456 | #if !BIT_PACKING_NO_DEBUG 457 | Debug.Assert(_bitPosition == newBit, "bitPosition should already be equal to newBit because it would have incremented each WriteUInt64"); 458 | #endif 459 | 460 | _bitPosition = newBit; 461 | } 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | ############################### 3 | # Core EditorConfig Options # 4 | ############################### 5 | root = true 6 | 7 | [*.{cs,csx,vb,vbx,md}] 8 | indent_style = space 9 | indent_size = 4 10 | tab_width = 4 11 | insert_final_newline = true 12 | charset = utf-8 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | 21 | # Code files 22 | [*.{cs,csx,vb,vbx}] 23 | trim_trailing_whitespace = true 24 | 25 | ############################### 26 | # .NET Coding Conventions # 27 | ############################### 28 | [*.{cs,vb}] 29 | 30 | # Organize usings 31 | dotnet_sort_system_directives_first = true 32 | dotnet_separate_import_directive_groups = false 33 | file_header_template = unset 34 | 35 | # this. preferences 36 | dotnet_style_qualification_for_event = false:suggestion 37 | dotnet_style_qualification_for_field = false:suggestion 38 | dotnet_style_qualification_for_method = false:suggestion 39 | dotnet_style_qualification_for_property = false:suggestion 40 | 41 | # Language keywords vs BCL types preferences 42 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 43 | dotnet_style_predefined_type_for_member_access = true:suggestion 44 | 45 | # Parentheses preferences 46 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion 47 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion 48 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion 49 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion 50 | 51 | # Modifier preferences 52 | dotnet_style_require_accessibility_modifiers = for_non_interface_members 53 | 54 | # Expression-level preferences 55 | dotnet_style_coalesce_expression = true 56 | dotnet_style_collection_initializer = true 57 | dotnet_style_explicit_tuple_names = true 58 | dotnet_style_namespace_match_folder = true 59 | dotnet_style_null_propagation = true 60 | dotnet_style_object_initializer = true 61 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 62 | dotnet_style_prefer_auto_properties = true:silent 63 | dotnet_style_prefer_compound_assignment = true 64 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 65 | dotnet_style_prefer_conditional_expression_over_return = true:silent 66 | dotnet_style_prefer_inferred_anonymous_type_member_names = true 67 | dotnet_style_prefer_inferred_tuple_names = true 68 | dotnet_style_prefer_is_null_check_over_reference_equality_method = false 69 | dotnet_style_prefer_simplified_boolean_expressions = true 70 | dotnet_style_prefer_simplified_interpolation = true 71 | 72 | # Field preferences 73 | dotnet_style_readonly_field = true:suggestion 74 | 75 | # Parameter preferences 76 | dotnet_code_quality_unused_parameters = all:warning 77 | 78 | # Suppression preferences 79 | dotnet_remove_unnecessary_suppression_exclusions = none 80 | 81 | # New line preferences 82 | dotnet_style_allow_multiple_blank_lines_experimental = true 83 | dotnet_style_allow_statement_immediately_after_block_experimental = true 84 | 85 | #### C# Coding Conventions #### 86 | 87 | # var preferences 88 | csharp_style_var_elsewhere = true:suggestion 89 | csharp_style_var_for_built_in_types = true:suggestion 90 | csharp_style_var_when_type_is_apparent = true:suggestion 91 | 92 | # Expression-bodied members 93 | csharp_style_expression_bodied_accessors = when_on_single_line:suggestion 94 | csharp_style_expression_bodied_constructors = false:silent 95 | csharp_style_expression_bodied_indexers = when_on_single_line:suggestion 96 | csharp_style_expression_bodied_lambdas = true:suggestion 97 | csharp_style_expression_bodied_local_functions = false 98 | csharp_style_expression_bodied_methods = false 99 | csharp_style_expression_bodied_operators = when_on_single_line:suggestion 100 | csharp_style_expression_bodied_properties = when_on_single_line:suggestion 101 | 102 | # Pattern matching preferences 103 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 104 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 105 | csharp_style_prefer_not_pattern = true 106 | csharp_style_prefer_pattern_matching = true 107 | csharp_style_prefer_switch_expression = true:silent 108 | 109 | # Null-checking preferences 110 | csharp_style_conditional_delegate_call = true:suggestion 111 | 112 | # Modifier preferences 113 | csharp_prefer_static_local_function = true 114 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async 115 | 116 | # Code-block preferences 117 | csharp_prefer_braces = when_multiline:suggestion 118 | csharp_prefer_simple_using_statement = false:silent 119 | 120 | # Expression-level preferences 121 | csharp_prefer_simple_default_expression = true 122 | csharp_style_deconstructed_variable_declaration = true 123 | csharp_style_implicit_object_creation_when_type_is_apparent = false 124 | csharp_style_inlined_variable_declaration = true 125 | csharp_style_pattern_local_over_anonymous_function = true 126 | csharp_style_prefer_index_operator = true 127 | csharp_style_prefer_range_operator = true 128 | csharp_style_throw_expression = true:silent 129 | csharp_style_unused_value_assignment_preference = discard_variable:silent 130 | csharp_style_unused_value_expression_statement_preference = discard_variable 131 | 132 | # 'using' directive preferences 133 | csharp_using_directive_placement = outside_namespace 134 | 135 | # New line preferences 136 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false 137 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion 138 | csharp_style_allow_embedded_statements_on_same_line_experimental = true 139 | 140 | #### C# Formatting Rules #### 141 | 142 | # New line preferences 143 | csharp_new_line_before_catch = true 144 | csharp_new_line_before_else = true 145 | csharp_new_line_before_finally = true 146 | csharp_new_line_before_members_in_anonymous_types = true 147 | csharp_new_line_before_members_in_object_initializers = true 148 | csharp_new_line_before_open_brace = all 149 | csharp_new_line_between_query_expression_clauses = true 150 | 151 | # Indentation preferences 152 | csharp_indent_block_contents = true 153 | csharp_indent_braces = false 154 | csharp_indent_case_contents = true 155 | csharp_indent_case_contents_when_block = true 156 | csharp_indent_labels = one_less_than_current 157 | csharp_indent_switch_labels = true 158 | 159 | # Space preferences 160 | csharp_space_after_cast = false 161 | csharp_space_after_colon_in_inheritance_clause = true 162 | csharp_space_after_comma = true 163 | csharp_space_after_dot = false 164 | csharp_space_after_keywords_in_control_flow_statements = true 165 | csharp_space_after_semicolon_in_for_statement = true 166 | csharp_space_around_binary_operators = before_and_after 167 | csharp_space_around_declaration_statements = false 168 | csharp_space_before_colon_in_inheritance_clause = true 169 | csharp_space_before_comma = false 170 | csharp_space_before_dot = false 171 | csharp_space_before_open_square_brackets = false 172 | csharp_space_before_semicolon_in_for_statement = false 173 | csharp_space_between_empty_square_brackets = false 174 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 175 | csharp_space_between_method_call_name_and_opening_parenthesis = false 176 | csharp_space_between_method_call_parameter_list_parentheses = false 177 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 178 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 179 | csharp_space_between_method_declaration_parameter_list_parentheses = false 180 | csharp_space_between_parentheses = false 181 | csharp_space_between_square_brackets = false 182 | 183 | # Wrapping preferences 184 | csharp_preserve_single_line_blocks = true 185 | csharp_preserve_single_line_statements = true 186 | 187 | #### Naming styles #### 188 | 189 | # Naming rules 190 | # rules from https://github.com/jasontaylordev/CleanArchitecture/blob/main/.editorconfig 191 | # modifcations: 192 | # upper_snake_case for constants 193 | # private static shohuld be camelcase 194 | # removing _readonly from static field rules 195 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion 196 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces 197 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase 198 | 199 | dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion 200 | dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces 201 | dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase 202 | 203 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion 204 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters 205 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase 206 | 207 | dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion 208 | dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods 209 | dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase 210 | 211 | dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion 212 | dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties 213 | dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase 214 | 215 | dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion 216 | dotnet_naming_rule.events_should_be_pascalcase.symbols = events 217 | dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase 218 | 219 | dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion 220 | dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables 221 | dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase 222 | 223 | dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion 224 | dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants 225 | dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase 226 | 227 | dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion 228 | dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters 229 | dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase 230 | 231 | dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion 232 | dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields 233 | dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase 234 | 235 | dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion 236 | dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields 237 | dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase 238 | 239 | dotnet_naming_rule.internal_fields_should_be__camelcase.severity = suggestion 240 | dotnet_naming_rule.internal_fields_should_be__camelcase.symbols = internal_fields 241 | dotnet_naming_rule.internal_fields_should_be__camelcase.style = _camelcase 242 | 243 | dotnet_naming_rule.private_static_fields_should_be__camelcase.severity = suggestion 244 | dotnet_naming_rule.private_static_fields_should_be__camelcase.symbols = private_static_fields 245 | dotnet_naming_rule.private_static_fields_should_be__camelcase.style = camelcase 246 | 247 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion 248 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields 249 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = upper_snake_case 250 | 251 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion 252 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields 253 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = upper_snake_case 254 | 255 | dotnet_naming_rule.internal_constant_fields_should_be_pascalcase.severity = suggestion 256 | dotnet_naming_rule.internal_constant_fields_should_be_pascalcase.symbols = internal_constant_fields 257 | dotnet_naming_rule.internal_constant_fields_should_be_pascalcase.style = upper_snake_case 258 | 259 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion 260 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields 261 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase 262 | 263 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion 264 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields 265 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = camelcase 266 | 267 | dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion 268 | dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums 269 | dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase 270 | 271 | dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion 272 | dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions 273 | dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase 274 | 275 | dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion 276 | dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members 277 | dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase 278 | 279 | # Symbol specifications 280 | 281 | dotnet_naming_symbols.interfaces.applicable_kinds = interface 282 | dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 283 | dotnet_naming_symbols.interfaces.required_modifiers = 284 | 285 | dotnet_naming_symbols.enums.applicable_kinds = enum 286 | dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 287 | dotnet_naming_symbols.enums.required_modifiers = 288 | 289 | dotnet_naming_symbols.events.applicable_kinds = event 290 | dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 291 | dotnet_naming_symbols.events.required_modifiers = 292 | 293 | dotnet_naming_symbols.methods.applicable_kinds = method 294 | dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 295 | dotnet_naming_symbols.methods.required_modifiers = 296 | 297 | dotnet_naming_symbols.properties.applicable_kinds = property 298 | dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 299 | dotnet_naming_symbols.properties.required_modifiers = 300 | 301 | dotnet_naming_symbols.public_fields.applicable_kinds = field 302 | dotnet_naming_symbols.public_fields.applicable_accessibilities = public 303 | dotnet_naming_symbols.public_fields.required_modifiers = 304 | 305 | dotnet_naming_symbols.private_fields.applicable_kinds = field 306 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 307 | dotnet_naming_symbols.private_fields.required_modifiers = 308 | 309 | dotnet_naming_symbols.private_static_fields.applicable_kinds = field 310 | dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 311 | dotnet_naming_symbols.private_static_fields.required_modifiers = static 312 | 313 | dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum 314 | dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 315 | dotnet_naming_symbols.types_and_namespaces.required_modifiers = 316 | 317 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 318 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 319 | dotnet_naming_symbols.non_field_members.required_modifiers = 320 | 321 | dotnet_naming_symbols.type_parameters.applicable_kinds = namespace 322 | dotnet_naming_symbols.type_parameters.applicable_accessibilities = * 323 | dotnet_naming_symbols.type_parameters.required_modifiers = 324 | 325 | dotnet_naming_symbols.private_constant_fields.applicable_kinds = field 326 | dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 327 | dotnet_naming_symbols.private_constant_fields.required_modifiers = const 328 | 329 | dotnet_naming_symbols.local_variables.applicable_kinds = local 330 | dotnet_naming_symbols.local_variables.applicable_accessibilities = local 331 | dotnet_naming_symbols.local_variables.required_modifiers = 332 | 333 | dotnet_naming_symbols.local_constants.applicable_kinds = local 334 | dotnet_naming_symbols.local_constants.applicable_accessibilities = local 335 | dotnet_naming_symbols.local_constants.required_modifiers = const 336 | 337 | dotnet_naming_symbols.parameters.applicable_kinds = parameter 338 | dotnet_naming_symbols.parameters.applicable_accessibilities = * 339 | dotnet_naming_symbols.parameters.required_modifiers = 340 | 341 | dotnet_naming_symbols.public_constant_fields.applicable_kinds = field 342 | dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public 343 | dotnet_naming_symbols.public_constant_fields.required_modifiers = const 344 | 345 | dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field 346 | dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public 347 | dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static 348 | 349 | dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field 350 | dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 351 | dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static 352 | 353 | dotnet_naming_symbols.public_static_fields.applicable_kinds = field 354 | dotnet_naming_symbols.public_static_fields.applicable_accessibilities = public 355 | dotnet_naming_symbols.public_static_fields.required_modifiers = readonly, static 356 | 357 | dotnet_naming_symbols.internal_fields.applicable_kinds = field 358 | dotnet_naming_symbols.internal_fields.applicable_accessibilities = internal 359 | dotnet_naming_symbols.internal_fields.required_modifiers = 360 | 361 | dotnet_naming_symbols.internal_constant_fields.applicable_kinds = field 362 | dotnet_naming_symbols.internal_constant_fields.applicable_accessibilities = internal 363 | dotnet_naming_symbols.internal_constant_fields.required_modifiers = const 364 | 365 | dotnet_naming_symbols.internal_static_readonly_fields.applicable_kinds = field 366 | dotnet_naming_symbols.internal_static_readonly_fields.applicable_accessibilities = internal 367 | dotnet_naming_symbols.internal_static_readonly_fields.required_modifiers = readonly, static 368 | 369 | dotnet_naming_symbols.internal_static_fields.applicable_kinds = field 370 | dotnet_naming_symbols.internal_static_fields.applicable_accessibilities = internal 371 | dotnet_naming_symbols.internal_static_fields.required_modifiers = readonly, static 372 | 373 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 374 | dotnet_naming_symbols.local_functions.applicable_accessibilities = * 375 | dotnet_naming_symbols.local_functions.required_modifiers = 376 | 377 | # Naming styles 378 | 379 | dotnet_naming_style.pascalcase.required_prefix = 380 | dotnet_naming_style.pascalcase.required_suffix = 381 | dotnet_naming_style.pascalcase.word_separator = 382 | dotnet_naming_style.pascalcase.capitalization = pascal_case 383 | 384 | dotnet_naming_style.ipascalcase.required_prefix = I 385 | dotnet_naming_style.ipascalcase.required_suffix = 386 | dotnet_naming_style.ipascalcase.word_separator = 387 | dotnet_naming_style.ipascalcase.capitalization = pascal_case 388 | 389 | dotnet_naming_style.tpascalcase.required_prefix = T 390 | dotnet_naming_style.tpascalcase.required_suffix = 391 | dotnet_naming_style.tpascalcase.word_separator = 392 | dotnet_naming_style.tpascalcase.capitalization = pascal_case 393 | 394 | dotnet_naming_style._camelcase.required_prefix = _ 395 | dotnet_naming_style._camelcase.required_suffix = 396 | dotnet_naming_style._camelcase.word_separator = 397 | dotnet_naming_style._camelcase.capitalization = camel_case 398 | 399 | dotnet_naming_style.camelcase.required_prefix = 400 | dotnet_naming_style.camelcase.required_suffix = 401 | dotnet_naming_style.camelcase.word_separator = 402 | dotnet_naming_style.camelcase.capitalization = camel_case 403 | 404 | dotnet_naming_style.s_camelcase.required_prefix = s_ 405 | dotnet_naming_style.s_camelcase.required_suffix = 406 | dotnet_naming_style.s_camelcase.word_separator = 407 | dotnet_naming_style.s_camelcase.capitalization = camel_case 408 | 409 | dotnet_naming_style.upper_snake_case.required_prefix = 410 | dotnet_naming_style.upper_snake_case.required_suffix = 411 | dotnet_naming_style.upper_snake_case.word_separator = _ 412 | dotnet_naming_style.upper_snake_case.capitalization = all_upper 413 | --------------------------------------------------------------------------------