Skip to content

Commit

Permalink
Updated MagicBB to use PEXT (from StockFish 16)
Browse files Browse the repository at this point in the history
  • Loading branch information
rudzen committed Feb 29, 2024
1 parent c9eec94 commit db7ae58
Showing 1 changed file with 110 additions and 100 deletions.
210 changes: 110 additions & 100 deletions src/Rudzoft.ChessLib/Types/MagicBB.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
/*
* Adapted from PortFish.
* Minor modernizations by Rudy Alex Kohn
* Adapted to c# from StockFish.
*/

using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
using Rudzoft.ChessLib.Hash;

namespace Rudzoft.ChessLib.Types;

public static class MagicBB
public sealed class Magics
{
private static readonly BitBoard[] RMasks = new BitBoard[64];
private static readonly BitBoard[] RMagics = new BitBoard[64];
private static readonly BitBoard[][] RAttacks = new BitBoard[64][];
private static readonly int[] RShifts = new int[64];
public BitBoard Mask { get; init; } = BitBoard.Empty;

public BitBoard Magic { get; set; } = BitBoard.Empty;

public BitBoard[] Attacks { get; set; } = new BitBoard[64];

private static readonly BitBoard[] BMasks = new BitBoard[64];
private static readonly BitBoard[] BMagics = new BitBoard[64];
private static readonly BitBoard[][] BAttacks = new BitBoard[64][];
private static readonly int[] BShifts = new int[64];
public uint Shift { get; set; }

private static readonly int[][] MagicBoosters =
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint Index(in BitBoard occupied)
{
return Bmi2.X64.IsSupported switch
{
true => (uint)Bmi2.X64.ParallelBitExtract(occupied.Value, Mask.Value),
var _ => (uint)((occupied & Mask).Value * Magic >> (int)Shift).Value
};
}
}

public static class MagicBB
{
private static readonly Magics[] RookMagics = new Magics[64];
private static readonly Magics[] BishopMagics = new Magics[64];

// Optimal PRNG seeds to pick the correct magics in the shortest time
private static readonly ulong[] Seeds =
[
[3191, 2184, 1310, 3618, 2091, 1308, 2452, 3996],
[1059, 3608, 605, 3234, 3326, 38, 2029, 3043]
728UL, 10316UL, 55013UL, 32803UL, 12281UL, 15100UL, 16645UL, 255UL
];

private static readonly byte[] BitCount8Bit = new byte[256];
Expand All @@ -44,35 +58,36 @@ static MagicBB()
Span<Direction> bishopDeltas = stackalloc Direction[]
{ Direction.NorthEast, Direction.SouthEast, Direction.SouthWest, Direction.NorthWest };

var occupancy = new BitBoard[4096];
var reference = new BitBoard[4096];
var rookTable = new BitBoard[0x19000]; // To store rook attacks
var bishopTable = new BitBoard[0x1480]; // To store bishop attacks

var epochs = new int[0x1000];
var occupancy = new BitBoard[0x1000];
var reference = new BitBoard[0x1000];
var rk = new RKiss();

ref var occupancyRef = ref MemoryMarshal.GetArrayDataReference(occupancy);
ref var referenceRef = ref MemoryMarshal.GetArrayDataReference(reference);
ref var epochsRef = ref MemoryMarshal.GetArrayDataReference(epochs);

InitMagics(
pt: PieceTypes.Rook,
attacks: RAttacks,
magics: RMagics,
masks: RMasks,
shifts: RShifts,
deltas: rookDeltas,
occupancyRef: ref occupancyRef,
referenceRef: ref referenceRef,
rk: rk
rookTable,
RookMagics,
rookDeltas,
ref occupancyRef,
ref referenceRef,
ref epochsRef,
rk
);

InitMagics(
pt: PieceTypes.Bishop,
attacks: BAttacks,
magics: BMagics,
masks: BMasks,
shifts: BShifts,
deltas: bishopDeltas,
occupancyRef: ref occupancyRef,
referenceRef: ref referenceRef,
rk: rk
bishopTable,
BishopMagics,
bishopDeltas,
ref occupancyRef,
ref referenceRef,
ref epochsRef,
rk
);

var end = Stopwatch.GetElapsedTime(start);
Expand All @@ -90,13 +105,13 @@ private static int PopCountMax15(ulong b)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BitBoard RookAttacks(this Square s, in BitBoard occ)
{
return RAttacks[s.AsInt()][((occ & RMasks[s.AsInt()]).Value * RMagics[s.AsInt()].Value) >> RShifts[s.AsInt()]];
return RookMagics[s.AsInt()].Attacks[RookMagics[s.AsInt()].Index(in occ)];
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BitBoard BishopAttacks(this Square s, in BitBoard occ)
{
return BAttacks[s.AsInt()][((occ & BMasks[s.AsInt()]).Value * BMagics[s.AsInt()].Value) >> BShifts[s.AsInt()]];
return BishopMagics[s.AsInt()].Attacks[BishopMagics[s.AsInt()].Index(in occ)];
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand All @@ -106,126 +121,121 @@ public static BitBoard QueenAttacks(this Square s, in BitBoard occ)
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint MagicIndex(PieceTypes pt, Square s, in BitBoard occ)
private static BitBoard SafeDistance(Square sq, Direction step)
{
return pt == PieceTypes.Rook
? (uint)(((occ & RMasks[s.AsInt()]).Value * RMagics[s.AsInt()].Value) >> RShifts[s.AsInt()])
: (uint)(((occ & BMasks[s.AsInt()]).Value * BMagics[s.AsInt()].Value) >> BShifts[s.AsInt()]);
var to = sq + step;
return to.IsOk && sq.Distance(to) <= 2 ? to.AsBb() : BitBoard.Empty;
}

// init_magics() computes all rook and bishop attacks at startup. Magic
// InitMagics() computes all rook and bishop attacks at startup. Magic
// bitboards are used to look up attacks of sliding pieces. As a reference see
// chessprogramming.wikispaces.com/Magic+Bitboards. In particular, here we
// use the so called "fancy" approach.

private static void InitMagics(
PieceTypes pt,
BitBoard[][] attacks,
BitBoard[] magics,
BitBoard[] masks,
int[] shifts,
BitBoard[] table,
Magics[] magics,
ReadOnlySpan<Direction> deltas,
ref BitBoard occupancyRef,
ref BitBoard referenceRef,
IRKiss rk)
ref int epochRef,
IRKiss rk
)
{
var cnt = 0;
var size = 0;
var rankMask = Rank.Rank1.RankBB() | Rank.Rank8.RankBB();
var fileMask = File.FileA.FileBB() | File.FileH.FileBB();
ref var magicsRef = ref MemoryMarshal.GetArrayDataReference(magics);

for (var s = Squares.a1; s <= Squares.h8; s++)
for (var s = 0; s < 64; s++)
{
var sq = new Square(s);
// Board edges are not considered in the relevant occupancies
var edges = (rankMask & ~sq.Rank) | (fileMask & ~sq.File);
var rank = sq.Rank;
var edges = (rankMask & ~rank) | (fileMask & ~sq.File);

// Given a square 's', the mask is the bitboard of sliding attacks from
// 's' computed on an empty board. The index must be big enough to contain
// all the attacks for each possible subset of the mask and so is 2 power
// the number of 1s of the mask. Hence we deduce the size of the shift to
// apply to the 64 or 32 bits word to get the index.
masks[s.AsInt()] = SlidingAttack(deltas, s, 0) & ~edges;
shifts[s.AsInt()] = 64 - PopCountMax15(masks[s.AsInt()].Value);
ref var m = ref Unsafe.Add(ref magicsRef, s);
m = new()
{
Mask = SlidingAttack(deltas, sq, 0) & ~edges,
};
m.Shift = (uint)(64 - m.Mask.Count);

// Set the offset for the attacks table of the square. We have individual
// table sizes for each square with "Fancy Magic Bitboards".
m.Attacks = sq == Square.A1 ? table : Unsafe.Add(ref magicsRef, s - 1).Attacks[size..];


// Use Carry-Rippler trick to enumerate all subsets of masks[s] and
// store the corresponding sliding attack bitboard in reference[].
var b = BitBoard.Empty;
var size = 0;
size = 0;
do
{
Unsafe.Add(ref occupancyRef, size) = b;
Unsafe.Add(ref referenceRef, size) = SlidingAttack(deltas, sq, b);
ref var reference = ref Unsafe.Add(ref referenceRef, size);
reference = SlidingAttack(deltas, sq, in b);

if (Bmi2.X64.IsSupported)
m.Attacks[Bmi2.X64.ParallelBitExtract(b.Value, m.Mask.Value)] = reference;

size++;
b = (b.Value - masks[s.AsInt()].Value) & masks[s.AsInt()];
b = (b.Value - m.Mask.Value) & m.Mask;
} while (b.IsNotEmpty);

// Set the offset for the table of the next square. We have individual
// table sizes for each square with "Fancy Magic Bitboards".
var booster = MagicBoosters[1][sq.Rank.AsInt()];
if (Bmi2.X64.IsSupported)
continue;

attacks[s.AsInt()] = new BitBoard[size];
rk.Seed = Seeds[rank.AsInt()];

// Find a magic for square 's' picking up an (almost) random number
// until we find the one that passes the verification test.
int i;
do
for (var i = 0; i < size;)
{
magics[s.AsInt()] = PickRandom(masks[s.AsInt()], rk, booster);
Array.Clear(attacks[s.AsInt()], 0, size);
for (m.Magic = BitBoard.Empty; (BitBoard.Create(m.Magic.Value * m.Mask.Value) >> 56).Value < 6;)
m.Magic = rk.Sparse();

// A good magic must map every possible occupancy to an index that
// looks up the correct sliding attack in the attacks[s] database.
// Note that we build up the database for square 's' as a side
// effect of verifying the magic.
for (i = 0; i < size; i++)
// effect of verifying the magic. Keep track of the attempt count
// and save it in epoch[], little speed-up trick to avoid resetting
// m.attacks[] after every failed attempt.
for (++cnt, i = 0; i < size; ++i)
{
var idx = MagicIndex(pt, s, in Unsafe.Add(ref occupancyRef, i));
var attack = attacks[s.AsInt()][idx];
ref var reference = ref Unsafe.Add(ref referenceRef, i);
var idx = m.Index(Unsafe.Add(ref occupancyRef, i));

if (attack.IsNotEmpty && attack != reference)
break;
ref var epoch = ref Unsafe.Add(ref epochRef, idx);

attacks[s.AsInt()][idx] = reference;
if (epoch < cnt)
{
epoch = cnt;
m.Attacks[idx] = Unsafe.Add(ref referenceRef, i);
}
else if (m.Attacks[idx] != Unsafe.Add(ref referenceRef, i))
break;
}
} while (i != size);
}
}
}

private static BitBoard SlidingAttack(ReadOnlySpan<Direction> deltas, Square sq, BitBoard occupied)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BitBoard SlidingAttack(ReadOnlySpan<Direction> deltas, Square sq, in BitBoard occupied)
{
var attack = BitBoard.Empty;

foreach (var delta in deltas)
{
for (var s = sq + delta; s.IsOk && s.Distance(s - delta) == 1; s += delta)
{
attack |= s;

if (occupied.Contains(s))
break;
}
var s = sq;
while (SafeDistance(s, delta) && !occupied.Contains(s))
attack |= (s += delta).AsBb();
}

return attack;
}

private static BitBoard PickRandom(BitBoard mask, IRKiss rk, int booster)
{
// Values s1 and s2 are used to rotate the candidate magic of a
// quantity known to be the optimal to quickly find the magics.
var s1 = booster & 63;
var s2 = (booster >> 6) & 63;

while (true)
{
ulong magic = rk.Rand();
magic = (magic >> s1) | (magic << (64 - s1));
magic &= rk.Rand();
magic = (magic >> s2) | (magic << (64 - s2));
magic &= rk.Rand();

if (BitCount8Bit[((mask.Value * magic) >> 56)] >= 6)
return magic;
}
}
}

0 comments on commit db7ae58

Please sign in to comment.