Skip to content

Commit

Permalink
feat: attributes to calculate bit count of a given range (#902)
Browse files Browse the repository at this point in the history
* feat: attributes to calculate bit count of a given range

* weaver tests

* generated tests

* implementing bitcount from range

* updating tests

* fixing test

* fixing bitcount breaking for no large value
  • Loading branch information
James-Frowen committed Aug 24, 2021
1 parent 7cfc6a1 commit 1c22ea6
Show file tree
Hide file tree
Showing 48 changed files with 1,692 additions and 38 deletions.
5 changes: 3 additions & 2 deletions Assets/Mirage/Runtime/Serialization/Packers/BitHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ MIT License
SOFTWARE.
*/

using System;
using UnityEngine;

namespace Mirage.Serialization
Expand All @@ -39,7 +40,7 @@ public static class BitHelper
/// <returns></returns>
public static int BitCount(float max, float precision)
{
return Mathf.CeilToInt(Mathf.Log(2 * max / precision, 2));
return Mathf.FloorToInt(Mathf.Log(2 * max / precision, 2)) + 1;
}

/// <summary>
Expand All @@ -53,7 +54,7 @@ public static int BitCount(float max, float precision)
/// <returns></returns>
public static int BitCount(ulong max)
{
return Mathf.CeilToInt(Mathf.Log(max, 2));
return (int)Math.Floor(Math.Log(max, 2)) + 1;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public sealed class VariableIntPacker
readonly bool throwIfOverLarge;

public VariableIntPacker(ulong smallValue, ulong mediumValue)
: this(smallValue, mediumValue, ulong.MaxValue, false) { }
: this(BitHelper.BitCount(smallValue), BitHelper.BitCount(mediumValue), 64, false) { }
public VariableIntPacker(ulong smallValue, ulong mediumValue, ulong largeValue, bool throwIfOverLarge = true)
: this(BitHelper.BitCount(smallValue), BitHelper.BitCount(mediumValue), BitHelper.BitCount(largeValue), throwIfOverLarge) { }

Expand Down
6 changes: 6 additions & 0 deletions Assets/Mirage/Runtime/Serialization/WeaverAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ public BitCountAttribute(int bitCount)
}
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter)]
public class BitCountFromRangeAttribute : Attribute
{
public BitCountFromRangeAttribute(int min, int max) { }
}

/// <summary>
/// Used along size <see cref="BitCountAttribute"/> to encodes a interager value using <see cref="ZigZag"/> so that both positive and negative values can be sent
/// </summary>
Expand Down
18 changes: 18 additions & 0 deletions Assets/Mirage/Weaver/Processors/SyncVarProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,10 @@ void WriteWithBitCount()
{
WriteZigZag();
}
if (syncVar.BitCountMinValue.HasValue)
{
WriteSubtractMinValue();
}

worker.Append(worker.Create(OpCodes.Conv_U8));
worker.Append(worker.Create(OpCodes.Ldc_I4, syncVar.BitCount.Value));
Expand All @@ -475,6 +479,11 @@ void WriteZigZag()

worker.Append(worker.Create(OpCodes.Call, encode));
}
void WriteSubtractMinValue()
{
worker.Append(worker.Create(OpCodes.Ldc_I4, syncVar.BitCountMinValue.Value));
worker.Append(worker.Create(OpCodes.Sub));
}
}


Expand Down Expand Up @@ -641,6 +650,10 @@ void ReadWithBitCount()
{
ReadZigZag();
}
if (syncVar.BitCountMinValue.HasValue)
{
ReadAddMinValue();
}

worker.Append(worker.Create(OpCodes.Stfld, syncVar.FieldDefinition.MakeHostGenericIfNeeded()));
}
Expand All @@ -653,6 +666,11 @@ void ReadZigZag()

worker.Append(worker.Create(OpCodes.Call, encode));
}
void ReadAddMinValue()
{
worker.Append(worker.Create(OpCodes.Ldc_I4, syncVar.BitCountMinValue.Value));
worker.Append(worker.Create(OpCodes.Add));
}
}
}
}
4 changes: 2 additions & 2 deletions Assets/Mirage/Weaver/Processors/SyncVars/BitCountFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public static (int? bitCount, OpCode? ConvertCode) GetBitCount(FieldDefinition s
int maxSize = GetTypeMaxSize(syncVar.FieldType, syncVar);

if (bitCount > maxSize)
throw new BitCountException($"BitCount can not be above target type size, bitCount:{bitCount}, type:{syncVar.FieldType.Name}, max size:{maxSize}", syncVar);
throw new BitCountException($"BitCount can not be above target type size, bitCount:{bitCount}, max size:{maxSize}, type:{syncVar.FieldType.Name}", syncVar);

return (bitCount, GetConvertType(syncVar.FieldType));
}
Expand Down Expand Up @@ -50,7 +50,7 @@ static int GetTypeMaxSize(TypeReference type, FieldDefinition syncVar)
/// </summary>
/// <param name="syncVar"></param>
/// <returns></returns>
static OpCode? GetConvertType(TypeReference type)
public static OpCode? GetConvertType(TypeReference type)
{
if (type.Is<byte>()) return OpCodes.Conv_I4;
if (type.Is<ushort>()) return OpCodes.Conv_I4;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using Mirage.Serialization;
using Mono.Cecil;
using Mono.Cecil.Cil;

namespace Mirage.Weaver.SyncVars
{
public static class BitCountFromRangeFinder
{
internal static (int? BitCount, OpCode? BitCountConvert, int? MinValue) GetBitFoundFromRange(FieldDefinition syncVar, bool hasBitCount)
{
CustomAttribute attribute = syncVar.GetCustomAttribute<BitCountFromRangeAttribute>();

if (hasBitCount)
throw new BitCountFromRangeException($"[BitCountFromRange] can't be used with [BitCount]", syncVar);

int min = (int)attribute.ConstructorArguments[0].Value;
int max = (int)attribute.ConstructorArguments[1].Value;

if (min >= max)
throw new BitCountFromRangeException("Max must be greater than min", syncVar);

long minAllowedMin = GetTypeMin(syncVar.FieldType, syncVar);
long maxAllowedMax = GetTypeMax(syncVar.FieldType, syncVar);

if (min < minAllowedMin)
throw new BitCountException($"Min must be less than types min value, min:{min}, min allowed:{minAllowedMin}, type:{syncVar.FieldType.Name}", syncVar);

if (max > maxAllowedMax)
throw new BitCountException($"Max must be greater than types max value, max:{max}, max allowed:{maxAllowedMax}, type:{syncVar.FieldType.Name}", syncVar);

// make sure to cast max to long so incase range is bigger than int value
long range = checked((long)max - min);
int bitCount = (int)Math.Floor(Math.Log(range, 2)) + 1;
if (bitCount < 0 || bitCount > 32)
throw new OverflowException($"Bit Count could not be calcualted, min:{min}, max:{max}, bitCount:{bitCount}");

int? minResult;
if (min == 0) minResult = null;
else minResult = min;

return (bitCount, BitCountFinder.GetConvertType(syncVar.FieldType), minResult);
}

static int GetTypeMax(TypeReference type, FieldDefinition syncVar)
{
if (type.Is<byte>()) return byte.MaxValue;
if (type.Is<ushort>()) return ushort.MaxValue;
if (type.Is<short>()) return short.MaxValue;
if (type.Is<int>()
|| type.Is<uint>()) return int.MaxValue;

if (type.Resolve().IsEnum)
{
// use underlying enum type for max size
TypeReference enumType = type.Resolve().GetEnumUnderlyingType();
return GetTypeMax(enumType, syncVar);
}

// long is not a support type because it is not commonly used and
// would take a lot more code to make it works with all possible
// ranges of values:
// - min and max both negative long values
// - min and max both above long.max
// we would need a type bigger than long in order to easily handle
// both of these, and that is before dealing with IL stuff

throw new BitCountFromRangeException($"{type.FullName} is not a supported type for [BitCountFromRange]", syncVar);
}

static int GetTypeMin(TypeReference type, FieldDefinition syncVar)
{
if (type.Is<byte>()) return byte.MinValue;
if (type.Is<ushort>()) return ushort.MinValue;
if (type.Is<short>()) return short.MinValue;
if (type.Is<int>()) return int.MinValue;
if (type.Is<uint>()) return 0;

if (type.Resolve().IsEnum)
{
// use underlying enum type for max size
TypeReference enumType = type.Resolve().GetEnumUnderlyingType();
return GetTypeMin(enumType, syncVar);
}

throw new BitCountFromRangeException($"{type.FullName} is not a supported type for [BitCountFromRange]", syncVar);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Assets/Mirage/Weaver/Processors/SyncVars/FoundSyncVar.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Runtime.CompilerServices;
using Mirage.Serialization;
using Mono.Cecil;
using Mono.Cecil.Cil;
using UnityEngine;
Expand Down Expand Up @@ -28,6 +30,7 @@ public FoundSyncVar(FieldDefinition fieldDefinition, int dirtyIndex)
public OpCode? BitCountConvert { get; private set; }

public bool UseZigZagEncoding { get; private set; }
public int? BitCountMinValue { get; private set; }

public MethodReference WriteFunction { get; private set; }
public MethodReference ReadFunction { get; private set; }
Expand Down Expand Up @@ -78,13 +81,18 @@ private static bool CheckWrapType(ModuleDefinition module, TypeReference origina
/// Finds any attribute values needed for this syncvar
/// </summary>
/// <param name="module"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ProcessAttributes()
{
HookMethod = HookMethodFinder.GetHookMethod(FieldDefinition, OriginalType);
HasHookMethod = HookMethod != null;

(BitCount, BitCountConvert) = BitCountFinder.GetBitCount(FieldDefinition);
UseZigZagEncoding = ZigZagFinder.HasZigZag(FieldDefinition, BitCount.HasValue);

// do this if check here so it doesn't override fields unless attribute exists
if (FieldDefinition.HasCustomAttribute<BitCountFromRangeAttribute>())
(BitCount, BitCountConvert, BitCountMinValue) = BitCountFromRangeFinder.GetBitFoundFromRange(FieldDefinition, BitCount.HasValue);
}

public void FindSerializeFunctions(Writers writers, Readers readers)
Expand Down
4 changes: 4 additions & 0 deletions Assets/Mirage/Weaver/WeaverExceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ internal class ZigZagException : SyncVarException
{
public ZigZagException(string message, MemberReference memberReference) : base(message, memberReference) { }
}
internal class BitCountFromRangeException : SyncVarException
{
public BitCountFromRangeException(string message, MemberReference memberReference) : base(message, memberReference) { }
}
}
8 changes: 8 additions & 0 deletions Assets/Tests/Generated/BitCountFromRangeTests.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// DO NOT EDIT: GENERATED BY BitCountFromRangeTestGenerator.cs

using System;
using System.Collections;
using Mirage.Serialization;
using Mirage.Tests.Runtime.ClientServer;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace Mirage.Tests.Runtime.Generated.BitCountFromRangeAttributeTests
{
[System.Serializable]
public enum MyByteEnum : byte
{
None = 0,
Slow = 1,
Fast = 2,
ReallyFast = 3,
}
public class BitCountRangeBehaviour_MyByteEnum_0_3 : NetworkBehaviour
{
[BitCountFromRange(0, 3)]
[SyncVar] public MyByteEnum myValue;

public event Action<MyByteEnum> onRpc;

[ClientRpc]
public void RpcSomeFunction([BitCountFromRange(0, 3)] MyByteEnum myParam)
{
onRpc?.Invoke(myParam);
}
}
public class BitCountRangeTest_MyByteEnum_0_3 : ClientServerSetup<BitCountRangeBehaviour_MyByteEnum_0_3>
{
const MyByteEnum value = (MyByteEnum)3;

[Test]
public void SyncVarIsBitPacked()
{
serverComponent.myValue = value;

using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
{
serverComponent.SerializeSyncVars(writer, true);

Assert.That(writer.BitPosition, Is.EqualTo(2));

using (PooledNetworkReader reader = NetworkReaderPool.GetReader(writer.ToArraySegment()))
{
clientComponent.DeserializeSyncVars(reader, true);
Assert.That(reader.BitPosition, Is.EqualTo(2));

Assert.That(clientComponent.myValue, Is.EqualTo(value));
}
}
}

// [UnityTest]
// [Ignore("Rpc not supported yet")]
public IEnumerator RpcIsBitPacked()
{
int called = 0;
clientComponent.onRpc += (v) => { called++; Assert.That(v, Is.EqualTo(value)); };

client.MessageHandler.UnregisterHandler<RpcMessage>();
int payloadSize = 0;
client.MessageHandler.RegisterHandler<RpcMessage>((player, msg) =>
{
// store value in variable because assert will throw and be catch by message wrapper
payloadSize = msg.payload.Count;
clientObjectManager.OnRpcMessage(msg);
});


serverComponent.RpcSomeFunction(value);
yield return null;
Assert.That(called, Is.EqualTo(1));
Assert.That(payloadSize, Is.EqualTo(1), $"%%BIT_COUNT%% bits is 1 bytes in payload");
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 1c22ea6

Please sign in to comment.