diff --git a/src/Rudzoft.ChessLib/Types/MagicBB.cs b/src/Rudzoft.ChessLib/Types/MagicBB.cs index 7a205e6..1e4b1a5 100644 --- a/src/Rudzoft.ChessLib/Types/MagicBB.cs +++ b/src/Rudzoft.ChessLib/Types/MagicBB.cs @@ -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]; @@ -44,35 +58,36 @@ static MagicBB() Span 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); @@ -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)] @@ -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 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 deltas, Square sq, BitBoard occupied) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BitBoard SlidingAttack(ReadOnlySpan 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; - } - } } \ No newline at end of file