refactor: Add BitMask256 utility for 256-bit bitmask operations (#2300)
### Summary - Create BitMask256 struct with scalar operations (benchmarks showed AVX2 vectorization provides no benefit for this size) - Refactor Map.cs to use BitMask256 for full Z range support (-128 to 127) - Remove SectorSpawnCache struct, use BitMask256 directly in manager - Update tests to use BitMask256 directly - Remove unused test assertions for old 64-bit behavior
This commit is contained in:
parent
d3b43f6f0e
commit
598223703f
7 changed files with 482 additions and 261 deletions
|
|
@ -16,7 +16,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Server.Buffers;
|
||||
using Server.Collections;
|
||||
|
|
@ -1152,13 +1151,13 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
|
|||
return false;
|
||||
}
|
||||
|
||||
// Bitmask approach inspired by Item.DropToWorld's m_OpenSlots pattern.
|
||||
// 256-bit bitmask approach for full Z range support (-128 to 127).
|
||||
// Each bit represents a Z level relative to minZ.
|
||||
// openSlots: bit set = Z level is not blocked
|
||||
// surfaces: bit set = Z level has a valid surface
|
||||
// Final result: lowest set bit in (surfaces & openSlots)
|
||||
var openSlots = ulong.MaxValue;
|
||||
ulong surfaces = 0;
|
||||
var openSlots = BitMask256.AllSet();
|
||||
var surfaces = BitMask256.AllClear();
|
||||
|
||||
// Track what types of blockers we encounter (for failure reason)
|
||||
var hasNonTransientBlocker = false;
|
||||
|
|
@ -1178,14 +1177,14 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
|
|||
if (isImpassable && !(canSwim && isWet))
|
||||
{
|
||||
// Impassable land blocks z in range (lowZ - 16, avgZ)
|
||||
openSlots &= ~CreateBlockerMask(lowZ - 16, avgZ, minZ);
|
||||
ApplyBlockerMask(ref openSlots, lowZ - 16, avgZ, minZ);
|
||||
hasNonTransientBlocker = true;
|
||||
}
|
||||
|
||||
// Surface: water for swimmers, passable land for walkers
|
||||
if (avgZ >= minZ && avgZ <= maxZ && (canSwim && isWet || !cantWalk && !isImpassable))
|
||||
{
|
||||
surfaces |= 1UL << (avgZ - minZ);
|
||||
surfaces.SetBit(avgZ - minZ);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1202,7 +1201,7 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
|
|||
// Exception: water tiles (Impassable | Wet) don't block swimming mobs
|
||||
if ((isSurface || isImpassable) && !(canSwim && isWet))
|
||||
{
|
||||
openSlots &= ~CreateBlockerMask(tile.Z - 16, tileTop, minZ);
|
||||
ApplyBlockerMask(ref openSlots, tile.Z - 16, tileTop, minZ);
|
||||
hasNonTransientBlocker = true;
|
||||
}
|
||||
|
||||
|
|
@ -1210,7 +1209,7 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
|
|||
if (tileTop >= minZ && tileTop <= maxZ &&
|
||||
(canSwim && isWet || !cantWalk && isSurface && !isImpassable))
|
||||
{
|
||||
surfaces |= 1UL << (tileTop - minZ);
|
||||
surfaces.SetBit(tileTop - minZ);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1233,7 +1232,7 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
|
|||
// Exception: water items (Impassable | Wet) don't block swimming mobs
|
||||
if ((isSurface || isImpassable) && !(canSwim && isWet))
|
||||
{
|
||||
openSlots &= ~CreateBlockerMask(item.Z - 16, itemTop, minZ);
|
||||
ApplyBlockerMask(ref openSlots, item.Z - 16, itemTop, minZ);
|
||||
|
||||
// Movable items are transient, non-movable are permanent
|
||||
if (item.Movable || item.CanDecay())
|
||||
|
|
@ -1250,7 +1249,7 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
|
|||
if (!item.Movable && itemTop >= minZ && itemTop <= maxZ &&
|
||||
(canSwim && isWet || !cantWalk && isSurface && !isImpassable))
|
||||
{
|
||||
surfaces |= 1UL << (itemTop - minZ);
|
||||
surfaces.SetBit(itemTop - minZ);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1261,18 +1260,20 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
|
|||
(m.AccessLevel == AccessLevel.Player || !m.Hidden))
|
||||
{
|
||||
// Mobiles block z in range (m.Z - 16, m.Z + 16)
|
||||
openSlots &= ~CreateBlockerMask(m.Z - 16, m.Z + 16, minZ);
|
||||
ApplyBlockerMask(ref openSlots, m.Z - 16, m.Z + 16, minZ);
|
||||
hasTransientBlocker = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the lowest unblocked surface using bit operations
|
||||
var validSurfaces = surfaces & openSlots;
|
||||
if (validSurfaces == 0)
|
||||
// Find the lowest unblocked surface
|
||||
var validSurfaces = surfaces.And(in openSlots);
|
||||
var lowestBit = validSurfaces.LowestSetBit();
|
||||
|
||||
if (lowestBit < 0)
|
||||
{
|
||||
// Determine failure reason based on what blockers we encountered
|
||||
// If no surfaces existed at all, that's also a non-transient issue (map geometry)
|
||||
if (hasNonTransientBlocker || surfaces == 0)
|
||||
if (hasNonTransientBlocker || surfaces.IsEmpty())
|
||||
{
|
||||
failureReason |= SpawnFailureReason.NonTransientBlocker;
|
||||
}
|
||||
|
|
@ -1285,10 +1286,7 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
|
|||
return false;
|
||||
}
|
||||
|
||||
// TrailingZeroCount gives the position of the lowest set bit
|
||||
var lowestBit = BitOperations.TrailingZeroCount(validSurfaces);
|
||||
spawnZ = minZ + lowestBit;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1316,10 +1314,10 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
|
|||
return false;
|
||||
}
|
||||
|
||||
// Bitmask approach: find surfaces within Z range, check for blockers.
|
||||
// 256-bit bitmask approach for full Z range support.
|
||||
// Unlike CanSpawnMobile, Surface+Impassable tiles (tables) are valid surfaces for items.
|
||||
var openSlots = ulong.MaxValue;
|
||||
ulong surfaces = 0;
|
||||
var openSlots = BitMask256.AllSet();
|
||||
var surfaces = BitMask256.AllClear();
|
||||
|
||||
// 1. Land tile
|
||||
var landTile = Tiles.GetLandTile(x, y);
|
||||
|
|
@ -1333,13 +1331,13 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
|
|||
// Impassable land blocks
|
||||
if (isImpassable)
|
||||
{
|
||||
openSlots &= ~CreateBlockerMask(lowZ - 16, avgZ, minZ);
|
||||
ApplyBlockerMask(ref openSlots, lowZ - 16, avgZ, minZ);
|
||||
}
|
||||
|
||||
// Passable land is a valid surface
|
||||
if (!isImpassable && avgZ >= minZ && avgZ <= maxZ)
|
||||
{
|
||||
surfaces |= 1UL << (avgZ - minZ);
|
||||
surfaces.SetBit(avgZ - minZ);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1354,13 +1352,13 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
|
|||
// Blocking: (surface || impassable) tiles block z in range (tile.Z - 16, tileTop)
|
||||
if (isSurface || isImpassable)
|
||||
{
|
||||
openSlots &= ~CreateBlockerMask(tile.Z - 16, tileTop, minZ);
|
||||
ApplyBlockerMask(ref openSlots, tile.Z - 16, tileTop, minZ);
|
||||
}
|
||||
|
||||
// Surface candidate: Surface flag (including Surface+Impassable like tables)
|
||||
if (isSurface && tileTop >= minZ && tileTop <= maxZ)
|
||||
{
|
||||
surfaces |= 1UL << (tileTop - minZ);
|
||||
surfaces.SetBit(tileTop - minZ);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1381,50 +1379,46 @@ public sealed partial class Map : IComparable<Map>, ISpanFormattable, ISpanParsa
|
|||
// Blocking: (surface || impassable) items block
|
||||
if (isSurface || isImpassable)
|
||||
{
|
||||
openSlots &= ~CreateBlockerMask(item.Z - 16, itemTop, minZ);
|
||||
ApplyBlockerMask(ref openSlots, item.Z - 16, itemTop, minZ);
|
||||
}
|
||||
|
||||
// Surface candidate: non-movable Surface items (including Surface+Impassable)
|
||||
if (!item.Movable && isSurface && itemTop >= minZ && itemTop <= maxZ)
|
||||
{
|
||||
surfaces |= 1UL << (itemTop - minZ);
|
||||
surfaces.SetBit(itemTop - minZ);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the lowest unblocked surface using bit operations
|
||||
var validSurfaces = surfaces & openSlots;
|
||||
if (validSurfaces == 0)
|
||||
// Find the lowest unblocked surface
|
||||
var validSurfaces = surfaces.And(in openSlots);
|
||||
var lowestBit = validSurfaces.LowestSetBit();
|
||||
|
||||
if (lowestBit < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lowestBit = BitOperations.TrailingZeroCount(validSurfaces);
|
||||
spawnZ = minZ + lowestBit;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a bitmask for a blocker range. Blocker blocks Z where blockLow < z < blockHigh.
|
||||
/// Bits are relative to minZ, clamped to [0, 63].
|
||||
/// Clears bits in the mask for Z levels blocked by an object.
|
||||
/// Converts (blockLow, blockHigh) exclusive world Z range to bit indices relative to minZ.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong CreateBlockerMask(int blockLow, int blockHigh, int minZ)
|
||||
private static void ApplyBlockerMask(ref BitMask256 mask, int blockLow, int blockHigh, int minZ)
|
||||
{
|
||||
// Blocked range is (blockLow, blockHigh) exclusive, = [blockLow + 1, blockHigh - 1] inclusive
|
||||
// Blocked range is (blockLow, blockHigh) exclusive = [blockLow + 1, blockHigh - 1] inclusive
|
||||
var startBit = blockLow - minZ + 1;
|
||||
var endBit = blockHigh - minZ - 1;
|
||||
|
||||
if (endBit < 0 || startBit > 63 || startBit > endBit)
|
||||
if (endBit < 0 || startBit > 255 || startBit > endBit)
|
||||
{
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
|
||||
startBit = Math.Max(0, startBit);
|
||||
endBit = Math.Min(63, endBit);
|
||||
|
||||
var bitCount = endBit - startBit + 1;
|
||||
var mask = bitCount >= 64 ? ulong.MaxValue : (1UL << bitCount) - 1;
|
||||
return mask << startBit;
|
||||
mask.ClearRange(Math.Max(0, startBit), Math.Min(255, endBit));
|
||||
}
|
||||
|
||||
private class ZComparer : IComparer<Item>
|
||||
|
|
|
|||
396
Projects/Server/Utilities/BitMask256.cs
Normal file
396
Projects/Server/Utilities/BitMask256.cs
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2025 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: BitMask256.cs *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace Server;
|
||||
|
||||
/// <summary>
|
||||
/// A 256-bit bitmask stored as 4 ulongs.
|
||||
/// Useful for tracking 256 discrete states such as:
|
||||
/// - Z levels in UO (-128 to 127 = 256 values)
|
||||
/// - Sector positions (16x16 = 256 tiles)
|
||||
/// Uses BMI2 for efficient bit selection when available.
|
||||
/// </summary>
|
||||
public struct BitMask256
|
||||
{
|
||||
public ulong Bits0, Bits1, Bits2, Bits3;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mask with all 256 bits set.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static BitMask256 AllSet() => new()
|
||||
{
|
||||
Bits0 = ulong.MaxValue,
|
||||
Bits1 = ulong.MaxValue,
|
||||
Bits2 = ulong.MaxValue,
|
||||
Bits3 = ulong.MaxValue
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mask with all bits cleared (default state).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static BitMask256 AllClear() => default;
|
||||
|
||||
/// <summary>
|
||||
/// Sets a single bit at the specified index (0-255).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetBit(int index)
|
||||
{
|
||||
if ((uint)index >= 256)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var segment = index >> 6;
|
||||
var localBit = index & 0x3F;
|
||||
var mask = 1UL << localBit;
|
||||
|
||||
switch (segment)
|
||||
{
|
||||
case 0: Bits0 |= mask; break;
|
||||
case 1: Bits1 |= mask; break;
|
||||
case 2: Bits2 |= mask; break;
|
||||
case 3: Bits3 |= mask; break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears a single bit at the specified index (0-255).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ClearBit(int index)
|
||||
{
|
||||
if ((uint)index >= 256)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var segment = index >> 6;
|
||||
var localBit = index & 0x3F;
|
||||
var mask = 1UL << localBit;
|
||||
|
||||
switch (segment)
|
||||
{
|
||||
case 0: Bits0 &= ~mask; break;
|
||||
case 1: Bits1 &= ~mask; break;
|
||||
case 2: Bits2 &= ~mask; break;
|
||||
case 3: Bits3 &= ~mask; break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of a single bit at the specified index (0-255).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool GetBit(int index)
|
||||
{
|
||||
if ((uint)index >= 256)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var segment = index >> 6;
|
||||
var localBit = index & 0x3F;
|
||||
var mask = 1UL << localBit;
|
||||
|
||||
return segment switch
|
||||
{
|
||||
0 => (Bits0 & mask) != 0,
|
||||
1 => (Bits1 & mask) != 0,
|
||||
2 => (Bits2 & mask) != 0,
|
||||
3 => (Bits3 & mask) != 0,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets all bits in the inclusive range [start, end].
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetRange(int start, int end)
|
||||
{
|
||||
if (end < 0 || start > 255 || start > end)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
start = Math.Max(0, start);
|
||||
end = Math.Min(255, end);
|
||||
|
||||
Bits0 |= CreateSegmentMask(start, end, 0);
|
||||
Bits1 |= CreateSegmentMask(start, end, 64);
|
||||
Bits2 |= CreateSegmentMask(start, end, 128);
|
||||
Bits3 |= CreateSegmentMask(start, end, 192);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all bits in the inclusive range [start, end].
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ClearRange(int start, int end)
|
||||
{
|
||||
if (end < 0 || start > 255 || start > end)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
start = Math.Max(0, start);
|
||||
end = Math.Min(255, end);
|
||||
|
||||
Bits0 &= ~CreateSegmentMask(start, end, 0);
|
||||
Bits1 &= ~CreateSegmentMask(start, end, 64);
|
||||
Bits2 &= ~CreateSegmentMask(start, end, 128);
|
||||
Bits3 &= ~CreateSegmentMask(start, end, 192);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a bitmask for the portion of [start, end] that falls within a 64-bit segment.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong CreateSegmentMask(int start, int end, int segmentOffset)
|
||||
{
|
||||
var localStart = start - segmentOffset;
|
||||
var localEnd = end - segmentOffset;
|
||||
|
||||
localStart = Math.Max(0, localStart);
|
||||
localEnd = Math.Min(63, localEnd);
|
||||
|
||||
if (localStart > 63 || localEnd < 0 || localStart > localEnd)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var bitCount = localEnd - localStart + 1;
|
||||
var mask = bitCount >= 64 ? ulong.MaxValue : (1UL << bitCount) - 1;
|
||||
return mask << localStart;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the bitwise AND of this mask with another.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly BitMask256 And(in BitMask256 other) => new()
|
||||
{
|
||||
Bits0 = Bits0 & other.Bits0,
|
||||
Bits1 = Bits1 & other.Bits1,
|
||||
Bits2 = Bits2 & other.Bits2,
|
||||
Bits3 = Bits3 & other.Bits3
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the bitwise OR of this mask with another.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly BitMask256 Or(in BitMask256 other) => new()
|
||||
{
|
||||
Bits0 = Bits0 | other.Bits0,
|
||||
Bits1 = Bits1 | other.Bits1,
|
||||
Bits2 = Bits2 | other.Bits2,
|
||||
Bits3 = Bits3 | other.Bits3
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the bitwise XOR of this mask with another.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly BitMask256 Xor(in BitMask256 other) => new()
|
||||
{
|
||||
Bits0 = Bits0 ^ other.Bits0,
|
||||
Bits1 = Bits1 ^ other.Bits1,
|
||||
Bits2 = Bits2 ^ other.Bits2,
|
||||
Bits3 = Bits3 ^ other.Bits3
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the bitwise AND-NOT (this & ~other).
|
||||
/// Clears bits in this mask that are set in the other mask.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly BitMask256 AndNot(in BitMask256 other) => new()
|
||||
{
|
||||
Bits0 = Bits0 & ~other.Bits0,
|
||||
Bits1 = Bits1 & ~other.Bits1,
|
||||
Bits2 = Bits2 & ~other.Bits2,
|
||||
Bits3 = Bits3 & ~other.Bits3
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the bitwise NOT of this mask.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly BitMask256 Not() => new()
|
||||
{
|
||||
Bits0 = ~Bits0,
|
||||
Bits1 = ~Bits1,
|
||||
Bits2 = ~Bits2,
|
||||
Bits3 = ~Bits3
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the total number of bits set (population count).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly int PopCount() =>
|
||||
BitOperations.PopCount(Bits0) +
|
||||
BitOperations.PopCount(Bits1) +
|
||||
BitOperations.PopCount(Bits2) +
|
||||
BitOperations.PopCount(Bits3);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if no bits are set.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool IsEmpty() => (Bits0 | Bits1 | Bits2 | Bits3) == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if all 256 bits are set.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool IsFull() =>
|
||||
Bits0 == ulong.MaxValue &&
|
||||
Bits1 == ulong.MaxValue &&
|
||||
Bits2 == ulong.MaxValue &&
|
||||
Bits3 == ulong.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of the lowest set bit (0-255), or -1 if no bits are set.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly int LowestSetBit()
|
||||
{
|
||||
if (Bits0 != 0)
|
||||
{
|
||||
return BitOperations.TrailingZeroCount(Bits0);
|
||||
}
|
||||
|
||||
if (Bits1 != 0)
|
||||
{
|
||||
return 64 + BitOperations.TrailingZeroCount(Bits1);
|
||||
}
|
||||
|
||||
if (Bits2 != 0)
|
||||
{
|
||||
return 128 + BitOperations.TrailingZeroCount(Bits2);
|
||||
}
|
||||
|
||||
if (Bits3 != 0)
|
||||
{
|
||||
return 192 + BitOperations.TrailingZeroCount(Bits3);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of the highest set bit (0-255), or -1 if no bits are set.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly int HighestSetBit()
|
||||
{
|
||||
if (Bits3 != 0)
|
||||
{
|
||||
return 255 - BitOperations.LeadingZeroCount(Bits3);
|
||||
}
|
||||
|
||||
if (Bits2 != 0)
|
||||
{
|
||||
return 191 - BitOperations.LeadingZeroCount(Bits2);
|
||||
}
|
||||
|
||||
if (Bits1 != 0)
|
||||
{
|
||||
return 127 - BitOperations.LeadingZeroCount(Bits1);
|
||||
}
|
||||
|
||||
if (Bits0 != 0)
|
||||
{
|
||||
return 63 - BitOperations.LeadingZeroCount(Bits0);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of the Nth set bit (0-indexed), or -1 if fewer than N+1 bits are set.
|
||||
/// Uses BMI2 PDEP for O(1) performance when available, otherwise O(popcount) fallback.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly int GetNthSetBit(int n)
|
||||
{
|
||||
if (n < 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var count0 = BitOperations.PopCount(Bits0);
|
||||
if (n < count0)
|
||||
{
|
||||
return GetNthBitInUlong(Bits0, n);
|
||||
}
|
||||
|
||||
n -= count0;
|
||||
|
||||
var count1 = BitOperations.PopCount(Bits1);
|
||||
if (n < count1)
|
||||
{
|
||||
return 64 + GetNthBitInUlong(Bits1, n);
|
||||
}
|
||||
|
||||
n -= count1;
|
||||
|
||||
var count2 = BitOperations.PopCount(Bits2);
|
||||
if (n < count2)
|
||||
{
|
||||
return 128 + GetNthBitInUlong(Bits2, n);
|
||||
}
|
||||
|
||||
n -= count2;
|
||||
|
||||
var count3 = BitOperations.PopCount(Bits3);
|
||||
if (n < count3)
|
||||
{
|
||||
return 192 + GetNthBitInUlong(Bits3, n);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int GetNthBitInUlong(ulong bits, int n)
|
||||
{
|
||||
// BMI2 PDEP: O(1) - deposits the nth selector bit into the position of the nth set bit
|
||||
if (Bmi2.X64.IsSupported)
|
||||
{
|
||||
var deposited = Bmi2.X64.ParallelBitDeposit(1UL << n, bits);
|
||||
return BitOperations.TrailingZeroCount(deposited);
|
||||
}
|
||||
|
||||
// Fallback: O(popcount) - clear n set bits, then find position of next one
|
||||
while (n > 0 && bits != 0)
|
||||
{
|
||||
bits &= bits - 1;
|
||||
n--;
|
||||
}
|
||||
|
||||
return bits == 0 ? -1 : BitOperations.TrailingZeroCount(bits);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue