From 339a9d99670f00349b326cb70b8828af40f94ced Mon Sep 17 00:00:00 2001 From: vis2k Date: Tue, 18 Oct 2022 16:25:19 +0200 Subject: [PATCH] feature: Utils.BitsRequired to prepare for BitTree delta compression --- Assets/Mirror/Core/Utils.cs | 100 +++++++++++++++++++++++ Assets/Mirror/Tests/Editor/UtilsTests.cs | 53 ++++++++++++ 2 files changed, 153 insertions(+) diff --git a/Assets/Mirror/Core/Utils.cs b/Assets/Mirror/Core/Utils.cs index 444a97c73..64458735c 100644 --- a/Assets/Mirror/Core/Utils.cs +++ b/Assets/Mirror/Core/Utils.cs @@ -80,6 +80,106 @@ public static int RoundBitsToFullBytes(int bits) return ((bits - 1) / 8) + 1; } + // calculate bits needed for a value range + // largest type we support is ulong, so use that as parameters + // min, max are both INCLUSIVE + // min=0, max=7 means 0..7 = 8 values in total = 3 bits required + public static int BitsRequired(ulong min, ulong max) + { + // make sure value is within range + // => throws exception because the developer should fix it immediately + if (min > max) + throw new ArgumentOutOfRangeException($"{nameof(BitsRequired)} min={min} needs to be <= max={max}"); + + // if min == max then we need 0 bits because it is only ever one value + if (min == max) + return 0; + + // normalize from min..max to 0..max-min + // example: + // min = 0, max = 7 => 7-0 = 7 (0..7 = 8 values needed) + // min = 4, max = 7 => 7-4 = 3 (0..3 = 4 values needed) + // + // CAREFUL: DO NOT ADD ANYTHING TO THIS VALUE. + // if min=0 and max=ulong.max then normalized = ulong.max, + // adding anything to it would make it overflow! + // (see tests!) + ulong normalized = max - min; + //UnityEngine.Debug.Log($"min={min} max={max} normalized={normalized}"); + + // .Net Core 3.1 has BitOperations.Log2(x) + // Unity doesn't, so we could use one of a dozen weird tricks: + // https://stackoverflow.com/questions/15967240/fastest-implementation-of-log2int-and-log2float + // including lookup tables, float exponent tricks for little endian, + // etc. + // + // ... or we could just hard code! + if (normalized < 2) return 1; + if (normalized < 4) return 2; + if (normalized < 8) return 3; + if (normalized < 16) return 4; + if (normalized < 32) return 5; + if (normalized < 64) return 6; + if (normalized < 128) return 7; + if (normalized < 256) return 8; + if (normalized < 512) return 9; + if (normalized < 1024) return 10; + if (normalized < 2048) return 11; + if (normalized < 4096) return 12; + if (normalized < 8192) return 13; + if (normalized < 16384) return 14; + if (normalized < 32768) return 15; + if (normalized < 65536) return 16; + if (normalized < 131072) return 17; + if (normalized < 262144) return 18; + if (normalized < 524288) return 19; + if (normalized < 1048576) return 20; + if (normalized < 2097152) return 21; + if (normalized < 4194304) return 22; + if (normalized < 8388608) return 23; + if (normalized < 16777216) return 24; + if (normalized < 33554432) return 25; + if (normalized < 67108864) return 26; + if (normalized < 134217728) return 27; + if (normalized < 268435456) return 28; + if (normalized < 536870912) return 29; + if (normalized < 1073741824) return 30; + if (normalized < 2147483648) return 31; + if (normalized < 4294967296) return 32; + if (normalized < 8589934592) return 33; + if (normalized < 17179869184) return 34; + if (normalized < 34359738368) return 35; + if (normalized < 68719476736) return 36; + if (normalized < 137438953472) return 37; + if (normalized < 274877906944) return 38; + if (normalized < 549755813888) return 39; + if (normalized < 1099511627776) return 40; + if (normalized < 2199023255552) return 41; + if (normalized < 4398046511104) return 42; + if (normalized < 8796093022208) return 43; + if (normalized < 17592186044416) return 44; + if (normalized < 35184372088832) return 45; + if (normalized < 70368744177664) return 46; + if (normalized < 140737488355328) return 47; + if (normalized < 281474976710656) return 48; + if (normalized < 562949953421312) return 49; + if (normalized < 1125899906842624) return 50; + if (normalized < 2251799813685248) return 51; + if (normalized < 4503599627370496) return 52; + if (normalized < 9007199254740992) return 53; + if (normalized < 18014398509481984) return 54; + if (normalized < 36028797018963968) return 55; + if (normalized < 72057594037927936) return 56; + if (normalized < 144115188075855872) return 57; + if (normalized < 288230376151711744) return 58; + if (normalized < 576460752303423488) return 59; + if (normalized < 1152921504606846976) return 60; + if (normalized < 2305843009213693952) return 61; + if (normalized < 4611686018427387904) return 62; + if (normalized < 9223372036854775808) return 63; + return 64; + } + public static bool IsPrefab(GameObject obj) { #if UNITY_EDITOR diff --git a/Assets/Mirror/Tests/Editor/UtilsTests.cs b/Assets/Mirror/Tests/Editor/UtilsTests.cs index 7acab8863..c23f80664 100644 --- a/Assets/Mirror/Tests/Editor/UtilsTests.cs +++ b/Assets/Mirror/Tests/Editor/UtilsTests.cs @@ -1,3 +1,4 @@ +using System; using NUnit.Framework; using UnityEngine; @@ -64,6 +65,58 @@ public void RoundBitsToFullBytes() Assert.That(Utils.RoundBitsToFullBytes(i), Is.EqualTo(8)); } + [Test] + public void BitsRequired() + { + // min > max should never work + Assert.Throws(() => Utils.BitsRequired(2, 1)); + + // 2..2 is only ever 1 value = 2, so 0 bits needed + Assert.That(Utils.BitsRequired(2, 2), Is.EqualTo(0)); + + // 3..4 are 2 values, so 1 bit + Assert.That(Utils.BitsRequired(3, 4), Is.EqualTo(1)); + + // 0..7 are 8 values, so 3 bits + Assert.That(Utils.BitsRequired(0, 7), Is.EqualTo(3)); + + // 4..7 are 4 values, so 2 bits + Assert.That(Utils.BitsRequired(4, 7), Is.EqualTo(2)); + + // 0..255 are 256 values, so 8 bits + Assert.That(Utils.BitsRequired(0, 255), Is.EqualTo(8)); + + // 128..255 are 128 values, so 7 bits + Assert.That(Utils.BitsRequired(128, 255), Is.EqualTo(7)); + + // ushort should always fit into 2 bytes + Assert.That(Utils.BitsRequired(0, ushort.MaxValue), Is.EqualTo(16)); + + // uint should always fit into 4 bytes + Assert.That(Utils.BitsRequired(0, uint.MaxValue), Is.EqualTo(32)); + + // ulong should always fit into 8 bytes + // this is the maximum range that ever fits into an ulong. + // a lot of things could go wrong, e.g. if we +1 to that range + // without being careful. + // THIS TEST IS HUGELY IMPORTANT. + Assert.That(Utils.BitsRequired(0, ulong.MaxValue), Is.EqualTo(64)); + + // since we hardcoded the values, let's test for 1..63 shifted bits + // just to be sure that all of the hard coded values are correct! + for (int bits = 1; bits < 64; ++bits) + { + // 1 bits => bitsMax = 1 + // 2 bits => bitsMax = 2 + // 3 bits => bitsMax = 4 + // etc. + // so check 0..bitsMax-1 + ulong bitsMax = 1ul << bits; + //UnityEngine.Debug.Log($"testing bitsMax={bitsMax-1} => bits={bits}"); + Assert.That(Utils.BitsRequired(0, bitsMax - 1), Is.EqualTo(bits)); + } + } + [Test] public void IsPointInScreen() {