Implement more efficient magics generator
authorH.G.Muller <hgm@hgm-xboard.(none)>
Fri, 9 Jan 2026 10:42:42 +0000 (11:42 +0100)
committerH.G.Muller <hgm@hgm-xboard.(none)>
Fri, 9 Jan 2026 10:42:42 +0000 (11:42 +0100)
Instead of randomly trying magics until a suitable one is found,
a routine is added that finds the magic through purposeful backtracking.
It also does not have to generate attacks to test for collisions; it
guarantees there will be none by preventing harmful collisions between
mask bits when these are placed in the index. That makes it very fast.
Verification of the generated magic is still done, but since the magic
should already be correct the loop for retrying is gone.

src/bitboard.cpp

index c85dd04..9eeb16c 100644 (file)
@@ -476,6 +476,42 @@ void Bitboards::init() {
 
 namespace {
 
+/// Fast generator for magic multipliers.
+/// Works by purposeful backtracking rather than random trying:
+/// recursively maps not-already-placed mask bits in the lookup key without spoiling
+/// any bits that were placed earlier.
+
+Bitboard puzzle(Bitboard key, Bitboard maskBitsToMap, Bitboard magic, int keyStart)
+{
+  int i, k;
+  Bitboard keyBits = AllSquares << keyStart;
+
+  for(k = SQ_MAX; ~maskBitsToMap & Square(k); k--) {} // find mask bit to map
+
+  for(i = (k > keyStart ? k : keyStart); i <= SQUARE_BIT_MASK; i++)
+  {
+      int shift = i - k;
+      Bitboard newBits = maskBitsToMap << shift;
+
+      if(key & newBits & keyBits) continue; // does not fit in, shift more
+
+      Bitboard newKey = key + newBits;
+      Bitboard changedKeyBits = (newKey ^ key) & keyBits;
+
+      if(changedKeyBits & key) continue; // carry spoiled key
+
+      Bitboard newMagic = magic | Bitboard(1) << shift;
+      Bitboard remainingMask = maskBitsToMap - (changedKeyBits >> shift);
+
+      if(remainingMask)
+          newMagic = puzzle(newKey, remainingMask, newMagic, keyStart);
+
+      if(newMagic) return newMagic; // done
+  }
+
+  return 0; // fail
+}
+
   // init_magics() computes all rook and bishop attacks at startup. Magic
   // bitboards are used to look up attacks of sliding pieces. As a reference see
   // www.chessprogramming.org/Magic_Bitboards. In particular, here we use the so
@@ -488,22 +524,11 @@ namespace {
   void init_magics(Bitboard table[], Magic magics[], std::map<DirectionCode, int> directions) {
 #endif
 
-    // Optimal PRNG seeds to pick the correct magics in the shortest time
-#ifndef PRECOMPUTED_MAGICS
-#ifdef LARGEBOARDS
-    int seeds[][RANK_NB] = { { 734, 10316, 55013, 32803, 12281, 15100,  16645, 255, 346, 89123 },
-                             { 734, 10316, 55013, 32803, 12281, 15100,  16645, 255, 346, 89123 } };
-#else
-    int seeds[][RANK_NB] = { { 8977, 44560, 54343, 38998,  5731, 95205, 104912, 17020 },
-                             {  728, 10316, 55013, 32803, 12281, 15100,  16645,   255 } };
-#endif
-#endif
-
     Bitboard* occupancy = new Bitboard[1 << (FILE_NB + RANK_NB - 4)];
     Bitboard* reference = new Bitboard[1 << (FILE_NB + RANK_NB - 4)];
     Bitboard edges, b;
     int* epoch = new int[1 << (FILE_NB + RANK_NB - 4)]();
-    int cnt = 0, size = 0;
+    int cnt = 0, size = 0, totalSize = 0;
 
     for (Square s = SQ_A1; s <= SQ_MAX; ++s)
     {
@@ -545,26 +570,14 @@ namespace {
         if (HasPext)
             continue;
 
-#ifndef PRECOMPUTED_MAGICS
-        PRNG rng(seeds[Is64Bit][rank_of(s)]);
+#ifdef PRECOMPUTED_MAGICS
+        // With large boards we always use an intelligent first try, as randomly trying until we succeed takes very long
+        if(magicsInit) m.magic = magicsInit[s]; else // use precomputed if there is one
 #endif
+        m.magic = puzzle(0, m.mask, 0, m.shift);
 
-        // Find a magic for square 's' picking up an (almost) random number
-        // until we find the one that passes the verification test.
-        for (int i = 0; i < size; )
         {
-            for (m.magic = 0; popcount((m.magic * m.mask) >> (SQUARE_NB - FILE_NB)) < FILE_NB - 2; )
-            {
-#ifdef LARGEBOARDS
-#ifdef PRECOMPUTED_MAGICS
-                m.magic = magicsInit[s];
-#else
-                m.magic = (rng.sparse_rand<Bitboard>() << 64) ^ rng.sparse_rand<Bitboard>();
-#endif
-#else
-                m.magic = rng.sparse_rand<Bitboard>();
-#endif
-            }
+            int i;
 
             // A good magic must map every possible occupancy to an index that
             // looks up the correct sliding attack in the attacks[s] database.
@@ -585,7 +598,11 @@ namespace {
                     break;
             }
         }
+        totalSize += size;
     }
+#ifndef NDEBUG
+    sync_cout << "# magic_init: size = " << totalSize << "(" << (totalSize*16 / 1024) << " KB)" << sync_endl;
+#endif
 
     delete[] occupancy;
     delete[] reference;