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:
Kamron Batman 2025-12-29 12:54:49 -08:00 committed by GitHub
parent d3b43f6f0e
commit 598223703f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 482 additions and 261 deletions

View file

@ -21,8 +21,6 @@ public class CanFitItemTests
Skip.If(!ServerFixture.TileDataLoaded, "TileData not loaded - client files required");
}
#region Basic Validation Tests
[Fact]
public void CanFitItem_InternalMapReturnsFalse()
{
@ -42,10 +40,6 @@ public class CanFitItemTests
Assert.False(map.CanFitItem(100, map.Height + 1, 0, 1));
}
#endregion
#region Land Surface Tests
private const int TestLandX = 1500;
private const int TestLandY = 1600;
@ -72,10 +66,6 @@ public class CanFitItemTests
Assert.False(result, "No surface 50 units above land");
}
#endregion
#region Surface + Impassable Tests (Tables, Furniture)
[SkippableFact]
public void CanFitItem_SurfaceImpassableMultiIsValidSurface()
{
@ -145,10 +135,6 @@ public class CanFitItemTests
}
}
#endregion
#region World Item Surface Tests
[SkippableFact]
public void CanFitItem_NonMovableWorldItemSurfaceWorks()
{
@ -215,10 +201,6 @@ public class CanFitItemTests
}
}
#endregion
#region Blocking Tests
[SkippableFact]
public void CanFitItem_ImpassableTileBlocksPlacement()
{
@ -278,10 +260,6 @@ public class CanFitItemTests
}
}
#endregion
#region Helper Methods and Classes
private TestMulti CreateSurfaceImpassableMulti(Map map, Point3D location, int surfaceZ)
{
// Use SurfaceImpassableTileId - a tile that has both Surface and Impassable flags
@ -322,6 +300,4 @@ public class CanFitItemTests
public override MultiComponentList Components => _components;
}
#endregion
}

View file

@ -9,8 +9,6 @@ namespace Server.Tests.Tests.Maps;
[Collection("Sequential Server Tests")]
public class CanSpawnMobileTests
{
#region Basic Validation Tests
[Fact]
public void CanSpawnMobile_InternalMapReturnsFalse()
{
@ -31,10 +29,6 @@ public class CanSpawnMobileTests
Assert.False(map.CanSpawnMobile(100, map.Height + 1, -128, 127, false, false, out _));
}
#endregion
#region Land Surface Tests
// Use coordinates near Britain which should be walkable land in both test and real data
private const int TestLandX = 1500;
private const int TestLandY = 1600;
@ -74,10 +68,6 @@ public class CanSpawnMobileTests
Assert.False(result, "cantWalk=true should not find land as valid surface");
}
#endregion
#region Mobile Blocking Tests
[Fact]
public void CanSpawnMobile_MobileBlocksSpawn()
{
@ -162,10 +152,6 @@ public class CanSpawnMobileTests
}
}
#endregion
#region Helper Classes
private class TestMobile : Mobile
{
public TestMobile()
@ -174,6 +160,4 @@ public class CanSpawnMobileTests
Hidden = false;
}
}
#endregion
}

View file

@ -23,8 +23,6 @@ public class CanSpawnMobileTileDataTests
Skip.If(!ServerFixture.TileDataLoaded, "TileData not loaded - client files required");
}
#region Multi-Based Tests
[SkippableFact]
public void CanSpawnMobile_FindsSurfaceOnMulti()
{
@ -241,10 +239,6 @@ public class CanSpawnMobileTileDataTests
}
}
#endregion
#region Real Map Data Tests
[SkippableFact]
public void CanSpawnMobile_MalasBuilding_FindsGroundFloor()
{
@ -290,10 +284,6 @@ public class CanSpawnMobileTileDataTests
Assert.True(spawnZ <= -40, $"Expected lowest floor around -50, got {spawnZ}");
}
#endregion
#region Helper Methods and Classes
private TestMulti CreateFloorMulti(Map map, Point3D location, int floorZ)
{
var multi = new TestMulti(CreateFloorComponents(floorZ));
@ -367,6 +357,4 @@ public class CanSpawnMobileTileDataTests
public override MultiComponentList Components => _components;
}
#endregion
}

View file

@ -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 &lt; z &lt; 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>

View 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 &amp; ~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);
}
}

View file

@ -5,14 +5,14 @@ using Xunit;
namespace UOContent.Tests;
public class SectorSpawnCacheTests
public class BitMask256Tests
{
[Fact]
public void SectorSpawnCache_InitialState_AllBitsZero()
public void BitMask256_InitialState_AllBitsZero()
{
var cache = new SectorSpawnCache();
var mask = BitMask256.AllClear();
Assert.Equal(0, cache.GetCount());
Assert.Equal(0, mask.PopCount());
}
[Theory]
@ -24,48 +24,48 @@ public class SectorSpawnCacheTests
[InlineData(191)]
[InlineData(192)]
[InlineData(255)]
public void SectorSpawnCache_SetBit_SetsCorrectBit(int bitIndex)
public void BitMask256_SetBit_SetsCorrectBit(int bitIndex)
{
var cache = new SectorSpawnCache();
var mask = BitMask256.AllClear();
cache.SetBit(bitIndex);
mask.SetBit(bitIndex);
Assert.True(cache.GetBit(bitIndex));
Assert.Equal(1, cache.GetCount());
Assert.True(mask.GetBit(bitIndex));
Assert.Equal(1, mask.PopCount());
}
[Fact]
public void SectorSpawnCache_SetMultipleBits_CountsCorrectly()
public void BitMask256_SetMultipleBits_CountsCorrectly()
{
var cache = new SectorSpawnCache();
var mask = BitMask256.AllClear();
cache.SetBit(0);
cache.SetBit(64);
cache.SetBit(128);
cache.SetBit(192);
mask.SetBit(0);
mask.SetBit(64);
mask.SetBit(128);
mask.SetBit(192);
Assert.Equal(4, cache.GetCount());
Assert.True(cache.GetBit(0));
Assert.True(cache.GetBit(64));
Assert.True(cache.GetBit(128));
Assert.True(cache.GetBit(192));
Assert.False(cache.GetBit(1));
Assert.Equal(4, mask.PopCount());
Assert.True(mask.GetBit(0));
Assert.True(mask.GetBit(64));
Assert.True(mask.GetBit(128));
Assert.True(mask.GetBit(192));
Assert.False(mask.GetBit(1));
}
[Fact]
public void SectorSpawnCache_ClearBit_ClearsCorrectBit()
public void BitMask256_ClearBit_ClearsCorrectBit()
{
var cache = new SectorSpawnCache();
var mask = BitMask256.AllClear();
cache.SetBit(50);
cache.SetBit(100);
Assert.Equal(2, cache.GetCount());
mask.SetBit(50);
mask.SetBit(100);
Assert.Equal(2, mask.PopCount());
cache.ClearBit(50);
mask.ClearBit(50);
Assert.False(cache.GetBit(50));
Assert.True(cache.GetBit(100));
Assert.Equal(1, cache.GetCount());
Assert.False(mask.GetBit(50));
Assert.True(mask.GetBit(100));
Assert.Equal(1, mask.PopCount());
}
[Theory]
@ -73,31 +73,31 @@ public class SectorSpawnCacheTests
[InlineData(1, 64)] // First bit of second ulong
[InlineData(2, 128)] // First bit of third ulong
[InlineData(3, 192)] // First bit of fourth ulong
public void SectorSpawnCache_GetNthBitPosition_FirstBitInEachUlong(int n, int expectedPosition)
public void BitMask256_GetNthSetBit_FirstBitInEachUlong(int n, int expectedPosition)
{
var cache = new SectorSpawnCache();
var mask = BitMask256.AllClear();
// Set first bit in each ulong
cache.SetBit(0);
cache.SetBit(64);
cache.SetBit(128);
cache.SetBit(192);
mask.SetBit(0);
mask.SetBit(64);
mask.SetBit(128);
mask.SetBit(192);
Assert.Equal(expectedPosition, cache.GetNthBitPosition(n));
Assert.Equal(expectedPosition, mask.GetNthSetBit(n));
}
[Fact]
public void SectorSpawnCache_GetNthBitPosition_WithinSingleUlong()
public void BitMask256_GetNthSetBit_WithinSingleUlong()
{
var cache = new SectorSpawnCache();
var mask = BitMask256.AllClear();
cache.SetBit(5);
cache.SetBit(10);
cache.SetBit(20);
mask.SetBit(5);
mask.SetBit(10);
mask.SetBit(20);
Assert.Equal(5, cache.GetNthBitPosition(0));
Assert.Equal(10, cache.GetNthBitPosition(1));
Assert.Equal(20, cache.GetNthBitPosition(2));
Assert.Equal(5, mask.GetNthSetBit(0));
Assert.Equal(10, mask.GetNthSetBit(1));
Assert.Equal(20, mask.GetNthSetBit(2));
}
}

View file

@ -15,130 +15,13 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
using Server.Collections;
using Server.Regions;
namespace Server.Engines.Spawners;
/// <summary>
/// Cached spawn position data for a 16x16 sector.
/// Uses a bitmap to track valid spawn positions (256 bits = 4 ulongs = 32 bytes).
/// </summary>
public struct SectorSpawnCache
{
public ulong Bits0;
public ulong Bits1;
public ulong Bits2;
public ulong Bits3;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly int GetCount() =>
BitOperations.PopCount(Bits0) +
BitOperations.PopCount(Bits1) +
BitOperations.PopCount(Bits2) +
BitOperations.PopCount(Bits3);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetBit(int bitIndex)
{
var ulongIndex = bitIndex >> 6; // / 64
var bitPosition = bitIndex & 0x3F; // % 64
switch (ulongIndex)
{
case 0: Bits0 |= 1UL << bitPosition; break;
case 1: Bits1 |= 1UL << bitPosition; break;
case 2: Bits2 |= 1UL << bitPosition; break;
case 3: Bits3 |= 1UL << bitPosition; break;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly bool GetBit(int bitIndex)
{
var ulongIndex = bitIndex >> 6;
var bitPosition = bitIndex & 0x3F;
return ulongIndex switch
{
0 => (Bits0 & (1UL << bitPosition)) != 0,
1 => (Bits1 & (1UL << bitPosition)) != 0,
2 => (Bits2 & (1UL << bitPosition)) != 0,
3 => (Bits3 & (1UL << bitPosition)) != 0,
_ => false
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ClearBit(int bitIndex)
{
var ulongIndex = bitIndex >> 6;
var bitPosition = bitIndex & 0x3F;
switch (ulongIndex)
{
case 0: Bits0 &= ~(1UL << bitPosition); break;
case 1: Bits1 &= ~(1UL << bitPosition); break;
case 2: Bits2 &= ~(1UL << bitPosition); break;
case 3: Bits3 &= ~(1UL << bitPosition); break;
}
}
/// <summary>
/// Gets the Nth set bit position (0-indexed) from the bitmap.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly int GetNthBitPosition(int n)
{
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;
return 192 + GetNthBitInUlong(Bits3, n);
}
[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; // Clear lowest set bit
n--;
}
return bits == 0 ? -1 : BitOperations.TrailingZeroCount(bits);
}
}
/// <summary>
/// Global manager for sector-based spawn position caching.
/// Shared across all spawners for efficient memory usage and house invalidation.
@ -147,8 +30,8 @@ public struct SectorSpawnCache
/// </summary>
public static class SectorSpawnCacheManager
{
private static readonly Dictionary<(Map, int, int), SectorSpawnCache> _landCaches = [];
private static readonly Dictionary<(Map, int, int), SectorSpawnCache> _waterCaches = [];
private static readonly Dictionary<(Map, int, int), BitMask256> _landCaches = [];
private static readonly Dictionary<(Map, int, int), BitMask256> _waterCaches = [];
/// <summary>
/// Marks a position as valid for spawning in the global cache.
@ -245,7 +128,7 @@ public static class SectorSpawnCacheManager
if (caches.TryGetValue((map, sx, sy), out var cache))
{
var count = cache.GetCount();
var count = cache.PopCount();
if (count > 0)
{
sectorList.Add((sx, sy, count));
@ -281,7 +164,7 @@ public static class SectorSpawnCacheManager
}
var indexInSector = targetIndex - currentIndex;
var bitPosition = cache.GetNthBitPosition(indexInSector);
var bitPosition = cache.GetNthSetBit(indexInSector);
var localX = bitPosition & (Map.SectorSize - 1);
var localY = bitPosition >> Map.SectorShift;